From a5572bd3da271761dfe0f9b69e4ab33be8df26d3 Mon Sep 17 00:00:00 2001 From: fedor Date: Mon, 22 Jan 2024 14:29:39 -0500 Subject: [PATCH] Build x86 binary To support Linux VMs on Intel aka x86_64 --- .cirrus.yml | 14 +++++--- .goreleaser.yml | 25 +++++++------- Sources/tart/Commands/Create.swift | 20 ++++++++++-- Sources/tart/Commands/Run.swift | 52 +++++++++++++++++++++--------- Sources/tart/Platform/Darwin.swift | 4 +++ Sources/tart/VM.swift | 18 ++++------- Sources/tart/VMConfig.swift | 7 ++++ gon.hcl | 5 ++- 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index e38bff2..63156e3 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/tart binary_artifacts: - path: .build/debug/tart + path: .build/$BUILD_ARCH-apple-macosx/tart task: only_if: $CIRRUS_TAG == '' && ($CIRRUS_USER_PERMISSION == 'write' || $CIRRUS_USER_PERMISSION == 'admin') diff --git a/.goreleaser.yml b/.goreleaser.yml index c04e735..b9e2f1b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,10 +3,9 @@ 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 @@ -16,10 +15,18 @@ builds: - arm64 binary: tart.app/Contents/MacOS/tart prebuilt: - path: tart.app/Contents/MacOS/tart + path: .build/arm64-apple-macosx/release/tart + - builder: prebuilt + goos: + - darwin + goarch: + - amd64 + binary: tart.app/Contents/MacOS/tart + prebuilt: + path: .build/x86_64-apple-macosx/release/tart archives: - - name_template: "{{ .ProjectName }}" + - name_template: "{{ .ProjectName }}-{{ .Arch }}" files: - src: Resources/embedded.provisionprofile dst: tart.app/Contents @@ -37,7 +44,7 @@ brews: 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 +53,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 a85de3e..cd27458 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,11 +38,20 @@ struct Create: AsyncParsableCommand { try tmpVMDirLock.lock() try await withTaskCancellationHandler(operation: { + #if arch(arm64) if let fromIPSW = fromIPSW { let ipswURL: URL if fromIPSW == "latest" { - ipswURL = try await VM.latestIPSWURL() + 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 { @@ -45,6 +60,7 @@ struct Create: AsyncParsableCommand { _ = 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 5969c34..9c0ab40 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 + #if arch(arm64) @Flag(help: "Boot into recovery mode") + #endif var recovery: Bool = false + #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 + #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,6 +72,7 @@ struct Run: AsyncParsableCommand { """, valueName: "path[:ro]")) var disk: [String] = [] + #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: """ @@ -78,6 +85,7 @@ struct Run: AsyncParsableCommand { """, 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 + #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 + #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,6 +248,7 @@ struct Run: AsyncParsableCommand { do { var resume = false + #if arch(arm64) if #available(macOS 14, *) { if FileManager.default.fileExists(atPath: vmDir.stateURL.path) { print("restoring VM state from a snapshot...") @@ -245,6 +258,7 @@ struct Run: AsyncParsableCommand { print("resuming VM...") } } + #endif try await vm!.start(recovery: recovery, resume: resume) @@ -290,6 +304,7 @@ struct Run: AsyncParsableCommand { sigusr1Src.setEventHandler { Task { do { + #if arch(arm64) if #available(macOS 14, *) { try vm!.configuration.validateSaveRestoreSupport() @@ -307,6 +322,7 @@ struct Run: AsyncParsableCommand { 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 de27f86..6f2fa85 100644 --- a/Sources/tart/Platform/Darwin.swift +++ b/Sources/tart/Platform/Darwin.swift @@ -6,6 +6,8 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible { } } +#if arch(arm64) + struct Darwin: PlatformSuspendable { var ecid: VZMacMachineIdentifier var hardwareModel: VZMacHardwareModel @@ -130,3 +132,5 @@ struct Darwin: PlatformSuspendable { } } } + +#endif diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 149bb0c..52d7499 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,6 +125,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } } + #if arch(arm64) init( vmDir: VMDirectory, ipswURL: URL, @@ -213,6 +202,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } } } + #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 { + #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 080e7fc..b0d192c 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: + #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 1e32b1f..f7cbd36 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 {