diff --git a/Build.json b/Build.json
new file mode 100644
index 0000000..60ae05f
--- /dev/null
+++ b/Build.json
@@ -0,0 +1,15 @@
+{
+ "steps": [
+ {
+ "name": "Build Playground.app",
+ "type": "macos-bundle",
+ "with": {
+ "name": "Playground",
+ "identifier": "dev.shaftui.playground",
+ "version": "1.0.0",
+ "product": "Playground",
+ "output": ".build/Playground.app"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Plugins/BuilderPlugin/AppleBundleStructure.swift b/Plugins/BuilderPlugin/AppleBundleStructure.swift
index d780d8c..4e337b8 100644
--- a/Plugins/BuilderPlugin/AppleBundleStructure.swift
+++ b/Plugins/BuilderPlugin/AppleBundleStructure.swift
@@ -20,6 +20,7 @@ struct AppleBundleStructure {
let infoPlistFile: URL
let appIconFile: URL
let mainExecutable: URL
+ let mainExecutableDSYM: URL
init(at bundleDirectory: URL, platform: ApplePlatform, appName: String) {
switch platform {
@@ -40,6 +41,7 @@ struct AppleBundleStructure {
appIconFile = resourcesDirectory.appendingPathComponent("AppIcon.icns")
mainExecutable = executableDirectory.appendingPathComponent(appName)
+ mainExecutableDSYM = mainExecutable.appendingPathExtension("dSYM")
}
func ensureDirectoriesExist() throws {
diff --git a/Plugins/BuilderPlugin/BuildSpec.swift b/Plugins/BuilderPlugin/BuildSpec.swift
index 864ef7f..a8c40cd 100644
--- a/Plugins/BuilderPlugin/BuildSpec.swift
+++ b/Plugins/BuilderPlugin/BuildSpec.swift
@@ -1,27 +1,90 @@
// {
-// "app": {
-// "name": "Playground",
-// "identifier": "dev.shaftui.playground",
-// "version": "1.0.0",
-// "product": "Playground"
-// }
+// "steps": [
+// {
+// "name": "Build ShaftBrowser.app",
+// "type": "macos-bundle",
+// "with": {
+// "name": "ShaftBrowser",
+// "identifier": "dev.shaftui.browser",
+// "version": "1.0.0",
+// "product": "ShaftBrowser",
+// "output": ".build/ShaftBrowser.app"
+// }
+// },
+// {
+// "name": "Build ShaftBrowser Helper.app",
+// "type": "macos-bundle",
+// "with": {
+// "name": "ShaftBrowser Helper",
+// "identifier": "dev.shaftui.browserhelper",
+// "version": "1.0.0",
+// "product": "ShaftBrowserHelper",
+// "output": ".build/ShaftBrowser Helper.app"
+// }
+// },
+// {
+// "name": "Move ShaftBrowser Helper.app to ShaftBrowser.app/Contents/Frameworks",
+// "type": "move",
+// "with": {
+// "source": ".build/ShaftBrowser Helper.app",
+// "destination": ".build/ShaftBrowser.app/Contents/Frameworks"
+// }
+// }
+// ]
// }
+
struct BuildSpec: Codable {
- let app: AppSpec?
+ let steps: [BuildStep]
}
-/// The specification for the app
-struct AppSpec: Codable {
- /// The name of the app
- let name: String
+enum BuildStepType: String, Codable {
+ case macOSBundle = "macos-bundle"
+ case move = "move"
+}
- /// The identifier of the app. This is usually a reverse domain name.
- let identifier: String
+enum BuildStep: Codable {
+ case macOSBundle(MacOSBundleInput)
+ case move(MoveInput)
- /// The version of the app
- let version: String
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let type = try container.decode(BuildStepType.self, forKey: .type)
+ switch type {
+ case .macOSBundle:
+ self = .macOSBundle(try container.decode(MacOSBundleInput.self, forKey: .with))
+ case .move:
+ self = .move(try container.decode(MoveInput.self, forKey: .with))
+ }
+ }
+
+ func encode(to encoder: Encoder) throws {
+ switch self {
+ case .macOSBundle(let input):
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(BuildStepType.macOSBundle, forKey: .type)
+ try container.encode(input, forKey: .with)
+ case .move(let input):
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(BuildStepType.move, forKey: .type)
+ try container.encode(input, forKey: .with)
+ }
+ }
- /// Name of the product defined in the package. The product will be used to
- /// create the bundle.
+ private enum CodingKeys: String, CodingKey {
+ case type
+ case with
+ }
+}
+
+struct MacOSBundleInput: Codable {
+ let name: String
+ let identifier: String
+ let version: String
let product: String
+ let output: String
+}
+
+struct MoveInput: Codable {
+ let source: String
+ let destination: String
}
diff --git a/Plugins/BuilderPlugin/Options.swift b/Plugins/BuilderPlugin/Options.swift
index 7cec06e..73dff47 100644
--- a/Plugins/BuilderPlugin/Options.swift
+++ b/Plugins/BuilderPlugin/Options.swift
@@ -2,7 +2,7 @@ import PackagePlugin
/// Options for the builder plugin command
struct BuilderOptions {
- var targetName: String
+ // var targetName: String
var configuration: BuilderConfiguration
}
@@ -16,14 +16,17 @@ enum BuilderConfiguration: String {
func extractOptions(from arguments: [String]) -> BuilderOptions {
var extractor = ArgumentExtractor(arguments)
- guard let targetName = extractor.remainingArguments.first else {
- printAndExit("Target name not provided")
- }
+ // guard let targetName = extractor.remainingArguments.first else {
+ // printAndExit("Target name not provided")
+ // }
let configurationString = extractor.extractOption(named: "--configuration").last ?? "debug"
guard let configuration = BuilderConfiguration(rawValue: configurationString) else {
printAndExit("Invalid configuration: \(configurationString)")
}
- return BuilderOptions(targetName: targetName, configuration: configuration)
+ return BuilderOptions(
+ // targetName: targetName,
+ configuration: configuration
+ )
}
diff --git a/Plugins/BuilderPlugin/Steps/MacOSBundleStep.swift b/Plugins/BuilderPlugin/Steps/MacOSBundleStep.swift
new file mode 100644
index 0000000..8e4c52f
--- /dev/null
+++ b/Plugins/BuilderPlugin/Steps/MacOSBundleStep.swift
@@ -0,0 +1,99 @@
+import Foundation
+import PackagePlugin
+
+func executeMacOSBundleStep(_ input: MacOSBundleInput, context: StepContext) {
+ // Ensure the product exists in the package
+ guard let product = context.findProduct(input.product) else {
+ print("Product \(input.product) not found. Valid products are:")
+ for product in context.package.products {
+ print(" - \(product.name)")
+ }
+ exit(1)
+ }
+
+ print("Building \(product.name) in \(context.configuration.rawValue) mode")
+
+ var buildParameters = PackageManager.BuildParameters(echoLogs: true)
+ buildParameters.otherLinkerFlags = ["-L.shaft/skia"]
+
+ let buildResult = try! context.packageManager.build(
+ .product(input.product),
+ parameters: buildParameters
+ )
+
+ if !buildResult.succeeded {
+ print("Build failed. See logs above for details.")
+ return
+ }
+
+ guard let mainArtifect = buildResult.builtArtifacts.first else {
+ print("No built artifacts found. Skipping bundle creation.")
+ return
+ }
+ print("Main artifact: \(mainArtifect.path.string)")
+
+ if mainArtifect.kind != .executable {
+ print("Main artifact is \(mainArtifect.kind). Skipping bundle creation.")
+ return
+ }
+
+ print("Creating bundle with input: \(input)")
+ createBundle(from: mainArtifect, input: input, configuration: context.configuration)
+}
+
+private func createBundle(
+ from artifect: PackageManager.BuildResult.BuiltArtifact,
+ input: MacOSBundleInput,
+ configuration: BuilderConfiguration
+) {
+ let outputDirectory = URL(fileURLWithPath: input.output)
+
+ print("Creating bundle at \(outputDirectory)")
+
+ let structure = AppleBundleStructure(
+ at: outputDirectory,
+ platform: .macOS,
+ appName: input.name
+ )
+
+ try! structure.ensureDirectoriesExist()
+
+ /// Copy the main executable
+ copyFile(from: artifect.path.string, to: structure.mainExecutable.path)
+
+ /// Copy .dSYM file in debug mode
+ if configuration == .debug {
+ copyFile(from: artifect.path.string + ".dSYM", to: structure.mainExecutableDSYM.path)
+ }
+
+ // Write the Info.plist
+ let infoPlist = """
+
+
+
+
+ CFBundleExecutable
+ \(input.product)
+ CFBundleIdentifier
+ \(input.identifier)
+ CFBundleName
+ \(input.name)
+ CFBundleVersion
+ \(input.version)
+
+
+ """
+
+ try! infoPlist.write(to: structure.infoPlistFile, atomically: true, encoding: .utf8)
+
+ print("Bundle created at \(outputDirectory)")
+}
+
+private func copyFile(from source: String, to destination: String) {
+ if FileManager.default.fileExists(atPath: destination) {
+ print("Removing existing file at \(destination)")
+ try! FileManager.default.removeItem(atPath: destination)
+ }
+ print("Copying \(source) to \(destination)")
+ try! FileManager.default.copyItem(atPath: source, toPath: destination)
+}
diff --git a/Plugins/BuilderPlugin/main.swift b/Plugins/BuilderPlugin/main.swift
index 4e6646e..02164e2 100644
--- a/Plugins/BuilderPlugin/main.swift
+++ b/Plugins/BuilderPlugin/main.swift
@@ -9,119 +9,54 @@ struct BuilderPlugin: CommandPlugin {
arguments: [String]
) async throws {
let options = extractOptions(from: arguments)
- let targetName = options.targetName
- // Ensure the product exists in the package
- guard let target = findTarget(targetName, in: context.package) else {
- print("Target \(targetName) not found. Valid targets are:")
- for target in context.package.targets {
- print(" - \(target.name)")
- }
+ let buildSpec = loadBuildSpec(directory: context.package.directory)
+ guard let buildSpec else {
+ print("No Build.json found in \(context.package.directory). Skipped.")
return
}
- print("Building \(target.name) in \(options.configuration.rawValue) mode")
-
- var buildParameters = PackagePlugin.PackageManager.BuildParameters(echoLogs: true)
- buildParameters.otherLinkerFlags = ["-L.shaft/skia"]
-
- let buildResult = try self.packageManager.build(
- .target(targetName),
- parameters: buildParameters
+ let context = StepContext(
+ package: context.package,
+ packageManager: self.packageManager,
+ configuration: options.configuration
)
- if !buildResult.succeeded {
- print("Build failed. See logs above for details.")
- return
- }
+ for step in buildSpec.steps {
+ print("Executing step: \(step)")
- guard let mainArtifect = buildResult.builtArtifacts.first else {
- print("No built artifacts found. Skipping bundle creation.")
- return
- }
-
- if mainArtifect.kind != .executable {
- print("Main artifact is \(mainArtifect.kind). Skipping bundle creation.")
- return
- }
-
- let buildSpec = findBuildSpec(in: target)
- guard let appSpec = buildSpec?.app else {
- print("No app spec found for \(target.name). Skipping bundle creation.")
- return
+ switch step {
+ case .macOSBundle(let input):
+ executeMacOSBundleStep(input, context: context)
+ // case .move(let input):
+ // executeMoveStep(input, context: context)
+ default:
+ print("Unknown step type: \(step)")
+ }
}
-
- print("Creating bundle with app spec: \(appSpec)")
- createBundle(from: mainArtifect, appSpec: appSpec)
}
}
-/// Find a target with the given name in the package
-func findTarget(_ targetName: String, in package: Package) -> Target? {
- return package.targets.first { $0.name == targetName }
-}
-
-/// Try read the Build.json file in the target directory
-func findBuildSpec(in target: Target) -> BuildSpec? {
- let path = target.directory.appending(subpath: "Build.json").string
+/// Try read the Build.json file in the package directory
+func loadBuildSpec(directory: Path) -> BuildSpec? {
+ let path = directory.appending(subpath: "Build.json").string
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
- do {
- let decoder = JSONDecoder()
- return try decoder.decode(BuildSpec.self, from: data)
- } catch {
- print("Error decoding BuildSpec: \(error)")
- return nil
- }
+ let decoder = JSONDecoder()
+ return try! decoder.decode(BuildSpec.self, from: data)
}
-func createBundle(from artifect: PackageManager.BuildResult.BuiltArtifact, appSpec: AppSpec) {
- let outputDirectory = URL(fileURLWithPath: artifect.path.string)
- .deletingLastPathComponent()
- .appendingPathComponent("\(appSpec.product).app")
-
- print("Creating bundle at \(outputDirectory)")
-
- let structure = AppleBundleStructure(
- at: outputDirectory,
- platform: .macOS,
- appName: appSpec.product
- )
-
- try! structure.ensureDirectoriesExist()
+struct StepContext {
+ let package: Package
+ let packageManager: PackageManager
+ let configuration: BuilderConfiguration
+}
- // Copy the main executable to the bundle
- if FileManager.default.fileExists(atPath: structure.mainExecutable.path) {
- print("Removing existing executable at \(structure.mainExecutable.path)")
- try! FileManager.default.removeItem(at: structure.mainExecutable)
+extension StepContext {
+ func findProduct(_ productName: String) -> Product? {
+ return package.products.first { $0.name == productName }
}
- try! FileManager.default.copyItem(
- at: URL(fileURLWithPath: artifect.path.string),
- to: structure.mainExecutable
- )
-
- // Write the Info.plist
- let infoPlist = """
-
-
-
-
- CFBundleExecutable
- \(appSpec.product)
- CFBundleIdentifier
- \(appSpec.identifier)
- CFBundleName
- \(appSpec.name)
- CFBundleVersion
- \(appSpec.version)
-
-
- """
-
- try! infoPlist.write(to: structure.infoPlistFile, atomically: true, encoding: .utf8)
-
- print("Bundle created at \(outputDirectory)")
}