Nucleic: Gitea Runner macOS VM Support

This commit is contained in:
2026-08-07 02:30:26 -07:00
parent d64b3f3e9b
commit b2f15883d8
9 changed files with 403 additions and 47 deletions
+77
View File
@@ -108,3 +108,80 @@ public enum PathResolution {
#endif
}
}
/// Cheap sanity checks on a `.ipsw` before Virtualization.framework sees it.
///
/// Lives beside ``PathResolution`` because it is the other half of the same
/// job — what the CLI does with a path an operator typed — and because
/// `RunnerCore` is the only target that unit-tests on both platforms.
///
/// The checks earn their place: `VZMacOSRestoreImage.image(from:)` reports no
/// progress at all while it works, and on a partial download it can sit for a
/// very long time rather than failing. Without these, "I pointed it at the
/// wrong file" and "my 21 GB download stopped at 4 GB" both present to the
/// operator as an unexplained hang.
public enum IPSWFile {
/// The floor a real macOS restore image clears by an order of magnitude —
/// they run ~15-22 GB. Anything under this is a truncated download, a
/// placeholder, or the wrong file entirely.
public static let minimumBytes: Int64 = 1_000_000_000
/// The first two bytes of every `.ipsw`: an IPSW is a zip archive.
static let zipMagic = Data([0x50, 0x4B]) // "PK"
/// Fails fast if `path` cannot be a usable restore image.
///
/// - Parameters:
/// - path: An already-resolved absolute path (see ``PathResolution``).
/// - label: What the file is, for error messages.
/// - Throws: ``CoreError/notFound(_:)`` if it is not there or unreadable,
/// ``CoreError/configInvalid(_:)`` if it is there but cannot be an IPSW.
public static func validate(path: String, label: String = "restore image") throws {
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else {
throw CoreError.notFound("\(label) not found at \(path)")
}
guard !isDirectory.boolValue else {
throw CoreError.configInvalid(
"\(label) at \(path) is a directory, not an .ipsw file"
)
}
let attributes = try? FileManager.default.attributesOfItem(atPath: path)
let size = (attributes?[.size] as? NSNumber)?.int64Value ?? 0
guard size >= minimumBytes else {
throw CoreError.configInvalid(
"\(label) at \(path) is only \(describeSize(size)) — a macOS restore image is "
+ "~15-22 GB. The download is most likely incomplete; delete the file and "
+ "fetch it again."
)
}
guard let handle = FileHandle(forReadingAtPath: path) else {
throw CoreError.notFound("\(label) at \(path) could not be opened for reading")
}
defer { try? handle.close() }
let magic = (try? handle.read(upToCount: zipMagic.count)) ?? Data()
guard magic == zipMagic else {
let found = magic.map { String(format: "%02x", $0) }.joined()
throw CoreError.configInvalid(
"\(label) at \(path) does not look like an .ipsw: expected a zip archive "
+ "(magic \"PK\", 504b) but the file starts with \(found.isEmpty ? "nothing" : found). "
+ "Check the path, or re-download the file."
)
}
}
/// A byte count an operator can compare against "~15 GB" at a glance.
static func describeSize(_ bytes: Int64) -> String {
let units = ["B", "KB", "MB", "GB", "TB"]
var value = Double(bytes)
var unit = 0
while value >= 1024, unit < units.count - 1 {
value /= 1024
unit += 1
}
return unit == 0 ? "\(Int(value)) B" : String(format: "%.1f %@", value, units[unit])
}
}