Skip to content

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.


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 + a prerun.sh/run.sh/run-chroot.sh triplet; 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 raw debootstrap means re-implementing the work pi-gen already did upstream.
  • tryboot support. pi-gen produces images that work with the Pi firmware’s native A/B tryboot flow, which is the path ADR-0011 commits to. No bootloader replacement, no GRUB, no u-boot.
  • Read-only-root option. pi-gen’s stage2/stage3 hooks 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.

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.

The KN-86 image extends pi-gen with three project-specific stages on top of the upstream base stages:

StageSourcePurpose
stage0pi-gen upstreamMinimal Debian bootstrap (debootstrap, locale, base packages).
stage1pi-gen upstreamFirst-boot init, fstab, raspberrypi-bootloader, kernel install.
stage2-litepi-gen upstreamPi OS Lite — no desktop.
stage-kn86-basethis repoHardening, package pruning, kiosk-user creation (nosh:nosh), polkit lockdown, journald → volatile, getty disable on tty2–6. See kiosk-mode.md.
stage-kn86-runtimethis reponOSh 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-firmwarethis repoDevice-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.

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:

#DeviceFSSizePurpose
p1/dev/mmcblk0p1FAT32~64 MBCommon boot region (autoboot.txt, bootcode, shared stage-1)
p2/dev/mmcblk0p2FAT32~256 MBBootfs slot A (kernel, cmdline.txt, config.txt, initramfs, dtbs)
p3/dev/mmcblk0p3FAT32~256 MBBootfs slot B
p4/dev/mmcblk0p4ext4(rest)/2Rootfs slot A
p5/dev/mmcblk0p5ext4(rest)/2Rootfs slot B
p6/dev/mmcblk0p6ext4~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.

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.

Terminal window
# 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).img

Cross-building from macOS:

Terminal window
docker run --rm -it --platform=linux/amd64 \
-v "$PWD":/work -w /work \
debian:bookworm \
/work/tools/sd-provision/build-image.sh

Build 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).

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.

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).