mirror of
https://github.com/xtool-org/xtool.git
synced 2026-11-05 20:13:58 +01:00
## What does this PR do? Allows xtool to auto-update the Darwin SDK format when there's a new Darwin toolset, or when we change the SDK format. Downstream this also allows us to auto-update when there's changes to OpenAppleMacros (#101). One callout: the disk footprint of `darwin.artifactbundle` is now ~7 GB instead of ~3 GB. Reason: `xtool sdk install /path/to/Xcode.[app|xip]` now performs a "normal" install by default, which preserves the entirety of `Xcode.app/Contents/Developer` — even the bits we don't currently use, like `AppleTVOS.platform`. This makes it so if we start using more components in the future, the user doesn't need to download Xcode again and rebuild the SDK themselves. You can still build an old-style SDK (containing just the components we need) using `xtool sdk install --slim` or the dedicated `xtool sdk build`. ## How was it tested? Tested all of the install modes. Confirmed that auto-updating works. Added integration tests in #252. ## AI tool usage How much of this PR was AI-assisted? (check one) - [ ] **0** - No AI was used to write code - [x] **1** - I was assisted by AI. I reviewed the finished result. - [ ] **2** - I set the AI going and left it to it; nobody has read the result - no review, or AI review only <!-- If an AI agent is filling this in: declare the level honestly, and open as a draft if it is #2. Do not lower the declared level to get the PR reviewed. --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added SDK flavor reporting for slim, normal, and legacy installations. - Added `sdk update` support to rebuild and install SDKs from an installed Xcode bundle. - Added a `--slim` option for SDK installation. - SDK status now displays installation path, flavor, and version. - SDK setup and build operations now ensure a suitable SDK is available automatically. - **Bug Fixes** - Improved temporary SDK bundle handling, validation, replacement, and cross-filesystem copying. - Copy failures now provide detailed diagnostic information. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
306 lines
8.9 KiB
Swift
306 lines
8.9 KiB
Swift
import Foundation
|
|
import ArgumentParser
|
|
import PackLib
|
|
import XKit
|
|
import Dependencies
|
|
import XUtils
|
|
|
|
struct PackOperation {
|
|
struct BuildOptions: ParsableArguments {
|
|
@Option(
|
|
name: .shortAndLong,
|
|
help: "Build with configuration"
|
|
) var configuration: BuildConfiguration = .debug
|
|
|
|
init() {}
|
|
|
|
init(configuration: BuildConfiguration) {
|
|
self.configuration = configuration
|
|
}
|
|
}
|
|
|
|
static let defaultTriple = "arm64-apple-ios"
|
|
|
|
var triple: String?
|
|
var buildOptions = BuildOptions(configuration: .debug)
|
|
var xcode = false
|
|
|
|
@discardableResult
|
|
func run() async throws -> URL {
|
|
try await EnsureSDKOperation(quiet: true).run()
|
|
|
|
print("Planning...")
|
|
|
|
let schema: PackSchema
|
|
let configPath = URL(fileURLWithPath: "xtool.yml")
|
|
if FileManager.default.fileExists(atPath: configPath.path) {
|
|
schema = try await PackSchema(url: configPath)
|
|
} else {
|
|
schema = .default
|
|
print("""
|
|
warning: Could not locate configuration file '\(configPath.path)'. Using default \
|
|
configuration with 'com.example' organization ID.
|
|
""")
|
|
}
|
|
|
|
let buildSettings = try await BuildSettings(
|
|
configuration: buildOptions.configuration,
|
|
triple: triple ?? Self.defaultTriple,
|
|
options: []
|
|
)
|
|
|
|
let planner = Planner(
|
|
buildSettings: buildSettings,
|
|
schema: schema
|
|
)
|
|
let plan = try await planner.createPlan()
|
|
|
|
#if os(macOS)
|
|
if xcode {
|
|
return try await XcodePacker(plan: plan).createProject()
|
|
}
|
|
#endif
|
|
|
|
let packer = Packer(
|
|
buildSettings: buildSettings,
|
|
plan: plan
|
|
)
|
|
let bundle = try await packer.pack()
|
|
|
|
let productsWithEntitlements = plan
|
|
.allProducts
|
|
.compactMap { p in p.entitlementsPath.map { (p, $0) } }
|
|
if !productsWithEntitlements.isEmpty {
|
|
let mapping = try await withThrowingTaskGroup(of: (URL, Entitlements).self) { group in
|
|
for (product, path) in productsWithEntitlements {
|
|
group.addTask {
|
|
let data = try await Data(reading: URL(fileURLWithPath: path))
|
|
let decoder = PropertyListDecoder()
|
|
let entitlements = try decoder.decode(Entitlements.self, from: data)
|
|
return (product.directory(inApp: bundle), entitlements)
|
|
}
|
|
}
|
|
return try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
|
|
}
|
|
print("Applying entitlements...")
|
|
try await Signer.first().sign(
|
|
app: bundle,
|
|
identity: .adhoc,
|
|
entitlementMapping: mapping,
|
|
progress: { _ in }
|
|
)
|
|
}
|
|
|
|
return bundle
|
|
}
|
|
}
|
|
|
|
struct DevXcodeCommand: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "generate-xcode-project",
|
|
abstract: "Generate Xcode project",
|
|
discussion: "This option does nothing on Linux"
|
|
)
|
|
|
|
func run() async throws {
|
|
try await PackOperation(xcode: true).run()
|
|
}
|
|
}
|
|
|
|
struct DevBuildCommand: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "build",
|
|
abstract: "Build app with SwiftPM",
|
|
discussion: """
|
|
This command builds the SwiftPM-based iOS app in the current directory
|
|
"""
|
|
)
|
|
|
|
@OptionGroup var packOptions: PackOperation.BuildOptions
|
|
|
|
@Flag(
|
|
name: .shortAndLong,
|
|
help: "Codesign the built app",
|
|
) var sign = false
|
|
|
|
@Flag(
|
|
name: .shortAndLong,
|
|
help: "Output a .ipa file instead of a .app"
|
|
) var ipa = false
|
|
|
|
@Option(
|
|
help: ArgumentHelp(
|
|
"Custom target triple to build for",
|
|
discussion: "Defaults to '\(PackOperation.defaultTriple)'"
|
|
)
|
|
) var triple: String?
|
|
|
|
func run() async throws {
|
|
let signingAuthToken: AuthToken?
|
|
if sign {
|
|
guard let token = try AuthToken.savedIfPresent() else {
|
|
throw Console.Error("`build --sign` requires logging in with `xtool auth`")
|
|
}
|
|
signingAuthToken = token
|
|
} else {
|
|
signingAuthToken = nil
|
|
}
|
|
|
|
let url = try await PackOperation(
|
|
triple: triple,
|
|
buildOptions: packOptions
|
|
).run()
|
|
|
|
if let signingAuthToken {
|
|
let installDelegate = XToolInstallerDelegate()
|
|
let installer = IntegratedInstaller(
|
|
auth: signingAuthToken.authData(),
|
|
delegate: installDelegate
|
|
)
|
|
do {
|
|
defer { print() }
|
|
try await installer.signInPlace(app: url)
|
|
} catch let error as CancellationError {
|
|
throw error
|
|
} catch {
|
|
print("Error: \(error)")
|
|
throw ExitCode.failure
|
|
}
|
|
}
|
|
|
|
let finalURL: URL
|
|
if ipa {
|
|
@Dependency(\.zipCompressor) var compressor
|
|
finalURL = url.deletingPathExtension().appendingPathExtension("ipa")
|
|
let tmpDir = try TemporaryDirectory(name: "Payload")
|
|
let payloadDir = tmpDir.url
|
|
try FileManager.default.moveItem(at: url, to: payloadDir.appendingPathComponent(url.lastPathComponent))
|
|
let ipaURL = try await compressor.compress(directory: payloadDir) { progress in
|
|
if let progress {
|
|
let percent = Int(progress * 100)
|
|
print("\rPackaging... \(percent)%", terminator: "")
|
|
} else {
|
|
print("\rPackaging...", terminator: "")
|
|
}
|
|
}
|
|
print()
|
|
try? FileManager.default.removeItem(at: finalURL)
|
|
try FileManager.default.moveItem(at: ipaURL, to: finalURL)
|
|
} else {
|
|
finalURL = url
|
|
}
|
|
|
|
print("Wrote to \(finalURL.path)")
|
|
}
|
|
}
|
|
|
|
struct DevRunCommand: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "run",
|
|
abstract: "Build and run app with SwiftPM",
|
|
discussion: """
|
|
This command deploys the SwiftPM-based iOS app in the current directory
|
|
"""
|
|
)
|
|
|
|
@OptionGroup var packOptions: PackOperation.BuildOptions
|
|
|
|
#if os(macOS)
|
|
@Flag(
|
|
name: .shortAndLong,
|
|
help: "Target the iOS Simulator"
|
|
) var simulator = false
|
|
|
|
var triple: String? {
|
|
if simulator {
|
|
#if arch(arm64)
|
|
return "arm64-apple-ios-simulator"
|
|
#elseif arch(x86_64)
|
|
return "x86_64-apple-ios-simulator"
|
|
#else
|
|
#error("Unsupported architecture")
|
|
#endif
|
|
}
|
|
return nil
|
|
}
|
|
#else
|
|
var triple: String? { nil }
|
|
#endif
|
|
|
|
@OptionGroup var connectionOptions: ConnectionOptions
|
|
|
|
func run() async throws {
|
|
let output = try await PackOperation(triple: triple, buildOptions: packOptions).run()
|
|
|
|
#if os(macOS)
|
|
if simulator {
|
|
try await SimInstallOperation(path: output).run()
|
|
return
|
|
}
|
|
#endif
|
|
|
|
let token = try AuthToken.saved()
|
|
|
|
let client = try await connectionOptions.client()
|
|
print("Installing to device: \(client.deviceName) (udid: \(client.udid))")
|
|
|
|
let installDelegate = XToolInstallerDelegate()
|
|
let installer = IntegratedInstaller(
|
|
auth: token.authData(),
|
|
delegate: installDelegate
|
|
)
|
|
|
|
defer { print() }
|
|
|
|
do {
|
|
try await installer.install(
|
|
app: output,
|
|
udid: client.udid,
|
|
lookupMode: .only(client.connectionType),
|
|
configureDevice: false,
|
|
)
|
|
} catch let error as CancellationError {
|
|
throw error
|
|
} catch {
|
|
print("\nError: \(error)")
|
|
throw ExitCode.failure
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DevCommand: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "dev",
|
|
abstract: "Build and run an xtool SwiftPM project",
|
|
subcommands: [
|
|
DevXcodeCommand.self,
|
|
DevBuildCommand.self,
|
|
DevRunCommand.self,
|
|
],
|
|
defaultSubcommand: DevRunCommand.self
|
|
)
|
|
}
|
|
|
|
extension BuildConfiguration: ExpressibleByArgument {}
|
|
|
|
#if os(macOS)
|
|
import Subprocess
|
|
|
|
struct SimInstallOperation {
|
|
var path: URL
|
|
|
|
// TODO: allow customizing this
|
|
var simulator = "booted"
|
|
|
|
func run() async throws {
|
|
try await Subprocess.run(
|
|
.path("/usr/bin/xcrun"),
|
|
arguments: ["simctl", "install", simulator, path.path],
|
|
output: .discarded
|
|
)
|
|
.checkSuccess()
|
|
print("Installed to simulator")
|
|
}
|
|
}
|
|
#endif
|