UTMScriptingCreateCommand.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. //
  2. // Copyright © 2023 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. @MainActor
  18. @objc(UTMScriptingCreateCommand)
  19. class UTMScriptingCreateCommand: NSCreateCommand, UTMScriptable {
  20. private var bytesInMib: Int {
  21. 1048576
  22. }
  23. private var bytesInGib: Int {
  24. 1073741824
  25. }
  26. private var data: UTMData? {
  27. (NSApp.scriptingDelegate as? AppDelegate)?.data
  28. }
  29. @objc override func performDefaultImplementation() -> Any? {
  30. if createClassDescription.implementationClassName == "UTMScriptingVirtualMachineImpl" {
  31. withScriptCommand(self) { [self] in
  32. guard let backend = resolvedKeyDictionary["backend"] as? AEKeyword, let backend = UTMScriptingBackend(rawValue: backend) else {
  33. throw ScriptingError.backendNotFound
  34. }
  35. guard let configuration = resolvedKeyDictionary["configuration"] as? [AnyHashable : Any] else {
  36. throw ScriptingError.configurationNotFound
  37. }
  38. if backend == .qemu {
  39. return try await createQemuVirtualMachine(from: configuration).objectSpecifier
  40. } else if backend == .apple {
  41. return try await createAppleVirtualMachine(from: configuration).objectSpecifier
  42. } else {
  43. throw ScriptingError.backendNotFound
  44. }
  45. }
  46. return nil
  47. } else {
  48. return super.performDefaultImplementation()
  49. }
  50. }
  51. private func createQemuVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
  52. guard let data = data else {
  53. throw ScriptingError.notReady
  54. }
  55. guard record["name"] as? String != nil else {
  56. throw ScriptingError.nameNotSpecified
  57. }
  58. guard let architecture = record["architecture"] as? String, let architecture = QEMUArchitecture(rawValue: architecture) else {
  59. throw ScriptingError.architectureNotSpecified
  60. }
  61. let machine = record["machine"] as? String
  62. let target = architecture.targetType.init(rawValue: machine ?? "") ?? architecture.targetType.default
  63. let config = UTMQemuConfiguration()
  64. config.system.architecture = architecture
  65. config.system.target = target
  66. config.reset(forArchitecture: architecture, target: target)
  67. config.qemu.hasHypervisor = true
  68. config.qemu.hasUefiBoot = true
  69. // add default drives
  70. config.drives.append(UTMQemuConfigurationDrive(forArchitecture: architecture, target: target, isExternal: true))
  71. var fixed = UTMQemuConfigurationDrive(forArchitecture: architecture, target: target)
  72. fixed.sizeMib = 64 * bytesInGib / bytesInMib
  73. config.drives.append(fixed)
  74. // add a default serial device
  75. var serial = UTMQemuConfigurationSerial()
  76. serial.mode = .ptty
  77. config.serials = [serial]
  78. // remove GUI devices
  79. config.displays = []
  80. config.sound = []
  81. // parse the remaining config
  82. let wrapper = UTMScriptingConfigImpl(config)
  83. try wrapper.updateConfiguration(from: record)
  84. // create the vm
  85. let vm = try await data.create(config: config)
  86. return UTMScriptingVirtualMachineImpl(for: vm, data: data)
  87. }
  88. private func createAppleVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
  89. guard let data = data else {
  90. throw ScriptingError.notReady
  91. }
  92. guard #available(macOS 13, *) else {
  93. throw ScriptingError.backendNotSupported
  94. }
  95. guard record["name"] as? String != nil else {
  96. throw ScriptingError.nameNotSpecified
  97. }
  98. let config = UTMAppleConfiguration()
  99. config.system.boot = try UTMAppleConfigurationBoot(for: .linux)
  100. config.virtualization.hasBalloon = true
  101. config.virtualization.hasEntropy = true
  102. config.networks = [UTMAppleConfigurationNetwork()]
  103. // remove any display devices
  104. config.displays = []
  105. // add a default serial device
  106. var serial = UTMAppleConfigurationSerial()
  107. serial.mode = .ptty
  108. config.serials = [serial]
  109. // add default drives
  110. config.drives.append(UTMAppleConfigurationDrive(existingURL: nil, isExternal: true))
  111. config.drives.append(UTMAppleConfigurationDrive(newSize: 64 * bytesInGib / bytesInMib))
  112. // parse the remaining config
  113. let wrapper = UTMScriptingConfigImpl(config)
  114. try wrapper.updateConfiguration(from: record)
  115. // create the vm
  116. let vm = try await data.create(config: config)
  117. return UTMScriptingVirtualMachineImpl(for: vm, data: data)
  118. }
  119. enum ScriptingError: Error, LocalizedError {
  120. case notReady
  121. case backendNotFound
  122. case backendNotSupported
  123. case configurationNotFound
  124. case nameNotSpecified
  125. case architectureNotSpecified
  126. var errorDescription: String? {
  127. switch self {
  128. case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingAppDelegate")
  129. case .backendNotFound: return NSLocalizedString("A valid backend must be specified.", comment: "UTMScriptingAppDelegate")
  130. case .backendNotSupported: return NSLocalizedString("This backend is not supported on your machine.", comment: "UTMScriptingAppDelegate")
  131. case .configurationNotFound: return NSLocalizedString("A valid configuration must be specified.", comment: "UTMScriptingAppDelegate")
  132. case .nameNotSpecified: return NSLocalizedString("No name specified in the configuration.", comment: "UTMScriptingAppDelegate")
  133. case .architectureNotSpecified: return NSLocalizedString("No architecture specified in the configuration.", comment: "UTMScriptingAppDelegate")
  134. }
  135. }
  136. }
  137. }