Skip to content

Commit

Permalink
Alternative approach - using a command plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Oct 17, 2024
1 parent e2080bc commit 9e69463
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 3 deletions.
19 changes: 17 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ var products: [Product] = [
.plugin(name: "CodeGeneratorPlugin", targets: ["CodeGeneratorPlugin"]),
.plugin(
name: "ExtensionBuilderPlugin",
targets: ["ExtensionBuilderPlugin"]
),
targets: ["ExtensionBuilderPlugin"]),

.plugin(
name: "ExtensionBuilderToolPlugin",
targets: ["ExtensionBuilderToolPlugin"]),
]

// Macros aren't supported on Windows before 5.9.1 and this sample uses them
Expand Down Expand Up @@ -111,6 +114,18 @@ var targets: [Target] = [
dependencies: ["ExtensionBuilder"]
),

// This is a build-time plugin that can generate a .gdextension file
// for an extension, from a .gdswift file
.plugin(
name: "ExtensionBuilderToolPlugin",
capability: .command(
intent: .custom(
verb: "make-extension",
description: "Generate a gdextension file for a SwiftGodot library."),
permissions: [.writeToPackageDirectory(reason: "To write the generated gdextension file.")]),
dependencies: ["ExtensionBuilder"]
),

// This allows the Swift code to call into the Godot bridge API (GDExtension)
.target(
name: "GDExtension"),
Expand Down
87 changes: 87 additions & 0 deletions Plugins/ExtensionBuilderToolPlugin/GodotConfigFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Foundation

/// Basic implementation of a config file parser.
/// This probably exists somewhere already!
class GodotConfigFile {
static let sectionPattern: Regex = #/\s*\[(?<section>\w+)\]\s*/#
static let assignmentPattern: Regex = #/\s*(?<key>\S+)\s*=\s*(?<value>.*)\s*/#
static let stringPattern: Regex = #/"(?<content>.*)"/#

var content: [String: [String: String]]
var sections: [String]
var _encoded: String?

init(_ url: URL) async throws {
var section = "_"
var values: [String: String] = [:]
var content: [String: [String: String]] = [:]
var sections: [String] = []

func endSection() {
if !values.isEmpty {
sections.append(section)
content[section] = values
}
}

for try await line in url.lines {
if let match = line.matches(of: Self.sectionPattern).first {
endSection()
section = String(match.section)
values = [:]
} else if let match = line.matches(of: Self.assignmentPattern).first {
values[String(match.key)] = String(match.value)
}
}
endSection()

self.content = content
self.sections = sections
}

/// Set a string value in a section.
func set(_ key: String, _ value: String, section: String) {
if content[section] == nil {
content[section] = [:]
}
content[section]?[key] = "\"\(value)\""
_encoded = nil
}

/// Get a string value from a section.
func get(_ key: String, section: String) -> String? {
if let entry = content[section]?[key],
let value = entry.matches(of: Self.stringPattern).first
{
return String(value.content)
}
return nil
}

/// Remove a section.
func remove(section: String) {
content.removeValue(forKey: section)
_encoded = nil
}

/// The string-encoded version of the file.
var encoded: String {
if _encoded == nil {
var output = ""
for (section, values) in content {
output.append("[\(section)]\n")
for (key, value) in values {
output.append("\(key) = \(value)\n")
}
}
_encoded = output
}

return _encoded!
}

/// Write the file to a URL as a UTF8 string.
func write(to url: URL) async throws {
try encoded.write(to: url, atomically: true, encoding: .utf8)
}
}
70 changes: 70 additions & 0 deletions Plugins/ExtensionBuilderToolPlugin/plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Created by Sam Deane on 16/10/24.
// All code (c) 2024 - present day, Elegant Chaos Limited.
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

import Foundation
import PackagePlugin

/// Takes `.gdswift` files and generates `.gdextension` files from them.
@main struct ExtensionBuilderCommandPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {
// Extract the target arguments (if there are none, we assume all).
var argExtractor = ArgumentExtractor(arguments)
let targetNames = argExtractor.extractOption(named: "target")
let targets =
targetNames.isEmpty
? context.package.targets
: try context.package.targets(named: targetNames)

let builder = try context.tool(named: "ExtensionBuilder").path

// Iterate over the targets we've been asked to format.
for target in targets {
// Skip any type of target that doesn't have source files.
// Note: We could choose to instead emit a warning or error here.
guard let target = target.sourceModule else { continue }

let inputFiles = target.sourceFiles.filter({ $0.path.extension == "gdswift" }).map { $0.path }
if inputFiles.isEmpty {
Diagnostics.warning("No .gdswift files found for \(target.name).")
continue
}

let settings = try await GodotConfigFile(URL(fileURLWithPath: inputFiles.first!.string))
let result = try packageManager.build(
.target(target.name),
parameters: .init(configuration: .debug, logging: .concise, echoLogs: true)
)
if result.succeeded {
for artifact in result.builtArtifacts.filter({ $0.kind != .executable }) {
settings.set("macos.debug", artifact.path.string, section: "libraries")
settings.set("macos.release", artifact.path.string.replacing("debug", with: "release"), section: "libraries")
}
} else {
Diagnostics.warning("Couldn't build \(target.name).")
}

let url = URL(fileURLWithPath: context.package.directory.appending("\(target.name).gdextension").string)
try await settings.write(to: url)
// var arguments = [context.package.directory, target.directory, context.package.directory]
// arguments.append(contentsOf: inputFiles)
// print(arguments)

// // Invoke `sometool` on the target directory, passing a configuration
// // file from the package directory.
// let sometoolExec = URL(fileURLWithPath: builder.string)
// let process = try Process.run(sometoolExec, arguments: arguments.map { $0.string })
// process.waitUntilExit()

// // Check whether the subprocess invocation was successful.
// if process.terminationReason == .exit && process.terminationStatus == 0 {
// print("Exported gdextension file for \(target.name).")
// } else {
// let problem = "\(process.terminationReason):\(process.terminationStatus)"
// Diagnostics.error("Exported gdextension failed: \(problem)")
// }
}

}
}
6 changes: 5 additions & 1 deletion Sources/ExtensionBuilder/ExtensionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
/// kicks off a build.
static func main() async {
let args = CommandLine.arguments
guard args.count >= 4 else {
guard args.count >= 5 else {
print(
"""
Usage: builder <package-directory> <target-directory> <output-directory> <input.gdextension> {<input.gdextension> ...}
Expand All @@ -45,6 +45,7 @@ import Foundation
)

await builder.process(inputs: args.dropFirst(4))
print("blah")
}

/// Returns the build directory for a given arch/platform/config.
Expand All @@ -60,6 +61,7 @@ import Foundation
if u.lastPathComponent == ".build" {
break
}
print(u)
} while true

return u.appending(path: "\(arch)-\(platform)").appending(path: config)
Expand Down Expand Up @@ -93,6 +95,8 @@ import Foundation

/// Process a single input file.
func process(_ inputURL: URL, outputURL: URL) async throws {
print(inputURL)

// read the input file
let content = try await GodotConfigFile(inputURL)

Expand Down

0 comments on commit 9e69463

Please sign in to comment.