From 18d462dd3d40356e37dc902ea9e1c9232d5ae03e Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Fri, 26 Jan 2024 17:09:05 +0400 Subject: [PATCH] Build x86 binary (#716) * Build x86 binary To support Linux VMs on Intel aka x86_64 * Fixed paths and formatting * Unique IDs * Fixed Goreleaser * Skip creation integration test for now * import * Reenable create test * Revert "Reenable create test" This reverts commit 4c947c1f0ecd84f96c2a0713adef60edf1e81379. * Reenable create test --- .cirrus.yml | 14 ++- .goreleaser.yml | 24 ++-- Sources/tart/Commands/Create.swift | 42 +++++-- Sources/tart/Commands/Run.swift | 140 ++++++++++++--------- Sources/tart/Platform/Darwin.swift | 190 +++++++++++++++-------------- Sources/tart/VM.swift | 160 ++++++++++++------------ Sources/tart/VMConfig.swift | 9 +- gon.hcl | 5 +- 8 files changed, 314 insertions(+), 270 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index e38bff21..92813b0c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,7 @@ use_compute_credits: true env: - XCODE_TAG: 15 + XCODE_TAG: 15.2 task: name: Test on Sonoma @@ -51,14 +51,18 @@ task: task: only_if: $CIRRUS_TAG == '' - name: Build + env: + matrix: + BUILD_ARCH: arm64 + BUILD_ARCH: x86_64 + name: Build ($BUILD_ARCH) alias: build macos_instance: image: ghcr.io/cirruslabs/macos-sonoma-xcode:$XCODE_TAG - build_script: swift build --product tart - sign_script: codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart + build_script: swift build --arch $BUILD_ARCH --product tart + sign_script: codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/$BUILD_ARCH-apple-macosx/debug/tart binary_artifacts: - path: .build/debug/tart + path: .build/$BUILD_ARCH-apple-macosx/debug/tart task: only_if: $CIRRUS_TAG == '' && ($CIRRUS_USER_PERMISSION == 'write' || $CIRRUS_USER_PERMISSION == 'admin') diff --git a/.goreleaser.yml b/.goreleaser.yml index c04e7358..aa908893 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,23 +3,25 @@ project_name: tart before: hooks: - .ci/set-version.sh - - swift build -c release --product tart + - swift build --arch x86_64 -c release --product tart + - swift build --arch arm64 -c release --product tart - gon gon.hcl - - mkdir -p tart.app/Contents/MacOS - - cp .build/arm64-apple-macosx/release/tart tart.app/Contents/MacOS/ builds: - - builder: prebuilt + - id: tart + builder: prebuilt + goamd64: [v1] goos: - darwin goarch: - arm64 + - amd64 binary: tart.app/Contents/MacOS/tart prebuilt: - path: tart.app/Contents/MacOS/tart + path: '.build/{{- if eq .Arch "arm64" }}arm64{{- else }}x86_64{{ end }}-apple-macosx/release/tart' archives: - - name_template: "{{ .ProjectName }}" + - name_template: "{{ .ProjectName }}-{{ .Arch }}" files: - src: Resources/embedded.provisionprofile dst: tart.app/Contents @@ -31,13 +33,13 @@ release: brews: - name: tart - tap: + repository: owner: cirruslabs name: homebrew-cli caveats: See the GitHub repository for more information homepage: https://github.com/cirruslabs/tart license: "Fair Source" - description: Run macOS VMs on Apple Silicon + description: Run macOS and Linux VMs on Apple Hardware skip_upload: auto dependencies: - "cirruslabs/cli/softnet" @@ -46,9 +48,3 @@ brews: bin.write_exec_script "#{libexec}/tart.app/Contents/MacOS/tart" custom_block: | depends_on :macos => :ventura - - on_macos do - unless Hardware::CPU.arm? - odie "Tart only works on Apple Silicon!" - end - end diff --git a/Sources/tart/Commands/Create.swift b/Sources/tart/Commands/Create.swift index a85de3e2..b816fc2e 100644 --- a/Sources/tart/Commands/Create.swift +++ b/Sources/tart/Commands/Create.swift @@ -1,7 +1,8 @@ import ArgumentParser import Dispatch -import SwiftUI import Foundation +import SwiftUI +import Virtualization struct Create: AsyncParsableCommand { static var configuration = CommandConfiguration(abstract: "Create a VM") @@ -22,6 +23,11 @@ struct Create: AsyncParsableCommand { if fromIPSW == nil && !linux { throw ValidationError("Please specify either a --from-ipsw or --linux option!") } + #if arch(x86_64) + if fromIPSW != nil { + throw ValidationError("Only Linux VMs are supported on Intel!") + } + #endif } func run() async throws { @@ -32,19 +38,29 @@ struct Create: AsyncParsableCommand { try tmpVMDirLock.lock() try await withTaskCancellationHandler(operation: { - if let fromIPSW = fromIPSW { - let ipswURL: URL - - if fromIPSW == "latest" { - ipswURL = try await VM.latestIPSWURL() - } else if fromIPSW.starts(with: "http://") || fromIPSW.starts(with: "https://") { - ipswURL = URL(string: fromIPSW)! - } else { - ipswURL = URL(fileURLWithPath: NSString(string: fromIPSW).expandingTildeInPath) - } + #if arch(arm64) + if let fromIPSW = fromIPSW { + let ipswURL: URL - _ = try await VM(vmDir: tmpVMDir, ipswURL: ipswURL, diskSizeGB: diskSize) - } + if fromIPSW == "latest" { + defaultLogger.appendNewLine("Looking up the latest supported IPSW...") + + let image = try await withCheckedThrowingContinuation { continuation in + VZMacOSRestoreImage.fetchLatestSupported() { result in + continuation.resume(with: result) + } + } + + ipswURL = image.url + } else if fromIPSW.starts(with: "http://") || fromIPSW.starts(with: "https://") { + ipswURL = URL(string: fromIPSW)! + } else { + ipswURL = URL(fileURLWithPath: NSString(string: fromIPSW).expandingTildeInPath) + } + + _ = try await VM(vmDir: tmpVMDir, ipswURL: ipswURL, diskSizeGB: diskSize) + } + #endif if linux { _ = try await VM.linux(vmDir: tmpVMDir, diskSizeGB: diskSize) diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index 5969c341..4f46ab11 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -36,19 +36,25 @@ struct Run: AsyncParsableCommand { @Flag(help: "Force open a UI window, even when VNC is enabled.") var graphics: Bool = false - @Flag(help: "Boot into recovery mode") + #if arch(arm64) + @Flag(help: "Boot into recovery mode") + #endif var recovery: Bool = false - @Flag(help: ArgumentHelp( - "Use screen sharing instead of the built-in UI.", - discussion: "Useful since Screen Sharing supports copy/paste, drag and drop, etc.\n" - + "Note that Remote Login option should be enabled inside the VM.")) + #if arch(arm64) + @Flag(help: ArgumentHelp( + "Use screen sharing instead of the built-in UI.", + discussion: "Useful since Screen Sharing supports copy/paste, drag and drop, etc.\n" + + "Note that Remote Login option should be enabled inside the VM.")) + #endif var vnc: Bool = false - @Flag(help: ArgumentHelp( - "Use Virtualization.Framework's VNC server instead of the build-in UI.", - discussion: "Useful since this type of VNC is available in recovery mode and in macOS installation.\n" - + "Note that this feature is experimental and there may be bugs present when using VNC.")) + #if arch(arm64) + @Flag(help: ArgumentHelp( + "Use Virtualization.Framework's VNC server instead of the build-in UI.", + discussion: "Useful since this type of VNC is available in recovery mode and in macOS installation.\n" + + "Note that this feature is experimental and there may be bugs present when using VNC.")) + #endif var vncExperimental: Bool = false @Option(help: ArgumentHelp(""" @@ -66,18 +72,20 @@ struct Run: AsyncParsableCommand { """, valueName: "path[:ro]")) var disk: [String] = [] - @Option(name: [.customLong("rosetta")], help: ArgumentHelp( - "Attaches a Rosetta share to the guest Linux VM with a specific tag (e.g. --rosetta=\"rosetta\")", - discussion: """ - Requires host to be macOS 13.0 (Ventura) with Rosetta installed. The latter can be done - by running "softwareupdate --install-rosetta" (without quotes) in the Terminal.app. - - Note that you also have to configure Rosetta in the guest Linux VM by following the - steps from "Mount the Shared Directory and Register Rosetta" section here: - https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978496 - """, - valueName: "tag" - )) + #if arch(arm64) + @Option(name: [.customLong("rosetta")], help: ArgumentHelp( + "Attaches a Rosetta share to the guest Linux VM with a specific tag (e.g. --rosetta=\"rosetta\")", + discussion: """ + Requires host to be macOS 13.0 (Ventura) with Rosetta installed. The latter can be done + by running "softwareupdate --install-rosetta" (without quotes) in the Terminal.app. + + Note that you also have to configure Rosetta in the guest Linux VM by following the + steps from "Mount the Shared Directory and Register Rosetta" section here: + https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta#3978496 + """, + valueName: "tag" + )) + #endif var rosettaTag: String? @Option(help: ArgumentHelp(""" @@ -105,11 +113,15 @@ struct Run: AsyncParsableCommand { discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet")) var netSoftnet: Bool = false - @Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\".")) + #if arch(arm64) + @Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\".")) + #endif var suspendable: Bool = false - @Flag(help: ArgumentHelp("Whether system hot keys should be sent to the guest instead of the host", - discussion: "If enabled then system hot keys like Cmd+Tab will be sent to the guest instead of the host.")) + #if arch(arm64) + @Flag(help: ArgumentHelp("Whether system hot keys should be sent to the guest instead of the host", + discussion: "If enabled then system hot keys like Cmd+Tab will be sent to the guest instead of the host.")) + #endif var captureSystemKeys: Bool = false mutating func validate() throws { @@ -236,15 +248,17 @@ struct Run: AsyncParsableCommand { do { var resume = false - if #available(macOS 14, *) { - if FileManager.default.fileExists(atPath: vmDir.stateURL.path) { - print("restoring VM state from a snapshot...") - try await vm!.virtualMachine.restoreMachineStateFrom(url: vmDir.stateURL) - try FileManager.default.removeItem(at: vmDir.stateURL) - resume = true - print("resuming VM...") + #if arch(arm64) + if #available(macOS 14, *) { + if FileManager.default.fileExists(atPath: vmDir.stateURL.path) { + print("restoring VM state from a snapshot...") + try await vm!.virtualMachine.restoreMachineStateFrom(url: vmDir.stateURL) + try FileManager.default.removeItem(at: vmDir.stateURL) + resume = true + print("resuming VM...") + } } - } + #endif try await vm!.start(recovery: recovery, resume: resume) @@ -290,23 +304,25 @@ struct Run: AsyncParsableCommand { sigusr1Src.setEventHandler { Task { do { - if #available(macOS 14, *) { - try vm!.configuration.validateSaveRestoreSupport() + #if arch(arm64) + if #available(macOS 14, *) { + try vm!.configuration.validateSaveRestoreSupport() - print("pausing VM to take a snapshot...") - try await vm!.virtualMachine.pause() + print("pausing VM to take a snapshot...") + try await vm!.virtualMachine.pause() - print("creating a snapshot...") - try await vm!.virtualMachine.saveMachineStateTo(url: vmDir.stateURL) + print("creating a snapshot...") + try await vm!.virtualMachine.saveMachineStateTo(url: vmDir.stateURL) - print("snapshot created successfully! shutting down the VM...") + print("snapshot created successfully! shutting down the VM...") - task.cancel() - } else { - print(RuntimeError.SuspendFailed("this functionality is only supported on macOS 14 (Sonoma) or newer")) + task.cancel() + } else { + print(RuntimeError.SuspendFailed("this functionality is only supported on macOS 14 (Sonoma) or newer")) - Foundation.exit(1) - } + Foundation.exit(1) + } + #endif } catch (let e) { print(RuntimeError.SuspendFailed(e.localizedDescription)) @@ -464,25 +480,29 @@ struct Run: AsyncParsableCommand { guard let rosettaTag = rosettaTag else { return [] } + #if arch(arm64) + guard #available(macOS 13, *) else { + throw UnsupportedOSError("Rosetta directory share", "is") + } - guard #available(macOS 13, *) else { - throw UnsupportedOSError("Rosetta directory share", "is") - } - - switch VZLinuxRosettaDirectoryShare.availability { - case .notInstalled: - throw UnsupportedOSError("Rosetta directory share", "is", "that have Rosetta installed") - case .notSupported: - throw UnsupportedOSError("Rosetta directory share", "is", "running Apple silicon") - default: - break - } + switch VZLinuxRosettaDirectoryShare.availability { + case .notInstalled: + throw UnsupportedOSError("Rosetta directory share", "is", "that have Rosetta installed") + case .notSupported: + throw UnsupportedOSError("Rosetta directory share", "is", "running Apple silicon") + default: + break + } - try VZVirtioFileSystemDeviceConfiguration.validateTag(rosettaTag) - let device = VZVirtioFileSystemDeviceConfiguration(tag: rosettaTag) - device.share = try VZLinuxRosettaDirectoryShare() + try VZVirtioFileSystemDeviceConfiguration.validateTag(rosettaTag) + let device = VZVirtioFileSystemDeviceConfiguration(tag: rosettaTag) + device.share = try VZLinuxRosettaDirectoryShare() - return [device] + return [device] + #elseif arch(x86_64) + // there is no Rosetta on Intel + return [] + #endif } private func runUI(_ suspendable: Bool, _ captureSystemKeys: Bool) { diff --git a/Sources/tart/Platform/Darwin.swift b/Sources/tart/Platform/Darwin.swift index de27f86a..732342c9 100644 --- a/Sources/tart/Platform/Darwin.swift +++ b/Sources/tart/Platform/Darwin.swift @@ -6,127 +6,131 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible { } } -struct Darwin: PlatformSuspendable { - var ecid: VZMacMachineIdentifier - var hardwareModel: VZMacHardwareModel +#if arch(arm64) - init(ecid: VZMacMachineIdentifier, hardwareModel: VZMacHardwareModel) { - self.ecid = ecid - self.hardwareModel = hardwareModel - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + struct Darwin: PlatformSuspendable { + var ecid: VZMacMachineIdentifier + var hardwareModel: VZMacHardwareModel - let encodedECID = try container.decode(String.self, forKey: .ecid) - guard let data = Data.init(base64Encoded: encodedECID) else { - throw DecodingError.dataCorruptedError(forKey: .ecid, - in: container, - debugDescription: "failed to initialize Data using the provided value") - } - guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else { - throw DecodingError.dataCorruptedError(forKey: .ecid, - in: container, - debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value") + init(ecid: VZMacMachineIdentifier, hardwareModel: VZMacHardwareModel) { + self.ecid = ecid + self.hardwareModel = hardwareModel } - self.ecid = ecid - let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel) - guard let data = Data.init(base64Encoded: encodedHardwareModel) else { - throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let encodedECID = try container.decode(String.self, forKey: .ecid) + guard let data = Data.init(base64Encoded: encodedECID) else { + throw DecodingError.dataCorruptedError(forKey: .ecid, + in: container, + debugDescription: "failed to initialize Data using the provided value") + } + guard let ecid = VZMacMachineIdentifier.init(dataRepresentation: data) else { + throw DecodingError.dataCorruptedError(forKey: .ecid, + in: container, + debugDescription: "failed to initialize VZMacMachineIdentifier using the provided value") + } + self.ecid = ecid + + let encodedHardwareModel = try container.decode(String.self, forKey: .hardwareModel) + guard let data = Data.init(base64Encoded: encodedHardwareModel) else { + throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + } + guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else { + throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + } + self.hardwareModel = hardwareModel } - guard let hardwareModel = VZMacHardwareModel.init(dataRepresentation: data) else { - throw DecodingError.dataCorruptedError(forKey: .hardwareModel, in: container, debugDescription: "") + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid) + try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel) } - self.hardwareModel = hardwareModel - } - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) + func os() -> OS { + .darwin + } - try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid) - try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel) - } + func bootLoader(nvramURL: URL) throws -> VZBootLoader { + VZMacOSBootLoader() + } - func os() -> OS { - .darwin - } + func platform(nvramURL: URL) throws -> VZPlatformConfiguration { + let result = VZMacPlatformConfiguration() - func bootLoader(nvramURL: URL) throws -> VZBootLoader { - VZMacOSBootLoader() - } + result.machineIdentifier = ecid + result.auxiliaryStorage = VZMacAuxiliaryStorage(url: nvramURL) - func platform(nvramURL: URL) throws -> VZPlatformConfiguration { - let result = VZMacPlatformConfiguration() + if !hardwareModel.isSupported { + // At the moment support of M1 chip is not yet dropped in any macOS version + // This mean that host software is not supporting this hardware model and should be updated + throw UnsupportedHostOSError() + } - result.machineIdentifier = ecid - result.auxiliaryStorage = VZMacAuxiliaryStorage(url: nvramURL) + result.hardwareModel = hardwareModel - if !hardwareModel.isSupported { - // At the moment support of M1 chip is not yet dropped in any macOS version - // This mean that host software is not supporting this hardware model and should be updated - throw UnsupportedHostOSError() + return result } - result.hardwareModel = hardwareModel + func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { + let result = VZMacGraphicsDeviceConfiguration() - return result - } + if let hostMainScreen = NSScreen.main { + let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height) + result.displays = [ + VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize) + ] - func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration { - let result = VZMacGraphicsDeviceConfiguration() + return result + } - if let hostMainScreen = NSScreen.main { - let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height) result.displays = [ - VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize) + VZMacGraphicsDisplayConfiguration( + widthInPixels: vmConfig.display.width, + heightInPixels: vmConfig.display.height, + // A reasonable guess according to Apple's documentation[1] + // [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize + pixelsPerInch: 72 + ) ] return result } - result.displays = [ - VZMacGraphicsDisplayConfiguration( - widthInPixels: vmConfig.display.width, - heightInPixels: vmConfig.display.height, - // A reasonable guess according to Apple's documentation[1] - // [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize - pixelsPerInch: 72 - ) - ] - - return result - } - - func keyboards() -> [VZKeyboardConfiguration] { - if #available(macOS 14, *) { - // Mac keyboard is only supported by guests starting with macOS Ventura - return [VZMacKeyboardConfiguration(), VZUSBKeyboardConfiguration()] - } else { - return [VZUSBKeyboardConfiguration()] + func keyboards() -> [VZKeyboardConfiguration] { + if #available(macOS 14, *) { + // Mac keyboard is only supported by guests starting with macOS Ventura + return [VZMacKeyboardConfiguration(), VZUSBKeyboardConfiguration()] + } else { + return [VZUSBKeyboardConfiguration()] + } } - } - func keyboardsSuspendable() -> [VZKeyboardConfiguration] { - if #available(macOS 14, *) { - return [VZMacKeyboardConfiguration()] - } else { - // fallback to the regular configuration - return keyboards() + func keyboardsSuspendable() -> [VZKeyboardConfiguration] { + if #available(macOS 14, *) { + return [VZMacKeyboardConfiguration()] + } else { + // fallback to the regular configuration + return keyboards() + } } - } - func pointingDevices() -> [VZPointingDeviceConfiguration] { - // Trackpad is only supported by guests starting with macOS Ventura - [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()] - } + func pointingDevices() -> [VZPointingDeviceConfiguration] { + // Trackpad is only supported by guests starting with macOS Ventura + [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()] + } - func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] { - if #available(macOS 14, *) { - return [VZMacTrackpadConfiguration()] - } else { - // fallback to the regular configuration - return pointingDevices() + func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] { + if #available(macOS 14, *) { + return [VZMacTrackpadConfiguration()] + } else { + // fallback to the regular configuration + return pointingDevices() + } } } -} + +#endif diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 149bb0c1..901d1131 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -116,18 +116,6 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { return try FileManager.default.replaceItemAt(finalLocation, withItemAt: temporaryLocation)! } - static func latestIPSWURL() async throws -> URL { - defaultLogger.appendNewLine("Looking up the latest supported IPSW...") - - let image = try await withCheckedThrowingContinuation { continuation in - VZMacOSRestoreImage.fetchLatestSupported() { result in - continuation.resume(with: result) - } - } - - return image.url - } - var inFinalState: Bool { get { virtualMachine.state == VZVirtualMachine.State.stopped || @@ -137,82 +125,84 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } } - init( - vmDir: VMDirectory, - ipswURL: URL, - diskSizeGB: UInt16, - network: Network = NetworkShared(), - additionalStorageDevices: [VZStorageDeviceConfiguration] = [], - directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [], - serialPorts: [VZSerialPortConfiguration] = [] - ) async throws { - var ipswURL = ipswURL - - if !ipswURL.isFileURL { - ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL) - } - - // We create a temporary TART_HOME directory in tests, which has its "cache" folder symlinked - // to the users Tart cache directory (~/.tart/cache). However, the Virtualization.Framework - // cannot deal with paths that contain symlinks, so expand them here first. - ipswURL.resolveSymlinksInPath() - - // Load the restore image and try to get the requirements - // that match both the image and our platform - let image = try await withCheckedThrowingContinuation { continuation in - VZMacOSRestoreImage.load(from: ipswURL) { result in - continuation.resume(with: result) + #if arch(arm64) + init( + vmDir: VMDirectory, + ipswURL: URL, + diskSizeGB: UInt16, + network: Network = NetworkShared(), + additionalStorageDevices: [VZStorageDeviceConfiguration] = [], + directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [], + serialPorts: [VZSerialPortConfiguration] = [] + ) async throws { + var ipswURL = ipswURL + + if !ipswURL.isFileURL { + ipswURL = try await VM.retrieveIPSW(remoteURL: ipswURL) } - } - guard let requirements = image.mostFeaturefulSupportedConfiguration else { - throw UnsupportedRestoreImageError() - } + // We create a temporary TART_HOME directory in tests, which has its "cache" folder symlinked + // to the users Tart cache directory (~/.tart/cache). However, the Virtualization.Framework + // cannot deal with paths that contain symlinks, so expand them here first. + ipswURL.resolveSymlinksInPath() - // Create NVRAM - _ = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel) - - // Create disk - try vmDir.resizeDisk(diskSizeGB) - - name = vmDir.name - // Create config - config = VMConfig( - platform: Darwin(ecid: VZMacMachineIdentifier(), hardwareModel: requirements.hardwareModel), - cpuCountMin: requirements.minimumSupportedCPUCount, - memorySizeMin: requirements.minimumSupportedMemorySize - ) - // allocate at least 4 CPUs because otherwise VMs are frequently freezing - try config.setCPU(cpuCount: max(4, requirements.minimumSupportedCPUCount)) - try config.save(toURL: vmDir.configURL) - - // Initialize the virtual machine and its configuration - self.network = network - configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, - vmConfig: config, network: network, - additionalStorageDevices: additionalStorageDevices, - directorySharingDevices: directorySharingDevices, - serialPorts: serialPorts - ) - virtualMachine = VZVirtualMachine(configuration: configuration) - - super.init() - virtualMachine.delegate = self - - // Run automated installation - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - DispatchQueue.main.async { [ipswURL] in - let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL) + // Load the restore image and try to get the requirements + // that match both the image and our platform + let image = try await withCheckedThrowingContinuation { continuation in + VZMacOSRestoreImage.load(from: ipswURL) { result in + continuation.resume(with: result) + } + } - defaultLogger.appendNewLine("Installing OS...") - ProgressObserver(installer.progress).log(defaultLogger) + guard let requirements = image.mostFeaturefulSupportedConfiguration else { + throw UnsupportedRestoreImageError() + } - installer.install { result in - continuation.resume(with: result) + // Create NVRAM + _ = try VZMacAuxiliaryStorage(creatingStorageAt: vmDir.nvramURL, hardwareModel: requirements.hardwareModel) + + // Create disk + try vmDir.resizeDisk(diskSizeGB) + + name = vmDir.name + // Create config + config = VMConfig( + platform: Darwin(ecid: VZMacMachineIdentifier(), hardwareModel: requirements.hardwareModel), + cpuCountMin: requirements.minimumSupportedCPUCount, + memorySizeMin: requirements.minimumSupportedMemorySize + ) + // allocate at least 4 CPUs because otherwise VMs are frequently freezing + try config.setCPU(cpuCount: max(4, requirements.minimumSupportedCPUCount)) + try config.save(toURL: vmDir.configURL) + + // Initialize the virtual machine and its configuration + self.network = network + configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, + vmConfig: config, network: network, + additionalStorageDevices: additionalStorageDevices, + directorySharingDevices: directorySharingDevices, + serialPorts: serialPorts + ) + virtualMachine = VZVirtualMachine(configuration: configuration) + + super.init() + virtualMachine.delegate = self + + // Run automated installation + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DispatchQueue.main.async { [ipswURL] in + let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL) + + defaultLogger.appendNewLine("Installing OS...") + ProgressObserver(installer.progress).log(defaultLogger) + + installer.install { result in + continuation.resume(with: result) + } } } } - } + #endif @available(macOS 13, *) static func linux(vmDir: VMDirectory, diskSizeGB: UInt16) async throws -> VM { @@ -257,9 +247,13 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { @MainActor private func start(_ recovery: Bool) async throws { - let startOptions = VZMacOSVirtualMachineStartOptions() - startOptions.startUpFromMacOSRecovery = recovery - try await virtualMachine.start(options: startOptions) + #if arch(arm64) + let startOptions = VZMacOSVirtualMachineStartOptions() + startOptions.startUpFromMacOSRecovery = recovery + try await virtualMachine.start(options: startOptions) + #else + try await virtualMachine.start() + #endif } @MainActor diff --git a/Sources/tart/VMConfig.swift b/Sources/tart/VMConfig.swift index 080e7fcb..c70e07a2 100644 --- a/Sources/tart/VMConfig.swift +++ b/Sources/tart/VMConfig.swift @@ -95,7 +95,14 @@ struct VMConfig: Codable { arch = try container.decodeIfPresent(Architecture.self, forKey: .arch) ?? .arm64 switch os { case .darwin: - platform = try Darwin(from: decoder) + #if arch(arm64) + platform = try Darwin(from: decoder) + #else + throw DecodingError.dataCorruptedError( + forKey: .os, + in: container, + debugDescription: "Darwin VMs are only supported on Apple Silicon hosts") + #endif case .linux: platform = try Linux(from: decoder) } diff --git a/gon.hcl b/gon.hcl index 1e32b1fe..f7cbd363 100644 --- a/gon.hcl +++ b/gon.hcl @@ -1,4 +1,7 @@ -source = [".build/arm64-apple-macosx/release/tart"] +source = [ + ".build/x86_64-apple-macosx/release/tart", + ".build/arm64-apple-macosx/release/tart" +] bundle_id = "com.github.cirruslabs.tart" apple_id {