-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial version of Shaft builder (#3)
- Loading branch information
Showing
8 changed files
with
272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import Foundation | ||
|
||
/// foo.app/ | ||
/// Contents/ | ||
/// Info.plist | ||
/// MacOS/ | ||
/// foo | ||
/// Resources/ | ||
/// foo.icns | ||
/// Libraries/ | ||
/// libfoo.dylib | ||
/// Frameworks/ | ||
/// Foo.framework | ||
struct AppleBundleStructure { | ||
let contentsDirectory: URL | ||
let resourcesDirectory: URL | ||
let librariesDirectory: URL | ||
let frameworksDirectory: URL | ||
let executableDirectory: URL | ||
let infoPlistFile: URL | ||
let appIconFile: URL | ||
let mainExecutable: URL | ||
|
||
init(at bundleDirectory: URL, platform: ApplePlatform, appName: String) { | ||
switch platform { | ||
case .macOS: | ||
contentsDirectory = bundleDirectory.appendingPathComponent("Contents") | ||
executableDirectory = contentsDirectory.appendingPathComponent("MacOS") | ||
resourcesDirectory = contentsDirectory.appendingPathComponent("Resources") | ||
frameworksDirectory = contentsDirectory.appendingPathComponent("Frameworks") | ||
case .iOS, .tvOS, .visionOS, .watchOS: | ||
contentsDirectory = bundleDirectory | ||
executableDirectory = contentsDirectory | ||
resourcesDirectory = contentsDirectory | ||
frameworksDirectory = contentsDirectory | ||
} | ||
|
||
librariesDirectory = contentsDirectory.appendingPathComponent("Libraries") | ||
infoPlistFile = contentsDirectory.appendingPathComponent("Info.plist") | ||
appIconFile = resourcesDirectory.appendingPathComponent("AppIcon.icns") | ||
|
||
mainExecutable = executableDirectory.appendingPathComponent(appName) | ||
} | ||
|
||
func ensureDirectoriesExist() throws { | ||
for directory in [ | ||
contentsDirectory, | ||
executableDirectory, | ||
resourcesDirectory, | ||
librariesDirectory, | ||
frameworksDirectory, | ||
] { | ||
try FileManager.default.createDirectory( | ||
at: directory, | ||
withIntermediateDirectories: true, | ||
attributes: nil | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
enum ApplePlatform: String { | ||
case iOS | ||
case macOS | ||
case tvOS | ||
case watchOS | ||
case visionOS | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// { | ||
// "app": { | ||
// "name": "Playground", | ||
// "identifier": "dev.shaftui.playground", | ||
// "version": "1.0.0", | ||
// "product": "Playground" | ||
// } | ||
// } | ||
struct BuildSpec: Codable { | ||
let app: AppSpec? | ||
} | ||
|
||
/// The specification for the app | ||
struct AppSpec: Codable { | ||
/// The name of the app | ||
let name: String | ||
|
||
/// The identifier of the app. This is usually a reverse domain name. | ||
let identifier: String | ||
|
||
/// The version of the app | ||
let version: String | ||
|
||
/// Name of the product defined in the package. The product will be used to | ||
/// create the bundle. | ||
let product: String | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import PackagePlugin | ||
|
||
/// Options for the builder plugin command | ||
struct BuilderOptions { | ||
var targetName: String | ||
|
||
var configuration: BuilderConfiguration | ||
} | ||
|
||
enum BuilderConfiguration: String { | ||
case debug | ||
|
||
case release | ||
} | ||
|
||
func extractOptions(from arguments: [String]) -> BuilderOptions { | ||
var extractor = ArgumentExtractor(arguments) | ||
|
||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Foundation | ||
|
||
func printAndExit(_ message: String, _ exitCode: Int32 = 1) -> Never { | ||
print(message) | ||
exit(exitCode) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import Foundation | ||
import PackagePlugin | ||
|
||
@main | ||
struct BuilderPlugin: CommandPlugin { | ||
|
||
func performCommand( | ||
context: PluginContext, | ||
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)") | ||
} | ||
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 | ||
) | ||
|
||
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 | ||
} | ||
|
||
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 | ||
} | ||
|
||
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 | ||
|
||
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 | ||
} | ||
} | ||
|
||
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() | ||
|
||
// Copy the main executable to the bundle | ||
try! FileManager.default.copyItem( | ||
at: URL(fileURLWithPath: artifect.path.string), | ||
to: structure.mainExecutable | ||
) | ||
|
||
// 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>\(appSpec.product)</string> | ||
<key>CFBundleIdentifier</key> | ||
<string>\(appSpec.identifier)</string> | ||
<key>CFBundleName</key> | ||
<string>\(appSpec.name)</string> | ||
<key>CFBundleVersion</key> | ||
<string>\(appSpec.version)</string> | ||
</dict> | ||
</plist> | ||
""" | ||
|
||
try! infoPlist.write(to: structure.infoPlistFile, atomically: true, encoding: .utf8) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"app": { | ||
"name": "Playground", | ||
"identifier": "dev.shaftui.playground", | ||
"version": "1.0.0", | ||
"product": "Playground" | ||
} | ||
} |