Compare commits

..
3 Commits
Author SHA1 Message Date
Nikolay Edigaryev 85dfa961c9 tart run: introduce --disk option (#156)
* tart run: introduce --disk option

* Document how to create disks using Disk Utility
2022-07-26 09:25:23 -04:00
Nikolay Edigaryev 3a74fc35e6 Introduce TART_HOME (#157) 2022-07-25 17:01:46 +03:00
Nikolay Edigaryev 7bf7f890c4 tart run: return exit status 2 when VM is already running (#155) 2022-07-21 07:53:47 -04:00
5 changed files with 74 additions and 19 deletions
+37 -4
View File
@@ -36,6 +36,14 @@ struct Run: AsyncParsableCommand {
@Flag var withSoftnet: Bool = false
@Option(help: ArgumentHelp("""
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"disk.bin:ro\")
""", discussion: """
Learn how to create a disk image using Disk Utility here:
https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
"""))
var disk: [String] = []
func validate() throws {
if vnc && vncExperimental {
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
@@ -43,9 +51,13 @@ struct Run: AsyncParsableCommand {
}
@MainActor
func run() async throws {
func run() async throws {
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir, withSoftnet: withSoftnet)
vm = try VM(
vmDir: vmDir,
withSoftnet: withSoftnet,
additionalDiskAttachments: additionalDiskAttachments()
)
let vncImpl: VNC? = try {
if vnc {
@@ -81,10 +93,10 @@ struct Run: AsyncParsableCommand {
} catch {
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
print("Virtual machine \"\(name)\" is already running!")
} else {
print(error)
Foundation.exit(2)
}
print(error)
Foundation.exit(1)
}
}
@@ -102,6 +114,27 @@ struct Run: AsyncParsableCommand {
}
}
func additionalDiskAttachments() throws -> [VZDiskImageStorageDeviceAttachment] {
var result: [VZDiskImageStorageDeviceAttachment] = []
let readOnlySuffix = ":ro"
for rawDisk in disk {
if rawDisk.hasSuffix(readOnlySuffix) {
result.append(try VZDiskImageStorageDeviceAttachment(
url: URL(fileURLWithPath: String(rawDisk.prefix(rawDisk.count - readOnlySuffix.count))),
readOnly: true
))
} else {
result.append(try VZDiskImageStorageDeviceAttachment(
url: URL(fileURLWithPath: rawDisk),
readOnly: false
))
}
}
return result
}
private func runUI() {
let nsApp = NSApplication.shared
nsApp.setActivationPolicy(.regular)
+16 -4
View File
@@ -1,11 +1,23 @@
import Foundation
struct Config {
public static let tartHomeDir: URL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)
let tartHomeDir: URL
let tartCacheDir: URL
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
init() {
var tartHomeDir: URL
if let customTartHome = ProcessInfo.processInfo.environment["TART_HOME"] {
tartHomeDir = URL(fileURLWithPath: customTartHome)
} else {
tartHomeDir = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)
}
self.tartHomeDir = tartHomeDir
tartCacheDir = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
}
static func jsonEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
+19 -9
View File
@@ -25,7 +25,10 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
var softnet: Softnet? = nil
init(vmDir: VMDirectory, withSoftnet: Bool = false) throws {
init(vmDir: VMDirectory,
withSoftnet: Bool = false,
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
name = vmDir.name
@@ -37,7 +40,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
softnet: softnet)
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
@@ -53,7 +56,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
let ipswCacheFolder = Config.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
let ipswCacheFolder = Config().tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
@@ -94,7 +97,13 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
}
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16, withSoftnet: Bool = false) async throws {
init(
vmDir: VMDirectory,
ipswURL: URL?,
diskSizeGB: UInt16,
withSoftnet: Bool = false,
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
) async throws {
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
// Load the restore image and try to get the requirements
@@ -132,7 +141,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config,
softnet: softnet)
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
@@ -183,7 +192,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
diskURL: URL,
auxStorage: VZMacAuxiliaryStorage,
vmConfig: VMConfig,
softnet: Softnet? = nil
softnet: Softnet? = nil,
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment]
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
@@ -247,9 +257,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
configuration.networkDevices = [vio]
// Storage
let attachment = try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
let storage = VZVirtioBlockDeviceConfiguration(attachment: attachment)
configuration.storageDevices = [storage]
var attachments = [try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)]
attachments.append(contentsOf: additionalDiskAttachments)
configuration.storageDevices = attachments.map { VZVirtioBlockDeviceConfiguration(attachment: $0) }
// Entropy
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
+1 -1
View File
@@ -1,7 +1,7 @@
import Foundation
class VMStorageLocal {
let baseURL: URL = Config.tartHomeDir.appendingPathComponent("vms", isDirectory: true)
let baseURL: URL = Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true)
private func vmURL(_ name: String) -> URL {
baseURL.appendingPathComponent(name, isDirectory: true)
+1 -1
View File
@@ -1,7 +1,7 @@
import Foundation
class VMStorageOCI {
let baseURL = Config.tartCacheDir.appendingPathComponent("OCIs", isDirectory: true)
let baseURL = Config().tartCacheDir.appendingPathComponent("OCIs", isDirectory: true)
private func vmURL(_ name: RemoteName) -> URL {
baseURL.appendingRemoteName(name)