Local Test Workflow
How Josh validates a freshly-built kn86-os-vDEV.img on macOS without bringing up the full prototype every time. Two paths: Path A writes the image to a real microSD and boots a real Pi Zero 2 W — this is the sole live-boot validation path. Path B is static rootfs inspection via debugfs / mount-loopback on macOS — it validates filesystem layout, unit files, and config content without booting anything. Live-boot validation that cannot run on real hardware belongs to Stage 1c bring-up (see below).
Related:
system-image-build.md— how the.imgis built. Internals; this doc does not duplicate them.kiosk-mode.md— what a “successful kiosk boot” actually means (auto-login, no display manager, read-only root).power-idle.md—kn86-cpufreq.service,schedutilgovernor, the items the smoke test checks.../../../CLAUDE.md— Canonical Hardware Specification (Pi Zero 2 W, Elecrow display, etc.). This doc does not restate spec values.
Prerequisites (one-time, on macOS)
Section titled “Prerequisites (one-time, on macOS)”# For Path B static inspection (ext4 rootfs via debugfs):brew install e2fsprogs # provides debugfs; hdiutil is macOS built-in# Optional: fuse-ext2 for read-only FUSE mount of the ext4 rootfsbrew install --cask macfuse && brew install fuse-ext2For Path A only:
- Raspberry Pi Imager — download from https://www.raspberrypi.com/software/.
- microSD card, ≥8 GB — any reputable A1-rated card.
- microSD reader — built-in or USB.
- The image artifact —
kn86-os-vDEV.imgproduced bysystem-image-build.md. The build script writes it totools/sd-provision/build/(path TBD until the build pipeline lands; check the build-script output line).
Path A — Real hardware (Pi Zero 2 W) — sole live-boot path
Section titled “Path A — Real hardware (Pi Zero 2 W) — sole live-boot path”This is the only path that exercises a real boot. Use it when you need to verify anything that requires the system to actually run: systemd ordering, runtime service behavior, GPIO, the SSD1322 OLED, the Pico 2 over UART, USB-MSC cartridge mounting, or the Elecrow display. If the prototype hardware is not available, use Path B for static inspection and defer live-boot validation to Stage 1c.
1. Flash with Pi Imager
Section titled “1. Flash with Pi Imager”- Open Raspberry Pi Imager.
- Click CHOOSE OS → Use custom, navigate to
kn86-os-vDEV.img, select it. - Click CHOOSE STORAGE, pick the microSD card. Double-check this — Pi Imager will overwrite whatever you point it at.
- Click the gear icon ⚙ for advanced options. Disable “Set hostname”, “Enable SSH”, “Set username and password”, and “Configure wireless LAN”. The KN-86 image is self-contained — Pi Imager’s customization layer interferes with the kiosk autologin chain and the read-only root.
- Click WRITE. Pi Imager flashes, then verifies. Roughly 3–5 minutes for an 8 GB card on a USB 3 reader.
- When verification passes, eject the card. Pi Imager prompts you to.
2. First boot
Section titled “2. First boot”- Insert the microSD into the Pi Zero 2 W.
- Power up via USB (a 5V/2.5A supply or a powered USB hub).
- What to expect, in order:
- The Pi’s onboard green LED blinks during initial boot (kernel handoff, fstab mount).
- The Elecrow shows kernel messages briefly, then clears.
- The kn86-nosh service starts. If a Pico 2 + cartridge slot are present, you should see the boot sequence (mission board, Bare Deck HUD per
../../software/runtime/bare-deck-terminal.md). - Total time: kernel-handoff → nOSh first frame is <8 s target per
kiosk-mode.md.
If you don’t have the prototype assembled past the keyboard / display stage, the runtime will likely fail its preconditions (no Pico, no /run/kn86/coproc.sock). That’s expected — proceed to the smoke-test checklist with whatever subsystems you have wired.
3. Smoke test
Section titled “3. Smoke test”Run the smoke-test checklist at the bottom of this doc.
Path B — Static rootfs inspection (debugfs / mount-loopback)
Section titled “Path B — Static rootfs inspection (debugfs / mount-loopback)”Use this path when you’re iterating on systemd unit files, config.txt / cmdline.txt content, kiosk-mode plumbing, the nosh user setup, or filesystem layout — anything that can be verified by reading files rather than running them. No QEMU, no emulation, no hardware needed. Inspection is instant.
Why QEMU cannot boot this image
Section titled “Why QEMU cannot boot this image”The pi-gen image is built for the Pi vendor firmware boot chain (VideoCore GPU bootloader + Pi-flavored kernel). Two QEMU approaches were evaluated and both fail:
-M virtwith kernel + initrd extracted from the image: Pi OS Lite’s kernel is built withoutVIRTIO_BLKorVIRTIO_NETdriver support — the kernel boots into initramfs and thelocal-blockscript in the initramfs hangs waiting for/dev/vda(the virtio-blk device) to appear. The device never appears because the kernel has no driver for it. Boot does not proceed past initramfs.-M raspi3b: Theraspi3bmachine type expects the Pi GPU firmware boot chain (bootcode.bin,start.elf, GPU mailbox handshake). A standard pi-gen trixie arm64 kernel skips the GPU mailbox on the assumption that the firmware already ran; under QEMU there is no firmware, so the kernel produces no serial output at all — the console is silent.
Decision: Shipping a custom kernel per slot with VIRTIO_BLK + VIRTIO_NET compiled in would unblock the -M virt path, but adds ~10 MB per slot to the image, requires rebuilding whenever the pi-gen kernel pin advances, and creates a separate kernel artifact to maintain in CI. The cost-to-value ratio is poor given that Stage 1c real-Pi bring-up is imminent and provides the runtime coverage QEMU would approximate — and approximation is all QEMU can offer, since it cannot model the Pi’s GPU boot chain, the Pico 2 UART link, or the SSD1322 OLED SPI bus.
QEMU is therefore not a supported path for live-boot validation of this image. Use Path A (real hardware) or Stage 1c.
Why not VirtualBox?
Section titled “Why not VirtualBox?”VirtualBox is the wrong tool here, and it gets asked about often enough to address head-on:
- The image is arm64. VirtualBox is x86_64-guest only; it has no arm64 emulation backend. Even on an Apple Silicon host, VirtualBox runs an x86_64 hypervisor and cannot boot an aarch64 kernel.
- Apple Silicon hosts can’t run x86_64 VirtualBox guests anyway. VirtualBox for Apple Silicon is in beta, and even on x86 Macs the project is shedding maintenance rather than gaining it.
If anyone (Josh included, future-self) wonders “couldn’t I just run this in VirtualBox” — no.
What Path B can validate
Section titled “What Path B can validate”Mount the image partitions read-only on macOS using hdiutil and inspect the filesystem contents directly:
IMG=tools/sd-provision/build/kn86-os-vDEV.img
# Attach all partitions read-only, no auto-mount.hdiutil attach -nomount -readonly "$IMG"# hdiutil prints device nodes; the rootfs slot A is p4.# Example: /dev/disk5s4 Linux (p4 - rootfs slot A)
# Mount the ext4 rootfs (macOS cannot mount ext4 natively; use fuse-ext2 or# inspect via debugfs — debugfs is available via 'brew install e2fsprogs').fuse-ext2 /dev/disk5s4 /tmp/kn86-rootfs -o ro,allow_other
# --- or, without FUSE, use debugfs directly ---debugfs /dev/disk5s4# Inside debugfs: ls, cat, stat, etc.# Example: debugfs> cat /etc/systemd/system/kn86-cpufreq.service
# Mount the boot FAT32 partition (macOS mounts FAT32 natively).mkdir -p /tmp/kn86-bootfssudo mount -t msdos -o ro /dev/disk5s2 /tmp/kn86-bootfsThings Path B can reliably check:
- Filesystem layout — partition table matches ADR-0011 (6 partitions, correct labels, correct sizes).
- File presence — unit files exist at expected paths (
/etc/systemd/system/kn86-*.service,/opt/nosh/bin/kn86-nosh, etc.). - Systemd unit content —
ExecStart=,ConditionPathExists=,WantedBy=, dependency ordering directives are correct. Read the unit files; you do not need to run them to verify the content. config.txtandcmdline.txt— device-tree overlays are listed,quiet/ro/overlayrootflags are present or absent as expected.- Kiosk-mode masking — symlinks in
/etc/systemd/system/*.service→/dev/nullconfirm masked units. - User and group entries —
/etc/passwdand/etc/groupshow thenosh:noshkiosk user. - Build-id —
/etc/kn86-build-idcontains the expected version string from the build.
Things Path B cannot validate:
- Runtime behavior of any systemd unit (whether it actually starts, ordering races,
ExecStartPost=side effects). systemctl status/journalctloutput — the system is not running.- Auto-login on tty1 (requires a running getty + PAM + autologin config to all cooperate).
kn86-cpufreq.servicesuccessfully writing theschedutilgovernor (the write target/sys/devices/system/cpu/...does not exist in a mounted image).- Read-only root behavior at runtime (overlayroot is a mount-time mechanism; you can verify
cmdline.txtcontainsoverlayroot=..., but not that it works). - GPU init, Elecrow display output, SSD1322 OLED, Pico 2 UART link, USB-MSC cartridge mounting.
- Any behavior that depends on the Pi firmware boot chain, device tree application, or hardware peripheral enumeration.
What replaces Path B for live-boot validation
Section titled “What replaces Path B for live-boot validation”Stage 1c — real Pi Zero 2 W bring-up (see ../hardware/build-specification.md §4 Stage 1c). Stage 1c is the correct gate for all runtime validation: it runs the actual image on the actual hardware, exercises the full boot chain, and catches timing and ordering issues that no emulator can surface. Path A in this doc is Stage 1c’s local equivalent once the prototype is on hand.
Smoke-test checklist
Section titled “Smoke-test checklist”Run after a fresh boot on Path A (real hardware). You’ll need a USB keyboard or a serial console hooked up. Sign in is automatic — you should already be at a nosh@…$ prompt without typing anything.
# 1. Auto-login fired on tty1 — already true if you got a prompt without typing.who # expect: nosh tty1 ...tty # expect: /dev/tty1
# 2. cpufreq service ran clean (one-shot).systemctl status kn86-cpufreq.service# Expect: "Active: inactive (dead)" with "ExecStart= ... status=0/SUCCESS"# (one-shot services go inactive after success — that's correct)
# 3. schedutil governor is in effect.cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor# Expect: schedutil
# 4. No display manager loaded.systemctl list-units --type=service | grep -iE 'lightdm|gdm|sddm'# Expect: empty output
# 5. tty2..tty6 silent (no getty).systemctl list-units 'getty@tty*.service'# Expect: only getty@tty1.service is active; tty2-tty6 do not appear or show as masked
# 6. Read-only root.touch /etc/foo# Expect: touch: cannot touch '/etc/foo': Read-only file systemAll six green = the kiosk + power-idle plumbing is intact. If any fail, see the next section.
Common failures and fixes
Section titled “Common failures and fixes”start.elf not found / Pi never boots (Path A)
Section titled “start.elf not found / Pi never boots (Path A)”Pi Imager’s “advanced options” customization can corrupt or unmount the boot partition. Re-flash with the gear-icon options all disabled per Path A step 1. If it still fails, try a different microSD card — older or knock-off cards are a frequent culprit on the Pi Zero 2 W.
kn86-nosh.service stuck in “activating” (Path A)
Section titled “kn86-nosh.service stuck in “activating” (Path A)”Expected when prototype hardware is partially assembled. The service waits on /run/kn86/coproc.sock, which only exists when a real Pico 2 is connected over UART. If the Pico is not yet wired, either ignore it (the rest of the boot is fine) or temporarily mask it:
sudo systemctl mask kn86-nosh.serviceDo not commit the masked unit back into the image. This is a debug-session-only override.
”Why isn’t VirtualBox an option?”
Section titled “”Why isn’t VirtualBox an option?””See “Why not VirtualBox?” in the Path B section above. Short version: arm64 image, x86_64-only hypervisor, doesn’t even start.
”Can’t I just boot this in QEMU?”
Section titled “”Can’t I just boot this in QEMU?””No. See the QEMU limitation explanation at the top of Path B. The short version: Pi OS Lite’s kernel ships without VIRTIO drivers (blocks -M virt), and -M raspi3b requires the Pi GPU firmware boot chain that QEMU doesn’t provide. Use Path A for live-boot validation.