144 lines
5.7 KiB
Swift
144 lines
5.7 KiB
Swift
import Foundation
|
|
|
|
#if canImport(Virtualization) && arch(arm64)
|
|
import Virtualization
|
|
|
|
/// What a macOS guest keeps between boots: the disk it was installed on, the
|
|
/// hardware it believes it is, and the storage the firmware writes to.
|
|
@available(macOS 13, *)
|
|
struct VirtualizationBundle {
|
|
let directory: URL
|
|
|
|
var isInstalled: Bool {
|
|
FileManager.default.fileExists(atPath: machineIdentifier.path)
|
|
}
|
|
|
|
var hasSavedState: Bool {
|
|
FileManager.default.fileExists(atPath: savedState.path)
|
|
}
|
|
|
|
var savedState: URL {
|
|
directory.appendingPathComponent("SavedState.vzvmsave")
|
|
}
|
|
|
|
var kernelImage: URL? {
|
|
let image = directory.appendingPathComponent("kernelcache")
|
|
return FileManager.default.fileExists(atPath: image.path) ? image : nil
|
|
}
|
|
|
|
/// A restore image carries the kernelcache the guest will boot, and that
|
|
/// kernelcache carries the kernel's symbols -- every name the agent asks
|
|
/// for, rather than a list the user has to write out.
|
|
func keepKernelImage(from restoreImage: URL) throws {
|
|
let listing = try run("/usr/bin/unzip", ["-Z1", restoreImage.path, "kernelcache.release.vma*"])
|
|
guard let entry = listing.split(separator: "\n").map(String.init).first else {
|
|
throw VirtualMachineError.launchFailed(reason: "That restore image carries no kernel for a virtual Mac")
|
|
}
|
|
|
|
_ = try run("/usr/bin/unzip", ["-o", "-j", restoreImage.path, entry, "-d", directory.path])
|
|
|
|
let extracted = directory.appendingPathComponent((entry as NSString).lastPathComponent)
|
|
guard extracted != kernelImageURL else { return }
|
|
try? FileManager.default.removeItem(at: kernelImageURL)
|
|
try FileManager.default.moveItem(at: extracted, to: kernelImageURL)
|
|
}
|
|
|
|
private var kernelImageURL: URL {
|
|
directory.appendingPathComponent("kernelcache")
|
|
}
|
|
|
|
@discardableResult
|
|
private func run(_ tool: String, _ arguments: [String]) throws -> String {
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: tool)
|
|
process.arguments = arguments
|
|
let output = Pipe()
|
|
process.standardOutput = output
|
|
process.standardError = FileHandle.nullDevice
|
|
try process.run()
|
|
let data = output.fileHandleForReading.readDataToEndOfFile()
|
|
process.waitUntilExit()
|
|
return String(decoding: data, as: UTF8.self)
|
|
}
|
|
|
|
func prepare(hardwareModel: VZMacHardwareModel, diskSizeInGigabytes: Int) throws {
|
|
try hardwareModel.dataRepresentation.write(to: self.hardwareModel)
|
|
|
|
let identifier = VZMacMachineIdentifier()
|
|
try identifier.dataRepresentation.write(to: machineIdentifier)
|
|
|
|
_ = try VZMacAuxiliaryStorage(creatingStorageAt: auxiliaryStorage, hardwareModel: hardwareModel)
|
|
|
|
guard FileManager.default.createFile(atPath: disk.path, contents: nil) else {
|
|
throw VirtualMachineError.launchFailed(reason: "Unable to make room for the guest's disk")
|
|
}
|
|
let handle = try FileHandle(forWritingTo: disk)
|
|
try handle.truncate(atOffset: UInt64(diskSizeInGigabytes) * 1024 * 1024 * 1024)
|
|
try handle.close()
|
|
}
|
|
|
|
func makeConfiguration(memory: Int, processors: Int) throws -> VZVirtualMachineConfiguration {
|
|
let platform = VZMacPlatformConfiguration()
|
|
platform.hardwareModel = try readHardwareModel()
|
|
platform.machineIdentifier = try readMachineIdentifier()
|
|
platform.auxiliaryStorage = VZMacAuxiliaryStorage(url: auxiliaryStorage)
|
|
|
|
let configuration = VZVirtualMachineConfiguration()
|
|
configuration.platform = platform
|
|
configuration.bootLoader = VZMacOSBootLoader()
|
|
configuration.cpuCount = processors
|
|
configuration.memorySize = UInt64(memory) * 1024 * 1024
|
|
|
|
let screen = VZMacGraphicsDeviceConfiguration()
|
|
screen.displays = [VZMacGraphicsDisplayConfiguration(widthInPixels: 2560, heightInPixels: 1600, pixelsPerInch: 220)]
|
|
configuration.graphicsDevices = [screen]
|
|
|
|
configuration.storageDevices = [
|
|
VZVirtioBlockDeviceConfiguration(attachment: try VZDiskImageStorageDeviceAttachment(url: disk, readOnly: false))
|
|
]
|
|
|
|
let network = VZVirtioNetworkDeviceConfiguration()
|
|
network.attachment = VZNATNetworkDeviceAttachment()
|
|
configuration.networkDevices = [network]
|
|
|
|
configuration.keyboards = [VZMacKeyboardConfiguration()]
|
|
configuration.pointingDevices = [VZMacTrackpadConfiguration()]
|
|
configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()]
|
|
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
|
|
|
|
return configuration
|
|
}
|
|
|
|
private func readHardwareModel() throws -> VZMacHardwareModel {
|
|
guard let model = VZMacHardwareModel(dataRepresentation: try Data(contentsOf: hardwareModel)) else {
|
|
throw VirtualMachineError.launchFailed(reason: "The guest's hardware model is unreadable")
|
|
}
|
|
return model
|
|
}
|
|
|
|
private func readMachineIdentifier() throws -> VZMacMachineIdentifier {
|
|
guard let identifier = VZMacMachineIdentifier(dataRepresentation: try Data(contentsOf: machineIdentifier)) else {
|
|
throw VirtualMachineError.launchFailed(reason: "The guest's identity is unreadable")
|
|
}
|
|
return identifier
|
|
}
|
|
|
|
private var disk: URL {
|
|
directory.appendingPathComponent("Disk.img")
|
|
}
|
|
|
|
private var auxiliaryStorage: URL {
|
|
directory.appendingPathComponent("AuxiliaryStorage")
|
|
}
|
|
|
|
private var hardwareModel: URL {
|
|
directory.appendingPathComponent("HardwareModel")
|
|
}
|
|
|
|
private var machineIdentifier: URL {
|
|
directory.appendingPathComponent("MachineIdentifier")
|
|
}
|
|
}
|
|
|
|
#endif
|