Skip to main content

utm-test

Build, run, and debug a UTM change for real. Covers building UTM, building all dependencies or just one after changing it, creating a disposable test VM by scripting and deleting it afterwards, where logs go, debugging, headless GUI checks, and the iOS simulator. Use this when you need to run UTM, reproduce a bug, or check that a change works in the app and not just that it compiles. Run it before /utm-review.

Quellinformationen

Repository
utmapp/UTM
Letzte Quellaktivität
22. September 2026 um 19:59
Erkannte Sprache von SKILL.md
Englisch
Sterne
35.677
Forks
1.835

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
utm-test
description
Build, run, and debug a UTM change for real. Covers building UTM, building all dependencies or just one after changing it, creating a disposable test VM by scripting and deleting it afterwards, where logs go, debugging, headless GUI checks, and the iOS simulator. Use this when you need to run UTM, reproduce a bug, or check that a change works in the app and not just that it compiles. Run it before /utm-review.
# utm-test This is the canonical copy. The per-agent entries in `.claude/commands/` and `.opencode/command/` point here. Commands assume macOS on arm64 and the repo root as the working directory. `$SCRATCH` means your agent's scratch directory, or `mktemp -d` if you don't have one. ## Ground rules - **Test only on VMs you created, and delete them when you're done.** Never start, change, or delete the user's VMs. Other agent sessions may be using UTM on the same machine. Ask before you kill a UTM process that you didn't launch. - **Run one UTM instance: your dev build.** `/Applications/UTM.app` has the same bundle id. Scripting then goes to whichever instance is running, and it can launch the release app instead of yours. A stale binary also silently ignores scripting parameters it doesn't know, so a new parameter can turn into a plain `start`. Check with `pgrep -fl "MacOS/UTM|QEMULauncher"` before you launch and after you finish. - **Report only what you saw.** If your test VM never runs the code path you changed, say so rather than implying the change was validated. Before you call a failure pre-existing, build `main` in a worktree and compare, and check the issue tracker. ## Build UTM ```sh xcodebuild -project UTM.xcodeproj -scheme macOS -configuration Debug \ -destination 'platform=macOS,arch=arm64' build > "$SCRATCH/build.log" 2>&1 grep -E "error:|^\*\* BUILD" "$SCRATCH/build.log" | sort -u APP=$(ls -dt ~/Library/Developer/Xcode/DerivedData/UTM-*/Build/Products/Debug/UTM.app | head -1) ``` - Match `^\*\* BUILD`. A bare `BUILD` can match a path. If a build fails, the app from the last good build is still on disk, so check the result before testing. - Swift code goes into `Contents/MacOS/UTM.debug.dylib`. To prove your change is in the binary you run, use `strings "$APP/Contents/MacOS/UTM.debug.dylib" | grep <marker>`. - The Debug build is signed through the local `CodeSigning.xcconfig`. Keep that signing when you intend to run the app. `CODE_SIGNING_ALLOWED=NO` drops the virtualization and networking entitlements. Even when signed, vmnet (Shared/Bridged) networking may fail locally, so test VMs should use emulated networking or none. - Compile-only checks for other platforms: - Add `CODE_SIGNING_ALLOWED=NO` and a separate `-derivedDataPath "$SCRATCH/dd-<scheme>"`. - Use `-destination 'generic/platform=iOS'` with scheme `iOS`, `iOS-SE`, or `iOS-Remote`, or `'generic/platform=visionOS'` with scheme `iOS`. - Add `ARCHS=arm64` to any `generic/` destination. Otherwise it tries to build universal and looks for `sysroot-*-arm64_x86_64`. - If SPM plugin validation fails after a package URL changes, pass `-skipPackagePluginValidation`. - **Baseline or per-commit builds:** don't check out over the user's tree. 1. `git worktree add --detach "$SCRATCH/wt" <rev>`. 2. Symlink every `sysroot-*` and `CodeSigning.xcconfig` into the worktree. A missing sysroot shows up as a misleading "missing resource" error. 3. Build with its own `-derivedDataPath`. 4. Remove the symlinks by name, then run `git worktree remove`. ## Dependencies QEMU, SPICE, GLib, and the other native dependencies reach the app as frameworks in `sysroot-<Platform>-<arch>/Frameworks`. Xcode embeds them and re-signs them. ### Full build Usually you don't need one. Download the `Sysroot-*` artifacts from a recent GitHub Actions run and extract them at the repo root. Build them yourself only when you're changing a dependency: ```sh ./scripts/build_dependencies.sh -p macos -a arm64 > "$SCRATCH/deps.log" 2>&1 # -p ios|ios-tci|ios_simulator|visionos|… grep -a "Building \|All done" "$SCRATCH/deps.log" | tail ``` - The build takes hours and deletes the sysroot before it starts. - It leaves source trees in `build-<Platform>-<arch>/`. Those trees are what the single-dependency path below needs, and CI artifacts don't include them. - A successful run ends with `All done` and writes `build-*/BUILD_SUCCESS`. - If a build dies from memory pressure (LLVM and Mesa are the heavy parts), set `NCPU=4`. - Source versions are pinned in `patches/sources`, and UTM's changes to them are in `patches/*.patch`. If you change a patch, check that it still applies to a fresh extract of the tarball: `patch -p1 --dry-run`. - If a staged sysroot crashes inside a framework on an older macOS, suspect the sysroot before your change. For example, a weak import of an SDK-new symbol is NULL on older systems. Check `nm -m <binary> | grep "weak external"`, `vtool -show-build <binary>`, and `git log -S<symbol> -- scripts/` against the sysroot's date. ### Rebuild one dependency after a change 1. **Rebuild the library in its tree.** The trees are gitignored, so there is no git safety net for your edits. - QEMU: build only the target you need: ```sh ninja -C build-macOS-arm64/qemu-<ver>-utm/build libqemu-aarch64-softmmu.dylib ``` A small edit takes seconds. `ninja -t targets all | grep <file>` finds object targets. - Meson projects such as spice-gtk, spice, and virglrenderer: `meson install -C build-macOS-arm64/<tree>/utm_build`. This installs into the sysroot's `lib/`. - Autotools projects: `make -C build-macOS-arm64/<tree> install`. 2. **Repackage it** into the framework the app embeds: ```sh scripts/fixup.sh -p macos -s sysroot-macos-arm64 <path/to/libfoo.N.dylib> ``` - This writes `Frameworks/foo.N.framework` (the name drops `lib` and the last extension), sets the `@rpath` install name, and points imports of other sysroot libraries at their frameworks. - It is safe to re-run on a sysroot that has already been fixed up. - It matches sysroot libraries by file name, so it also works on a sysroot downloaded from CI, whose install names record the CI machine's paths. - `-i` rewrites only the imports of an executable, such as `libexec/virgl_render_server`, in place. - For iOS, pass `-p ios` and the iOS sysroot. That produces the flat framework layout. 3. **Check the framework, then rebuild UTM** so Xcode copies it into the app: ```sh otool -L sysroot-macos-arm64/Frameworks/<name>.framework/Versions/A/<name> | tail -n +2 | grep -v '@rpath\|/System/\|/usr/lib/' # prints nothing strings "$APP/Contents/Frameworks/<name>.framework/Versions/A/<name>" | grep -c <marker> # after xcodebuild ``` Testing a stale framework and concluding "no effect" is the most common way to waste time here. ### Swift packages (CocoaSpice, QEMUKit, SwiftTerm) These are remote packages pinned in `Package.resolved`, not the sibling checkouts. There are two ways to test a change to one: - **Quick patch:** edit the checkout in `~/Library/Developer/Xcode/DerivedData/UTM-*/SourcePackages/checkouts/<Pkg>`. The files are read-only, so `chmod u+w` first. A normal `xcodebuild` picks the edit up. Revert afterwards with `git checkout -- <file>` inside the checkout. - **Local override:** add the local package folder to `project.pbxproj` as a folder reference (`lastKnownFileType = wrapper; path = ../<Pkg>`). Xcode then prefers it over the remote package. Remove the reference, and bump the pin once the package change is pushed. ## Run a disposable test VM Launch the dev build yourself. Launching the binary directly is also how you capture its stdout and pass it environment variables. Wait with `is running`. Anything else sent before the app registers, even `tell application "$APP"`, launches a second instance. ```sh caffeinate -d -u -t 3600 & # a sleeping display blocks Apple VM start and hides dialogs nohup "$APP/Contents/MacOS/UTM" > "$SCRATCH/utm-stdout.log" 2>&1 & until [ "$(osascript -e "application \"$APP\" is running")" = true ]; do sleep 1; done utm() { osascript -e "tell application \"$APP\"" -e "$1" -e "end tell"; } ``` Create a QEMU VM that boots to the EDK2 UEFI shell. It has no disk and no NIC (a NIC would add a PXE delay). It reaches `Shell>` in about 12 s: ```sh VM="utm-test-$$" utm "make new virtual machine with properties {backend:qemu, configuration:{name:\"$VM\", architecture:\"aarch64\", drives:{}, network interfaces:{}, displays:{{hardware:\"virtio-gpu-gl-pci\"}}}}" utm "start virtual machine named \"$VM\"" utm "get status of virtual machine named \"$VM\"" # stopped|starting|started|paused|… ``` - **Shape the VM to the code path you're testing.** The configuration record accepts `memory`, `cpu cores`, `hypervisor` (default true; use false for TCG), `uefi`, `drives` (`{{guest size:<MiB>}}`, or `{{removable:true, source:POSIX file "…"}}` for an ISO), `network interfaces`, `serial ports`, `displays` (`{}` for headless, or `hardware:"virtio-ramfb"` to use the 2D path instead of GL), and `qemu additional arguments`. - **Apple backend:** `{backend:apple, configuration:{name:"…"}}` creates a Linux VM with no display. The full vocabulary is in `Scripting/UTM.sdef`. - **Change the configuration** of a stopped VM: 1. `set c to configuration of vm` 2. Edit fields of `c`. 3. `update configuration of vm with c`. Lists such as `drives` are replaced whole, so any element you leave out is removed. - **Other verbs:** - `start … saving false`: disposable run. - `stop … by force|kill|request`. - `suspend … saving true`. - `duplicate virtual machine named "X" with properties {configuration:{name:"Y"}}`. `duplicate` returns before the bundle exists on disk. - `$APP/Contents/MacOS/utmctl` has the same verbs. It exits 0 even when it fails, so read its output. It hangs if UTM's main thread is blocked. - **When testing a new verb or parameter, try it on your throwaway VM first.** Confirm that the new parameter takes effect before you point it anywhere else. A `delete` on a child object that can't be resolved has been applied to the parent VM. - **Bundles** are at `~/Library/Containers/com.utmapp.UTM/Data/Documents/<name>.utm` (`config.plist`, `Data/`). If you edit `config.plist` by hand, quit UTM first. For plists that contain dates or data, use `plutil -p` or PlistBuddy; `plutil -convert json` fails on them. ### Verify it really worked A `started` status proves only that the process launched. - **Screenshot the VM window by ID.** This works even when other windows cover it. Look at the image; don't assume. ```sh WID=$(osascript -l JavaScript -e "ObjC.import('CoreGraphics'); ObjC.deepUnwrap(ObjC.castRefToObject(\$.CGWindowListCopyWindowInfo(\$.kCGWindowListOptionAll, 0))).filter(w => w.kCGWindowOwnerName == 'UTM' && w.kCGWindowName == '$VM' && w.kCGWindowIsOnscreen).map(w => w.kCGWindowNumber).join('\n')") screencapture -x -o -l "$WID" "$SCRATCH/vm.png" && sips -Z 1200 "$SCRATCH/vm.png" ``` - A healthy run shows the EDK2 banner, `Mapping table`, and `Shell>`. - A black, torn, or frozen frame is a real failure. - Use `kCGWindowName == 'UTM'` for the library window. Don't match on size: a 500×500 offscreen helper window also exists. - A disposable run's window is titled "<VM> – Disposable Mode" in Accessibility, but "<VM>" here. - **Serial I/O without the GUI:** the default ptty serial port carries the firmware console both ways. ```sh P=$(utm "get address of serial port 1 of virtual machine named \"$VM\"") cat "$P" > "$SCRATCH/serial.txt" & printf 'ver\r' > "$P" ``` The shell's reply lands in `serial.txt`, with ANSI escapes. - **Check the status again** after the checks above. A guest that crashed can still report `started` for a moment. - **Performance:** measure before and after in the same session, and re-run outliers. Say whether Debug Log was on (see below). ### Clean up ```sh utm "stop virtual machine named \"$VM\" by force" until [ "$(utm "get status of virtual machine named \"$VM\"")" = stopped ]; do sleep 1; done utm "delete virtual machine named \"$VM\"" # no confirmation; refused while running utm quit; pkill caffeinate pgrep -fl "MacOS/UTM|QEMULauncher" # expect nothing of yours ``` - `quit` fails with -128 while any VM is running or paused, because a confirmation dialog blocks it. - `pkill` on UTM can orphan `QEMULauncher`, which keeps `efi_vars.fd` locked ("Is another process using the image"). Kill any leftover launcher of yours. - Remove every temporary hook, log line, and dependency-tree edit. `/utm-review` flags stray diagnostics, and `git status` doesn't see the gitignored `build-*` trees. Tag temporary code with a marker you can grep for. ## Observe and debug - **App stdout is the main log.** Everything `QEMULogging` receives is `NSLog`ed there: the QEMU command line and environment, `[CocoaSpice]`, `GSpice-*`, QMP, and app logging. - **`Data/debug.log` in the bundle** only gets the launch header, even with Debug Log on. - **The unified log:** `log show --last 5m --info --debug --style compact --predicate 'process IN {"UTM","QEMUHelper","QEMULauncher"}'`. Use `/usr/bin/log` in zsh, where `log` is a builtin. - **Swift `logger.debug` is hidden** at the default level. Temporarily set `logLevel` to `.debug` in `Services/UTMLoggingSwift.swift`, and don't commit it. - **To get output from CocoaSpice**, use `g_message()`, which always passes the handler in `CSMain.m`. `g_debug()` output only appears with SPICE debugging on. - **QEMU's own stderr reaches no log.** QEMU runs in `QEMULauncher` under `QEMUHelper` (XPC), sandboxed, with an environment built explicitly by `Services/UTMQemuSystem.m`. It does not inherit the app's environment. - To trace inside QEMU, write to a file in `~/Library/Group Containers/WDNLXAD4W8.com.utmapp.UTM/`. - To pass an environment variable to QEMU, add it in `UTMQemuSystem.m` temporarily. - `ps eww -p <launcher pid>` shows what QEMU actually got. - **The VM's Debug Log setting** injects `MTL_DEBUG_LAYER`, `MVK_DEBUG`, `VK_LOADER_DEBUG`, `MESA_DEBUG`, and `VIRGL_LOG_LEVEL` into QEMU, which costs performance. - **Hangs:** run `sample <pid> 2 -file "$SCRATCH/utm.sample"` and read the main thread's stack. - **Crashes:** reports are at `~/Library/Logs/DiagnosticReports/UTM*.ips`. The file is JSON after the first line; use `threads[<triggered>].frames` together with `usedImages`. - **lldb:** the Debug build has `get-task-allow`, so you can attach and script it: `lldb -p $(pgrep -f "Debug/UTM.app/Contents/MacOS/UTM$") --batch -o '…' -o detach`.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen