Skip to content

Commit

Permalink
Build x86 binary (#716)
Browse files Browse the repository at this point in the history
* 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 4c947c1.

* Reenable create test
  • Loading branch information
fkorotkov authored Jan 26, 2024
1 parent cd6a97f commit 18d462d
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 270 deletions.
14 changes: 9 additions & 5 deletions .cirrus.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use_compute_credits: true

env:
XCODE_TAG: 15
XCODE_TAG: 15.2

task:
name: Test on Sonoma
Expand Down Expand Up @@ -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')
Expand Down
24 changes: 10 additions & 14 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
42 changes: 29 additions & 13 deletions Sources/tart/Commands/Create.swift
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
140 changes: 80 additions & 60 deletions Sources/tart/Commands/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand All @@ -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("""
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 18d462d

Please sign in to comment.