Merge pull request #7862 from meisenbarth/scripting-macos-create

scripting: allow creating macOS VMs with the Apple backend
This commit is contained in:
osy
2026-09-19 10:44:22 -05:00
committed by GitHub
3 changed files with 64 additions and 3 deletions
+11
View File
@@ -31,6 +31,11 @@
</property>
</class-extension>
<enumeration name="operating system" code="ApOe" description="Guest operating system for an Apple virtual machine.">
<enumerator name="linux" code="ApOl" description="Linux guest."/>
<enumerator name="macOS" code="ApOm" description="macOS guest (Apple silicon only)."/>
</enumeration>
<enumeration name="backend" code="VmEb" description="Backend type.">
<enumerator name="apple" code="ApPl" description="Apple Virtualization.framework backend."/>
<enumerator name="qemu" code="QeMu" description="QEMU backend."/>
@@ -713,6 +718,12 @@
description="List of display configuration.">
<type type="apple display configuration" list="yes"/>
</property>
<property name="os" code="ApOs" type="operating system"
description="Guest operating system. Only used when creating; defaults to Linux."/>
<property name="macRecoveryIpsw" code="ApIp" type="file"
description="IPSW recovery image used to install macOS. Required when os is macOS."/>
</record-type>
<record-type name="apple directory share configuration" code="ApDs" description="Apple directory share configuration.">
+6
View File
@@ -51,6 +51,12 @@ import ScriptingBridge
case detailed = 0x6c776474 /* 'lwdt' */
}
// MARK: UTMScriptingOperatingSystem
@objc public enum UTMScriptingOperatingSystem : AEKeyword {
case linux = 0x41704f6c /* 'ApOl' */
case macOS = 0x41704f6d /* 'ApOm' */
}
// MARK: UTMScriptingBackend
@objc public enum UTMScriptingBackend : AEKeyword {
case apple = 0x4170506c /* 'ApPl' */
+47 -3
View File
@@ -15,6 +15,7 @@
//
import Foundation
import Virtualization
@MainActor
@objc(UTMScriptingCreateCommand)
@@ -103,12 +104,49 @@ class UTMScriptingCreateCommand: NSCreateCommand, UTMScriptable {
throw ScriptingError.nameNotSpecified
}
let config = UTMAppleConfiguration()
config.system.boot = try UTMAppleConfigurationBoot(for: .linux)
// enumerations arrive as an AEKeyword and not a string
var bootOS = UTMAppleConfigurationBoot.OperatingSystem.linux
if let osKeyword = record["os"] as? AEKeyword {
guard let scriptingOS = UTMScriptingOperatingSystem(rawValue: osKeyword) else {
throw ScriptingError.operatingSystemNotSupported
}
switch scriptingOS {
case .linux: bootOS = .linux
case .macOS: bootOS = .macOS
}
}
config.system.boot = try UTMAppleConfigurationBoot(for: bootOS)
var macGuestMajorVersion = 0
if bootOS == .macOS {
guard let ipsw = record["macRecoveryIpsw"] as? URL else {
throw ScriptingError.macRecoveryIpswNotSpecified
}
config.system.boot.macRecoveryIpswURL = ipsw
#if arch(arm64)
// the hardware model must match the IPSW the caller chose
let image = try await VZMacOSRestoreImage.image(from: ipsw)
guard let model = image.mostFeaturefulSupportedConfiguration?.hardwareModel else {
throw ScriptingError.ipswNotSupported
}
config.system.macPlatform = UTMAppleConfigurationMacPlatform(newHardware: model)
macGuestMajorVersion = image.operatingSystemVersion.majorVersion
#else
throw ScriptingError.operatingSystemNotSupported
#endif
}
config.virtualization.hasBalloon = true
config.virtualization.hasEntropy = true
config.networks = [UTMAppleConfigurationNetwork()]
// remove any display devices
config.displays = []
if bootOS == .macOS {
// macOS is not usable headless so it gets the same devices as the wizard
config.displays = [UTMAppleConfigurationDisplay(width: 1920, height: 1200)]
config.virtualization.hasAudio = true
config.virtualization.keyboard = .generic
config.virtualization.pointer = macGuestMajorVersion >= 13 ? .trackpad : .mouse
} else {
// remove any display devices
config.displays = []
}
// add a default serial device
var serial = UTMAppleConfigurationSerial()
serial.mode = .ptty
@@ -131,6 +169,9 @@ class UTMScriptingCreateCommand: NSCreateCommand, UTMScriptable {
case configurationNotFound
case nameNotSpecified
case architectureNotSpecified
case operatingSystemNotSupported
case macRecoveryIpswNotSpecified
case ipswNotSupported
var errorDescription: String? {
switch self {
@@ -140,6 +181,9 @@ class UTMScriptingCreateCommand: NSCreateCommand, UTMScriptable {
case .configurationNotFound: return NSLocalizedString("A valid configuration must be specified.", comment: "UTMScriptingAppDelegate")
case .nameNotSpecified: return NSLocalizedString("No name specified in the configuration.", comment: "UTMScriptingAppDelegate")
case .architectureNotSpecified: return NSLocalizedString("No architecture specified in the configuration.", comment: "UTMScriptingAppDelegate")
case .operatingSystemNotSupported: return NSLocalizedString("The specified operating system is not supported on your machine.", comment: "UTMScriptingAppDelegate")
case .macRecoveryIpswNotSpecified: return NSLocalizedString("An IPSW recovery image must be specified to create a macOS virtual machine.", comment: "UTMScriptingAppDelegate")
case .ipswNotSupported: return NSLocalizedString("Your machine does not support running this IPSW.", comment: "UTMScriptingAppDelegate")
}
}
}