From 456ebc1c7b04e1c00fb9e20ab018c5bb520d6ce0 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Mon, 21 Nov 2022 21:56:27 +0400 Subject: [PATCH] Rosetta support (#324) --- Sources/tart/Commands/Run.swift | 74 ++++++++++++++++++++++++++++++--- Sources/tart/VM.swift | 38 ++++++----------- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index 8df0b43..fa7a277 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -48,6 +48,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" + )) + var rosettaTag: String? + @Option(help: ArgumentHelp(""" Additional directory shares with an optional read-only specifier\n(e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\") """, discussion: """ @@ -107,7 +121,7 @@ struct Run: AsyncParsableCommand { vmDir: vmDir, network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(), additionalDiskAttachments: additionalDiskAttachments, - directoryShares: directoryShares() + directorySharingDevices: directoryShares() + rosettaDirectoryShare() ) let vncImpl: VNC? = try { @@ -242,8 +256,22 @@ struct Run: AsyncParsableCommand { return result } - func directoryShares() throws -> [DirectoryShare] { - var result: [DirectoryShare] = [] + func directoryShares() throws -> [VZDirectorySharingDeviceConfiguration] { + if dir.isEmpty { + return [] + } + + guard #available(macOS 13, *) else { + throw UnsupportedOSError("directory sharing", "is") + } + + struct DirectoryShare { + let name: String + let path: URL + let readOnly: Bool + } + + var directoryShares: [DirectoryShare] = [] for rawDir in dir { let splits = rawDir.split(maxSplits: 2) { $0 == ":" } @@ -264,10 +292,46 @@ struct Run: AsyncParsableCommand { let (name, path) = (String(splits[0]), String(splits[1])) - result.append(DirectoryShare(name: name, path: URL(fileURLWithPath: NSString(string: path).expandingTildeInPath), readOnly: readOnly)) + directoryShares.append(DirectoryShare( + name: name, + path: URL(fileURLWithPath: NSString(string: path).expandingTildeInPath), + readOnly: readOnly) + ) } - return result + var directories: [String : VZSharedDirectory] = Dictionary() + directoryShares.forEach { directories[$0.name] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) } + + let automountTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag + let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag) + sharingDevice.share = VZMultipleDirectoryShare(directories: directories) + + return [sharingDevice] + } + + private func rosettaDirectoryShare() throws -> [VZDirectorySharingDeviceConfiguration] { + guard let rosettaTag = rosettaTag else { + return [] + } + + 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 + } + + try VZVirtioFileSystemDeviceConfiguration.validateTag(rosettaTag) + let device = VZVirtioFileSystemDeviceConfiguration(tag: rosettaTag) + device.share = try VZLinuxRosettaDirectoryShare() + + return [device] } private func runUI() { diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index ae7af6a..08300a7 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -14,8 +14,8 @@ struct DownloadFailed: Error { struct UnsupportedOSError: Error, CustomStringConvertible { let description: String - init(_ what: String, _ plural: String) { - description = "error: \(what) \(plural) only supported on hosts running macOS 13.0 (Ventura) or newer" + init(_ what: String, _ plural: String, _ requires: String = "running macOS 13.0 (Ventura) or newer") { + description = "error: \(what) \(plural) only supported on hosts \(requires)" } } @@ -40,7 +40,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { init(vmDir: VMDirectory, network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [], - directoryShares: [DirectoryShare] = [] + directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [] ) throws { name = vmDir.name config = try VMConfig.init(fromURL: vmDir.configURL) @@ -54,7 +54,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, vmConfig: config, network: network, additionalDiskAttachments: additionalDiskAttachments, - directoryShares: directoryShares) + directorySharingDevices: directorySharingDevices + ) virtualMachine = VZVirtualMachine(configuration: configuration) super.init() @@ -130,7 +131,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { ipswURL: URL, diskSizeGB: UInt16, network: Network = NetworkShared(), - additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [] + additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [], + directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [] ) async throws { var ipswURL = ipswURL @@ -177,7 +179,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL, vmConfig: config, network: network, additionalDiskAttachments: additionalDiskAttachments, - directoryShares: []) + directorySharingDevices: directorySharingDevices + ) virtualMachine = VZVirtualMachine(configuration: configuration) super.init() @@ -253,7 +256,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { vmConfig: VMConfig, network: Network = NetworkShared(), additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment], - directoryShares: [DirectoryShare] + directorySharingDevices: [VZDirectorySharingDeviceConfiguration] ) throws -> VZVirtualMachineConfiguration { let configuration = VZVirtualMachineConfiguration() @@ -297,19 +300,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { // Entropy configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()] - // Directory share - if #available(macOS 13, *) { - var directories: [String : VZSharedDirectory] = Dictionary() - directoryShares.forEach { directories[$0.name] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) } - - let automountTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag - let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag) - sharingDevice.share = VZMultipleDirectoryShare(directories: directories) - - configuration.directorySharingDevices = [sharingDevice] - } else if !directoryShares.isEmpty { - throw UnsupportedOSError("directory sharing", "is") - } + // Directory sharing devices + configuration.directorySharingDevices = directorySharingDevices try configuration.validate() @@ -331,9 +323,3 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { sema.signal() } } - -struct DirectoryShare { - let name: String - let path: URL - let readOnly: Bool -}