# QEMU iteration loop Wrappers around qemu for the build-and-test cycle. Mac host with qemu 8+ and the x86_64 OVMF firmware. Apple Silicon hosts run x86_64 guests via TCG emulation (no HVF for cross-arch) — first boot takes ~5-10 minutes while cloud-init installs the build deps; subsequent boots are fast. ## Scripts | Script | Job | |--------|-----| | `vm-up.sh` | Start the builder VM. First run: builds a cloud-init seed and lets the VM provision itself; subsequent runs: just boots. | | `vm-up.sh --stop` | Graceful shutdown via the qemu monitor socket. | | `vm-up.sh --reprovision` | Wipe state and re-run cloud-init from scratch. | | `vm-build.sh` | rsync the repo into the builder, run `./build.sh`, copy artifacts back to `./out/`. | | `vm-test-install.sh` | Boot the most recent ISO from `./out/` in a fresh disposable target VM with the install config attached. | | `common.sh` | Shared paths, OVMF location, qemu invocation pattern. Sourced by the others. | All scripts write VM state under `../../.vms/` (gitignored). Disk images, the cloud qcow2, per-VM NVRAM, the generated SSH key for the builder, serial logs, and monitor sockets all live there. ## First-time setup The cloud image needs to be downloaded once. From the `build/` root: ```sh mkdir -p .vms/images curl -fL -o .vms/images/alpine-cloud.qcow2 \ https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/cloud/generic_alpine-3.23.4-x86_64-uefi-cloudinit-r0.qcow2 qemu-img resize .vms/images/alpine-cloud.qcow2 +30G ``` Then start the builder: ```sh ./tools/qemu/vm-up.sh ``` On the first run this generates an ed25519 SSH keypair, writes cloud-init user-data with that pubkey baked in, builds a NoCloud seed ISO with `hdiutil`, and boots the VM with both the cloud image (as overlay-on-base) and the seed attached. Cloud-init installs the build deps and starts sshd. The script polls for sshd reachability every 10s for up to 15 minutes and exits when the VM is ready. After the first successful boot, a `.vms/.builder-provisioned` marker file is dropped. On subsequent runs the VM boots without the seed attached (cloud-init has already run). ## The iteration cycle ```sh # Once per shell session — or whenever the VM has been stopped: ./tools/qemu/vm-up.sh # Edit files in apkovl-src/, installer/, mkimg/, etc., then: ./tools/qemu/vm-build.sh # rsync + build inside builder ./tools/qemu/vm-test-install.sh # boot the new ISO in a fresh target VM ``` `vm-build.sh` runs in your terminal, blocks until the build finishes, copies artifacts back. `vm-test-install.sh` boots the most recent ISO in a fresh disposable target VM. Pass `--daemonize` to background it for scripted ssh-driven installs. If you have `dosfstools` + `mtools` (`brew install dosfstools mtools`), it also stages `tools/qemu/test-install.yaml` onto a FAT image labeled `MTN-CFG` on a second virtio disk; the installer auto-detects that path. Without those tools, copy the config into the live env over ssh (`ssh ... 'cat > /tmp/install.yaml' < tools/qemu/test-install.yaml`) and pass `--config /tmp/install.yaml` to `mountaineer-install`. ## When the loop breaks Common failure modes: - **`apk add` fails on a package.** A name in `apkovl-src/common/etc/apk/world` or in `mkimg/mkimg.mountaineer-server.sh` is wrong, in the wrong repo, or not packaged for x86_64. Fix the name; re-run `vm-build.sh`. - **mkimage profile rejected.** The `boot_efi` token or feature names may not match what current aports expects. Read the mkimage script's error; edit the profile. - **Cloud-init didn't finish.** Tail `.vms/logs/builder-serial.log` to see what happened during the first boot. Common: network can't reach Alpine repos (qemu user-mode NAT should always work — usually a DNS or upstream-block issue on the host); a package name in user-data is invalid (fix and `vm-up.sh --reprovision`). - **Builder VM seems stuck.** `./tools/qemu/vm-up.sh --stop` then `--reprovision`. - **Target VM doesn't see the install config.** On macOS, install `dosfstools` + `mtools` via Homebrew, or skip the FAT image and use the ssh-cat workflow above. See `../../build_notes.md` for the full iteration recipe. ## Cleanup Wipe all VM state and start fresh: ```sh ./tools/qemu/vm-up.sh --stop # if running rm -rf .vms/ ``` The next `./tools/qemu/vm-up.sh` re-downloads the cloud image (200 MB) and re-provisions from scratch. ## Why a cloud image instead of the standard ISO The original plan was the Alpine virt ISO + manual `setup-alpine`. The cloud image with cloud-init lets us declare provisioning state in `user-data` and have the VM configure itself on first boot — no manual keyboard time, no per-developer divergence in builder setup. The tradeoff is 200 MB instead of 67 MB on download, and a bigger dependency chain (cloud-init itself). For a build host the bigger image is the right call; for the *installed* Mountaineer system we still build from scratch and cloud-init never touches it.