Skip to content

Architecture

Understanding how the Raspberry Pi Image Builder works under the hood.

The build system creates hybrid images by combining:

  • Raspberry Pi OS - Boot partition with firmware, bootloader, and config
  • Debian ARM64 - Root filesystem with full userspace
  • RaspiOS Packages - Kernel and firmware installed via APT (with auto-update support)
  • Custom Services - Modular components composed during build

The result is a Debian system with full Raspberry Pi hardware support and automatic kernel/firmware updates.

Problem: Raspberry Pi uses proprietary firmware and the RP1 chip for I/O (Ethernet, USB, GPIO). Standard Debian ARM64 kernels lack these drivers.

Solution: Keep Raspberry Pi OS boot partition and kernel packages, but use Debian for everything else.

Benefits:

  • Full hardware support (RP1, WiFi, Bluetooth, GPIO)
  • Automatic kernel/firmware updates via apt upgrade
  • Debianโ€™s package ecosystem and stability
  • No manual kernel compilation or firmware management

Traditional Approach:

  • Merge images
  • Chroot into ARM64 rootfs from x86_64 host
  • Use qemu-user-static for emulation
  • Install packages

Our Approach:

  • Install packages in native ARM64 QEMU VM before merge
  • Merge pre-configured Debian image with RaspiOS boot

Advantages:

  • Simpler: No complex chroot setup
  • Faster: Native ARM64 execution, no user-mode overhead
  • Cleaner: Merge script just copies files, no package management
  • Reproducible: Same environment every build
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Stage 1: Download & Prepare โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Download RaspiOS Lite image โ”‚
โ”‚ โ€ข Download Debian cloud/generic ARM64 image โ”‚
โ”‚ โ€ข Parse service configuration โ”‚
โ”‚ โ€ข Resolve service dependencies โ”‚
โ”‚ โ€ข Combine setup scripts from all services โ”‚
โ”‚ โ€ข Create setup.iso (contains setup scripts + files) โ”‚
โ”‚ โ€ข Generate cloud-init seed.img OR inject first-boot service โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Stage 2: QEMU Setup (ARM64 VM) โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Boot Debian ARM64 in QEMU โ”‚
โ”‚ โ€ข Cloud-init or first-boot service creates user โ”‚
โ”‚ โ€ข Mount setup.iso โ”‚
โ”‚ โ€ข Execute combined setup.sh: โ”‚
โ”‚ - Add RaspiOS APT repository + pinning โ”‚
โ”‚ - Install RaspiOS kernel packages (raspberrypi-kernel) โ”‚
โ”‚ - Install RaspiOS firmware packages โ”‚
โ”‚ - Install service packages (docker, incus, etc.) โ”‚
โ”‚ - Copy configuration files to /etc/setupfiles/ โ”‚
โ”‚ - Copy first-boot scripts โ”‚
โ”‚ โ€ข Auto-shutdown when complete โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Stage 3: Merge โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Call merge-debian-raspios.sh โ”‚
โ”‚ โ€ข Keep RaspiOS boot partition (FAT32): โ”‚
โ”‚ - bootloader, config.txt, cmdline.txt โ”‚
โ”‚ โ€ข Backup RaspiOS /etc/fstab โ”‚
โ”‚ โ€ข Replace root partition with Debian (ext4): โ”‚
โ”‚ - Delete RaspiOS rootfs โ”‚
โ”‚ - rsync Debian rootfs (with RaspiOS packages installed) โ”‚
โ”‚ - Restore RaspiOS fstab (correct partition UUIDs) โ”‚
โ”‚ - Create /boot/firmware mount point โ”‚
โ”‚ โ€ข Resize root partition to fill image โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Stage 4: Compress โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Run PiShrink to minimize filesystem โ”‚
โ”‚ โ€ข Compress with xz (parallel, level 6) โ”‚
โ”‚ โ€ข Generate checksums (SHA256) โ”‚
โ”‚ โ€ข Output: image-name.img.xz โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

RaspiOS Image:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ /dev/loop0p1 โ”‚ /dev/loop0p2 โ”‚
โ”‚ boot (FAT32) โ”‚ root (ext4) โ”‚
โ”‚ 512MB โ”‚ ~2GB โ”‚
โ”‚ ================== โ”‚ ==================== โ”‚
โ”‚ โ€ข bootloader โ”‚ โ€ข RaspiOS rootfs โ”‚
โ”‚ โ€ข kernel โ”‚ โ€ข (will be replaced) โ”‚
โ”‚ โ€ข firmware โ”‚ โ”‚
โ”‚ โ€ข config.txt โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Debian Image:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ /dev/loop1p1 (or p2, auto-detected) โ”‚
โ”‚ root (ext4) โ”‚
โ”‚ ~2-4GB โ”‚
โ”‚ ========================================== โ”‚
โ”‚ โ€ข Debian userspace โ”‚
โ”‚ โ€ข RaspiOS kernel packages (from QEMU) โ”‚
โ”‚ โ€ข Service packages (from QEMU) โ”‚
โ”‚ โ€ข /etc/setupfiles/ (configs) โ”‚
โ”‚ โ€ข First-boot scripts โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Hybrid Image:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ /dev/mmcblk0p1 โ”‚ /dev/mmcblk0p2 โ”‚
โ”‚ boot (FAT32) โ”‚ root (ext4) โ”‚
โ”‚ 512MB โ”‚ 8GB (expanded) โ”‚
โ”‚ ================== โ”‚ ============================ โ”‚
โ”‚ โ€ข RaspiOS bootload โ”‚ โ€ข Debian userspace โ”‚
โ”‚ โ€ข RaspiOS kernel* โ”‚ โ€ข RaspiOS kernel packages โ”‚
โ”‚ โ€ข RaspiOS firmware โ”‚ โ€ข RaspiOS firmware packages โ”‚
โ”‚ โ€ข config.txt โ”‚ โ€ข RaspiOS fstab โ”‚
โ”‚ โ”‚ โ€ข /boot/firmware โ†’ p1 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
(kept from RaspiOS) (replaced with Debian)
* Kernel files also in /boot/ and /lib/modules/ from APT packages

Each service is a self-contained module:

services/
โ””โ”€โ”€ <service-name>/
โ”œโ”€โ”€ setup.sh # Runs in QEMU (package installation)
โ”œโ”€โ”€ first-boot/
โ”‚ โ””โ”€โ”€ init.sh # Runs on first boot (runtime config)
โ”œโ”€โ”€ setupfiles/ # Static files โ†’ /etc/setupfiles/
โ”‚ โ””โ”€โ”€ config.xyz
โ”œโ”€โ”€ depends.sh # Optional: dependencies
โ””โ”€โ”€ motd.sh # Optional: MOTD content
BUILD TIME (QEMU):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ setup.sh โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Install packages (apt install ...) โ”‚
โ”‚ โ€ข Configure system settings โ”‚
โ”‚ โ€ข Create users/groups โ”‚
โ”‚ โ€ข Set up repositories โ”‚
โ”‚ โ€ข Copy setupfiles/ to /etc/setupfiles/ โ”‚
โ”‚ โ€ข Install first-boot/init.sh โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
FIRST BOOT (Raspberry Pi):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ rpi-first-boot.service (one-time) โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Expand root partition to fill SD card โ”‚
โ”‚ โ€ข Set persistent network interface names โ”‚
โ”‚ โ€ข Reboot โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ services-first-boot.service (one-time) โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Execute base/first-boot/init.sh โ”‚
โ”‚ - Configure network bridges (br-wan, br-lan) โ”‚
โ”‚ - Set up DHCP server (if br-lan) โ”‚
โ”‚ โ€ข Execute service/first-boot/init.sh โ”‚
โ”‚ - Download images (HAOS, OpenWrt) โ”‚
โ”‚ - Create containers/VMs โ”‚
โ”‚ - Detect and configure hardware โ”‚
โ”‚ - Start services โ”‚
โ”‚ โ€ข Disable itself โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
RUNTIME:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Services running โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ€ข Docker containers โ”‚
โ”‚ โ€ข Incus VMs/containers โ”‚
โ”‚ โ€ข WiFi hotspot โ”‚
โ”‚ โ€ข OpenWrt router โ”‚
โ”‚ โ€ข etc. โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Services can declare dependencies:

Example: services/haos/depends.sh

Terminal window
DEPENDS_ON="qemu"

Build process:

  1. Parse requested services: qemu+docker+haos
  2. Resolve dependencies:
    • qemu (no dependencies)
    • docker (no dependencies)
    • haos โ†’ requires qemu
  3. Build order: base โ†’ qemu โ†’ docker โ†’ haos
  4. Combine setup.sh from all services

Dynamic image example: debian/qemu+docker+haos

Process:

  1. Create temporary directory: images/debian-qemu-docker-haos/
  2. Copy base config: images/debian/config.sh
  3. Override variables:
    Terminal window
    OUTPUT_IMAGE="debian-qemu-docker-haos.img"
    SERVICES="base qemu docker haos"
  4. Combine setup scripts:
    Terminal window
    cat services/base/setup.sh \
    services/qemu/setup.sh \
    services/docker/setup.sh \
    services/haos/setup.sh \
    > setup.sh
  5. Merge setupfiles:
    Terminal window
    cp -r services/base/setupfiles/* setupfiles/
    cp -r services/qemu/setupfiles/* setupfiles/
    cp -r services/docker/setupfiles/* setupfiles/
    cp -r services/haos/setupfiles/* setupfiles/
  6. Inject first-boot scripts:
    Terminal window
    # Aggregate all init.sh into services-first-boot.sh
  7. Build image

File: /etc/apt/sources.list.d/raspi.sources

Types: deb
URIs: http://archive.raspberrypi.com/debian/
Suites: trixie
Components: main
Signed-By: /usr/share/keyrings/raspberrypi-archive-keyring.pgp

File: /etc/apt/preferences.d/raspi-pin

# Pin RaspiOS kernel and firmware packages
Package: raspberrypi-kernel raspberrypi-bootloader libraspberrypi* firmware-brcm80211
Pin: release o=Raspberry Pi Foundation
Pin-Priority: 1001
# Default to Debian for everything else
Package: *
Pin: release o=Debian
Pin-Priority: 500
Terminal window
sudo apt update
# Fetches package lists from:
# - Debian repositories (default)
# - RaspiOS repository (for kernel/firmware)
sudo apt upgrade
# Upgrades:
# - raspberrypi-kernel โ†’ from RaspiOS repo (priority 1001)
# - raspberrypi-bootloader โ†’ from RaspiOS repo (priority 1001)
# - libraspberrypi* โ†’ from RaspiOS repo (priority 1001)
# - firmware-brcm80211 โ†’ from RaspiOS repo (priority 1001)
# - All other packages โ†’ from Debian repos (priority 500)

Result: Kernel and firmware stay in sync with RaspiOS, userspace packages track Debian.

Use when:

  • Using Debian cloud images
  • Need cloud-init features (network config, metadata, etc.)

Files:

  • cloudinit/user-data - User creation, SSH config, runcmd
  • cloudinit/meta-data - Instance ID, hostname
  • cloudinit/seed.img - Auto-generated ISO (CIDATA volume)

Boot process:

  1. QEMU mounts seed.img (cloud-init config)
  2. QEMU mounts setup.iso (build scripts)
  3. Cloud-init creates user and runs runcmd
  4. runcmd mounts setup.iso and executes setup.sh
  5. VM shuts down after setup

Use when:

  • Using generic Debian images
  • Donโ€™t need cloud-init
  • Want minimal dependencies

Files:

  • first-boot/setup-runner.sh - Creates user, runs setup
  • first-boot/setup-runner.service - Systemd one-shot service

Boot process:

  1. Autobuild injects files into Debian image (before QEMU)
  2. Autobuild enables systemd service via chroot
  3. QEMU boots Debian
  4. Systemd starts setup-runner.service
  5. Service creates user, mounts setup.iso, runs setup.sh
  6. Service disables itself
  7. VM shuts down after setup

1. Build Time (QEMU):

  • Install packages needed for hardware detection
  • Copy detection scripts to /etc/setupfiles/

2. First Boot (Raspberry Pi):

  • Detect network interfaces (eth0, eth1)
  • Detect WiFi adapters (wlan0, wlan1)
  • Detect USB Zigbee dongles
  • Configure services based on detected hardware

File: services/base/first-boot/init.sh

Terminal window
# Detect eth1 (second NIC)
if ip link show eth1 >/dev/null 2>&1; then
# Dual NIC mode
# br-wan (eth0) - WAN with DHCP client
# br-lan (eth1) - LAN with DHCP server + NAT
else
# Single NIC mode
# br-wan (eth0) - WAN with DHCP client
fi

Result:

  • Single NIC: WAN only
  • Dual NIC: WAN + LAN with DHCP/NAT

File: services/hotspot/first-boot/init.sh

Terminal window
# Detect WiFi interfaces
wlan0_exists=$(ip link show wlan0 2>/dev/null)
wlan1_exists=$(ip link show wlan1 2>/dev/null)
if [ -n "$wlan0_exists" && -n "$wlan1_exists" ](/raspberry-builds/docs/--n-"$wlan0_exists"-&&--n-"$wlan1_exists"-); then
# Dual-band: 2.4GHz on wlan0, 5GHz on wlan1
elif [ -n "$wlan0_exists" ](/raspberry-builds/docs/--n-"$wlan0_exists"-); then
# Single-band: 5GHz on wlan0
fi
# Determine bridge
if ip link show br-lan >/dev/null 2>&1; then
BRIDGE="br-lan"
else
BRIDGE="br-wan"
fi

Result:

  • Dual WiFi: 2.4GHz + 5GHz APs
  • Single WiFi: 5GHz AP only
  • Adapts to available bridge

File: services/haos/first-boot/init.sh

Terminal window
# Scan USB serial devices
for device in /dev/ttyUSB* /dev/ttyACM*; do
device_info=$(udevadm info -q property -n "$device")
# Check vendor for known Zigbee coordinators
if echo "$device_info" | grep -qiE "(FTDI|Silicon_Labs|Texas_Instruments|dresden_elektronik|ITead|Sonoff)"; then
# Extract USB IDs
USB_VENDOR=$(echo "$device_info" | grep "ID_VENDOR_ID=" | cut -d'=' -f2)
USB_PRODUCT=$(echo "$device_info" | grep "ID_MODEL_ID=" | cut -d'=' -f2)
# Pass through to Home Assistant VM
incus config device add haos zigbee-dongle usb \
vendorid="$USB_VENDOR" \
productid="$USB_PRODUCT" \
required=false
fi
done

Result: Zigbee coordinators automatically available in Home Assistant.

Autobuild logic:

Terminal window
# Debian image size
DEBIAN_SIZE=$(qemu-img info --output=json debian.raw | jq -r '.["virtual-size"]')
# Add overhead (1-2GB for services, temp files, expansion)
FINAL_SIZE=$((DEBIAN_SIZE + 2GB))
# Override with config.sh IMAGE_SIZE if specified

During merge (merge-debian-raspios.sh):

  1. Create output image with IMAGE_SIZE
  2. Resize partition table
  3. Expand root partition to fill available space
  4. Resize ext4 filesystem

On first boot (rpi-first-boot.sh):

  1. Expand root partition to fill SD card
  2. Resize ext4 filesystem to match
  3. Reboot to apply changes

Result: Image expands to fill entire SD card, regardless of size.

The architecture uses:

  • Hybrid approach - RaspiOS boot + Debian rootfs
  • QEMU ARM64 - Native package installation before merge
  • Modular services - Composable image components
  • APT pinning - Automatic kernel/firmware updates
  • Hardware detection - Runtime configuration based on detected hardware
  • Two boot modes - Cloud-init or first-boot service

This design provides:

  • Full Raspberry Pi hardware support
  • Safe automatic updates
  • Easy customization
  • Reproducible builds
  • Team collaboration via version control

Next: Learn about the build system