mirror of https://github.com/cirruslabs/tart.git
Refactored UI (#22)
* Simplified crafting of a VM config * Configure display * Propagate VM name to the title
This commit is contained in:
parent
466c3c568a
commit
75b5d63387
|
|
@ -39,8 +39,20 @@ struct Run: AsyncParsableCommand {
|
|||
|
||||
struct MainApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
VMView(vm: vm!)
|
||||
WindowGroup(vm!.name) {
|
||||
Group {
|
||||
VMView(vm: vm!).onAppear {
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
}
|
||||
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
|
||||
}.commands {
|
||||
// Remove some standard menu options
|
||||
CommandGroup(replacing: .help, addition: {})
|
||||
CommandGroup(replacing: .newItem, addition: {})
|
||||
CommandGroup(replacing: .pasteboard, addition: {})
|
||||
CommandGroup(replacing: .textEditing, addition: {})
|
||||
CommandGroup(replacing: .undoRedo, addition: {})
|
||||
CommandGroup(replacing: .windowSize, addition: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +68,9 @@ struct VMView: NSViewRepresentable {
|
|||
@ObservedObject var vm: VM
|
||||
|
||||
func makeNSView(context: Context) -> NSViewType {
|
||||
VZVirtualMachineView()
|
||||
let machineView = VZVirtualMachineView()
|
||||
machineView.capturesSystemKeys = true
|
||||
return machineView
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSViewType, context: Context) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ struct Set: AsyncParsableCommand {
|
|||
@Option(help: "VM memory size in megabytes")
|
||||
var memory: UInt16?
|
||||
|
||||
@Option(help: "VM display settings in a format of <width>x<height>(x<dpi>)?. For example, 1200x800 or 1200x800x72")
|
||||
var display: VMDisplayConfig?
|
||||
|
||||
func run() async throws {
|
||||
do {
|
||||
let vmStorage = VMStorage()
|
||||
|
|
@ -27,6 +30,18 @@ struct Set: AsyncParsableCommand {
|
|||
try vmConfig.setMemory(memorySize: UInt64(memory) * 1024 * 1024)
|
||||
}
|
||||
|
||||
if let display = display {
|
||||
if (display.width > 0) {
|
||||
vmConfig.display.width = display.width
|
||||
}
|
||||
if (display.height > 0) {
|
||||
vmConfig.display.height = display.height
|
||||
}
|
||||
if (display.dpi > 0) {
|
||||
vmConfig.display.dpi = display.dpi
|
||||
}
|
||||
}
|
||||
|
||||
try vmConfig.save(toURL: vmDir.configURL)
|
||||
|
||||
Foundation.exit(0)
|
||||
|
|
@ -37,3 +52,16 @@ struct Set: AsyncParsableCommand {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VMDisplayConfig: ExpressibleByArgument {
|
||||
public init(argument: String) {
|
||||
let parts = argument.components(separatedBy: "x").map {
|
||||
Int($0) ?? 0
|
||||
}
|
||||
self = VMDisplayConfig(
|
||||
width: parts[safe: 0] ?? 0,
|
||||
height: parts[safe: 1] ?? 0,
|
||||
dpi: parts[safe: 2] ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
subscript (safe index: Index) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
|
@ -18,36 +18,31 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
var sema = DispatchSemaphore(value: 0)
|
||||
|
||||
// VM's config
|
||||
var vmConfig: VMConfig
|
||||
var name: String
|
||||
|
||||
// VM's config
|
||||
var config: VMConfig
|
||||
|
||||
init(vmDir: VMDirectory) throws {
|
||||
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
|
||||
|
||||
self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
name = vmDir.name
|
||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
|
||||
let configuration = try VM.craftConfiguration(
|
||||
diskURL: vmDir.diskURL,
|
||||
ecid: vmConfig.ecid,
|
||||
auxStorage: auxStorage,
|
||||
hardwareModel: vmConfig.hardwareModel,
|
||||
cpuCount: vmConfig.cpuCount,
|
||||
memorySize: vmConfig.memorySize,
|
||||
macAddress: vmConfig.macAddress
|
||||
)
|
||||
|
||||
self.virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
|
||||
self.virtualMachine.delegate = self
|
||||
virtualMachine.delegate = self
|
||||
}
|
||||
|
||||
static func retrieveLatestIPSW() 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)
|
||||
}
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -65,16 +60,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
|
||||
let data: Data = try await withCheckedThrowingContinuation { continuation in
|
||||
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
|
||||
if error != nil {
|
||||
continuation.resume(throwing: error!)
|
||||
return
|
||||
}
|
||||
if (data == nil) {
|
||||
continuation.resume(throwing: DownloadFailed())
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: data!)
|
||||
}
|
||||
if error != nil {
|
||||
continuation.resume(throwing: error!)
|
||||
return
|
||||
}
|
||||
if (data == nil) {
|
||||
continuation.resume(throwing: DownloadFailed())
|
||||
return
|
||||
}
|
||||
continuation.resume(returning: data!)
|
||||
}
|
||||
ProgressObserver(downloadedTask.progress).log(defaultLogger)
|
||||
downloadedTask.resume()
|
||||
}
|
||||
|
|
@ -90,8 +85,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
// 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)
|
||||
}
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
|
||||
guard let requirements = image.mostFeaturefulSupportedConfiguration else {
|
||||
|
|
@ -107,91 +102,77 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
try diskFileHandle.truncate(atOffset: diskSize)
|
||||
try diskFileHandle.close()
|
||||
|
||||
name = vmDir.name
|
||||
// Create config
|
||||
self.vmConfig = VMConfig(
|
||||
config = VMConfig(
|
||||
hardwareModel: requirements.hardwareModel,
|
||||
cpuCountMin: requirements.minimumSupportedCPUCount,
|
||||
memorySizeMin: requirements.minimumSupportedMemorySize
|
||||
)
|
||||
try self.vmConfig.save(toURL: vmDir.configURL)
|
||||
try config.save(toURL: vmDir.configURL)
|
||||
|
||||
// Initialize the virtual machine and its configuration
|
||||
let configuration = try VM.craftConfiguration(
|
||||
diskURL: vmDir.diskURL,
|
||||
ecid: self.vmConfig.ecid,
|
||||
auxStorage: auxStorage,
|
||||
hardwareModel: requirements.hardwareModel,
|
||||
cpuCount: self.vmConfig.cpuCount,
|
||||
memorySize: self.vmConfig.memorySize,
|
||||
macAddress: self.vmConfig.macAddress
|
||||
)
|
||||
self.virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
super.init()
|
||||
|
||||
self.virtualMachine.delegate = self
|
||||
virtualMachine.delegate = self
|
||||
|
||||
// Run automated installation
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
DispatchQueue.main.async {
|
||||
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
|
||||
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
|
||||
|
||||
defaultLogger.appendNewLine("Installing OS...")
|
||||
ProgressObserver(installer.progress).log(defaultLogger)
|
||||
defaultLogger.appendNewLine("Installing OS...")
|
||||
ProgressObserver(installer.progress).log(defaultLogger)
|
||||
|
||||
installer.install { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
installer.install { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run() async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
DispatchQueue.main.async {
|
||||
self.virtualMachine.start(completionHandler: { result in
|
||||
continuation.resume(with: result)
|
||||
})
|
||||
}
|
||||
self.virtualMachine.start(completionHandler: { result in
|
||||
continuation.resume(with: result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sema.wait()
|
||||
}
|
||||
|
||||
static func craftConfiguration(
|
||||
diskURL: URL,
|
||||
ecid: VZMacMachineIdentifier,
|
||||
auxStorage: VZMacAuxiliaryStorage,
|
||||
hardwareModel: VZMacHardwareModel,
|
||||
cpuCount: Int,
|
||||
memorySize: UInt64,
|
||||
macAddress: VZMACAddress
|
||||
) throws -> VZVirtualMachineConfiguration {
|
||||
static func craftConfiguration(diskURL: URL, auxStorage: VZMacAuxiliaryStorage, vmConfig: VMConfig) throws -> VZVirtualMachineConfiguration {
|
||||
let configuration = VZVirtualMachineConfiguration()
|
||||
|
||||
// Boot loader
|
||||
configuration.bootLoader = VZMacOSBootLoader()
|
||||
|
||||
// CPU and memory
|
||||
configuration.cpuCount = cpuCount
|
||||
configuration.memorySize = memorySize
|
||||
configuration.cpuCount = vmConfig.cpuCount
|
||||
configuration.memorySize = vmConfig.memorySize
|
||||
|
||||
// Platform
|
||||
let platform = VZMacPlatformConfiguration()
|
||||
|
||||
platform.machineIdentifier = ecid
|
||||
platform.machineIdentifier = vmConfig.ecid
|
||||
platform.auxiliaryStorage = auxStorage
|
||||
platform.hardwareModel = hardwareModel
|
||||
platform.hardwareModel = vmConfig.hardwareModel
|
||||
|
||||
configuration.platform = platform
|
||||
|
||||
// Display
|
||||
let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
|
||||
guard let mainScreen = NSScreen.main else {
|
||||
throw NoMainScreenFoundError()
|
||||
}
|
||||
graphicsDeviceConfiguration.displays = [
|
||||
VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
|
||||
VZMacGraphicsDisplayConfiguration(
|
||||
widthInPixels: vmConfig.display.width,
|
||||
heightInPixels: vmConfig.display.height,
|
||||
pixelsPerInch: vmConfig.display.dpi
|
||||
)
|
||||
]
|
||||
configuration.graphicsDevices = [graphicsDeviceConfiguration]
|
||||
|
||||
|
|
@ -202,7 +183,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||
// Networking
|
||||
let vio = VZVirtioNetworkDeviceConfiguration()
|
||||
vio.attachment = VZNATNetworkDeviceAttachment()
|
||||
vio.macAddress = macAddress
|
||||
vio.macAddress = vmConfig.macAddress
|
||||
configuration.networkDevices = [vio]
|
||||
|
||||
// Storage
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class LessThanMinimalResourcesError: NSObject, LocalizedError {
|
|||
|
||||
override var description: String {
|
||||
get {
|
||||
return "LessThanMinimalResourcesError: \(self.userExplanation)"
|
||||
"LessThanMinimalResourcesError: \(userExplanation)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,16 @@ enum CodingKeys: String, CodingKey {
|
|||
case memorySizeMin
|
||||
case memorySize
|
||||
case macAddress
|
||||
case display
|
||||
}
|
||||
|
||||
struct VMConfig: Encodable, Decodable {
|
||||
struct VMDisplayConfig: Codable {
|
||||
var width: Int = 1024
|
||||
var height: Int = 768
|
||||
var dpi: Int = 72
|
||||
}
|
||||
|
||||
struct VMConfig: Codable {
|
||||
var version: Int = 1
|
||||
var ecid: VZMacMachineIdentifier
|
||||
var hardwareModel: VZMacHardwareModel
|
||||
|
|
@ -34,6 +41,8 @@ struct VMConfig: Encodable, Decodable {
|
|||
var memorySizeMin: UInt64
|
||||
private(set) var memorySize: UInt64
|
||||
var macAddress: VZMACAddress
|
||||
|
||||
var display: VMDisplayConfig = VMDisplayConfig()
|
||||
|
||||
init(
|
||||
ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
|
||||
|
|
@ -44,11 +53,11 @@ struct VMConfig: Encodable, Decodable {
|
|||
) {
|
||||
self.ecid = ecid
|
||||
self.hardwareModel = hardwareModel
|
||||
self.cpuCountMin = cpuCountMin
|
||||
self.cpuCount = cpuCountMin
|
||||
self.memorySizeMin = memorySizeMin
|
||||
self.memorySize = memorySizeMin
|
||||
self.macAddress = macAddress
|
||||
self.cpuCountMin = cpuCountMin
|
||||
self.memorySizeMin = memorySizeMin
|
||||
cpuCount = cpuCountMin
|
||||
memorySize = memorySizeMin
|
||||
}
|
||||
|
||||
init(fromURL: URL) throws {
|
||||
|
|
@ -65,7 +74,7 @@ struct VMConfig: Encodable, Decodable {
|
|||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.version = try container.decode(Int.self, forKey: .version)
|
||||
version = try container.decode(Int.self, forKey: .version)
|
||||
|
||||
let encodedECID = try container.decode(String.self, forKey: .ecid)
|
||||
guard let data = Data.init(base64Encoded: encodedECID) else {
|
||||
|
|
@ -89,10 +98,10 @@ struct VMConfig: Encodable, Decodable {
|
|||
}
|
||||
self.hardwareModel = hardwareModel
|
||||
|
||||
self.cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
|
||||
self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
|
||||
self.memorySizeMin = try container.decode(UInt64.self, forKey: .memorySizeMin)
|
||||
self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
|
||||
cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
|
||||
cpuCount = try container.decode(Int.self, forKey: .cpuCount)
|
||||
memorySizeMin = try container.decode(UInt64.self, forKey: .memorySizeMin)
|
||||
memorySize = try container.decode(UInt64.self, forKey: .memorySize)
|
||||
|
||||
let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
|
||||
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
|
||||
|
|
@ -102,24 +111,27 @@ struct VMConfig: Encodable, Decodable {
|
|||
debugDescription: "failed to initialize VZMacAddress using the provided value")
|
||||
}
|
||||
self.macAddress = macAddress
|
||||
|
||||
display = try container.decodeIfPresent(VMDisplayConfig.self, forKey: .display) ?? VMDisplayConfig()
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.version, forKey: .version)
|
||||
try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
|
||||
try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
|
||||
try container.encode(self.cpuCountMin, forKey: .cpuCountMin)
|
||||
try container.encode(self.cpuCount, forKey: .cpuCount)
|
||||
try container.encode(self.memorySizeMin, forKey: .memorySizeMin)
|
||||
try container.encode(self.memorySize, forKey: .memorySize)
|
||||
try container.encode(self.macAddress.string, forKey: .macAddress)
|
||||
try container.encode(version, forKey: .version)
|
||||
try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
|
||||
try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
|
||||
try container.encode(cpuCountMin, forKey: .cpuCountMin)
|
||||
try container.encode(cpuCount, forKey: .cpuCount)
|
||||
try container.encode(memorySizeMin, forKey: .memorySizeMin)
|
||||
try container.encode(memorySize, forKey: .memorySize)
|
||||
try container.encode(macAddress.string, forKey: .macAddress)
|
||||
try container.encode(display, forKey: .display)
|
||||
}
|
||||
|
||||
mutating func setCPU(cpuCount: Int) throws {
|
||||
if cpuCount < self.cpuCountMin {
|
||||
throw LessThanMinimalResourcesError("VM should have \(self.cpuCountMin) CPU cores"
|
||||
if cpuCount < cpuCountMin {
|
||||
throw LessThanMinimalResourcesError("VM should have \(cpuCountMin) CPU cores"
|
||||
+ " at minimum (requested \(cpuCount))")
|
||||
}
|
||||
|
||||
|
|
@ -127,9 +139,9 @@ struct VMConfig: Encodable, Decodable {
|
|||
}
|
||||
|
||||
mutating func setMemory(memorySize: UInt64) throws {
|
||||
if memorySize < self.memorySizeMin {
|
||||
throw LessThanMinimalResourcesError("VM should have \(self.memorySizeMin) bytes"
|
||||
+ " of memory at minimum (requested \(self.memorySizeMin))")
|
||||
if memorySize < memorySizeMin {
|
||||
throw LessThanMinimalResourcesError("VM should have \(memorySizeMin) bytes"
|
||||
+ " of memory at minimum (requested \(memorySizeMin))")
|
||||
}
|
||||
|
||||
self.memorySize = memorySize
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ struct AlreadyInitializedVMDirectoryError: Error {
|
|||
}
|
||||
|
||||
struct VMDirectory {
|
||||
var name: String
|
||||
var baseURL: URL
|
||||
|
||||
var configURL: URL {
|
||||
self.baseURL.appendingPathComponent("config.json")
|
||||
baseURL.appendingPathComponent("config.json")
|
||||
}
|
||||
var diskURL: URL {
|
||||
self.baseURL.appendingPathComponent("disk.bin")
|
||||
baseURL.appendingPathComponent("disk.bin")
|
||||
}
|
||||
var nvramURL: URL {
|
||||
self.baseURL.appendingPathComponent("nvram.bin")
|
||||
baseURL.appendingPathComponent("nvram.bin")
|
||||
}
|
||||
|
||||
var initialized: Bool {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ struct VMStorage {
|
|||
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
|
||||
|
||||
func create(_ name: String) throws -> VMDirectory {
|
||||
let vmDir = VMDirectory(baseURL: vmURL(name))
|
||||
let vmDir = VMDirectory(name: name, baseURL: vmURL(name))
|
||||
|
||||
try vmDir.initialize()
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ struct VMStorage {
|
|||
}
|
||||
|
||||
func read(_ name: String) throws -> VMDirectory {
|
||||
let vmDir = VMDirectory(baseURL: vmURL(name))
|
||||
let vmDir = VMDirectory(name: name, baseURL: vmURL(name))
|
||||
|
||||
try vmDir.validate()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue