Another Debian build system? Why Gaia Build System?
Easy to use build system for Debian-based embedded Linux distributions
I had already written a blog post about PhobOS, but I realized that I never actually wrote anything about the build system that makes PhobOS possible.

Why Another Build System?
Building an embedded Linux distribution from scratch is a daunting task. Traditionally, you’d find yourself wrestling with complex toolchains, obscure vendor BSP, cof cof Yocto, manual configuration of bootloaders, and the constant struggle to ensure your build is reproducible across different environments.
Gaia brings modern DevOps AI-native practices to the world of embedded systems. Instead of manual, error-prone steps, Gaia uses containers (relying on Podman) to ensure that every build happens in a clean, isolated, and perfectly reproducible environment. This eliminates the “it works on my machine” problem that plagues many embedded build pipelines.
On top of that, Gaia’s recipe system is composable: a distribution is described by a small JSON file that pulls together recipes, each also described by JSON metadata, from per-SoC cookbooks. The Gaia core ships a large set of reusable recipes and configurations that go beyond the kernel and bootloader, covering specific configuration customizations and software that is not available in the default Debian feed. As a result, the kernel, bootloader, device tree, root filesystem, and that extra software are all built and assembled as a single, repeatable unit. The root filesystem can be the “normal”, mutable rootfs by default, or can be managed as an OSTree (atomic, commit changes) artifact through the PhobOS cookbook, without losing the ability to develop and evaluate fast using apt-get install. The pipeline can also emit a Software Bill of Materials (SBOM) directly.
Furthermore, Gaia is designed to be AI-native. In 2026 if you are not integrating your tooling with AI you are behind. Through the Mimir AI agent, it provides a natural language interface to orchestrate complex build processes, allowing users to describe their intentions (e.g., ‘build for X board’) and letting the agent handle the intricacies of command execution and error recovery.

These are the strengths every comparison below is measured against, so the rest of this post focuses on where the other tools differ rather than re-explaining them. Also this is a way to show why I had decided at the end to build a new build system instead of using existing ones.
Why Not Debootstrap
debootstrap is a fantastic tool for creating the basic root filesystem of a Debian-based system. If your goal is simply to have a minimal Debian environment, debootstrap is excellent.
However, an embedded Linux system is much more than just a root filesystem. You need:
- A kernel tailored for your specific hardware.
- A bootloader (like U-Boot) configured for your SoC.
- Device Tree Blobs (DTBs) that describe your hardware to the kernel.
- Specific initramfs configurations for your startup process.
debootstrap only handles the first part. You would still need to manually orchestrate the kernel build, the bootloader configuration, and the deployment of all these pieces. Gaia automates this entire orchestration, tying the kernel, bootloader, device tree, and root filesystem together into a single, deployable image.
Comparison with ELBE

ELBE, the E.mbedded L.inux B.uild E.nvironment, maintained by Linutronix, Debian-centric build system (GPL-3.0, written in Python, with a history stretching back to the late 2000s) that assembles embedded root filesystems exclusively from Debian binary packages. Its defining idea is to avoid cross-compilation entirely: instead of building packages from source, ELBE spins up a full virtualized build environment, an initvm created with libvirt/QEMU, debootstraps a Debian base inside it, installs the project’s package list with apt, and then applies finetuning commands in a chroot. For cross-architecture targets the same QEMU VM simply emulates the target arch, so “own applications are built natively on the target architecture in a chroot environment using qemu” rather than cross-compiled.
An ELBE project is a single XML file describing the mirror, suite, package list, finetuning steps, and the output artifacts. A trimmed excerpt from the official examples:
<project>
<name>ARMexample</name>
<buildtype>armel</buildtype>
<suite>bookworm</suite>
</project>
<target>
<hostname>myARM</hostname>
<console>ttyS0,115200</console>
<package>
<tar><name>nfsroot.tar.gz</name></tar>
</package>
<finetuning>
<rm>/usr/share/doc</rm>
<adduser groups='audio' passwd='huhu' shell='/bin/zsh'>manut</adduser>
</finetuning>
<pkg-list>
<pkg>bash</pkg>
<pkg>openssh-server</pkg>
</pkg-list>
</target>
Everything hardware-specific, like the kernel image, U-Boot, the device tree, and the bootloader config, has to be smuggled in by the author: as a package from a custom repository, as an embedded tarball archive (elbe chg_archive), or as finetuning command/cp steps. The pipeline itself has a fixed shape: debootstrap → install packages → copy to the target → finetuning → bake into a tar/image/ISO.
The difference with Gaia is one of scope. Both produce a plain, mutable Debian rootfs. Where they diverge is everything around it. ELBE is deliberately a root-filesystem builder: it is excellent at producing that rootfs and at keeping the toolchain consistent by building on the target architecture, but the kernel, U-Boot, and device tree are not in its model; they arrive as pre-supplied packages or “fine-tuned” files. Gaia is a product build system: the kernel, bootloader, and device tree are first-class, composable JSON recipes that can be re-used and have properties overlaid per-SoC cookbooks.
Below is a more feature-to-feature comparison:
| Capability | ELBE | Gaia Build System |
|---|---|---|
| Recipe model | Single XML project file: package list + finetuning commands + output spec | JSON distro file that composes recipes from multiple cookbooks (searchForRecipesOn, excludeRecipes); per-SoC cookbooks (cookbook-nxp, cookbook-rpi, cookbook-synaptics, cookbook-qcom, cookbook-intel) |
| Isolation | Full libvirt/QEMU virtual machine (initvm) | Container-native (Podman) build images, can also leverage from qemu-user-static binfmt to emulate other architectures |
| Kernel | Installed as a Debian binary package (or pulled from a custom repository), or built as a Debian package | Built from source as part of a core recipe that can be overlaid, with device-tree blobs wired in automatically |
| Bootloader / DTB | Not part of the model; smuggled in via custom packages, embedded archives, or finetuning commands | Built from source as part of a core recipe that can be overlaid per-SoC cookbook |
| Rootfs management | Mutable Debian rootfs (tarball, SD image, UBI volumes, install ISO) | Mutable Debian rootfs (tarball/image); optional OSTree (atomic, content-addressed) via the PhobOS cookbook |
| SBOM | Yes, via elbe cyclonedx-sbom (CycloneDX, generated post-build from the build directory) | Yes (--sbom flag, emitted directly by the pipeline) generates CycloneDX SBOM of all the components, including debian packages |
| Board coverage | Per-board example XMLs in the repo (e.g. BeagleBone Black); board knowledge is not packaged as reusable recipes | 15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64 |
| Language / runtime | Python | TypeScript on Deno + Python Xonsh cookbooks |
| AI support | No | Yes (Mimir AI agent for natural language build orchestration) |
| Maturity | ~15 years (Linutronix company maintenance, 4,700+ commits) | 3 years |
Comparison with Armbian

Armbian is a well-known player in the embedded space. It is worth being precise about what it is, because it is often reduced to “a place to download Debian images for single board computers.” Under the hood, armbian/build is a full build framework written in Bash (GPL-2.0, developed since 2013) that compiles real components from source: it builds U-Boot, builds the Linux kernel (producing linux-image, linux-headers, and linux-dtbs DEB packages), assembles an armbian-firmware package, partitions the final disk image. It supports hundreds of boards across Allwinner, Rockchip, Amlogic, NXP, MediaTek, Sophgo, StarFive, Spacemit, and more.
Armbian encodes a board as a .csc (or .conf) file. These are not purely declarative: they mix shell variable assignments with embedded, executable Bash functions and hook registration. A representative, fairly clean example from the repo is nanopi-r5s.csc:
BOARD_NAME="NanoPi R5S"
BOARDFAMILY="rockchip64"
BOOTCONFIG="nanopi-r5s-rk3568_defconfig"
BOOT_FDT_FILE="rockchip/rk3568-bnanopi-r5s-linux.dtb"
SRC_EXTLINUX="no"
IMAGE_PARTITION_TABLE="gpt"
FULL_DESKTOP="yes"
BOOTBRANCH_BOARD="tag:v2026.07"
BOOTPATCHDIR="v2026.07"
OVERLAY_PREFIX="nanopi-r5s"
DEFAULT_OVERLAYS="dwmac-rk3568"
Other boards in the same directory pull in far more machinery. Several .csc files register hook functions that run at build time, for example post_family_tweaks__* and post_family_config__* functions that run mkdir -p, branch on $BRANCH, call run_host_command_logged, or emit display_alert, and they enable extensions (Bash files in extensions/ that register hooks such as fetch_custom_uboot, build_custom_uboot, armbian_kernel_config, pre_umount_final_image__*).
So the unit of customization in Armbian is a board + board-family + extension hierarchy of shell scripts, and the build pipeline has a fixed shape: compile.sh → cli_standard_build_run() → full_build_packages_rootfs_and_image(), with extensions and board config injecting behavior at defined hook points. Another point is that all at end is a Debian package, this could be a pro or a con. A pro because it’s consistent and prevents conflicts with files or configurations that other packages could touch. But, sometimes some external, non Debian feed, dependency adds more complexity to the build process.
The difference with Gaia is structural. Gaia expresses the same outcome (build a kernel, build U-Boot, assemble the device tree, produce a rootfs image) as composable JSON recipes drawn from per-SoC cookbooks, with no embedded shell required in the recipe itself and no rigid board→family→source hierarchy imposed on the author. Where Armbian’s extension model is hook-based (a board config calls into a pipeline whose stages are fixed), Gaia’s model is compositional (a distro file selects which recipes run, and the recipes are ordinary, inspectable steps).
Below is a more feature-to-feature comparison:
| Capability | Armbian | Gaia Build System |
|---|---|---|
| Recipe model | .csc/.conf shell files: variable assignments plus embedded executable Bash functions and hook registration | JSON distro file that composes recipes from multiple cookbooks; recipes are inspectable steps, no embedded shell required. Each step executes a file, or a list of files, which could be binary or any script/language. |
| Customization mechanism | Board → board-family → extension hierarchy; extensions register named build hooks (fetch_custom_uboot, armbian_kernel_config, …) | Per-SoC cookbooks (cookbook-nxp, cookbook-rpi, cookbook-synaptics, cookbook-qcom, cookbook-intel); a distro selects/excludes recipes |
| Kernel | Compiled from source, packaged as DEB (linux-image, linux-headers, linux-dtbs) | Built as part of the per-SoC BSP recipe, with device-tree blobs, re-using the core kernel build recipe |
| Bootloader | U-Boot compiled from source, multiple boot scenarios (spl-blobs, binman, blobless, …), packaged as DEB | Built by the per-board BSP cookbook, re-using the core U-Boot build recipe |
| Rootfs | Debian base assembled from DEB packages | Normal mutable rootfs by default; optional OSTree (atomic, content-addressed) via the PhobOS cookbook |
| Isolation | Runs in Docker (optional; also supports plain host / sudo) | Container-native (Podman). Leverage of qemu-user-static binfmt to run foreign architecture binaries |
| SBOM | Not part of the pipeline | Yes (--sbom flag), generates CycloneDX from all the components |
| Board coverage | Hundreds of boards across many SoC families | 15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64 |
| Language / runtime | Bash (+ small Python helpers) | TypeScript on Deno + Python Xonsh cookbooks |
| AI support | No | Yes (Mimir AI agent for natural language build orchestration) |
| Maturity | 10+ years (large community) | 3 years |
Comparison with Debos
Debos has been gaining some traction on the last months, since Arduino have been using it to create Debian images for the Arduino Uno Q. It’s a Go project (Apache-2.0) that assembles a Debian rootfs by running a sequence of declarative YAML actions, debootstrap, apt, download, overlay, run, image-partition, pack, and so on, inside an isolated environment (Docker by default, or a KVM/QEMU virtual machine via fakemachine for hardware emulation and cross-architecture builds through qemu-user-static).
Gaia was built to solve a different (larger) problem. The two overlap on the “produce a bootable Debian image for a specific board” goal, but they diverge on almost every layer below that goal:
Below a more feature-to-feature comparison:
| Capability | Debos | Gaia Build System |
|---|---|---|
| Recipe model | YAML action list; a mix of metadata and scripts under YAML | JSON distro file that composes recipes from multiple cookbooks, JSON recipes metadata. The actual metadata and actions, scripts/executables are detached from each other, making it clean, better to read and maintainable |
| Kernel | Installs a prebuilt linux-image-* Debian package. Or build from source, but there is no a default recipe or way of do it. It depends on how the user will want to handle it. | Built as part of the per-SoC BSP recipe, with device-tree blobs, re-using the core kernel build recipe |
| Bootloader / DTB | Like on the Kernel story: installs a prebuilt bootloader or builds from source, the build of the bootloader and configuration are not handled by default | Handled by the per-board BSP cookbook; initramfs and bootloader are part of the model (useInitramfs, excludeRecipes: [grub, u-boot]) |
| Rootfs management | Tarball / raw image partition | Normal mutable rootfs by default; optional OSTree (atomic, content-addressed) via the PhobOS cookbook |
| SBOM | Not by default | Yes, generates CycloneDX SBOM of all components |
| Board coverage (out-of-box) | Board recipes live in the separate debos-recipes repo, RPi 3, RPi 64, LePotato, Pine A64+, and Wandboard, mostly last updated a few to eight years ago | 15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64 |
| Language / runtime | Go | TypeScript on Deno + Python Xonsh cookbooks |
| AI support | No | Yes (Mimir AI agent for natural language build orchestration) |
| Maturity | 9 years | 3 years |
The clearest way to see the difference is to look at what a “build for a board” actually looks like in each, for the same target: a 64-bit Raspberry Pi (a Debian-based arm64 system booting through U-Boot).
Debos’s rpi64 recipe is honest about what it is doing: it downloads the Raspberry Pi firmware tarball from GitHub, debootstraps a Debian base, installs u-boot-rpi and linux-image-arm64 from the Debian repository, then hand-wires the boot partition with a series of overlay and run actions, copying config.txt, copying DTBs out of the kernel package, cleaning up firmware files, and finally laying out an MSDOS/FAT32 partition table with image-partition. Every hardware-specific detail is spelled out by the author, step by step, in that one recipe.
With Gaia, the equivalent, “build for the Raspberry Pi 4B”, is a small declarative distro file that says what the target is and where the recipes live:
{
"name": "DeimOS-Reference",
"machine": "rpi4b",
"arch": "linux/arm64",
"useInitramfs": true,
"maxImgSize": 3072,
"searchForRecipesOn": [
"../gaia/cookbook",
"./cookbook"
],
"excludeRecipes": [
"grub",
"u-boot-ram",
"microsoft",
"powershell",
"docker",
"initramfs-php",
"neofetch"
]
}
… and the board-specific machinery, building the kernel, building U-Boot, assembling the device tree, packaging the rootfs, is provided by the Raspberry Pi BSP cookbook rather than hand-coded into the recipe. That is the core architectural difference: Debos is a general-purpose action runner you script yourself; Gaia is a BSP-aware build system that turns board knowledge into reusable, composable recipes.
Conclusion
The problem was never the rootfs. It was everything underneath it: a kernel for the SoC, a U-Boot that boots it, a device tree, an initramfs, the software Debian doesn’t ship. ELBE and Debos hand all of that back to me to wire up by hand, once per board. Armbian actually builds it, but only if I pour my board into its fixed pipeline of shell hooks. I wanted those pieces to be plain, composable recipes I could read, reuse and override, with the whole image falling out of one small declarative file. Nothing worked that way, so I built the thing that does.
Gaia is an AI-first, modern build system designed to be easy to use. The whole architecture is shaped around AI: declarative intent, composable recipes an agent can read, deterministic steps it can retry, errors it can recover from. Through Mimir, the AI agent, you describe what you want in natural language, ‘build for X board’, and Gaia handles the orchestration. It’s a modern DevOps approach for embedded Linux, experimenting also with natural language at the CI/CD pipelines. Reproducible container builds, clean JSON recipes, and an agent as the operator while you review the diff. The other tools were made for a human at a terminal; Gaia was built the other way around, making embedded builds actually easy to use with AI.
So that’s why I built Gaia. If it sounds like your day-to-day, tell me where Gaia fits, or where it falls short. That’s the feedback that decides where it goes next.