Files
UTM/Configuration/UTMQemuConfiguration+Arguments.swift
T
osy 0086417b97 snapshots: manage QEMU snapshots through a backend
Snapshots were read straight out of the QEMU images by the scripting
commands, which only worked while the VM was off and knew nothing beyond
the tags in the image.

Put a backend behind the operations instead. Which one is chosen depends on
whether the VM is running, and each says what it can do in that state:

- A running VM goes through the QEMU monitor, which saves and loads the
  whole machine along with the disks.
- A VM that is off goes through qemu-img, which can only save and revert
  disk contents. Nothing may touch the images while QEMU has them open, so
  a snapshot of disks alone is left alone until the VM stops.

Either way a snapshot spans every writable image, the UEFI variable store
included, because QEMU refuses to load one that any of them is missing. A
snapshot that some of them lack, such as one taken before a drive was added,
is kept but cannot be restored, which is not the same as its state being
gone: reverting only some of the drives would leave them at different points
in time. Reclaiming space rebuilds an image without its snapshots, so it is
refused while there are any.

UTMSnapshotService sits above the backend and keeps the manifest and the
stored state consistent: it marks entries whose state has disappeared when
an operation fails, and the first time a VM is opened it adopts whatever
snapshots are already in the images so that snapshots taken by other tools
are not lost. Both wait until the VM is stopped, because a running QEMU
lists the snapshots of every drive together, removable ones included.

Suspending is folded into the same mechanism. A VM now suspends to a
snapshot of its own rather than always the same name, so a suspended state
can be kept as a snapshot without copying it, and restoring a snapshot
makes the VM resume from it. The state is deleted once nothing refers to
it, where it used to be left behind in the image. One that cannot be deleted
right away stays listed in the manifest and is tried again the next time.

Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5
2026-09-22 10:57:30 -05:00

1330 lines
46 KiB
Swift

//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
#if os(macOS)
import Virtualization // for getting network interfaces
#endif
/// Build QEMU arguments from config
@MainActor extension UTMQemuConfiguration {
/// Helper function to generate a final argument
/// - Parameter string: Argument fragment
/// - Returns: Final argument fragment
private func f(_ string: String = "") -> QEMUArgumentFragment {
QEMUArgumentFragment(final: string)
}
/// Shared group ID for main process and helper
var appGroupIdentifier: String? {
let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
if let appGroup, !appGroup.hasPrefix("invalid.") {
return appGroup
}
return nil
}
/// Shared between helper and main process to store Unix sockets
var socketURL: URL {
#if os(iOS) || os(visionOS)
return FileManager.default.temporaryDirectory
#else
let helper = Bundle.main.infoDictionary?["HelperIdentifier"] as? String
// default to unsigned sandbox path
var parentURL: URL = FileManager.default.homeDirectoryForCurrentUser
parentURL.deleteLastPathComponent()
parentURL.deleteLastPathComponent()
parentURL.appendPathComponent(helper ?? "com.utmapp.QEMUHelper")
parentURL.appendPathComponent("Data")
parentURL.appendPathComponent("tmp")
if let appGroup = appGroupIdentifier {
if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
return containerURL
}
}
return parentURL
#endif
}
/// Return the socket file for communicating with SPICE
var spiceSocketURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("spice")
}
/// Return the socket file for communicating with SWTPM
var swtpmSocketURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
}
/// Used only if in remote server mode.
var monitorPipeURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp")
}
/// Used only if in remote server mode.
var guestAgentPipeURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga")
}
/// Used only if in remote server mode.
var spiceTlsKeyUrl: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("pem")
}
/// Used only if in remote server mode.
var spiceTlsCertUrl: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("crt")
}
/// Used by virglrenderer to allocate shmem
var shmemDirectoryURL: URL {
socketURL
}
/// Used for placeholder images
var placeholderUrl: URL {
#if os(macOS)
URL(fileURLWithPath: "/dev/null")
#else
let empty = FileManager.default.temporaryDirectory.appendingPathComponent("empty")
FileManager.default.createFile(atPath: empty.path, contents: nil)
return empty
#endif
}
/// Global setting to always use file lock or not
private var isUseFileLock: Bool {
return UserDefaults.standard.value(forKey: "UseFileLock") == nil || UserDefaults.standard.bool(forKey: "UseFileLock")
}
/// Special arguments should disable use of bootindex
private var shouldDisableBootIndex: Bool {
// currently, we only identified this issue in PPC machines
guard system.architecture == .ppc || system.architecture == .ppc64 else {
return false
}
let arguments = parsedUserArguments
for (index, arg) in arguments.enumerated() {
// when user specifies '-prom-env boot-device=hd:,\yaboot' for example, it should inhibit bootindex
if arg == "-prom-env" && index != arguments.count - 1 && arguments[index+1].starts(with: "boot-device=") {
return true
}
}
return false
}
/// Combined generated and user specified arguments.
@QEMUArgumentBuilder var allArguments: [QEMUArgument] {
generatedArguments
userArguments
}
/// Only UTM generated arguments.
@QEMUArgumentBuilder var generatedArguments: [QEMUArgument] {
f("-L")
resourceURL
f()
f("-S") // startup stopped
spiceArguments
networkArguments
displayArguments
serialArguments
cpuArguments
machineArguments
architectureArguments
soundArguments
if isUsbUsed {
usbArguments
}
otherInputsArguments
drivesArguments
sharingArguments
miscArguments
}
/// Take user arguments and replace any quotes
private var parsedUserArguments: [String] {
var list = [String]()
let regex = try! NSRegularExpression(pattern: "((?:[^\"\\s]*\"[^\"]*\"[^\"\\s]*)+|[^\"\\s]+)")
for arg in qemu.additionalArguments {
let argString = arg.string
if argString.count > 0 {
let range = NSRange(argString.startIndex..<argString.endIndex, in: argString)
let split = regex.matches(in: argString, options: [], range: range)
for match in split {
let matchRange = Range(match.range(at: 1), in: argString)!
var fragment = argString[matchRange]
if fragment.first == "\"" && fragment.last == "\"" {
fragment = fragment.dropFirst().dropLast()
}
list.append(String(fragment))
}
}
}
return list
}
@QEMUArgumentBuilder private var userArguments: [QEMUArgument] {
for arg in parsedUserArguments {
f(arg)
}
}
@QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
f("-spice")
if let port = qemu.spiceServerPort {
if qemu.isSpiceServerTlsEnabled {
"tls-port=\(port)"
"tls-channel=default"
"x509-key-file="
spiceTlsKeyUrl
"x509-cert-file="
spiceTlsCertUrl
"x509-cacert-file="
spiceTlsCertUrl
} else {
"port=\(port)"
}
} else {
"unix=on"
"addr=\(spiceSocketURL.lastPathComponent)"
}
if let _ = qemu.spiceServerPassword {
"password-secret=secspice0"
} else {
"disable-ticketing=on"
}
if !isRemoteSpice {
"image-compression=off"
"playback-compression=off"
"streaming-video=off"
} else {
"streaming-video=filter"
}
"gl=\(glBackend)"
f()
f("-chardev")
if isRemoteSpice {
"pipe"
"path="
monitorPipeURL
} else {
"spiceport"
"name=org.qemu.monitor.qmp.0"
}
"id=org.qemu.monitor.qmp"
f()
f("-mon")
f("chardev=org.qemu.monitor.qmp,mode=control")
if !isSparc { // disable -vga and other default devices
// prevent QEMU default devices, which leads to duplicate CD drive (fix #2538)
// see https://github.com/qemu/qemu/blob/6005ee07c380cbde44292f5f6c96e7daa70f4f7d/docs/qdev-device-use.txt#L382
f("-nodefaults")
f("-vga")
f("none")
}
if let password = qemu.spiceServerPassword {
// assume anyone who can read this is in our trust domain
f("-object")
f("secret,id=secspice0,data=\(password)")
}
}
private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice {
if isRemoteSpice {
let rawValue = display.rawValue
if rawValue.hasSuffix("-gl") {
return AnyQEMUConstant(rawValue: String(rawValue.dropLast(3)))!
} else if rawValue.contains("-gl-") {
return AnyQEMUConstant(rawValue: String(rawValue.replacingOccurrences(of: "-gl-", with: "-")))!
} else {
return display
}
} else {
return display
}
}
private func shouldSkipDisplay(_ display: UTMQemuConfigurationDisplay) -> Bool {
return display.hardware.rawValue == QEMUDisplayDevice_m68k.nubus_macfb.rawValue
}
@QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
if displays.isEmpty {
f("-nographic")
} else if isSparc { // only one display supported
f("-vga")
displays[0].hardware
if let vgaRamSize = displays[0].vgaRamMib {
"vgamem_mb=\(vgaRamSize)"
}
f()
} else {
for display in displays {
if !shouldSkipDisplay(display) {
f("-device")
filterDisplayIfRemote(display.hardware)
if let vgaRamSize = displays[0].vgaRamMib {
"vgamem_mb=\(vgaRamSize)"
}
if display.hardware.rawValue.lowercased().contains("vga") && isClassicMacNewWorld {
"edid=on"
}
if isDisplayGLSupported(display) && isVulkanSupported {
"hostmem=8G"
"blob=true"
"venus=true"
}
if isDisplayGLSupported(display) && isNeptuneSupported {
if !isVulkanSupported {
"hostmem=8G"
"blob=true"
}
"neptune=true"
}
f()
}
}
}
}
private func isDisplayGLSupported(_ display: UTMQemuConfigurationDisplay) -> Bool {
display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
}
private var isGLSupported: Bool {
displays.contains { display in
isDisplayGLSupported(display)
}
}
private var rendererBackend: UTMQEMURendererBackend {
let rawValue = UserDefaults.standard.integer(forKey: "QEMURendererBackend")
return UTMQEMURendererBackend(rawValue: rawValue) ?? .qemuRendererBackendDefault
}
private var glBackend: String {
if isGLSupported && !isRemoteSpice {
#if os(macOS)
if rendererBackend == .qemuRendererBackendCGL {
return "core"
} else {
return "es"
}
#else
return "on"
#endif
} else {
return "off"
}
}
private var vulkanDriver: UTMQEMUVulkanDriver {
let rawValue = UserDefaults.standard.integer(forKey: "QEMUVulkanDriver")
return UTMQEMUVulkanDriver(rawValue: rawValue) ?? .qemuVulkanDriverDefault
}
private var directXDriver: UTMQEMUDirectXDriver {
let rawValue = UserDefaults.standard.integer(forKey: "QEMUDirectXDriver")
return UTMQEMUDirectXDriver(rawValue: rawValue) ?? .qemuDirectXDriverDefault
}
private var isVulkanSupported: Bool {
isGLSupported &&
(rendererBackend == .qemuRendererBackendAngleMetal || rendererBackend == .qemuRendererBackendDefault) &&
vulkanDriver != .qemuVulkanDriverDisabled
}
private var isNeptuneSupported: Bool {
if #available(macOS 13, iOS 16, *) {
return isGLSupported &&
(rendererBackend == .qemuRendererBackendAngleMetal || rendererBackend == .qemuRendererBackendDefault) &&
directXDriver != .qemuDirectXDriverDisabled
} else {
return false
}
}
private var isSparc: Bool {
system.architecture == .sparc || system.architecture == .sparc64
}
private var isRemoteSpice: Bool {
qemu.spiceServerPort != nil
}
private var isClassicMacM68K: Bool {
system.architecture == .m68k && system.target.rawValue == QEMUTarget_m68k.q800.rawValue
}
private var isClassicMacNewWorld: Bool {
[.ppc, .ppc64].contains(system.architecture) && system.target.rawValue == QEMUTarget_ppc.mac99.rawValue
}
@QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
for i in serials.indices {
f("-chardev")
switch serials[i].mode {
case .builtin:
f("spiceport,id=term\(i),name=com.utmapp.terminal.\(i)")
case .tcpClient:
"socket"
"id=term\(i)"
"port=\(serials[i].tcpPort ?? 1234)"
"host=\(serials[i].tcpHostAddress ?? "example.com")"
"server=off"
f()
case .tcpServer:
"socket"
"id=term\(i)"
"port=\(serials[i].tcpPort ?? 1234)"
"host=\(serials[i].isRemoteConnectionAllowed == true ? "0.0.0.0" : "127.0.0.1")"
"server=on"
"wait=\(serials[i].isWaitForConnection == true ? "on" : "off")"
f()
#if os(macOS)
case .ptty:
f("pty,id=term\(i)")
#endif
}
switch serials[i].target {
case .autoDevice:
f("-serial")
f("chardev:term\(i)")
case .manualDevice:
f("-device")
f("\(serials[i].hardware?.rawValue ?? "invalid"),chardev=term\(i)")
case .monitor:
f("-mon")
f("chardev=term\(i),mode=readline")
case .gdb:
f("-gdb")
f("chardev:term\(i)")
}
}
}
@QEMUArgumentBuilder private var cpuArguments: [QEMUArgument] {
if system.cpu.rawValue == system.architecture.cpuType.default.rawValue {
// if default and not hypervisor, we don't pass any -cpu argument for x86 and use host for ARM
if isHypervisorUsed {
#if arch(x86_64)
if let cpu = highestIntelCPUConfigurationForHost() {
f("-cpu")
f(cpu)
}
#else
f("-cpu")
f("host")
#endif
} else if system.architecture == .aarch64 {
// ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
f("-cpu")
f("cortex-a72")
} else if system.architecture == .arm {
// ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
f("-cpu")
f("cortex-a15")
} else if system.architecture == .x86_64, let cpu = highestIntelCPUConfigurationForHost() {
f("-cpu")
f(cpu)
}
} else {
f("-cpu")
system.cpu
for flag in system.cpuFlagsAdd {
"+\(flag.rawValue)"
}
for flag in system.cpuFlagsRemove {
"-\(flag.rawValue)"
}
f()
}
let emulatedCpuCount = self.emulatedCpuCount
f("-smp")
"cpus=\(emulatedCpuCount.1)"
"sockets=1"
"cores=\(emulatedCpuCount.0)"
"threads=\(emulatedCpuCount.1/emulatedCpuCount.0)"
f()
}
private static func sysctlIntRead(_ name: String) -> UInt64 {
var value: UInt64 = 0
var size = MemoryLayout<UInt64>.size
sysctlbyname(name, &value, &size, nil, 0)
return value
}
private var emulatedCpuCount: (Int, Int) {
let singleCpu = (1, 1)
let hostPhysicalCpu = Int(Self.sysctlIntRead("hw.physicalcpu"))
let hostLogicalCpu = Int(Self.sysctlIntRead("hw.logicalcpu"))
let userCpu = system.cpuCount
if userCpu > 0 || hostPhysicalCpu == 0 {
return (userCpu, userCpu) // user override
}
// SPARC5 defaults to single CPU
if isSparc {
return singleCpu
}
#if arch(arm64)
let hostPcorePhysicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.physicalcpu"))
let hostPcoreLogicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.logicalcpu"))
// in ARM we can only emulate other weak architectures
let weakArchitectures: [QEMUArchitecture] = [.alpha, .arm, .aarch64, .avr, .mips, .mips64, .mipsel, .mips64el, .ppc, .ppc64, .riscv32, .riscv64, .xtensa, .xtensaeb]
if weakArchitectures.contains(system.architecture) {
if hostPcorePhysicalCpu > 0 {
return (hostPcorePhysicalCpu, hostPcoreLogicalCpu)
} else {
return (hostPhysicalCpu, hostLogicalCpu)
}
} else {
return singleCpu
}
#elseif arch(x86_64)
// in x86 we can emulate weak on strong
return (hostPhysicalCpu, hostLogicalCpu)
#else
return singleCpu
#endif
}
private var isHypervisorUsed: Bool {
system.architecture.hasHypervisorSupport && qemu.hasHypervisor
}
private var isTSOUsed: Bool {
system.architecture.hasTSOSupport && qemu.hasTSO
}
private var isUsbUsed: Bool {
system.architecture.hasUsbSupport && system.target.hasUsbSupport && input.usbBusSupport != .disabled
}
private var isSecureBootUsed: Bool {
system.architecture.hasSecureBootSupport && system.target.hasSecureBootSupport && qemu.hasTPMDevice
}
@QEMUArgumentBuilder private var machineArguments: [QEMUArgument] {
f("-machine")
system.target
f(machineProperties)
if isHypervisorUsed {
f("-accel")
"hvf"
if isTSOUsed {
"tso=on"
}
if isVulkanSupported {
"ipa-granule-size=0x1000"
}
f()
} else {
f("-accel")
"tcg"
if system.isForceMulticore {
"thread=multi"
}
let tbSize = system.jitCacheSize > 0 ? system.jitCacheSize : system.memorySize / 4
"tb-size=\(tbSize)"
#if WITH_JIT
// use mirror mapping when we don't have JIT entitlements
if !UTMCapabilities.current.contains(.hasJitEntitlements) {
"split-wx=on"
}
#endif
f()
}
}
private var machineProperties: String {
let target = system.target.rawValue
let architecture = system.architecture.rawValue
var properties = qemu.machinePropertyOverride ?? ""
if isPcCompatible {
properties = properties.appendingDefaultPropertyName("vmport", value: "off")
// disable PS/2 emulation if we are not legacy input and it's not explicitly enabled
if isUsbUsed && !qemu.hasPS2Controller {
properties = properties.appendingDefaultPropertyName("i8042", value: "off")
}
#if os(macOS)
if useCoreAudioBackend && sound.contains(where: { $0.hardware.rawValue == "pcspk" }) {
properties = properties.appendingDefaultPropertyName("pcspk-audiodev", value: "audio1")
}
#endif
// disable HPET because it causes issues for some OS and also hinders performance
properties = properties.appendingDefaultPropertyName("hpet", value: "off")
}
if target == "virt" || target.hasPrefix("virt-") && !architecture.hasPrefix("riscv") {
if #available(macOS 12.4, iOS 15.5, *, *) {
// default highmem value is fine here
} else {
// a kernel panic is triggered on M1 Max if highmem=on and running < macOS 12.4
properties = properties.appendingDefaultPropertyName("highmem", value: "off")
}
// required to boot Windows ARM on TCG
if system.architecture == .aarch64 && !isHypervisorUsed {
properties = properties.appendingDefaultPropertyName("virtualization", value: "on")
}
// required for > 8 CPUs, improves performance regardless
if system.architecture == .aarch64 {
properties = properties.appendingDefaultPropertyName("gic-version", value: "3")
}
}
if isClassicMacM68K {
if sound.contains(where: { $0.hardware.rawValue == QEMUSoundDevice_m68k.asc.rawValue }) {
properties = properties.appendingDefaultPropertyName("audiodev", value: "audio0")
}
}
if isClassicMacNewWorld {
properties = properties.appendingDefaultPropertyName("via", value: "pmu")
}
return properties
}
@QEMUArgumentBuilder private var architectureArguments: [QEMUArgument] {
if system.architecture == .x86_64 || system.architecture == .i386 {
f("-global")
f("PIIX4_PM.disable_s3=1") // applies for pc-i440fx-* types
f("-global")
f("ICH9-LPC.disable_s3=1") // applies for pc-q35-* types
}
if qemu.hasUefiBoot, let prefix = UTMQemuConfigurationQEMU.uefiImagePrefix(forArchitecture: system.architecture) {
let secure = isSecureBootUsed ? "-secure" : ""
let bios = resourceURL.appendingPathComponent("\(prefix)\(secure)-code.fd")
let vars = qemu.efiVarsURL ?? URL(fileURLWithPath: "/\(QEMUPackageFileName.efiVariables.rawValue)")
if !hasCustomBios && FileManager.default.fileExists(atPath: bios.path) {
f("-drive")
"if=pflash"
"format=raw"
"unit=0"
"file.filename="
bios
"file.locking=off"
"readonly=on"
f()
f("-drive")
"if=pflash"
"unit=1"
"file.filename="
vars
if !isUseFileLock {
"file.locking=off"
}
f()
}
}
if isClassicMacM68K {
let declrom = resourceURL.appendingPathComponent("m68k-declrom")
f("-device")
"nubus-virtio-mmio"
"romfile="
declrom
f()
}
if isClassicMacNewWorld {
let ndrvloader = resourceURL.appendingPathComponent("ppc-ndrvloader")
f("-device")
"loader"
"addr=0x4000000"
"file="
ndrvloader
f()
f("-prom-env")
f("boot-command=init-program go")
}
f("-m")
system.memorySize
f()
}
var hasCustomBios: Bool {
for drive in drives {
if drive.imageType == .disk || drive.imageType == .cd {
if drive.interface == .pflash {
return true
}
} else if drive.imageType == .bios || drive.imageType == .linuxKernel {
return true
}
}
return false
}
private var resourceURL: URL {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("qemu", isDirectory: true)
}
private var soundBackend: UTMQEMUSoundBackend {
let value = UserDefaults.standard.integer(forKey: "QEMUSoundBackend")
if let backend = UTMQEMUSoundBackend(rawValue: value), backend != .qemuSoundBackendMax {
return backend
} else {
return .qemuSoundBackendDefault
}
}
private var useCoreAudioBackend: Bool {
#if os(iOS) || os(visionOS)
return false
#else
// only support SPICE audio if we are running remotely
if isRemoteSpice {
return false
}
// pcspk doesn't work with SPICE audio
if sound.contains(where: { $0.hardware.rawValue == "pcspk" }) {
return true
}
if soundBackend == .qemuSoundBackendCoreAudio {
return true
}
return false
#endif
}
private func isInternalAudioDevice(_ device: any QEMUSoundDevice) -> Bool {
[QEMUSoundDevice_i386.pcspk.rawValue, QEMUSoundDevice_ppc.screamer.rawValue, QEMUSoundDevice_m68k.asc.rawValue].contains(device.rawValue)
}
@QEMUArgumentBuilder private var soundArguments: [QEMUArgument] {
if sound.isEmpty {
f("-audio")
f("none")
} else if sound.contains(where: { $0.hardware.rawValue == "screamer" }) {
#if os(iOS) || os(visionOS)
f("-audio")
f("none")
#else
// force CoreAudio backend for mac99 which only supports 44100 Hz
f("-audio")
f("coreaudio")
#endif
}
if useCoreAudioBackend {
f("-audiodev")
"coreaudio"
f("id=audio1")
}
f("-audiodev")
"spice"
f("id=audio0")
// screamer has no extra device, pcspk is handled in machineProperties
for _sound in sound.filter({ !isInternalAudioDevice($0.hardware) }) {
f("-device")
_sound.hardware
if _sound.hardware.rawValue.contains("hda") {
f()
f("-device")
if soundBackend == .qemuSoundBackendCoreAudio && useCoreAudioBackend {
"hda-output"
"audiodev=audio1"
} else {
"hda-duplex"
"audiodev=audio0"
}
f()
} else {
if soundBackend == .qemuSoundBackendCoreAudio && useCoreAudioBackend {
f("audiodev=audio1")
} else {
f("audiodev=audio0")
}
}
}
}
@QEMUArgumentBuilder private var drivesArguments: [QEMUArgument] {
var busInterfaceMap: [String: Int] = [:]
let disableBootindex = shouldDisableBootIndex
for drive in drives {
let hasImage = !drive.isExternal && drive.imageURL != nil
if drive.imageType == .disk || drive.imageType == .cd {
driveArgument(for: drive, busInterfaceMap: &busInterfaceMap, disableBootIndex: disableBootindex)
} else if hasImage {
switch drive.imageType {
case .bios:
f("-bios")
drive.imageURL!
case .linuxKernel:
f("-kernel")
drive.imageURL!
case .linuxInitrd:
f("-initrd")
drive.imageURL!
case .linuxDtb:
f("-dtb")
drive.imageURL!
default:
f()
}
f()
}
}
}
private var isPcCompatible: Bool {
guard system.architecture == .x86_64 || system.architecture == .i386 else {
return false
}
return system.target.rawValue.starts(with: "pc") || system.target.rawValue == "q35" || system.target.rawValue == "isapc"
}
/// These machines are hard coded to have one IDE unit per bus in QEMU
private var isIdeInterfaceSingleUnit: Bool {
isPcCompatible ||
system.target.rawValue == "microvm" ||
system.target.rawValue == "cubieboard" ||
system.target.rawValue == "highbank" ||
system.target.rawValue == "midway" ||
system.target.rawValue == "xlnx_zcu102"
}
@QEMUArgumentBuilder private func driveArgument(for drive: UTMQemuConfigurationDrive, busInterfaceMap: inout [String: Int], disableBootIndex: Bool = false) -> [QEMUArgument] {
let isRemovable = drive.imageType == .cd || drive.isExternal
let isCd = drive.imageType == .cd && drive.interface != .floppy
var bootindex = busInterfaceMap["boot", default: 0]
var busindex = busInterfaceMap[drive.interface.rawValue, default: 0]
var realInterface = QEMUDriveInterface.none
if drive.interface == .ide {
f("-device")
if isCd {
"ide-cd"
} else {
"ide-hd"
}
if drive.interfaceVersion >= 1 && !isIdeInterfaceSingleUnit {
"bus=ide.\(busindex / 2)"
"unit=\(busindex % 2)"
} else {
"bus=ide.\(busindex)"
}
busindex += 1
"drive=drive\(drive.id)"
if !disableBootIndex {
"bootindex=\(bootindex)"
}
bootindex += 1
f()
} else if drive.interface == .scsi {
var bus = "scsi"
if system.architecture != .sparc && system.architecture != .sparc64 && system.architecture != .m68k {
bus = "scsi0"
if busindex == 0 {
f("-device")
f("lsi53c895a,id=scsi0")
}
}
if system.architecture == .m68k && system.target.rawValue == QEMUTarget_m68k.virt.rawValue {
bus = "scsi0"
if busindex == 0 {
f("-device")
f("virtio-scsi-device,id=scsi0")
}
}
f("-device")
if isCd {
"scsi-cd"
} else {
"scsi-hd"
}
"bus=\(bus).0"
"channel=0"
"scsi-id=\(busindex)"
busindex += 1
"drive=drive\(drive.id)"
if !disableBootIndex {
"bootindex=\(bootindex)"
}
bootindex += 1
f()
} else if drive.interface == .virtio {
f("-device")
if system.architecture == .s390x {
"virtio-blk-ccw"
} else if system.architecture == .m68k {
"virtio-blk-device"
} else {
"virtio-blk-pci"
}
"drive=drive\(drive.id)"
"serial=\(drive.serial)"
if !disableBootIndex {
"bootindex=\(bootindex)"
}
bootindex += 1
f()
} else if drive.interface == .nvme {
f("-device")
"nvme"
"drive=drive\(drive.id)"
"serial=\(drive.id)"
if !disableBootIndex {
"bootindex=\(bootindex)"
}
bootindex += 1
f()
} else if drive.interface == .usb {
f("-device")
// use usb 3 bus for virt system, unless using legacy input setting (this mirrors the code in argsForUsb)
let isUsb3 = isUsbUsed && system.target.rawValue.hasPrefix("virt")
"usb-storage"
"drive=drive\(drive.id)"
"removable=\(isRemovable)"
if !disableBootIndex {
"bootindex=\(bootindex)"
}
bootindex += 1
if isUsb3 {
"bus=usb-bus.0"
}
f()
} else if drive.interface == .floppy {
if isPcCompatible {
f("-device")
"isa-fdc"
"id=fdc\(busindex)"
if !disableBootIndex {
"bootindexA=\(bootindex)"
}
bootindex += 1
f()
f("-device")
"floppy"
"unit=0"
"bus=fdc\(busindex).0"
busindex += 1
"drive=drive\(drive.id)"
f()
} else {
realInterface = drive.interface
}
} else {
realInterface = drive.interface
}
busInterfaceMap["boot"] = bootindex
busInterfaceMap[drive.interface.rawValue] = busindex
f("-drive")
switch realInterface {
case .ide:
"if=ide"
case .scsi:
"if=scsi"
case .sd:
"if=sd"
case .mtd:
"if=mtd"
case .floppy:
"if=floppy"
case .pflash:
"if=pflash"
default:
"if=none"
}
if isCd {
"media=cdrom"
} else {
"media=disk"
}
"id=drive\(drive.id)"
if let imageURL = drive.imageURL {
"file.filename="
imageURL
} else if !isCd {
"file.filename="
placeholderUrl
}
if drive.isReadOnly || isCd {
if drive.imageURL != nil {
"file.locking=off"
}
"readonly=on"
} else {
"discard=unmap"
"detect-zeroes=unmap"
}
if !isUseFileLock && (!isCd || drive.imageURL != nil) {
"file.locking=off"
}
f()
}
@QEMUArgumentBuilder private var usbArguments: [QEMUArgument] {
if system.target.rawValue.hasPrefix("virt") {
f("-device")
f("nec-usb-xhci,id=usb-bus")
} else {
f("-usb")
}
if !isClassicMacNewWorld {
f("-device")
f("usb-tablet,bus=usb-bus.0")
}
if !qemu.hasPS2Controller {
f("-device")
f("usb-mouse,bus=usb-bus.0")
f("-device")
f("usb-kbd,bus=usb-bus.0")
}
#if WITH_USB
let maxDevices = input.maximumUsbShare
let buses = (maxDevices + 2) / 3
if input.usbBusSupport == .usb3_0 {
var controller = "qemu-xhci"
if isPcCompatible {
controller = "nec-usb-xhci"
}
for i in 0..<buses {
f("-device")
f("\(controller),id=usb-controller-\(i)")
}
} else {
for i in 0..<buses {
f("-device")
f("ich9-usb-ehci1,id=usb-controller-\(i)")
f("-device")
f("ich9-usb-uhci1,masterbus=usb-controller-\(i).0,firstport=0,multifunction=on")
f("-device")
f("ich9-usb-uhci2,masterbus=usb-controller-\(i).0,firstport=2,multifunction=on")
f("-device")
f("ich9-usb-uhci3,masterbus=usb-controller-\(i).0,firstport=4,multifunction=on")
}
}
// set up usb forwarding
for i in 0..<maxDevices {
f("-chardev")
f("spicevmc,name=usbredir,id=usbredirchardev\(i)")
f("-device")
f("usb-redir,chardev=usbredirchardev\(i),id=usbredirdev\(i),bus=usb-controller-\(i/3).0")
}
#endif
}
@QEMUArgumentBuilder private var otherInputsArguments: [QEMUArgument] {
if isClassicMacNewWorld {
f("-device")
f("virtio-tablet-pci")
}
if isClassicMacM68K {
f("-device")
f("virtio-tablet-device")
}
}
private func parseNetworkSubnet(from network: UTMQemuConfigurationNetwork) -> (start: String, end: String, mask: String)? {
guard let net = network.vlanGuestAddress else {
return nil
}
let components = net.split(separator: "/")
let address: String
let binaryMask: UInt32
guard components.count >= 1 else {
return nil
}
if components.count == 2 {
var netmaskAddr = in_addr()
if inet_pton(AF_INET, String(components[1]), &netmaskAddr) == 1 {
binaryMask = UInt32(bigEndian: netmaskAddr.s_addr)
} else {
let topbits = Int(components[1])
guard let topbits = topbits, topbits >= 0 && topbits < 32 else {
return nil
}
binaryMask = (0xFFFFFFFF as UInt32) << (32 - topbits)
}
} else {
binaryMask = 0xFFFFFF00
}
address = String(components[0])
var networkAddr = in_addr()
let netmask = in_addr(s_addr: in_addr_t(bigEndian: binaryMask))
guard inet_pton(AF_INET, address, &networkAddr) == 1 else {
return nil
}
let firstAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr & netmask.s_addr) + 1).bigEndian)
let lastAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr | ~netmask.s_addr) - 1).bigEndian)
let firstAddrStr = String(cString: inet_ntoa(firstAddr))
let lastAddrStr = String(cString: inet_ntoa(lastAddr))
let netmaskStr = String(cString: inet_ntoa(netmask))
return (network.vlanDhcpStartAddress ?? firstAddrStr, network.vlanDhcpEndAddress ?? lastAddrStr, netmaskStr)
}
#if os(macOS)
private var defaultBridgedInterface: String {
VZBridgedNetworkInterface.networkInterfaces.first?.identifier ?? "en0"
}
#endif
@QEMUArgumentBuilder private var networkArguments: [QEMUArgument] {
for i in networks.indices {
if (isSparc && networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue) ||
(isClassicMacM68K && networks[i].hardware.rawValue == QEMUNetworkDevice_m68k.dp8393x.rawValue) {
f("-net")
"nic"
if networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue {
"model=lance"
}
"macaddr=\(networks[i].macAddress)"
"netdev=net\(i)"
f()
} else {
f("-device")
networks[i].hardware
"mac=\(networks[i].macAddress)"
"netdev=net\(i)"
f()
}
f("-netdev")
var useVMnet = false
#if os(macOS)
if networks[i].mode == .shared {
useVMnet = true
"vmnet-shared"
"id=net\(i)"
} else if networks[i].mode == .bridged {
useVMnet = true
"vmnet-bridged"
"id=net\(i)"
"ifname=\(networks[i].bridgeInterface ?? defaultBridgedInterface)"
} else if networks[i].mode == .host {
useVMnet = true
"vmnet-host"
"id=net\(i)"
if let netUuid = networks[i].hostNetUuid {
"net-uuid=\(netUuid)"
}
} else {
"user"
"id=net\(i)"
}
#else
"user"
"id=net\(i)"
#endif
if networks[i].isIsolateFromHost {
if useVMnet {
"isolated=on"
} else {
"restrict=on"
}
}
if useVMnet {
if let subnet = parseNetworkSubnet(from: networks[i]) {
"start-address=\(subnet.start)"
"end-address=\(subnet.end)"
"subnet-mask=\(subnet.mask)"
}
if let nat66prefix = networks[i].vlanGuestAddressIPv6 {
"nat66-prefix=\(nat66prefix)"
}
} else {
if let guestAddress = networks[i].vlanGuestAddress {
"net=\(guestAddress)"
}
if let hostAddress = networks[i].vlanHostAddress {
"host=\(hostAddress)"
}
if let guestAddressIPv6 = networks[i].vlanGuestAddressIPv6 {
"ipv6-net=\(guestAddressIPv6)"
}
if let hostAddressIPv6 = networks[i].vlanHostAddressIPv6 {
"ipv6-host=\(hostAddressIPv6)"
}
if let dhcpStartAddress = networks[i].vlanDhcpStartAddress {
"dhcpstart=\(dhcpStartAddress)"
}
if let dnsServerAddress = networks[i].vlanDnsServerAddress {
"dns=\(dnsServerAddress)"
}
if let dnsServerAddressIPv6 = networks[i].vlanDnsServerAddressIPv6 {
"ipv6-dns=\(dnsServerAddressIPv6)"
}
if let dnsSearchDomain = networks[i].vlanDnsSearchDomain {
"dnssearch=\(dnsSearchDomain)"
}
if let dhcpDomain = networks[i].vlanDhcpDomain {
"domainname=\(dhcpDomain)"
}
for forward in networks[i].portForward {
"hostfwd=\(forward.protocol.rawValue.lowercased()):\(forward.hostAddress ?? ""):\(forward.hostPort)-\(forward.guestAddress ?? ""):\(forward.guestPort)"
}
}
f()
}
if networks.count == 0 {
f("-nic")
f("none")
}
}
private var isSpiceAgentUsed: Bool {
guard system.architecture.hasAgentSupport && system.target.hasAgentSupport else {
return false
}
return sharing.hasClipboardSharing || sharing.directoryShareMode == .webdav || displays.contains(where: { $0.isDynamicResolution })
}
@QEMUArgumentBuilder private var sharingArguments: [QEMUArgument] {
if system.architecture.hasAgentSupport && system.target.hasAgentSupport {
f("-device")
f("virtio-serial")
f("-device")
f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
f("-chardev")
if isRemoteSpice {
"pipe"
"path="
guestAgentPipeURL
} else {
"spiceport"
"name=org.qemu.guest_agent.0"
}
"id=org.qemu.guest_agent"
f()
}
if isSpiceAgentUsed {
f("-device")
f("virtserialport,chardev=vdagent,name=com.redhat.spice.0")
f("-chardev")
f("spicevmc,id=vdagent,debug=0,name=vdagent")
if sharing.directoryShareMode == .webdav {
f("-device")
f("virtserialport,chardev=charchannel1,id=channel1,name=org.spice-space.webdav.0")
f("-chardev")
f("spiceport,name=org.spice-space.webdav.0,id=charchannel1")
}
}
if system.architecture.hasSharingSupport && sharing.directoryShareMode == .virtfs, let url = sharing.directoryShareUrl {
f("-fsdev")
"local"
"id=virtfs0"
"path="
url
"security_model=mapped-xattr"
if sharing.isDirectoryShareReadOnly {
"readonly=on"
}
f()
f("-device")
if system.architecture == .s390x {
"virtio-9p-ccw"
} else if system.architecture == .m68k {
"virtio-9p-device"
} else {
"virtio-9p-pci"
}
"fsdev=virtfs0"
if isClassicMacM68K || isClassicMacNewWorld {
"mount_tag=share_1"
} else {
"mount_tag=share"
}
}
}
private func cleanupName(_ name: String) -> String {
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let filteredString = name.components(separatedBy: allowedCharacterSet.inverted)
.joined(separator: "")
return filteredString
}
@QEMUArgumentBuilder private var miscArguments: [QEMUArgument] {
f("-name")
f(cleanupName(information.name))
if qemu.isDisposable {
f("-snapshot")
}
f("-uuid")
f(information.uuid.uuidString)
if qemu.hasRTCLocalTime {
f("-rtc")
f("base=localtime")
}
if qemu.hasRNGDevice {
f("-device")
f("virtio-rng-pci")
}
if qemu.hasBalloonDevice {
f("-device")
f("virtio-balloon-pci")
}
if qemu.hasTPMDevice {
tpmArguments
}
}
@QEMUArgumentBuilder private var tpmArguments: [QEMUArgument] {
f("-chardev")
"socket"
"id=chrtpm0"
"path=\(swtpmSocketURL.lastPathComponent)"
f()
f("-tpmdev")
"emulator"
"id=tpm0"
"chardev=chrtpm0"
f()
f("-device")
if system.target.rawValue.hasPrefix("virt") {
"tpm-crb-device"
} else if system.architecture == .ppc64 {
"tpm-spapr"
} else {
"tpm-tis"
}
"tpmdev=tpm0"
f()
}
}
@MainActor
private extension UTMQemuConfiguration {
#if arch(x86_64)
func highestIntelCPUConfigurationForHost() -> String? {
let cpufamily = Self.sysctlIntRead("hw.cpufamily")
// source: https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/mach/machine.h
switch cpufamily {
case 0x78ea4fbc: return "Penryn"
case 0x6b5a4cd2: return "Nehalem"
case 0x573b5eec: return "Westmere"
case 0x5490b78c: return "SandyBridge"
case 0x1f65e835: return "IvyBridge"
case 0x10b282dc: return "Haswell"
case 0x582ed09c: return "Broadwell"
case 0x37fc219f /* Skylake */, 0x0f817246 /* Kabylake */, 0x1cf8a03e /* Cometlake */: return "Skylake-Client"
case 0x38435547 /* Icelake */: return "Icelake-Server" // client doesn't exist
default: return nil
}
}
#else
func highestIntelCPUConfigurationForHost() -> String? {
return "Skylake-Client"
}
#endif
}
private extension String {
func appendingDefaultPropertyName(_ name: String, value: String) -> String {
if !self.contains(name + "=") {
return self.appending("\(self.count > 0 ? "," : "")\(name)=\(value)")
} else {
return self
}
}
}