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.

Informações da origem

Repositório
utmapp/UTM
Última atividade na origem
22 de setembro de 2026 às 19:59
Idioma detectado do SKILL.md
inglês
Estrelas
35.677
Forks
1.835

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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`.
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub