System Image Build
How the flashable SD artifact (.img) is constructed end-to-end: pipeline choice, base OS pinning, stage layout, partition table, build-host setup, and CI integration. Read this if you are about to add a new system service, change a device-tree overlay, or reshape the SD partitions; engineers tweaking a single systemd unit should also skim this for stage placement.
boot-and-systemd.md— what runs after the image boots.device-tree-overlays.md— overlays baked into the image.update-system.md— how the field-deployed.kn86fwpayload is constructed from a built slot tarball.release-setup.md— CI release pipeline (currently emulator-only; system-image CI will graft onto the same workflow surface).../../adr/ADR-0011-device-firmware-update-system.md— partition layout and update model this image must satisfy.
Pipeline choice: pi-gen
Section titled “Pipeline choice: pi-gen”The system image is built with pi-gen (the Raspberry Pi Foundation’s official Raspberry Pi OS image generator), targeting the Lite arm64 flavor. We do not use debootstrap directly. Reasons:
- Stage management. pi-gen ships a stage-numbered overlay system (
stage0/stage1/stage2/…) that maps cleanly onto our base/runtime/firmware layering. Each stage is a directory of shell hooks + aprerun.sh/run.sh/run-chroot.shtriplet; adding “the KN-86 nOSh runtime” is one new stage directory. - Pi-firmware bundling. pi-gen embeds the vendor-supplied VideoCore GPU bootloader (
bootcode.bin,start*.elf,fixup*.dat) and the Pi-flavored kernel into the image with the right partition layout. Doing this from rawdebootstrapmeans re-implementing the work pi-gen already did upstream. trybootsupport. pi-gen produces images that work with the Pi firmware’s native A/Btrybootflow, which is the path ADR-0011 commits to. No bootloader replacement, no GRUB, no u-boot.- Read-only-root option. pi-gen’s
stage2/stage3hooks include built-in support for overlayroot/overlayfs configuration (kiosk-mode.md). We don’t have to roll our own.
Fallback: if pi-gen ever blocks us (e.g., a pi-gen regression that drags in a package we can’t take, or a base-image bug), debootstrap + manual partition assembly is the escape hatch — but it is a ~1-week setback, not a parallel maintained pipeline.
Base Debian release pinning
Section titled “Base Debian release pinning”The base OS is Raspberry Pi OS Lite (arm64), currently the Bookworm-based release. Exact pinning — including the snapshot.debian.org timestamp and the pi-gen tag — is TBD pending bring-up. Once Stage 0 (Bring-up) of ../hardware/build-specification.md §4 lands a known-good kernel + Raspberry Pi OS combination that survives the 50-cycle USB-MSC regression test in ADR-0011 §Risks #1, that triplet (kernel version, pi-gen tag, snapshot.debian.org timestamp) becomes the v1 pin and is committed to tools/sd-provision/pi-gen-pin.env. Do not bump the pin without re-running the 50-cycle test.
Stage definitions
Section titled “Stage definitions”The KN-86 image extends pi-gen with three project-specific stages on top of the upstream base stages:
| Stage | Source | Purpose |
|---|---|---|
stage0 | pi-gen upstream | Minimal Debian bootstrap (debootstrap, locale, base packages). |
stage1 | pi-gen upstream | First-boot init, fstab, raspberrypi-bootloader, kernel install. |
stage2-lite | pi-gen upstream | Pi OS Lite — no desktop. |
stage-kn86-base | this repo | Hardening, package pruning, kiosk-user creation (nosh:nosh), polkit lockdown, journald → volatile, getty disable on tty2–6. See kiosk-mode.md. |
stage-kn86-runtime | this repo | nOSh binary + assets installed under /opt/nosh/. systemd units copied into /etc/systemd/system/. udev rules (cartridge USB-MSC subscriber per update-system.md §SD partition layout). See boot-and-systemd.md. |
stage-kn86-firmware | this repo | Device-tree overlays for SSD1322 OLED, USB hub topology, UART0 to Pico 2 (device-tree-overlays.md). config.txt snippets. Pico 2 firmware UF2 deposited at /lib/firmware/kn86-pico.uf2. CIPHER-LINE bezel/tty assets. |
Each stage-kn86-* lives at tools/sd-provision/pi-gen-stages/<stage>/ in this repo and is symlinked into the pi-gen working tree at build time by the provisioning script.
Output .img layout
Section titled “Output .img layout”The output of pi-gen + our stage overlays is a single .img written with the six-partition A/B layout from ADR-0011 §SD partition layout. Do not invent alternative partition tables — that table is the source of truth. Quick reference:
| # | Device | FS | Size | Purpose |
|---|---|---|---|---|
| p1 | /dev/mmcblk0p1 | FAT32 | ~64 MB | Common boot region (autoboot.txt, bootcode, shared stage-1) |
| p2 | /dev/mmcblk0p2 | FAT32 | ~256 MB | Bootfs slot A (kernel, cmdline.txt, config.txt, initramfs, dtbs) |
| p3 | /dev/mmcblk0p3 | FAT32 | ~256 MB | Bootfs slot B |
| p4 | /dev/mmcblk0p4 | ext4 | (rest)/2 | Rootfs slot A |
| p5 | /dev/mmcblk0p5 | ext4 | (rest)/2 | Rootfs slot B |
| p6 | /dev/mmcblk0p6 | ext4 | ~512 MB | /home/shared — Universal Deck State, cart save passthrough cache. Never written by the image build or the flasher. |
A first-boot install populates slot A only (p2 + p4). Slot B (p3 + p5) is left empty until the first successful field update. p6 is created empty and is owned by nOSh’s deck-state writer at runtime.
Build-host setup
Section titled “Build-host setup”pi-gen is Linux x86_64 only. From an Apple Silicon Mac, run the build inside a --platform=linux/amd64 Docker container; without the platform flag, docker run silently uses aarch64 and the build will fail in subtle places.
# From repo root, on a Linux x86_64 host (or in an amd64 container):cd tools/sd-provision./build-image.sh \ --pi-gen-tag $(grep '^PI_GEN_TAG=' pi-gen-pin.env | cut -d= -f2) \ --output build/kn86-v$(git describe --tags).imgCross-building from macOS:
docker run --rm -it --platform=linux/amd64 \ -v "$PWD":/work -w /work \ debian:bookworm \ /work/tools/sd-provision/build-image.shBuild duration: ~30–45 min on a fresh Docker layer cache, ~10 min warm. Output is a single .img of the full SD layout, plus a .kn86fw payload built from the slot-A bootfs+rootfs (update-system.md).
Reproducibility
Section titled “Reproducibility”The build is reproducible to the level pi-gen offers — any two runs against the same pi-gen-pin.env + the same source tree produce byte-identical .img files modulo a build timestamp embedded in /etc/kn86-build-id. We do not currently strip the timestamp; if reproducibility-by-hash becomes a release-CI requirement, that’s a one-line patch to the stage-kn86-runtime/run.sh hook.
CI integration
Section titled “CI integration”Today the release CI in release-setup.md builds emulator binaries only. System-image CI will graft onto the same release.yml workflow as a new job (Build (kn86-image)) that runs the tools/sd-provision/build-image.sh script in an ubuntu-latest runner with --platform=linux/amd64 (GitHub Actions runners are already x86_64), uploads the .img and .kn86fw as release assets to the private monorepo only (no public mirror — system images carry kernel + closed-source firmware blobs), and runs the SHA-256 verification step that the existing kn86fw builder performs.
Trigger model: same tag schema as the emulator (v0.2.0, v0.2.0-rc1). The image-build job is gated on the tools/sd-provision/pi-gen-pin.env file existing (a missing pin is a release blocker — see “Base Debian release pinning” above).