Nucleic: Gitea Runner macOS VM Support

This commit is contained in:
2026-08-07 01:40:57 -07:00
parent 697a4cdcd0
commit ab4ed9c8a1
4 changed files with 302 additions and 7 deletions
+27 -7
View File
@@ -35,7 +35,13 @@ struct ImageCommand: AsyncParsableCommand {
var name: String = "default"
/// A local `.ipsw`; omit to download the latest supported image.
@Option(name: .long, help: "Path to a local .ipsw (default: download the latest supported).")
///
/// Resolved through ``PathResolution`` so it works whether or not the
/// shell got to the glob first: `--ipsw '~/Downloads/UniversalMac_27*.ipsw'`
/// and the unquoted form both land on the same file.
@Option(
name: .long,
help: "Path to a local .ipsw; may be a glob (default: download the latest supported).")
var ipsw: String?
/// Nominal guest disk size, overriding `guest.diskGB`.
@@ -44,6 +50,12 @@ struct ImageCommand: AsyncParsableCommand {
func run() async throws {
CLI.bootstrapLogging(verbose: options.verbose)
// Before anything else touches the disk: an unresolvable --ipsw is
// an argument error, and an argument error should not first make the
// operator wait on a config load and a free-space check.
let resolvedIPSW = try ipsw.map { try PathResolution.resolve($0, label: "--ipsw") }
var config = try options.loadConfig()
if let diskGB {
config.guest.diskGB = diskGB
@@ -64,7 +76,7 @@ struct ImageCommand: AsyncParsableCommand {
let printer = ProgressPrinter()
let builder = ImageBuilder(store: store)
let imageName = name
let ipswPath = ipsw
let ipswPath = resolvedIPSW
let frozenConfig = config
// `image build` runs `VZMacOSInstaller` and then boots the guest, so
@@ -201,20 +213,28 @@ struct ImageCommand: AsyncParsableCommand {
/// Optional Xcode `.xip` to install into the guest. Adds tens of
/// gigabytes; omitted by default.
@Option(name: .customLong("xcode-xip"), help: "Path to an Xcode .xip to install into the guest.")
@Option(
name: .customLong("xcode-xip"),
help: "Path to an Xcode .xip to install into the guest; may be a glob.")
var xcodeXIP: String?
func run() async throws {
CLI.bootstrapLogging(verbose: options.verbose)
// Same treatment as `image build --ipsw`, and for the same reason:
// this is a long path to a big file that people reach for with a
// glob. Resolved first so a bad one costs nothing.
let resolvedXIP = try xcodeXIP.map { try PathResolution.resolve($0, label: "--xcode-xip") }
if let resolvedXIP, !FileManager.default.fileExists(atPath: resolvedXIP) {
throw ValidationError("no file at \(resolvedXIP)")
}
let config = try options.loadConfig()
let store = VMStore(config: config)
guard try store.image(named: name) != nil else {
throw ValidationError("no image named '\(name)'")
}
if let xcodeXIP, !FileManager.default.fileExists(atPath: RunnerConfig.expandTilde(xcodeXIP)) {
throw ValidationError("no file at \(RunnerConfig.expandTilde(xcodeXIP))")
}
CLI.note("provisioning base image '\(name)' in place — stop the daemon before doing this")
@@ -222,7 +242,7 @@ struct ImageCommand: AsyncParsableCommand {
let builder = ImageBuilder(store: store)
let imageName = name
let frozenConfig = config
let xipPath = xcodeXIP.map(RunnerConfig.expandTilde)
let xipPath = resolvedXIP
// Boots the image to run provision.sh in it, so it needs the run
// loop for exactly the reason `image build` does.