Skip to content

scripting: add an install command for macOS guests - #7863

Merged
osy merged 2 commits into
utmapp:mainfrom
meisenbarth:scripting-install-command
Sep 19, 2026
Merged

osy merged 2 commits into
utmapp:mainfrom
meisenbarth:scripting-install-command

Conversation

@meisenbarth

Copy link
Copy Markdown
Contributor

Follow-up to #6092 and #7862.

What is blocked

VMDisplayAppleWindowController.windowDidLoad shows a confirmation sheet before calling requestInstallVM, so a macOS install cannot be driven from a script without someone clicking OK.

This does not remove the sheet

That is deliberate. It guards a genuinely destructive action — it erases the primary drive — and a GUI user who opens a VM that happens to carry an IPSW should keep being asked.

It is also structurally unavailable to a script: showConfirmAlert calls beginSheetModal(for: window!), so it needs a window. The scripted start verb calls data.run(...), which loads the window controller — that is what raises the sheet in the first place.

So this adds a separate verb that goes straight to installVM(with:) and never opens a window. The confirmation moves rather than disappearing: asking for install is the confirmation, which is why the sdef entry says DESTRUCTIVE in as many words. This seemed to match the guidance in CONTRIBUTING.md about putting things most users do not need into the scripting interface rather than the UI.

tell application "UTM"
  install virtual machine "my-macos-vm"
end tell

It also clears the IPSW on success

As the GUI does in didCompleteInstallation. That clearing lives in the window controller's delegate callback, and a scripted install has no window — so without it the IPSW outlives the install, and the next start that does open a display window asks "Would you like to install macOS?" over a guest that is already installed. Measured before the fix: the registry still held the IPSW after a completed scripted install.

Two guards

installVM returns silently when the VM is not stopped, so calling it unguarded would report success having done nothing. Both cases were exercised:

call result
install a Linux VM "This virtual machine has no IPSW recovery image configured, so there is nothing to install from."
install a running VM "The virtual machine must be stopped before this operation can be performed."

Testing

Built against main from a clean baseline, Xcode 26.6, macOS arm64. This branch applies and builds independently of #7862.

check result
baseline / this branch alone clean, 0 errors
install resolves as a verb yes — decompiles as install virtual machine
no confirmation sheet at any point yes
install runs to completion yes — 21 GB written, framework returned success
IPSW cleared afterwards yes — the registry entry no longer carries it
subsequent start opens a display window, no reinstall prompt, guest boots to Setup Assistant
errors reach the caller yes

Install timeline, macOS 26.6.2 (25G83):

00:00  started, no sheet
00:51  2.4 GB
02:31  19 GB
03:21  21 GB — install returned OK

No generated-code changes are needed for this one.

meisenbarth and others added 2 commits September 19, 2026 09:34
VMDisplayAppleWindowController.windowDidLoad shows a confirmation sheet
before calling requestInstallVM, so a macOS install cannot be driven from
a script without someone clicking OK.

This does not remove the sheet. It guards a genuinely destructive action
(it erases the primary drive) and a GUI user who opens a VM that happens
to carry an IPSW should keep being asked. It is also structurally
unavailable to a script: showConfirmAlert calls
beginSheetModal(for: window!), so it needs a window, and the scripted
`start` verb calls data.run(...), which loads the window controller and
raises the sheet in the first place.

So this adds a separate `install` verb that goes straight to
installVM(with:) and never opens a window. The confirmation moves rather
than disappearing: asking for `install` is the confirmation, which is why
the sdef entry says DESTRUCTIVE in as many words. This matches the
CONTRIBUTING.md guidance about putting things most users do not need into
the scripting interface rather than the UI.

It also clears the IPSW on success, as the GUI does in
didCompleteInstallation. That clearing lives in the window controller's
delegate callback, and a scripted install has no window, so without it
the IPSW outlives the install and the next `start` that does open a
display window asks "Would you like to install macOS?" over a guest that
is already installed. Measured before the fix: the registry still held
the IPSW after a completed scripted install.

Two guards, because installVM returns silently when the VM is not
stopped and calling it unguarded would report success having done
nothing: a VM with no IPSW configured, and a VM that is not stopped, each
fail with an error naming the cause.

Verified against main on Xcode 26.6 / Apple Silicon: no confirmation
sheet at any point, a macOS 26.6.2 install run to completion (21 GB,
framework returned success), the IPSW cleared from the registry
afterwards, and a subsequent `start` opening a display window with no
reinstall prompt and the guest booting to Setup Assistant.

UTMScripting.swift gains the install() method that bridge-gen.sh
generates for this sdef change.

Refs utmapp#6092

Assisted-by: Claude:claude-opus-5
A macOS install takes several minutes, well past the default two minute
Apple Event timeout. A plain `install` therefore failed with -1712 while
the installation kept running, and a script reacting to that error by
retrying or starting the VM would race the install.

`install` now returns as soon as the installation starts, and the new
`query install` verb reports whether it is still running and how far it
has progressed, or raises the error if it failed. The installation only
counts as finished once the VM has stopped by itself, so the VM can be
started right away.

Other verbs fail with a clear error while an installation started from
a script is in progress. This includes `start`, which would otherwise
open a window that asks to install macOS again. As in the GUI, `stop`
cancels the installation.

Assisted-by: Claude:claude-opus-5
@osy
osy force-pushed the scripting-install-command branch from 0c07c9f to 5d395ab Compare September 19, 2026 14:49
@osy
osy merged commit fa30b64 into utmapp:main Sep 19, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] - Add 'create' options to utmctl to facilitate creating VM's from command line

2 participants