Skip to content

Commit

Permalink
Shaft builder v0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
xtyxtyx committed Jan 1, 2025
1 parent d0129db commit b0a4286
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 117 deletions.
15 changes: 15 additions & 0 deletions Build.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
2 changes: 2 additions & 0 deletions Plugins/BuilderPlugin/AppleBundleStructure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,6 +41,7 @@ struct AppleBundleStructure {
appIconFile = resourcesDirectory.appendingPathComponent("AppIcon.icns")

mainExecutable = executableDirectory.appendingPathComponent(appName)
mainExecutableDSYM = mainExecutable.appendingPathExtension("dSYM")
}

func ensureDirectoriesExist() throws {
Expand Down
97 changes: 80 additions & 17 deletions Plugins/BuilderPlugin/BuildSpec.swift
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 8 additions & 5 deletions Plugins/BuilderPlugin/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PackagePlugin

/// Options for the builder plugin command
struct BuilderOptions {
var targetName: String
// var targetName: String

var configuration: BuilderConfiguration
}
Expand All @@ -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
)
}
99 changes: 99 additions & 0 deletions Plugins/BuilderPlugin/Steps/MacOSBundleStep.swift
Original file line number Diff line number Diff line change
@@ -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 = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>\(input.product)</string>
<key>CFBundleIdentifier</key>
<string>\(input.identifier)</string>
<key>CFBundleName</key>
<string>\(input.name)</string>
<key>CFBundleVersion</key>
<string>\(input.version)</string>
</dict>
</plist>
"""

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)
}
Loading

0 comments on commit b0a4286

Please sign in to comment.