1043 lines
36 KiB
Swift
1043 lines
36 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 between helper and main process to store Unix sockets
|
|
var socketURL: URL {
|
|
#if os(iOS) || os(visionOS)
|
|
return FileManager.default.temporaryDirectory
|
|
#else
|
|
let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
|
|
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 = appGroup, !appGroup.hasPrefix("invalid.") {
|
|
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 sever mode.
|
|
var monitorPipeURL: URL {
|
|
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp")
|
|
}
|
|
|
|
/// Used only if in remote sever mode.
|
|
var guestAgentPipeURL: URL {
|
|
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga")
|
|
}
|
|
|
|
/// Used only if in remote sever mode.
|
|
var spiceTlsKeyUrl: URL {
|
|
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("pem")
|
|
}
|
|
|
|
/// Used only if in remote sever mode.
|
|
var spiceTlsCertUrl: URL {
|
|
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("crt")
|
|
}
|
|
|
|
/// 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
|
|
if !sound.isEmpty {
|
|
soundArguments
|
|
}
|
|
if isUsbUsed {
|
|
usbArguments
|
|
}
|
|
drivesArguments
|
|
sharingArguments
|
|
miscArguments
|
|
}
|
|
|
|
@QEMUArgumentBuilder private var userArguments: [QEMUArgument] {
|
|
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)!
|
|
let fragment = argString[matchRange]
|
|
f(fragment.replacingOccurrences(of: "\"", with: ""))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@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)"
|
|
}
|
|
"disable-ticketing=on"
|
|
if !isRemoteSpice {
|
|
"image-compression=off"
|
|
"playback-compression=off"
|
|
"streaming-video=off"
|
|
} else {
|
|
"streaming-video=filter"
|
|
}
|
|
"gl=\(isGLSupported && !isRemoteSpice ? "on" : "off")"
|
|
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")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
@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 {
|
|
f("-device")
|
|
filterDisplayIfRemote(display.hardware)
|
|
if let vgaRamSize = displays[0].vgaRamMib {
|
|
"vgamem_mb=\(vgaRamSize)"
|
|
}
|
|
f()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var isGLSupported: Bool {
|
|
displays.contains { display in
|
|
display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
|
|
}
|
|
}
|
|
|
|
private var isSparc: Bool {
|
|
system.architecture == .sparc || system.architecture == .sparc64
|
|
}
|
|
|
|
private var isRemoteSpice: Bool {
|
|
qemu.spiceServerPort != nil
|
|
}
|
|
|
|
@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)
|
|
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 {
|
|
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"
|
|
}
|
|
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 target.hasPrefix("pc") || target.hasPrefix("q35") || target == "isapc" {
|
|
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 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
|
|
if system.architecture == .aarch64 && emulatedCpuCount.0 > 8 {
|
|
properties = properties.appendingDefaultPropertyName("gic-version", value: "3")
|
|
}
|
|
}
|
|
if target == "mac99" {
|
|
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 secure = isSecureBootUsed ? "-secure" : ""
|
|
let code = system.target.rawValue == "microvm" ? "microvm" : "code"
|
|
let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)\(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="
|
|
vars
|
|
f()
|
|
}
|
|
}
|
|
f("-m")
|
|
system.memorySize
|
|
f()
|
|
}
|
|
|
|
private 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 {
|
|
Bundle.main.url(forResource: "qemu", withExtension: nil)!
|
|
}
|
|
|
|
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
|
|
}
|
|
// force CoreAudio backend for mac99 which only supports 44100 Hz
|
|
// pcspk doesn't work with SPICE audio
|
|
if sound.contains(where: { $0.hardware.rawValue == "screamer" || $0.hardware.rawValue == "pcspk" }) {
|
|
return true
|
|
}
|
|
if soundBackend == .qemuSoundBackendCoreAudio {
|
|
return true
|
|
}
|
|
return false
|
|
#endif
|
|
}
|
|
|
|
@QEMUArgumentBuilder private var soundArguments: [QEMUArgument] {
|
|
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({ $0.hardware.rawValue != "screamer" && $0.hardware.rawValue != "pcspk" }) {
|
|
f("-device")
|
|
_sound.hardware
|
|
if _sound.hardware.rawValue.contains("hda") {
|
|
f()
|
|
f("-device")
|
|
if soundBackend == .qemuSoundBackendCoreAudio {
|
|
"hda-output"
|
|
"audiodev=audio1"
|
|
} else {
|
|
"hda-duplex"
|
|
"audiodev=audio0"
|
|
}
|
|
f()
|
|
} else {
|
|
if soundBackend == .qemuSoundBackendCoreAudio {
|
|
f("audiodev=audio1")
|
|
} else {
|
|
f("audiodev=audio0")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@QEMUArgumentBuilder private var drivesArguments: [QEMUArgument] {
|
|
var busInterfaceMap: [String: Int] = [:]
|
|
for drive in drives {
|
|
let hasImage = !drive.isExternal && drive.imageURL != nil
|
|
if drive.imageType == .disk || drive.imageType == .cd {
|
|
driveArgument(for: drive, busInterfaceMap: &busInterfaceMap)
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// These machines are hard coded to have one IDE unit per bus in QEMU
|
|
private var isIdeInterfaceSingleUnit: Bool {
|
|
system.target.rawValue.contains("q35") ||
|
|
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]) -> [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)"
|
|
"bootindex=\(bootindex)"
|
|
bootindex += 1
|
|
f()
|
|
} else if drive.interface == .scsi {
|
|
var bus = "scsi"
|
|
if system.architecture != .sparc && system.architecture != .sparc64 {
|
|
bus = "scsi0"
|
|
if busindex == 0 {
|
|
f("-device")
|
|
f("lsi53c895a,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)"
|
|
"bootindex=\(bootindex)"
|
|
bootindex += 1
|
|
f()
|
|
} else if drive.interface == .virtio {
|
|
f("-device")
|
|
if system.architecture == .s390x {
|
|
"virtio-blk-ccw"
|
|
} else {
|
|
"virtio-blk-pci"
|
|
}
|
|
"drive=drive\(drive.id)"
|
|
"bootindex=\(bootindex)"
|
|
bootindex += 1
|
|
f()
|
|
} else if drive.interface == .nvme {
|
|
f("-device")
|
|
"nvme"
|
|
"drive=drive\(drive.id)"
|
|
"serial=\(drive.id)"
|
|
"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)"
|
|
"bootindex=\(bootindex)"
|
|
bootindex += 1
|
|
if isUsb3 {
|
|
"bus=usb-bus.0"
|
|
}
|
|
f()
|
|
} else if drive.interface == .floppy {
|
|
if system.target.rawValue.hasPrefix("q35") {
|
|
f("-device")
|
|
"isa-fdc"
|
|
"id=fdc\(busindex)"
|
|
"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="
|
|
imageURL
|
|
} else if !isCd {
|
|
"file.filename=/dev/null"
|
|
"file.locking=off"
|
|
}
|
|
if drive.isReadOnly || isCd {
|
|
"readonly=on"
|
|
} else {
|
|
"discard=unmap"
|
|
"detect-zeroes=unmap"
|
|
}
|
|
f()
|
|
}
|
|
|
|
@QEMUArgumentBuilder private var usbArguments: [QEMUArgument] {
|
|
if system.target.rawValue.hasPrefix("virt") {
|
|
f("-device")
|
|
f("nec-usb-xhci,id=usb-bus")
|
|
} else {
|
|
f("-usb")
|
|
}
|
|
f("-device")
|
|
f("usb-tablet,bus=usb-bus.0")
|
|
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 system.target.rawValue.hasPrefix("pc") || system.target.rawValue.hasPrefix("q35") {
|
|
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
|
|
}
|
|
|
|
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 {
|
|
f("-net")
|
|
"nic"
|
|
"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)"
|
|
} 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 {
|
|
"virtio-9p-pci"
|
|
}
|
|
"fsdev=virtfs0"
|
|
"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()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|