Skip to content

Commit 1d42ceb

Browse files
committed
scripting: add make virtual machine
1 parent 4b4b33d commit 1d42ceb

File tree

8 files changed

+241
-139
lines changed

8 files changed

+241
-139
lines changed

Platform/macOS/UTMPatches.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ extension NSApplication {
123123
}
124124
}
125125

126+
@objc func handleCreateCommand(_ command: NSCreateCommand) {
127+
(scriptingDelegate as? AppDelegate)?.handleCreateCommand(command)
128+
}
129+
126130
fileprivate static func patchApplicationScripting() {
127131
patch(#selector(Self.value(forKey:)),
128132
with: #selector(Self.xxx_value(forKey:)),

Scripting/UTM.sdef

Lines changed: 6 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
33

4-
<dictionary title="UTM Terminology">
4+
<dictionary title="UTM Terminology" xmlns:xi="http://www.w3.org/2003/XInclude">
55

6-
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
7-
<access-group identifier="com.utmapp.UTM.vm-access" />
8-
9-
<enumeration name="printing error handling" code="enum">
10-
<enumerator name="standard" code="lwst" description="Standard PostScript error handling">
11-
<cocoa boolean-value="NO"/>
12-
</enumerator>
13-
<enumerator name="detailed" code="lwdt" description="print a detailed report of PostScript errors">
14-
<cocoa boolean-value="YES"/>
15-
</enumerator>
16-
</enumeration>
17-
18-
<command name="close" code="coreclos" description="Close a document.">
19-
<cocoa class="NSCloseCommand"/>
20-
<access-group identifier="*"/>
21-
<direct-parameter type="specifier" requires-access="r" description="the document(s) or window(s) to close."/>
22-
</command>
23-
24-
<command name="quit" code="aevtquit" description="Quit the application.">
25-
<cocoa class="NSQuitCommand"/>
26-
</command>
27-
28-
<command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
29-
<cocoa class="NSCountCommand"/>
30-
<access-group identifier="*"/>
31-
<direct-parameter type="specifier" requires-access="r" description="The objects to be counted."/>
32-
<parameter name="each" code="kocl" type="type" optional="yes" description="The class of objects to be counted." hidden="yes">
33-
<cocoa key="ObjectClass"/>
34-
</parameter>
35-
<result type="integer" description="The count."/>
36-
</command>
37-
38-
<command name="exists" code="coredoex" description="Verify that an object exists.">
39-
<cocoa class="NSExistsCommand"/>
40-
<access-group identifier="*"/>
41-
<direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
42-
<result type="boolean" description="Did the object(s) exist?"/>
43-
</command>
44-
45-
<class name="application" code="capp" description="The application's top-level scripting object.">
46-
<cocoa class="NSApplication"/>
47-
<property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
48-
<property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
49-
<cocoa key="isActive"/>
50-
</property>
51-
<property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
52-
<element type="window" access="r">
53-
<cocoa key="orderedWindows"/>
54-
</element>
55-
<responds-to command="quit">
56-
<cocoa method="handleQuitScriptCommand:"/>
57-
</responds-to>
58-
</class>
59-
60-
<class name="window" code="cwin" description="A window.">
61-
<cocoa class="NSWindow"/>
62-
<property name="name" code="pnam" type="text" access="r" description="The title of the window.">
63-
<cocoa key="title"/>
64-
</property>
65-
<property name="id" code="ID " type="integer" access="r" description="The unique identifier of the window.">
66-
<cocoa key="uniqueID"/>
67-
</property>
68-
<property name="index" code="pidx" type="integer" description="The index of the window, ordered front to back.">
69-
<cocoa key="orderedIndex"/>
70-
</property>
71-
<property name="bounds" code="pbnd" type="rectangle" description="The bounding rectangle of the window.">
72-
<cocoa key="boundsAsQDRect"/>
73-
</property>
74-
<property name="closeable" code="hclb" type="boolean" access="r" description="Does the window have a close button?">
75-
<cocoa key="hasCloseBox"/>
76-
</property>
77-
<property name="miniaturizable" code="ismn" type="boolean" access="r" description="Does the window have a minimize button?">
78-
<cocoa key="isMiniaturizable"/>
79-
</property>
80-
<property name="miniaturized" code="pmnd" type="boolean" description="Is the window minimized right now?">
81-
<cocoa key="isMiniaturized"/>
82-
</property>
83-
<property name="resizable" code="prsz" type="boolean" access="r" description="Can the window be resized?">
84-
<cocoa key="isResizable"/>
85-
</property>
86-
<property name="visible" code="pvis" type="boolean" description="Is the window visible right now?">
87-
<cocoa key="isVisible"/>
88-
</property>
89-
<property name="zoomable" code="iszm" type="boolean" access="r" description="Does the window have a zoom button?">
90-
<cocoa key="isZoomable"/>
91-
</property>
92-
<property name="zoomed" code="pzum" type="boolean" description="Is the window zoomed right now?">
93-
<cocoa key="isZoomed"/>
94-
</property>
95-
<responds-to command="close">
96-
<cocoa method="handleCloseScriptCommand:"/>
97-
</responds-to>
98-
</class>
99-
100-
</suite>
6+
<xi:include href="file:///System/Library/ScriptingDefinitions/CocoaStandard.sdef" xpointer="xpointer(/dictionary/suite)"/>
1017

1028
<suite name="UTM Suite" code="UTMs" description="UTM virtual machines scripting suite.">
1039
<access-group identifier="com.utmapp.UTM.vm-access" />
@@ -109,6 +15,9 @@
10915
<property name="auto terminate" code="kRlW" type="boolean" description="Auto terminate the application when all windows are closed?">
11016
<cocoa key="isAutoTerminate"/>
11117
</property>
18+
<responds-to command="make">
19+
<cocoa method="handleCreateCommand:"/>
20+
</responds-to>
11221
</class-extension>
11322

11423
<enumeration name="backend" code="VmEb" description="Backend type.">
@@ -463,7 +372,7 @@
463372
<property name="name" code="pnam" type="text"
464373
description="Virtual machine name."/>
465374

466-
<property name="notes" code="QcAr" type="text"
375+
<property name="notes" code="QcNt" type="text"
467376
description="User-specified notes."/>
468377

469378
<property name="architecture" code="QcAr" type="text"

Scripting/UTMScriptable.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ extension UTMScriptable {
2525
/// - body: What to do
2626
@MainActor
2727
func withScriptCommand<Result>(_ command: NSScriptCommand, body: @MainActor @escaping () async throws -> Result) {
28-
guard command.evaluatedReceivers as? Self == self else {
29-
return
30-
}
3128
command.suspendExecution()
3229
// we need to run this in next event loop due to the need to return before calling resume
3330
DispatchQueue.main.async {

Scripting/UTMScripting.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public enum UTMScripting: String {
2222
case guestProcess = "guest process"
2323
case serialPort = "serial port"
2424
case virtualMachine = "virtual machine"
25-
case window = "window"
2625
}
2726

2827
import AppKit
@@ -38,6 +37,13 @@ import ScriptingBridge
3837
var isRunning: Bool { get }
3938
}
4039

40+
// MARK: UTMScriptingSaveOptions
41+
@objc public enum UTMScriptingSaveOptions : AEKeyword {
42+
case yes = 0x79657320 /* 'yes ' */
43+
case no = 0x6e6f2020 /* 'no ' */
44+
case ask = 0x61736b20 /* 'ask ' */
45+
}
46+
4147
// MARK: UTMScriptingPrintingErrorHandling
4248
@objc public enum UTMScriptingPrintingErrorHandling : AEKeyword {
4349
case standard = 0x6c777374 /* 'lwst' */
@@ -133,23 +139,39 @@ import ScriptingBridge
133139

134140
// MARK: UTMScriptingGenericMethods
135141
@objc public protocol UTMScriptingGenericMethods {
136-
@objc optional func close() // Close a document.
142+
@objc optional func closeSaving(_ saving: UTMScriptingSaveOptions, savingIn: URL!) // Close a document.
143+
@objc optional func saveIn(_ in_: URL!, as: Any!) // Save a document.
144+
@objc optional func printWithProperties(_ withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
145+
@objc optional func delete() // Delete an object.
146+
@objc optional func duplicateTo(_ to: SBObject!, withProperties: [AnyHashable : Any]!) // Copy an object.
147+
@objc optional func moveTo(_ to: SBObject!) // Move an object to a new location.
137148
}
138149

139150
// MARK: UTMScriptingApplication
140151
@objc public protocol UTMScriptingApplication: SBApplicationProtocol {
152+
@objc optional func documents() -> SBElementArray
141153
@objc optional func windows() -> SBElementArray
142154
@objc optional var name: String { get } // The name of the application.
143155
@objc optional var frontmost: Bool { get } // Is this the active application?
144156
@objc optional var version: String { get } // The version number of the application.
145-
@objc optional func quit() // Quit the application.
157+
@objc optional func `open`(_ x: Any!) -> Any // Open a document.
158+
@objc optional func print(_ x: Any!, withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
159+
@objc optional func quitSaving(_ saving: UTMScriptingSaveOptions) // Quit the application.
146160
@objc optional func exists(_ x: Any!) -> Bool // Verify that an object exists.
147161
@objc optional func virtualMachines() -> SBElementArray
148162
@objc optional var autoTerminate: Bool { get } // Auto terminate the application when all windows are closed?
149163
@objc optional func setAutoTerminate(_ autoTerminate: Bool) // Auto terminate the application when all windows are closed?
150164
}
151165
extension SBApplication: UTMScriptingApplication {}
152166

167+
// MARK: UTMScriptingDocument
168+
@objc public protocol UTMScriptingDocument: SBObjectProtocol, UTMScriptingGenericMethods {
169+
@objc optional var name: String { get } // Its name.
170+
@objc optional var modified: Bool { get } // Has it been modified since the last save?
171+
@objc optional var file: URL { get } // Its location on disk, if it has one.
172+
}
173+
extension SBObject: UTMScriptingDocument {}
174+
153175
// MARK: UTMScriptingWindow
154176
@objc public protocol UTMScriptingWindow: SBObjectProtocol, UTMScriptingGenericMethods {
155177
@objc optional var name: String { get } // The title of the window.
@@ -163,6 +185,7 @@ extension SBApplication: UTMScriptingApplication {}
163185
@objc optional var visible: Bool { get } // Is the window visible right now?
164186
@objc optional var zoomable: Bool { get } // Does the window have a zoom button?
165187
@objc optional var zoomed: Bool { get } // Is the window zoomed right now?
188+
@objc optional var document: UTMScriptingDocument { get } // The document whose contents are displayed in the window.
166189
@objc optional func setIndex(_ index: Int) // The index of the window, ordered front to back.
167190
@objc optional func setBounds(_ bounds: NSRect) // The bounding rectangle of the window.
168191
@objc optional func setMiniaturized(_ miniaturized: Bool) // Is the window minimized right now?
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
17+
import Foundation
18+
19+
@MainActor
20+
@objc extension AppDelegate: UTMScriptable {
21+
private var bytesInMib: Int {
22+
1048576
23+
}
24+
25+
private var bytesInGib: Int {
26+
1073741824
27+
}
28+
29+
@objc func handleCreateCommand(_ command: NSCreateCommand) {
30+
if command.createClassDescription.implementationClassName == "UTMScriptingVirtualMachineImpl" {
31+
let properties = command.resolvedKeyDictionary
32+
withScriptCommand(command) { [self] in
33+
guard let backend = properties["backend"] as? AEKeyword, let backend = UTMScriptingBackend(rawValue: backend) else {
34+
throw ScriptingError.backendNotFound
35+
}
36+
guard let configuration = properties["configuration"] as? [AnyHashable : Any] else {
37+
throw ScriptingError.configurationNotFound
38+
}
39+
if backend == .qemu {
40+
return try await createQemuVirtualMachine(from: configuration).objectSpecifier
41+
} else if backend == .apple {
42+
return try await createAppleVirtualMachine(from: configuration).objectSpecifier
43+
} else {
44+
throw ScriptingError.backendNotFound
45+
}
46+
}
47+
} else {
48+
command.performDefaultImplementation()
49+
}
50+
}
51+
52+
private func createQemuVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
53+
guard let data = data else {
54+
throw ScriptingError.notReady
55+
}
56+
guard record["name"] as? String != nil else {
57+
throw ScriptingError.nameNotSpecified
58+
}
59+
guard let architecture = record["architecture"] as? String, let architecture = QEMUArchitecture(rawValue: architecture) else {
60+
throw ScriptingError.architectureNotSpecified
61+
}
62+
let machine = record["machine"] as? String
63+
let target = architecture.targetType.init(rawValue: machine ?? "") ?? architecture.targetType.default
64+
let config = UTMQemuConfiguration()
65+
config.system.architecture = architecture
66+
config.system.target = target
67+
config.reset(forArchitecture: architecture, target: target)
68+
config.qemu.hasHypervisor = true
69+
config.qemu.hasUefiBoot = true
70+
// add default drives
71+
config.drives.append(UTMQemuConfigurationDrive(forArchitecture: architecture, target: target, isExternal: true))
72+
var fixed = UTMQemuConfigurationDrive(forArchitecture: architecture, target: target)
73+
fixed.sizeMib = 64 * bytesInGib / bytesInMib
74+
config.drives.append(fixed)
75+
// add a default serial device
76+
var serial = UTMQemuConfigurationSerial()
77+
serial.mode = .ptty
78+
config.serials = [serial]
79+
// remove GUI devices
80+
config.displays = []
81+
config.sound = []
82+
// parse the remaining config
83+
let wrapper = UTMScriptingConfigImpl(config)
84+
try wrapper.updateConfiguration(from: record)
85+
// create the vm
86+
let vm = try await data.create(config: config)
87+
return UTMScriptingVirtualMachineImpl(for: vm, data: data)
88+
}
89+
90+
private func createAppleVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
91+
guard let data = data else {
92+
throw ScriptingError.notReady
93+
}
94+
guard #available(macOS 13, *) else {
95+
throw ScriptingError.backendNotSupported
96+
}
97+
guard record["name"] as? String != nil else {
98+
throw ScriptingError.nameNotSpecified
99+
}
100+
let config = UTMAppleConfiguration()
101+
config.system.boot = try UTMAppleConfigurationBoot(for: .linux)
102+
config.virtualization.hasBalloon = true
103+
config.virtualization.hasEntropy = true
104+
config.networks = [UTMAppleConfigurationNetwork()]
105+
// remove any display devices
106+
config.displays = []
107+
// add a default serial device
108+
var serial = UTMAppleConfigurationSerial()
109+
serial.mode = .ptty
110+
config.serials = [serial]
111+
// add default drives
112+
config.drives.append(UTMAppleConfigurationDrive(existingURL: nil, isExternal: true))
113+
config.drives.append(UTMAppleConfigurationDrive(newSize: 64 * bytesInGib / bytesInMib))
114+
// parse the remaining config
115+
let wrapper = UTMScriptingConfigImpl(config)
116+
try wrapper.updateConfiguration(from: record)
117+
// create the vm
118+
let vm = try await data.create(config: config)
119+
return UTMScriptingVirtualMachineImpl(for: vm, data: data)
120+
}
121+
122+
enum ScriptingError: Error, LocalizedError {
123+
case notReady
124+
case backendNotFound
125+
case backendNotSupported
126+
case configurationNotFound
127+
case nameNotSpecified
128+
case architectureNotSpecified
129+
130+
var errorDescription: String? {
131+
switch self {
132+
case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingAppDelegate")
133+
case .backendNotFound: return NSLocalizedString("A valid backend must be specified.", comment: "UTMScriptingAppDelegate")
134+
case .backendNotSupported: return NSLocalizedString("This backend is not supported on your machine.", comment: "UTMScriptingAppDelegate")
135+
case .configurationNotFound: return NSLocalizedString("A valid configuration must be specified.", comment: "UTMScriptingAppDelegate")
136+
case .nameNotSpecified: return NSLocalizedString("No name specified in the configuration.", comment: "UTMScriptingAppDelegate")
137+
case .architectureNotSpecified: return NSLocalizedString("No architecture specified in the configuration.", comment: "UTMScriptingAppDelegate")
138+
}
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)