UTMQemuConfiguration+Arguments.swift 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. #if os(macOS)
  18. import Virtualization // for getting network interfaces
  19. #endif
  20. /// Build QEMU arguments from config
  21. @MainActor extension UTMQemuConfiguration {
  22. /// Helper function to generate a final argument
  23. /// - Parameter string: Argument fragment
  24. /// - Returns: Final argument fragment
  25. private func f(_ string: String = "") -> QEMUArgumentFragment {
  26. QEMUArgumentFragment(final: string)
  27. }
  28. /// Shared between helper and main process to store Unix sockets
  29. var socketURL: URL {
  30. #if os(iOS) || os(visionOS)
  31. return FileManager.default.temporaryDirectory
  32. #else
  33. let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
  34. let helper = Bundle.main.infoDictionary?["HelperIdentifier"] as? String
  35. // default to unsigned sandbox path
  36. var parentURL: URL = FileManager.default.homeDirectoryForCurrentUser
  37. parentURL.deleteLastPathComponent()
  38. parentURL.deleteLastPathComponent()
  39. parentURL.appendPathComponent(helper ?? "com.utmapp.QEMUHelper")
  40. parentURL.appendPathComponent("Data")
  41. parentURL.appendPathComponent("tmp")
  42. if let appGroup = appGroup, !appGroup.hasPrefix("invalid.") {
  43. if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
  44. return containerURL
  45. }
  46. }
  47. return parentURL
  48. #endif
  49. }
  50. /// Return the socket file for communicating with SPICE
  51. var spiceSocketURL: URL {
  52. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("spice")
  53. }
  54. /// Return the socket file for communicating with SWTPM
  55. var swtpmSocketURL: URL {
  56. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
  57. }
  58. /// Used only if in remote server mode.
  59. var monitorPipeURL: URL {
  60. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp")
  61. }
  62. /// Used only if in remote server mode.
  63. var guestAgentPipeURL: URL {
  64. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga")
  65. }
  66. /// Used only if in remote server mode.
  67. var spiceTlsKeyUrl: URL {
  68. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("pem")
  69. }
  70. /// Used only if in remote server mode.
  71. var spiceTlsCertUrl: URL {
  72. socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("crt")
  73. }
  74. /// Used for placeholder images
  75. var placeholderUrl: URL {
  76. #if os(macOS)
  77. URL(fileURLWithPath: "/dev/null")
  78. #else
  79. let empty = FileManager.default.temporaryDirectory.appendingPathComponent("empty")
  80. FileManager.default.createFile(atPath: empty.path, contents: nil)
  81. return empty
  82. #endif
  83. }
  84. /// Global setting to always use file lock or not
  85. private var isUseFileLock: Bool {
  86. return UserDefaults.standard.value(forKey: "UseFileLock") == nil || UserDefaults.standard.bool(forKey: "UseFileLock")
  87. }
  88. /// Special arguments should disable use of bootindex
  89. private var shouldDisableBootIndex: Bool {
  90. // currently, we only identified this issue in PPC machines
  91. guard system.architecture == .ppc || system.architecture == .ppc64 else {
  92. return false
  93. }
  94. let arguments = parsedUserArguments
  95. for (index, arg) in arguments.enumerated() {
  96. // when user specifies '-prom-env boot-device=hd:,\yaboot' for example, it should inhibit bootindex
  97. if arg == "-prom-env" && index != arguments.count - 1 && arguments[index+1].starts(with: "boot-device=") {
  98. return true
  99. }
  100. }
  101. return false
  102. }
  103. /// Combined generated and user specified arguments.
  104. @QEMUArgumentBuilder var allArguments: [QEMUArgument] {
  105. generatedArguments
  106. userArguments
  107. }
  108. /// Only UTM generated arguments.
  109. @QEMUArgumentBuilder var generatedArguments: [QEMUArgument] {
  110. f("-L")
  111. resourceURL
  112. f()
  113. f("-S") // startup stopped
  114. spiceArguments
  115. networkArguments
  116. displayArguments
  117. serialArguments
  118. cpuArguments
  119. machineArguments
  120. architectureArguments
  121. soundArguments
  122. if isUsbUsed {
  123. usbArguments
  124. }
  125. otherInputsArguments
  126. drivesArguments
  127. sharingArguments
  128. miscArguments
  129. }
  130. /// Take user arguments and replace any quotes
  131. private var parsedUserArguments: [String] {
  132. var list = [String]()
  133. let regex = try! NSRegularExpression(pattern: "((?:[^\"\\s]*\"[^\"]*\"[^\"\\s]*)+|[^\"\\s]+)")
  134. for arg in qemu.additionalArguments {
  135. let argString = arg.string
  136. if argString.count > 0 {
  137. let range = NSRange(argString.startIndex..<argString.endIndex, in: argString)
  138. let split = regex.matches(in: argString, options: [], range: range)
  139. for match in split {
  140. let matchRange = Range(match.range(at: 1), in: argString)!
  141. var fragment = argString[matchRange]
  142. if fragment.first == "\"" && fragment.last == "\"" {
  143. fragment = fragment.dropFirst().dropLast()
  144. }
  145. list.append(String(fragment))
  146. }
  147. }
  148. }
  149. return list
  150. }
  151. @QEMUArgumentBuilder private var userArguments: [QEMUArgument] {
  152. for arg in parsedUserArguments {
  153. f(arg)
  154. }
  155. }
  156. @QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
  157. f("-spice")
  158. if let port = qemu.spiceServerPort {
  159. if qemu.isSpiceServerTlsEnabled {
  160. "tls-port=\(port)"
  161. "tls-channel=default"
  162. "x509-key-file="
  163. spiceTlsKeyUrl
  164. "x509-cert-file="
  165. spiceTlsCertUrl
  166. "x509-cacert-file="
  167. spiceTlsCertUrl
  168. } else {
  169. "port=\(port)"
  170. }
  171. } else {
  172. "unix=on"
  173. "addr=\(spiceSocketURL.lastPathComponent)"
  174. }
  175. if let _ = qemu.spiceServerPassword {
  176. "password-secret=secspice0"
  177. } else {
  178. "disable-ticketing=on"
  179. }
  180. if !isRemoteSpice {
  181. "image-compression=off"
  182. "playback-compression=off"
  183. "streaming-video=off"
  184. } else {
  185. "streaming-video=filter"
  186. }
  187. "gl=\(isGLSupported && !isRemoteSpice ? "on" : "off")"
  188. f()
  189. f("-chardev")
  190. if isRemoteSpice {
  191. "pipe"
  192. "path="
  193. monitorPipeURL
  194. } else {
  195. "spiceport"
  196. "name=org.qemu.monitor.qmp.0"
  197. }
  198. "id=org.qemu.monitor.qmp"
  199. f()
  200. f("-mon")
  201. f("chardev=org.qemu.monitor.qmp,mode=control")
  202. if !isSparc { // disable -vga and other default devices
  203. // prevent QEMU default devices, which leads to duplicate CD drive (fix #2538)
  204. // see https://github.com/qemu/qemu/blob/6005ee07c380cbde44292f5f6c96e7daa70f4f7d/docs/qdev-device-use.txt#L382
  205. f("-nodefaults")
  206. f("-vga")
  207. f("none")
  208. }
  209. if let password = qemu.spiceServerPassword {
  210. // assume anyone who can read this is in our trust domain
  211. f("-object")
  212. f("secret,id=secspice0,data=\(password)")
  213. }
  214. }
  215. private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice {
  216. if isRemoteSpice {
  217. let rawValue = display.rawValue
  218. if rawValue.hasSuffix("-gl") {
  219. return AnyQEMUConstant(rawValue: String(rawValue.dropLast(3)))!
  220. } else if rawValue.contains("-gl-") {
  221. return AnyQEMUConstant(rawValue: String(rawValue.replacingOccurrences(of: "-gl-", with: "-")))!
  222. } else {
  223. return display
  224. }
  225. } else {
  226. return display
  227. }
  228. }
  229. private func shouldSkipDisplay(_ display: UTMQemuConfigurationDisplay) -> Bool {
  230. return display.hardware.rawValue == QEMUDisplayDevice_m68k.nubus_macfb.rawValue
  231. }
  232. @QEMUArgumentBuilder private var displayArguments: [QEMUArgument] {
  233. if displays.isEmpty {
  234. f("-nographic")
  235. } else if isSparc { // only one display supported
  236. f("-vga")
  237. displays[0].hardware
  238. if let vgaRamSize = displays[0].vgaRamMib {
  239. "vgamem_mb=\(vgaRamSize)"
  240. }
  241. f()
  242. } else {
  243. for display in displays {
  244. if !shouldSkipDisplay(display) {
  245. f("-device")
  246. filterDisplayIfRemote(display.hardware)
  247. if let vgaRamSize = displays[0].vgaRamMib {
  248. "vgamem_mb=\(vgaRamSize)"
  249. }
  250. if display.hardware.rawValue.lowercased().contains("vga") && isClassicMacNewWorld {
  251. "edid=on"
  252. }
  253. f()
  254. }
  255. }
  256. }
  257. }
  258. private var isGLSupported: Bool {
  259. displays.contains { display in
  260. display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl")
  261. }
  262. }
  263. private var isSparc: Bool {
  264. system.architecture == .sparc || system.architecture == .sparc64
  265. }
  266. private var isRemoteSpice: Bool {
  267. qemu.spiceServerPort != nil
  268. }
  269. private var isClassicMacM68K: Bool {
  270. system.architecture == .m68k && system.target.rawValue == QEMUTarget_m68k.q800.rawValue
  271. }
  272. private var isClassicMacNewWorld: Bool {
  273. [.ppc, .ppc64].contains(system.architecture) && system.target.rawValue == QEMUTarget_ppc.mac99.rawValue
  274. }
  275. @QEMUArgumentBuilder private var serialArguments: [QEMUArgument] {
  276. for i in serials.indices {
  277. f("-chardev")
  278. switch serials[i].mode {
  279. case .builtin:
  280. f("spiceport,id=term\(i),name=com.utmapp.terminal.\(i)")
  281. case .tcpClient:
  282. "socket"
  283. "id=term\(i)"
  284. "port=\(serials[i].tcpPort ?? 1234)"
  285. "host=\(serials[i].tcpHostAddress ?? "example.com")"
  286. "server=off"
  287. f()
  288. case .tcpServer:
  289. "socket"
  290. "id=term\(i)"
  291. "port=\(serials[i].tcpPort ?? 1234)"
  292. "host=\(serials[i].isRemoteConnectionAllowed == true ? "0.0.0.0" : "127.0.0.1")"
  293. "server=on"
  294. "wait=\(serials[i].isWaitForConnection == true ? "on" : "off")"
  295. f()
  296. #if os(macOS)
  297. case .ptty:
  298. f("pty,id=term\(i)")
  299. #endif
  300. }
  301. switch serials[i].target {
  302. case .autoDevice:
  303. f("-serial")
  304. f("chardev:term\(i)")
  305. case .manualDevice:
  306. f("-device")
  307. f("\(serials[i].hardware?.rawValue ?? "invalid"),chardev=term\(i)")
  308. case .monitor:
  309. f("-mon")
  310. f("chardev=term\(i),mode=readline")
  311. case .gdb:
  312. f("-gdb")
  313. f("chardev:term\(i)")
  314. }
  315. }
  316. }
  317. @QEMUArgumentBuilder private var cpuArguments: [QEMUArgument] {
  318. if system.cpu.rawValue == system.architecture.cpuType.default.rawValue {
  319. // if default and not hypervisor, we don't pass any -cpu argument for x86 and use host for ARM
  320. if isHypervisorUsed {
  321. #if arch(x86_64)
  322. if let cpu = highestIntelCPUConfigurationForHost() {
  323. f("-cpu")
  324. f(cpu)
  325. }
  326. #else
  327. f("-cpu")
  328. f("host")
  329. #endif
  330. } else if system.architecture == .aarch64 {
  331. // ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
  332. f("-cpu")
  333. f("cortex-a72")
  334. } else if system.architecture == .arm {
  335. // ARM64 QEMU does not support "-cpu default" so we hard code a sensible default
  336. f("-cpu")
  337. f("cortex-a15")
  338. } else if system.architecture == .x86_64, let cpu = highestIntelCPUConfigurationForHost() {
  339. f("-cpu")
  340. f(cpu)
  341. }
  342. } else {
  343. f("-cpu")
  344. system.cpu
  345. for flag in system.cpuFlagsAdd {
  346. "+\(flag.rawValue)"
  347. }
  348. for flag in system.cpuFlagsRemove {
  349. "-\(flag.rawValue)"
  350. }
  351. f()
  352. }
  353. let emulatedCpuCount = self.emulatedCpuCount
  354. f("-smp")
  355. "cpus=\(emulatedCpuCount.1)"
  356. "sockets=1"
  357. "cores=\(emulatedCpuCount.0)"
  358. "threads=\(emulatedCpuCount.1/emulatedCpuCount.0)"
  359. f()
  360. }
  361. private static func sysctlIntRead(_ name: String) -> UInt64 {
  362. var value: UInt64 = 0
  363. var size = MemoryLayout<UInt64>.size
  364. sysctlbyname(name, &value, &size, nil, 0)
  365. return value
  366. }
  367. private var emulatedCpuCount: (Int, Int) {
  368. let singleCpu = (1, 1)
  369. let hostPhysicalCpu = Int(Self.sysctlIntRead("hw.physicalcpu"))
  370. let hostLogicalCpu = Int(Self.sysctlIntRead("hw.logicalcpu"))
  371. let userCpu = system.cpuCount
  372. if userCpu > 0 || hostPhysicalCpu == 0 {
  373. return (userCpu, userCpu) // user override
  374. }
  375. // SPARC5 defaults to single CPU
  376. if isSparc {
  377. return singleCpu
  378. }
  379. #if arch(arm64)
  380. let hostPcorePhysicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.physicalcpu"))
  381. let hostPcoreLogicalCpu = Int(Self.sysctlIntRead("hw.perflevel0.logicalcpu"))
  382. // in ARM we can only emulate other weak architectures
  383. let weakArchitectures: [QEMUArchitecture] = [.alpha, .arm, .aarch64, .avr, .mips, .mips64, .mipsel, .mips64el, .ppc, .ppc64, .riscv32, .riscv64, .xtensa, .xtensaeb]
  384. if weakArchitectures.contains(system.architecture) {
  385. if hostPcorePhysicalCpu > 0 {
  386. return (hostPcorePhysicalCpu, hostPcoreLogicalCpu)
  387. } else {
  388. return (hostPhysicalCpu, hostLogicalCpu)
  389. }
  390. } else {
  391. return singleCpu
  392. }
  393. #elseif arch(x86_64)
  394. // in x86 we can emulate weak on strong
  395. return (hostPhysicalCpu, hostLogicalCpu)
  396. #else
  397. return singleCpu
  398. #endif
  399. }
  400. private var isHypervisorUsed: Bool {
  401. system.architecture.hasHypervisorSupport && qemu.hasHypervisor
  402. }
  403. private var isTSOUsed: Bool {
  404. system.architecture.hasTSOSupport && qemu.hasTSO
  405. }
  406. private var isUsbUsed: Bool {
  407. system.architecture.hasUsbSupport && system.target.hasUsbSupport && input.usbBusSupport != .disabled
  408. }
  409. private var isSecureBootUsed: Bool {
  410. system.architecture.hasSecureBootSupport && system.target.hasSecureBootSupport && qemu.hasTPMDevice
  411. }
  412. @QEMUArgumentBuilder private var machineArguments: [QEMUArgument] {
  413. f("-machine")
  414. system.target
  415. f(machineProperties)
  416. if isHypervisorUsed {
  417. f("-accel")
  418. "hvf"
  419. if isTSOUsed {
  420. "tso=on"
  421. }
  422. f()
  423. } else {
  424. f("-accel")
  425. "tcg"
  426. if system.isForceMulticore {
  427. "thread=multi"
  428. }
  429. let tbSize = system.jitCacheSize > 0 ? system.jitCacheSize : system.memorySize / 4
  430. "tb-size=\(tbSize)"
  431. #if WITH_JIT
  432. // use mirror mapping when we don't have JIT entitlements
  433. if !UTMCapabilities.current.contains(.hasJitEntitlements) {
  434. "split-wx=on"
  435. }
  436. #endif
  437. f()
  438. }
  439. }
  440. private var machineProperties: String {
  441. let target = system.target.rawValue
  442. let architecture = system.architecture.rawValue
  443. var properties = qemu.machinePropertyOverride ?? ""
  444. if isPcCompatible {
  445. properties = properties.appendingDefaultPropertyName("vmport", value: "off")
  446. // disable PS/2 emulation if we are not legacy input and it's not explicitly enabled
  447. if isUsbUsed && !qemu.hasPS2Controller {
  448. properties = properties.appendingDefaultPropertyName("i8042", value: "off")
  449. }
  450. #if os(macOS)
  451. if useCoreAudioBackend && sound.contains(where: { $0.hardware.rawValue == "pcspk" }) {
  452. properties = properties.appendingDefaultPropertyName("pcspk-audiodev", value: "audio1")
  453. }
  454. #endif
  455. // disable HPET because it causes issues for some OS and also hinders performance
  456. properties = properties.appendingDefaultPropertyName("hpet", value: "off")
  457. }
  458. if target == "virt" || target.hasPrefix("virt-") && !architecture.hasPrefix("riscv") {
  459. if #available(macOS 12.4, iOS 15.5, *, *) {
  460. // default highmem value is fine here
  461. } else {
  462. // a kernel panic is triggered on M1 Max if highmem=on and running < macOS 12.4
  463. properties = properties.appendingDefaultPropertyName("highmem", value: "off")
  464. }
  465. // required to boot Windows ARM on TCG
  466. if system.architecture == .aarch64 && !isHypervisorUsed {
  467. properties = properties.appendingDefaultPropertyName("virtualization", value: "on")
  468. }
  469. // required for > 8 CPUs
  470. if system.architecture == .aarch64 && emulatedCpuCount.0 > 8 {
  471. properties = properties.appendingDefaultPropertyName("gic-version", value: "3")
  472. }
  473. }
  474. if isClassicMacM68K {
  475. if sound.contains(where: { $0.hardware.rawValue == QEMUSoundDevice_m68k.asc.rawValue }) {
  476. properties = properties.appendingDefaultPropertyName("audiodev", value: "audio0")
  477. }
  478. }
  479. if isClassicMacNewWorld {
  480. properties = properties.appendingDefaultPropertyName("via", value: "pmu")
  481. }
  482. return properties
  483. }
  484. @QEMUArgumentBuilder private var architectureArguments: [QEMUArgument] {
  485. if system.architecture == .x86_64 || system.architecture == .i386 {
  486. f("-global")
  487. f("PIIX4_PM.disable_s3=1") // applies for pc-i440fx-* types
  488. f("-global")
  489. f("ICH9-LPC.disable_s3=1") // applies for pc-q35-* types
  490. }
  491. if qemu.hasUefiBoot, let prefix = UTMQemuConfigurationQEMU.uefiImagePrefix(forArchitecture: system.architecture) {
  492. let secure = isSecureBootUsed ? "-secure" : ""
  493. let bios = resourceURL.appendingPathComponent("\(prefix)\(secure)-code.fd")
  494. let vars = qemu.efiVarsURL ?? URL(fileURLWithPath: "/\(QEMUPackageFileName.efiVariables.rawValue)")
  495. if !hasCustomBios && FileManager.default.fileExists(atPath: bios.path) {
  496. f("-drive")
  497. "if=pflash"
  498. "format=raw"
  499. "unit=0"
  500. "file.filename="
  501. bios
  502. "file.locking=off"
  503. "readonly=on"
  504. f()
  505. f("-drive")
  506. "if=pflash"
  507. "unit=1"
  508. "file.filename="
  509. vars
  510. if !isUseFileLock {
  511. "file.locking=off"
  512. }
  513. f()
  514. }
  515. }
  516. if isClassicMacM68K {
  517. let declrom = resourceURL.appendingPathComponent("m68k-declrom")
  518. f("-device")
  519. "nubus-virtio-mmio"
  520. "romfile="
  521. declrom
  522. f()
  523. }
  524. if isClassicMacNewWorld {
  525. let ndrvloader = resourceURL.appendingPathComponent("ppc-ndrvloader")
  526. f("-device")
  527. "loader"
  528. "addr=0x4000000"
  529. "file="
  530. ndrvloader
  531. f()
  532. f("-prom-env")
  533. f("boot-command=init-program go")
  534. }
  535. f("-m")
  536. system.memorySize
  537. f()
  538. }
  539. private var hasCustomBios: Bool {
  540. for drive in drives {
  541. if drive.imageType == .disk || drive.imageType == .cd {
  542. if drive.interface == .pflash {
  543. return true
  544. }
  545. } else if drive.imageType == .bios || drive.imageType == .linuxKernel {
  546. return true
  547. }
  548. }
  549. return false
  550. }
  551. private var resourceURL: URL {
  552. FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("qemu", isDirectory: true)
  553. }
  554. private var soundBackend: UTMQEMUSoundBackend {
  555. let value = UserDefaults.standard.integer(forKey: "QEMUSoundBackend")
  556. if let backend = UTMQEMUSoundBackend(rawValue: value), backend != .qemuSoundBackendMax {
  557. return backend
  558. } else {
  559. return .qemuSoundBackendDefault
  560. }
  561. }
  562. private var useCoreAudioBackend: Bool {
  563. #if os(iOS) || os(visionOS)
  564. return false
  565. #else
  566. // only support SPICE audio if we are running remotely
  567. if isRemoteSpice {
  568. return false
  569. }
  570. // pcspk doesn't work with SPICE audio
  571. if sound.contains(where: { $0.hardware.rawValue == "pcspk" }) {
  572. return true
  573. }
  574. if soundBackend == .qemuSoundBackendCoreAudio {
  575. return true
  576. }
  577. return false
  578. #endif
  579. }
  580. private func isInternalAudioDevice(_ device: any QEMUSoundDevice) -> Bool {
  581. [QEMUSoundDevice_i386.pcspk.rawValue, QEMUSoundDevice_ppc.screamer.rawValue, QEMUSoundDevice_m68k.asc.rawValue].contains(device.rawValue)
  582. }
  583. @QEMUArgumentBuilder private var soundArguments: [QEMUArgument] {
  584. if sound.isEmpty {
  585. f("-audio")
  586. f("none")
  587. } else if sound.contains(where: { $0.hardware.rawValue == "screamer" }) {
  588. #if os(iOS) || os(visionOS)
  589. f("-audio")
  590. f("none")
  591. #else
  592. // force CoreAudio backend for mac99 which only supports 44100 Hz
  593. f("-audio")
  594. f("coreaudio")
  595. #endif
  596. }
  597. if useCoreAudioBackend {
  598. f("-audiodev")
  599. "coreaudio"
  600. f("id=audio1")
  601. }
  602. f("-audiodev")
  603. "spice"
  604. f("id=audio0")
  605. // screamer has no extra device, pcspk is handled in machineProperties
  606. for _sound in sound.filter({ !isInternalAudioDevice($0.hardware) }) {
  607. f("-device")
  608. _sound.hardware
  609. if _sound.hardware.rawValue.contains("hda") {
  610. f()
  611. f("-device")
  612. if soundBackend == .qemuSoundBackendCoreAudio && useCoreAudioBackend {
  613. "hda-output"
  614. "audiodev=audio1"
  615. } else {
  616. "hda-duplex"
  617. "audiodev=audio0"
  618. }
  619. f()
  620. } else {
  621. if soundBackend == .qemuSoundBackendCoreAudio && useCoreAudioBackend {
  622. f("audiodev=audio1")
  623. } else {
  624. f("audiodev=audio0")
  625. }
  626. }
  627. }
  628. }
  629. @QEMUArgumentBuilder private var drivesArguments: [QEMUArgument] {
  630. var busInterfaceMap: [String: Int] = [:]
  631. let disableBootindex = shouldDisableBootIndex
  632. for drive in drives {
  633. let hasImage = !drive.isExternal && drive.imageURL != nil
  634. if drive.imageType == .disk || drive.imageType == .cd {
  635. driveArgument(for: drive, busInterfaceMap: &busInterfaceMap, disableBootIndex: disableBootindex)
  636. } else if hasImage {
  637. switch drive.imageType {
  638. case .bios:
  639. f("-bios")
  640. drive.imageURL!
  641. case .linuxKernel:
  642. f("-kernel")
  643. drive.imageURL!
  644. case .linuxInitrd:
  645. f("-initrd")
  646. drive.imageURL!
  647. case .linuxDtb:
  648. f("-dtb")
  649. drive.imageURL!
  650. default:
  651. f()
  652. }
  653. f()
  654. }
  655. }
  656. }
  657. private var isPcCompatible: Bool {
  658. guard system.architecture == .x86_64 || system.architecture == .i386 else {
  659. return false
  660. }
  661. return system.target.rawValue.starts(with: "pc") || system.target.rawValue == "q35" || system.target.rawValue == "isapc"
  662. }
  663. /// These machines are hard coded to have one IDE unit per bus in QEMU
  664. private var isIdeInterfaceSingleUnit: Bool {
  665. isPcCompatible ||
  666. system.target.rawValue == "microvm" ||
  667. system.target.rawValue == "cubieboard" ||
  668. system.target.rawValue == "highbank" ||
  669. system.target.rawValue == "midway" ||
  670. system.target.rawValue == "xlnx_zcu102"
  671. }
  672. @QEMUArgumentBuilder private func driveArgument(for drive: UTMQemuConfigurationDrive, busInterfaceMap: inout [String: Int], disableBootIndex: Bool = false) -> [QEMUArgument] {
  673. let isRemovable = drive.imageType == .cd || drive.isExternal
  674. let isCd = drive.imageType == .cd && drive.interface != .floppy
  675. var bootindex = busInterfaceMap["boot", default: 0]
  676. var busindex = busInterfaceMap[drive.interface.rawValue, default: 0]
  677. var realInterface = QEMUDriveInterface.none
  678. if drive.interface == .ide {
  679. f("-device")
  680. if isCd {
  681. "ide-cd"
  682. } else {
  683. "ide-hd"
  684. }
  685. if drive.interfaceVersion >= 1 && !isIdeInterfaceSingleUnit {
  686. "bus=ide.\(busindex / 2)"
  687. "unit=\(busindex % 2)"
  688. } else {
  689. "bus=ide.\(busindex)"
  690. }
  691. busindex += 1
  692. "drive=drive\(drive.id)"
  693. if !disableBootIndex {
  694. "bootindex=\(bootindex)"
  695. }
  696. bootindex += 1
  697. f()
  698. } else if drive.interface == .scsi {
  699. var bus = "scsi"
  700. if system.architecture != .sparc && system.architecture != .sparc64 && system.architecture != .m68k {
  701. bus = "scsi0"
  702. if busindex == 0 {
  703. f("-device")
  704. f("lsi53c895a,id=scsi0")
  705. }
  706. }
  707. if system.architecture == .m68k && system.target.rawValue == QEMUTarget_m68k.virt.rawValue {
  708. bus = "scsi0"
  709. if busindex == 0 {
  710. f("-device")
  711. f("virtio-scsi-device,id=scsi0")
  712. }
  713. }
  714. f("-device")
  715. if isCd {
  716. "scsi-cd"
  717. } else {
  718. "scsi-hd"
  719. }
  720. "bus=\(bus).0"
  721. "channel=0"
  722. "scsi-id=\(busindex)"
  723. busindex += 1
  724. "drive=drive\(drive.id)"
  725. if !disableBootIndex {
  726. "bootindex=\(bootindex)"
  727. }
  728. bootindex += 1
  729. f()
  730. } else if drive.interface == .virtio {
  731. f("-device")
  732. if system.architecture == .s390x {
  733. "virtio-blk-ccw"
  734. } else if system.architecture == .m68k {
  735. "virtio-blk-device"
  736. } else {
  737. "virtio-blk-pci"
  738. }
  739. "drive=drive\(drive.id)"
  740. if !disableBootIndex {
  741. "bootindex=\(bootindex)"
  742. }
  743. bootindex += 1
  744. f()
  745. } else if drive.interface == .nvme {
  746. f("-device")
  747. "nvme"
  748. "drive=drive\(drive.id)"
  749. "serial=\(drive.id)"
  750. if !disableBootIndex {
  751. "bootindex=\(bootindex)"
  752. }
  753. bootindex += 1
  754. f()
  755. } else if drive.interface == .usb {
  756. f("-device")
  757. // use usb 3 bus for virt system, unless using legacy input setting (this mirrors the code in argsForUsb)
  758. let isUsb3 = isUsbUsed && system.target.rawValue.hasPrefix("virt")
  759. "usb-storage"
  760. "drive=drive\(drive.id)"
  761. "removable=\(isRemovable)"
  762. if !disableBootIndex {
  763. "bootindex=\(bootindex)"
  764. }
  765. bootindex += 1
  766. if isUsb3 {
  767. "bus=usb-bus.0"
  768. }
  769. f()
  770. } else if drive.interface == .floppy {
  771. if isPcCompatible {
  772. f("-device")
  773. "isa-fdc"
  774. "id=fdc\(busindex)"
  775. if !disableBootIndex {
  776. "bootindexA=\(bootindex)"
  777. }
  778. bootindex += 1
  779. f()
  780. f("-device")
  781. "floppy"
  782. "unit=0"
  783. "bus=fdc\(busindex).0"
  784. busindex += 1
  785. "drive=drive\(drive.id)"
  786. f()
  787. } else {
  788. realInterface = drive.interface
  789. }
  790. } else {
  791. realInterface = drive.interface
  792. }
  793. busInterfaceMap["boot"] = bootindex
  794. busInterfaceMap[drive.interface.rawValue] = busindex
  795. f("-drive")
  796. switch realInterface {
  797. case .ide:
  798. "if=ide"
  799. case .scsi:
  800. "if=scsi"
  801. case .sd:
  802. "if=sd"
  803. case .mtd:
  804. "if=mtd"
  805. case .floppy:
  806. "if=floppy"
  807. case .pflash:
  808. "if=pflash"
  809. default:
  810. "if=none"
  811. }
  812. if isCd {
  813. "media=cdrom"
  814. } else {
  815. "media=disk"
  816. }
  817. "id=drive\(drive.id)"
  818. if let imageURL = drive.imageURL {
  819. "file.filename="
  820. imageURL
  821. } else if !isCd {
  822. "file.filename="
  823. placeholderUrl
  824. }
  825. if drive.isReadOnly || isCd {
  826. if drive.imageURL != nil {
  827. "file.locking=off"
  828. }
  829. "readonly=on"
  830. } else {
  831. "discard=unmap"
  832. "detect-zeroes=unmap"
  833. }
  834. if !isUseFileLock {
  835. "file.locking=off"
  836. }
  837. f()
  838. }
  839. @QEMUArgumentBuilder private var usbArguments: [QEMUArgument] {
  840. if system.target.rawValue.hasPrefix("virt") {
  841. f("-device")
  842. f("nec-usb-xhci,id=usb-bus")
  843. } else {
  844. f("-usb")
  845. }
  846. if !isClassicMacNewWorld {
  847. f("-device")
  848. f("usb-tablet,bus=usb-bus.0")
  849. }
  850. if !qemu.hasPS2Controller {
  851. f("-device")
  852. f("usb-mouse,bus=usb-bus.0")
  853. f("-device")
  854. f("usb-kbd,bus=usb-bus.0")
  855. }
  856. #if WITH_USB
  857. let maxDevices = input.maximumUsbShare
  858. let buses = (maxDevices + 2) / 3
  859. if input.usbBusSupport == .usb3_0 {
  860. var controller = "qemu-xhci"
  861. if isPcCompatible {
  862. controller = "nec-usb-xhci"
  863. }
  864. for i in 0..<buses {
  865. f("-device")
  866. f("\(controller),id=usb-controller-\(i)")
  867. }
  868. } else {
  869. for i in 0..<buses {
  870. f("-device")
  871. f("ich9-usb-ehci1,id=usb-controller-\(i)")
  872. f("-device")
  873. f("ich9-usb-uhci1,masterbus=usb-controller-\(i).0,firstport=0,multifunction=on")
  874. f("-device")
  875. f("ich9-usb-uhci2,masterbus=usb-controller-\(i).0,firstport=2,multifunction=on")
  876. f("-device")
  877. f("ich9-usb-uhci3,masterbus=usb-controller-\(i).0,firstport=4,multifunction=on")
  878. }
  879. }
  880. // set up usb forwarding
  881. for i in 0..<maxDevices {
  882. f("-chardev")
  883. f("spicevmc,name=usbredir,id=usbredirchardev\(i)")
  884. f("-device")
  885. f("usb-redir,chardev=usbredirchardev\(i),id=usbredirdev\(i),bus=usb-controller-\(i/3).0")
  886. }
  887. #endif
  888. }
  889. @QEMUArgumentBuilder private var otherInputsArguments: [QEMUArgument] {
  890. if isClassicMacNewWorld {
  891. f("-device")
  892. f("virtio-tablet-pci")
  893. }
  894. if isClassicMacM68K {
  895. f("-device")
  896. f("virtio-tablet-device")
  897. }
  898. }
  899. private func parseNetworkSubnet(from network: UTMQemuConfigurationNetwork) -> (start: String, end: String, mask: String)? {
  900. guard let net = network.vlanGuestAddress else {
  901. return nil
  902. }
  903. let components = net.split(separator: "/")
  904. let address: String
  905. let binaryMask: UInt32
  906. guard components.count >= 1 else {
  907. return nil
  908. }
  909. if components.count == 2 {
  910. var netmaskAddr = in_addr()
  911. if inet_pton(AF_INET, String(components[1]), &netmaskAddr) == 1 {
  912. binaryMask = UInt32(bigEndian: netmaskAddr.s_addr)
  913. } else {
  914. let topbits = Int(components[1])
  915. guard let topbits = topbits, topbits >= 0 && topbits < 32 else {
  916. return nil
  917. }
  918. binaryMask = (0xFFFFFFFF as UInt32) << (32 - topbits)
  919. }
  920. } else {
  921. binaryMask = 0xFFFFFF00
  922. }
  923. address = String(components[0])
  924. var networkAddr = in_addr()
  925. let netmask = in_addr(s_addr: in_addr_t(bigEndian: binaryMask))
  926. guard inet_pton(AF_INET, address, &networkAddr) == 1 else {
  927. return nil
  928. }
  929. let firstAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr & netmask.s_addr) + 1).bigEndian)
  930. let lastAddr = in_addr(s_addr: (in_addr_t(bigEndian: networkAddr.s_addr | ~netmask.s_addr) - 1).bigEndian)
  931. let firstAddrStr = String(cString: inet_ntoa(firstAddr))
  932. let lastAddrStr = String(cString: inet_ntoa(lastAddr))
  933. let netmaskStr = String(cString: inet_ntoa(netmask))
  934. return (network.vlanDhcpStartAddress ?? firstAddrStr, network.vlanDhcpEndAddress ?? lastAddrStr, netmaskStr)
  935. }
  936. #if os(macOS)
  937. private var defaultBridgedInterface: String {
  938. VZBridgedNetworkInterface.networkInterfaces.first?.identifier ?? "en0"
  939. }
  940. #endif
  941. @QEMUArgumentBuilder private var networkArguments: [QEMUArgument] {
  942. for i in networks.indices {
  943. if (isSparc && networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue) ||
  944. (isClassicMacM68K && networks[i].hardware.rawValue == QEMUNetworkDevice_m68k.dp8393x.rawValue) {
  945. f("-net")
  946. "nic"
  947. if networks[i].hardware.rawValue == QEMUNetworkDevice_sparc.lance.rawValue {
  948. "model=lance"
  949. }
  950. "macaddr=\(networks[i].macAddress)"
  951. "netdev=net\(i)"
  952. f()
  953. } else {
  954. f("-device")
  955. networks[i].hardware
  956. "mac=\(networks[i].macAddress)"
  957. "netdev=net\(i)"
  958. f()
  959. }
  960. f("-netdev")
  961. var useVMnet = false
  962. #if os(macOS)
  963. if networks[i].mode == .shared {
  964. useVMnet = true
  965. "vmnet-shared"
  966. "id=net\(i)"
  967. } else if networks[i].mode == .bridged {
  968. useVMnet = true
  969. "vmnet-bridged"
  970. "id=net\(i)"
  971. "ifname=\(networks[i].bridgeInterface ?? defaultBridgedInterface)"
  972. } else if networks[i].mode == .host {
  973. useVMnet = true
  974. "vmnet-host"
  975. "id=net\(i)"
  976. if let netUuid = networks[i].hostNetUuid {
  977. "net-uuid=\(netUuid)"
  978. }
  979. } else {
  980. "user"
  981. "id=net\(i)"
  982. }
  983. #else
  984. "user"
  985. "id=net\(i)"
  986. #endif
  987. if networks[i].isIsolateFromHost {
  988. if useVMnet {
  989. "isolated=on"
  990. } else {
  991. "restrict=on"
  992. }
  993. }
  994. if useVMnet {
  995. if let subnet = parseNetworkSubnet(from: networks[i]) {
  996. "start-address=\(subnet.start)"
  997. "end-address=\(subnet.end)"
  998. "subnet-mask=\(subnet.mask)"
  999. }
  1000. if let nat66prefix = networks[i].vlanGuestAddressIPv6 {
  1001. "nat66-prefix=\(nat66prefix)"
  1002. }
  1003. } else {
  1004. if let guestAddress = networks[i].vlanGuestAddress {
  1005. "net=\(guestAddress)"
  1006. }
  1007. if let hostAddress = networks[i].vlanHostAddress {
  1008. "host=\(hostAddress)"
  1009. }
  1010. if let guestAddressIPv6 = networks[i].vlanGuestAddressIPv6 {
  1011. "ipv6-net=\(guestAddressIPv6)"
  1012. }
  1013. if let hostAddressIPv6 = networks[i].vlanHostAddressIPv6 {
  1014. "ipv6-host=\(hostAddressIPv6)"
  1015. }
  1016. if let dhcpStartAddress = networks[i].vlanDhcpStartAddress {
  1017. "dhcpstart=\(dhcpStartAddress)"
  1018. }
  1019. if let dnsServerAddress = networks[i].vlanDnsServerAddress {
  1020. "dns=\(dnsServerAddress)"
  1021. }
  1022. if let dnsServerAddressIPv6 = networks[i].vlanDnsServerAddressIPv6 {
  1023. "ipv6-dns=\(dnsServerAddressIPv6)"
  1024. }
  1025. if let dnsSearchDomain = networks[i].vlanDnsSearchDomain {
  1026. "dnssearch=\(dnsSearchDomain)"
  1027. }
  1028. if let dhcpDomain = networks[i].vlanDhcpDomain {
  1029. "domainname=\(dhcpDomain)"
  1030. }
  1031. for forward in networks[i].portForward {
  1032. "hostfwd=\(forward.protocol.rawValue.lowercased()):\(forward.hostAddress ?? ""):\(forward.hostPort)-\(forward.guestAddress ?? ""):\(forward.guestPort)"
  1033. }
  1034. }
  1035. f()
  1036. }
  1037. if networks.count == 0 {
  1038. f("-nic")
  1039. f("none")
  1040. }
  1041. }
  1042. private var isSpiceAgentUsed: Bool {
  1043. guard system.architecture.hasAgentSupport && system.target.hasAgentSupport else {
  1044. return false
  1045. }
  1046. return sharing.hasClipboardSharing || sharing.directoryShareMode == .webdav || displays.contains(where: { $0.isDynamicResolution })
  1047. }
  1048. @QEMUArgumentBuilder private var sharingArguments: [QEMUArgument] {
  1049. if system.architecture.hasAgentSupport && system.target.hasAgentSupport {
  1050. f("-device")
  1051. f("virtio-serial")
  1052. f("-device")
  1053. f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
  1054. f("-chardev")
  1055. if isRemoteSpice {
  1056. "pipe"
  1057. "path="
  1058. guestAgentPipeURL
  1059. } else {
  1060. "spiceport"
  1061. "name=org.qemu.guest_agent.0"
  1062. }
  1063. "id=org.qemu.guest_agent"
  1064. f()
  1065. }
  1066. if isSpiceAgentUsed {
  1067. f("-device")
  1068. f("virtserialport,chardev=vdagent,name=com.redhat.spice.0")
  1069. f("-chardev")
  1070. f("spicevmc,id=vdagent,debug=0,name=vdagent")
  1071. if sharing.directoryShareMode == .webdav {
  1072. f("-device")
  1073. f("virtserialport,chardev=charchannel1,id=channel1,name=org.spice-space.webdav.0")
  1074. f("-chardev")
  1075. f("spiceport,name=org.spice-space.webdav.0,id=charchannel1")
  1076. }
  1077. }
  1078. if system.architecture.hasSharingSupport && sharing.directoryShareMode == .virtfs, let url = sharing.directoryShareUrl {
  1079. f("-fsdev")
  1080. "local"
  1081. "id=virtfs0"
  1082. "path="
  1083. url
  1084. "security_model=mapped-xattr"
  1085. if sharing.isDirectoryShareReadOnly {
  1086. "readonly=on"
  1087. }
  1088. f()
  1089. f("-device")
  1090. if system.architecture == .s390x {
  1091. "virtio-9p-ccw"
  1092. } else if system.architecture == .m68k {
  1093. "virtio-9p-device"
  1094. } else {
  1095. "virtio-9p-pci"
  1096. }
  1097. "fsdev=virtfs0"
  1098. if isClassicMacM68K || isClassicMacNewWorld {
  1099. "mount_tag=share_1"
  1100. } else {
  1101. "mount_tag=share"
  1102. }
  1103. }
  1104. }
  1105. private func cleanupName(_ name: String) -> String {
  1106. let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
  1107. let filteredString = name.components(separatedBy: allowedCharacterSet.inverted)
  1108. .joined(separator: "")
  1109. return filteredString
  1110. }
  1111. @QEMUArgumentBuilder private var miscArguments: [QEMUArgument] {
  1112. f("-name")
  1113. f(cleanupName(information.name))
  1114. if qemu.isDisposable {
  1115. f("-snapshot")
  1116. }
  1117. f("-uuid")
  1118. f(information.uuid.uuidString)
  1119. if qemu.hasRTCLocalTime {
  1120. f("-rtc")
  1121. f("base=localtime")
  1122. }
  1123. if qemu.hasRNGDevice {
  1124. f("-device")
  1125. f("virtio-rng-pci")
  1126. }
  1127. if qemu.hasBalloonDevice {
  1128. f("-device")
  1129. f("virtio-balloon-pci")
  1130. }
  1131. if qemu.hasTPMDevice {
  1132. tpmArguments
  1133. }
  1134. }
  1135. @QEMUArgumentBuilder private var tpmArguments: [QEMUArgument] {
  1136. f("-chardev")
  1137. "socket"
  1138. "id=chrtpm0"
  1139. "path=\(swtpmSocketURL.lastPathComponent)"
  1140. f()
  1141. f("-tpmdev")
  1142. "emulator"
  1143. "id=tpm0"
  1144. "chardev=chrtpm0"
  1145. f()
  1146. f("-device")
  1147. if system.target.rawValue.hasPrefix("virt") {
  1148. "tpm-crb-device"
  1149. } else if system.architecture == .ppc64 {
  1150. "tpm-spapr"
  1151. } else {
  1152. "tpm-tis"
  1153. }
  1154. "tpmdev=tpm0"
  1155. f()
  1156. }
  1157. }
  1158. @MainActor
  1159. private extension UTMQemuConfiguration {
  1160. #if arch(x86_64)
  1161. func highestIntelCPUConfigurationForHost() -> String? {
  1162. let cpufamily = Self.sysctlIntRead("hw.cpufamily")
  1163. // source: https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/mach/machine.h
  1164. switch cpufamily {
  1165. case 0x78ea4fbc: return "Penryn"
  1166. case 0x6b5a4cd2: return "Nehalem"
  1167. case 0x573b5eec: return "Westmere"
  1168. case 0x5490b78c: return "SandyBridge"
  1169. case 0x1f65e835: return "IvyBridge"
  1170. case 0x10b282dc: return "Haswell"
  1171. case 0x582ed09c: return "Broadwell"
  1172. case 0x37fc219f /* Skylake */, 0x0f817246 /* Kabylake */, 0x1cf8a03e /* Cometlake */: return "Skylake-Client"
  1173. case 0x38435547 /* Icelake */: return "Icelake-Server" // client doesn't exist
  1174. default: return nil
  1175. }
  1176. }
  1177. #else
  1178. func highestIntelCPUConfigurationForHost() -> String? {
  1179. return "Skylake-Client"
  1180. }
  1181. #endif
  1182. }
  1183. private extension String {
  1184. func appendingDefaultPropertyName(_ name: String, value: String) -> String {
  1185. if !self.contains(name + "=") {
  1186. return self.appending("\(self.count > 0 ? "," : "")\(name)=\(value)")
  1187. } else {
  1188. return self
  1189. }
  1190. }
  1191. }