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.

alt text

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 bootloader, 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, the setup script drops you into a Docker dev container, so the only thing your host really needs is Docker and the Compose plugin. That dev container is the build environment, and most recipes simply run their steps there, because it is already a pinned and reproducible place to run things. What needs more than that asks for it explicitly: where the toolchain version actually matters, a recipe brings its own container, the kernel and the U-Boot recipes drive a compose.yaml that pins pergamos/bsp-builder, and any recipe can set hostAsContainer with a containerImage of its own to have its steps executed inside it. Foreign architectures are handled by registering the qemu-user-static binfmt handlers through pergamos/binfmt container. Nothing is ever compiled against whatever happens to be installed on your machine, because your machine only ever provides Docker, and that is what actually eliminates the “it works on my machine” problem that plagues many embedded build pipelines.

On top of that, Gaia’s recipe system is composable, and this is the part that deserves a concrete example instead of an adjective.

A recipe is a JSON file that contains only metadata. It declares what to fetch and which executable files run at each stage of the pipeline. This is the kernel recipe from the Gaia core, cookbook/recipes-kernel/linux/linux.json:

{
    "name": "linux",
    "type": "kernel",
    "priority": 0,
    "source": "https://github.com/gaiaBuildSystem/linux.git",
    "support": [ "linux/arm64", "linux/amd64" ],
    "ref": {
        "linux/arm64": "8cd9520d35a6c38db6567e97dd93b1f11f185dc6",
        "linux/amd64": "8cd9520d35a6c38db6567e97dd93b1f11f185dc6"
    },
    "hostDeps": [
        "git", "make"
    ],
    "fetchRecipes":  [ "fetch.ts" ],
    "buildRecipes":  [ "build.ts" ],
    "deployRecipes": [ "deploy.ts" ],
    "sbomRecipes":   [ "linux-sbom.ts" ]
}

Note that there is no logic in there. fetch.ts, build.ts, deploy.ts and linux-sbom.ts are ordinary executable files sitting right next to the JSON, and Gaia does not care what they are. Across the cookbooks today those steps are written in TypeScript, Xonsh, Bash and Python, or you name it, if can be executed you can use it. The metadata and the actions are detached from each other, and that is precisely what keeps both of them readable. The stages themselves are fixed and named, fetch, patch, build, deploy (with before/after variants), bundle, initramfs, sbom and clean, and a recipe only fills in the ones it actually needs.

Now the part that makes it composable. A distro lists the cookbooks to search in searchForRecipesOn, and Gaia collects every recipe it finds there, keyed by name. When two cookbooks ship a recipe with the same name, the highest priority wins, and if the winner sets "merge": true its stage script lists and dependency lists are merged into the lower priority one instead of replacing it. That is the whole override model, and it is why a board only has to describe its delta. This is the entire Raspberry Pi kernel recipe, from cookbook-rpi:

{
    "name": "linux",
    "type": "kernel",
    "priority": 1,
    "source": "https://github.com/gaiaBuildSystem/linux",
    "support": [ "linux/arm64" ],
    "ref": {
        "linux/arm64": "4be9f8d83a21ca8d3c4e5497a17f354046d5c04a"
    },
    "afterDeployRecipes": [ "dtb.ts" ],
    "merge": true
}

Same name, higher priority. It swaps the git reference, narrows support to arm64, and appends one extra step after deploy to handle the device tree blobs. fetch.ts, build.ts, deploy.ts and the SBOM step are inherited from the core recipe, untouched. Eleven lines of JSON and one script: that is the complete Raspberry Pi kernel delta.

That is what easy to use means in this post, and it is worth being concrete about what it buys. You can read a board definition without running it, because there is no code in it to run. You can run a single step on its own, build.ts is just a file, so debugging a kernel build does not mean sitting through the whole pipeline to find out. And you can change a board without forking it: those eleven lines did not copy the core recipe, they overlaid it, so a fix landing upstream in fetch.ts or build.ts arrives on the Raspberry Pi too. In every other build system in this post, a board is a file somebody copied once and now owns forever.

The Gaia core ships a set of these 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 root filesystem 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 that follow the CycloneDX specification.

Furthermore, Gaia is designed to be AI-native. 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. I wrote a whole post about it, Mimir - Gaia Build System AI Agent, with videos of Mimir failing a build three times, reading its own error logs and fixing itself until the image was built, plus a proof of concept of a CI/CD pipeline driven by natural language intention.

alt text

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.

debootstrap is used by many Debian-based build systems as the foundation for creating their minimal root filesystem. On Gaia I had decided use the .tar from the official Docker Debian images instead, that was made by running debootstrap anyway.

Comparison with Yocto

The Yocto Project the most popular build system. It’s not really specialized for generating Debian based systems, but when you talk about embedded Linux it’s always the one someone comes to nowhere shouting “JUST USE YOCTO”.

Under it, BitBake (a weird mixture of python metadata and bash 🤨) reads a graph of recipes and builds everything from source.

I’m a big critic of Yocto. It’s a powerful tool, no way to say otherwise, but it’s not for everything. It is a very complex system, with a steep learning curve, hard even for AI agents. It can be an overkill for many embedded projects. It requires a lot of time and resources to set up and maintain, especially for smaller teams or projects that don’t need the overengineering of it.

One of the reasons I built Gaia was to have a build system that is easier to use than Yocto, fast to iterate, AI friendly, while still providing the necessary features for building customized embedded Linux products.

Gaia focuses on simplicity, composability, and leveraging existing Debian packages where possible, rather than building everything from source.

Below is a more feature-to-feature comparison:

CapabilityYocto / OpenEmbeddedGaia Build System
Recipe modelBitBake metadata (.bb, .bbappend, .bbclass, .conf): a DSL of its own, with task graph, class inheritance and override syntaxJSON distro file that composes JSON recipes discovered across the cookbooks listed in searchForRecipesOn; a recipe is pure metadata whose stages point to separate executable files in any language, and same-name recipes are overlaid by priority + merge
UserspaceEverything built from source, including the cross-toolchainDebian binary archive, plus recipes for what Debian does not ship
KernelBuilt from source by the BSP layer recipeBuilt from source as part of the per-SoC BSP recipe, re-using the core kernel build recipe
Bootloader / DTBBuilt from source by the BSP layer recipeBuilt from source by the per-board BSP cookbook
Rootfs managementImage recipes (core-image-*), read-write or read-only; OSTree through third-party layers such as meta-updaterNormal mutable rootfs by default; optional OSTree (atomic, content-addressed) via the PhobOS cookbook
First buildHours and tens of GB of disk (much faster afterwards thanks to the sstate-cache)Minutes, only the board-specific components are actually compiled
IsolationHost build by default; containers via CROPS or kasDocker dev container as the build environment; recipes that need a specific toolchain bring their own pinned container (compose.yaml or hostAsContainer), with qemu-user-static binfmt for foreign architectures
SBOMYes, SPDX (create-spdx), per package and with source provenanceYes (--sbom flag), CycloneDX of all components including Debian packages
Board coverageEvery major vendor ships a meta- BSP layer15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64
Language / runtimePython + the BitBake DSL (weird mixture of metadata, Python and shell)TypeScript on Deno (core); recipe steps in TypeScript, Xonsh, Bash or whatever could be executed
AI supportNoYes (Mimir AI agent for natural language build orchestration)
Maturity15+ years as Yocto, OpenEmbedded lineage back to 2003, Linux Foundation project backed by every major silicon vendor3 years

Comparison with ELBE

alt text

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 systems primarily out of Debian binary packages. Its defining idea is to avoid cross-compilation: instead of cross-building a distribution 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.

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>

The flow is easy to follow, and that is a virtue: debootstrap the build environment → optionally build your own packages with pbuilder → install the package list → finetuning → partition and write the image → pack. What comes out is a product rather than just a rootfs, bootable images, UBI volumes or installer ISOs, licence reports, CycloneDX SBOMs, and update packages that an elbe-updated daemon applies on the running target. It builds your kernel and bootloader too, as long as the source tree arrives already debianized.

The whole project is a single XML document validated against a schema. To my taste there are better ways to express metadata in 2026. Worse, that metadata is not really just metadata: <finetuning> carries shell commands, <file> embeds entire config files, <device-command> runs tune2fs, and the BeagleBone example writes a systemd network unit and calls u-boot-update from inside the XML. Metadata and logic end up in the same document, which is exactly what Gaia refuses to do, a recipe is inert JSON and every command lives in an ordinary executable beside it. And after fifteen years the board knowledge is still thin: the whole repository ships one real board, a BeagleBone Black. No Raspberry Pi, no i.MX, no Rockchip. Whatever you are actually shipping on, you start by writing its XML yourself.

Below is a more feature-to-feature comparison:

CapabilityELBEGaia Build System
Recipe modelSingle XML project file: package list + finetuning commands + output specJSON distro file that composes JSON recipes discovered across the cookbooks listed in searchForRecipesOn; a recipe is pure metadata whose stages point to separate executable files in any language, and same-name recipes are overlaid by priority + merge
IsolationFull libvirt/QEMU virtual machine (initvm)Docker dev container as the build environment; recipes that need a specific toolchain bring their own pinned container (compose.yaml or hostAsContainer), with qemu-user-static binfmt for foreign architectures
KernelA Debian package like any other: from Debian, from a custom repository, or built from source by <pbuilder> (source package, git or svn) if the tree carries a debian/ directoryBuilt from source as part of a core recipe that can be overlaid, with device-tree blobs wired in automatically
Bootloader / DTBNo bootloader or device-tree concept in the schema. U-Boot builds via <pbuilder> like any source package, but placing it is manual (<grub-install>, <binary offset="...">); DTBs ride inside linux-image-*Built from source as part of a core recipe that can be overlaid per-SoC cookbook
Rootfs managementMutable Debian rootfs (tarball, SD image, UBI volumes, install ISO)Mutable Debian rootfs (tarball/image); optional OSTree (atomic, content-addressed) via the PhobOS cookbook
SBOMYes, 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 coveragePer-board example XMLs in the repo (e.g. BeagleBone Black); board knowledge is not packaged as reusable recipes15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64
Language / runtimePythonTypeScript on Deno (core); recipe steps in TypeScript, Xonsh, Bash or whatever could be executed
AI supportNoYes (Mimir AI agent for natural language build orchestration)
Maturity~15 years (Linutronix company maintenance, 4,700+ commits)3 years

Comparison with Armbian

alt text

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.shcli_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 the same one as with ELBE. A .csc is not metadata with a little shell embedded in it, it is a shell file that happens to contain metadata: sourced at build time, free to branch on $BRANCH, call run_host_command_logged or register hooks, and readable by nothing except Bash. Gaia describes the recipe and the steps executions instead of programming it directly. Metadata is metadata and execution is execution. That eleven-line Raspberry Pi kernel recipe, that I exemplified on the start of the blog post, is JSON that cannot execute anything, and the one board-specific command it needs lives in dtb.ts beside it, a file you can open, run and debug on its own. Same outcome, kernel, U-Boot, device tree and image, without a board→family→extension hierarchy of shell in between.

Below is a more feature-to-feature comparison:

CapabilityArmbianGaia Build System
Recipe model.csc/.conf shell files: variable assignments plus embedded executable Bash functions and hook registrationJSON distro file that composes JSON recipes discovered across the cookbooks listed in searchForRecipesOn; a recipe is pure metadata whose stages point to separate executable files in any language, and same-name recipes are overlaid by priority + merge
Customization mechanismBoard → 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
KernelCompiled 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
BootloaderU-Boot compiled from source, multiple boot scenarios (spl-blobs, binman, blobless, …), packaged as DEBBuilt by the per-board BSP cookbook, re-using the core U-Boot build recipe
RootfsDebian base assembled from DEB packagesNormal mutable rootfs by default; optional OSTree (atomic, content-addressed) via the PhobOS cookbook
IsolationRuns in Docker, with qemu-user-static binfmt for foreign architecturesDocker dev container as the build environment; recipes that need a specific toolchain bring their own pinned container (compose.yaml or hostAsContainer), with qemu-user-static binfmt for foreign architectures
SBOMNot part of the pipelineYes (--sbom flag), generates CycloneDX from all the components
Board coverageHundreds of boards across many SoC families15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64
Language / runtimeBash (+ small Python helpers)TypeScript on Deno (core); recipe steps in TypeScript, Xonsh, Bash or whatever could be executed
AI supportNoYes (Mimir AI agent for natural language build orchestration)
Maturity10+ years (large community)3 years

Comparison with Debos

Debos 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). I put “YAML” in quotes because it is more like an pre YAML file, since it has some non-spec syntax, includes templates and if-else logic, that needs to be pre-processed by the build tool.

The clearest way to see the difference with Gaia 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",
    "version": {
        "major": 0,
        "minor": 0,
        "patch": 0,
        "build": 0
    },
    "maxImgSize": 3072,
    "useInitramfs": true,
    "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. searchForRecipesOn points at the core cookbook and the RPi one, and the eleven-line kernel overlay from the beginning of this post is what makes the difference between them. It is the same mixture again: the “YAML” says what to build and how to build it in the same breath, run actions carrying inline scripts, overlay steps spelling out each hardware detail. That is the difference: the Gaia distro file above says only what the target is and where the recipes live, the kernel, U-Boot and device tree come from the cookbook, it again a matter to maintain a clear separation of concerns between metadata and execution.

Below a more feature-to-feature comparison:

CapabilityDebosGaia Build System
Recipe model”YAML” action list; a mix of metadata and scripts under their own YAML template that needs to be pre-processedJSON distro file that composes JSON recipes discovered across the cookbooks listed in searchForRecipesOn; a recipe is pure metadata whose stages point to separate executable files in any language, and same-name recipes are overlaid by priority + merge
KernelInstalls 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 / DTBLike on the Kernel story: installs a prebuilt bootloader or builds from source, the build of the bootloader and configuration are not handled by defaultHandled by the per-board BSP cookbook; initramfs and bootloader are part of the model (useInitramfs, excludeRecipes: [grub, u-boot])
Rootfs managementTarball / raw image partitionNormal mutable rootfs by default; optional OSTree (atomic, content-addressed) via the PhobOS cookbook
IsolationDocker by default, or a KVM/QEMU VM through fakemachine; qemu-user-static for foreign architecturesDocker dev container as the build environment; recipes that need a specific toolchain bring their own pinned container (compose.yaml or hostAsContainer), with qemu-user-static binfmt for foreign architectures
SBOMNot by default (I know that there is work on the front in Debos)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. Last updated from eight years ago15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64
Language / runtimeGo, bash, shell for the recipes mixtureTypeScript on Deno (core); recipe steps in TypeScript, Xonsh, Bash or whatever could be executed
AI supportNoYes (Mimir AI agent for natural language build orchestration)
Maturity9 years3 years

Comparison with Yoe

Yoe is the newest project here and, honestly, the closest to Gaia in intent. It comes from the people behind the Yocto-based Yoe Distribution, who in their own words “took what we learned from many years of maintaining and building products with the Yoe Distribution, started over, and began building the tool we always wanted”. It is a single static Go binary (Apache-2.0, repository opened in March 2026), it builds everything from source into apk or deb packages against an Alpine or a Debian backend, sandboxes each unit’s build with bubblewrap, caches on content-addressed input hashing, and ships classes for Go, Rust, CMake, autotools, Node, Bun and Python. The README is refreshingly upfront that it is “an experiment in progress” and that “not everything in the documentation has been implemented yet”.

It is chasing the same two ideas Gaia is, a configuration that a human and an AI can both read, and a loop fast enough to actually iterate in. The machine definition is a genuinely nice piece of design, more expressive than Gaia’s distro JSON: the kernel, its defconfig, the command line and the partition layout are all right there, first-class.

machine(
    name = "raspberrypi4",
    arch = "arm64",
    kernel = kernel(
        unit = "linux-rpi4",
        defconfig = "bcm2711_defconfig",
        cmdline = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait rw",
    ),
    packages = ["rpi-firmware", "rpi4-config"],
    partitions = [
        partition(label = "boot", type = "vfat", size = "64M", contents = ["kernel", "dtbs", "firmware"]),
        partition(label = "rootfs", type = "ext4", size = "1G", root = True),
    ],
)

Then you open the unit that machine points at, and the familiar thing happens. This is linux-rpi4.star, trimmed:

    tasks = [
        task("build", steps=[
            "make ARCH=arm64 bcm2711_defconfig",
            "make ARCH=arm64 HOSTCFLAGS=\"$CPPFLAGS\" -j$NPROC Image modules dtbs",
            "install -D arch/arm64/boot/Image $DESTDIR/boot/kernel8.img",
            "install -D arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb $DESTDIR/boot/bcm2711-rpi-4-b.dtb",
            "make ARCH=arm64 INSTALL_MOD_PATH=$DESTDIR DEPMOD=true modules_install",
            "rm -f $DESTDIR/lib/modules/*/build $DESTDIR/lib/modules/*/source",
        ]),
    ],

A task with shell commands embedded as strings inside the metadata 🥲.

Starlark is a far better host for this than a templated YAML, an XML schema or a .csc: it gives you functions, reuse through load() and computed values without bolting a pre-processor onto the front, and it stays hermetic while doing it. That is the thing YAML can only fake, it has to reach for a templating layer, and the templating layer is a program. It is the best version of this pattern in the whole post. But it is still the same pattern: the shell lives inside the metadata. To know what building that kernel does, you evaluate a Starlark file and then read the strings it produced.

Gaia’s JSON is not a weaker version of that, it is the other answer to the same question. Instead of a language expressive enough to compute the configuration, the configuration stays inert data and the two jobs move elsewhere: abstraction into recipe overlay by priority + merge, logic into real executables. The Gaia equivalent of that unit is a JSON file with no commands in it at all and a build.ts next to it that you can open, run and debug on its own, and that a board recipe can overlay.

Below is a more feature-to-feature comparison:

CapabilityYoeGaia Build System
Recipe modelStarlark .star files: machine(), unit(), task(), with the build commands embedded as shell strings inside the metadata; language classes for Go, Rust, CMake, autotools, Node, Bun, PythonJSON distro file that composes JSON recipes discovered across the cookbooks listed in searchForRecipesOn; a recipe is pure metadata whose stages point to separate executable files in any language, and same-name recipes are overlaid by priority + merge
UserspaceEverything built from source into apk/deb packages, on an Alpine (musl) or Debian backendDebian binary archive, plus recipes for what Debian does not ship
KernelBuilt from source, declared per machine (kernel(unit=, defconfig=, cmdline=)) with the make and install lines inside the unitBuilt from source as part of a core recipe overlaid per SoC, with device-tree blobs wired in automatically
Bootloader / DTBBuilt from source as ordinary units, the boot chain listed in the machine’s packages and the boot partition contents (the BeaglePlay assembles tiboot3.bintispl.binu-boot.img)Built from source by the per-board BSP cookbook
Rootfs managementPackage-based image, updated on device from a project feed (apk upgrade, apt update)Mutable Debian rootfs by default; optional OSTree (atomic, content-addressed, with rollback) via the PhobOS cookbook
IsolationPer-unit builds sandboxed with bubblewrap inside toolchain containers; content-addressed input hashing for cache hitsDocker dev container as the build environment; recipes that need a specific toolchain bring their own pinned container (compose.yaml or hostAsContainer), with qemu-user-static binfmt for foreign architectures
SBOMNot yet, discussed in the docs as a goal the caching design is meant to enableYes (--sbom flag), CycloneDX of all components including Debian packages
Board coverageRaspberry Pi 4 and 5, BeaglePlay, plus QEMU arm64/x86-64 (Arduino UNO Q documented as a target)15+ platforms across NXP, Raspberry Pi, Synaptics, Qualcomm, Toradex, Intel, plus QEMU arm64/x86-64
Language / runtimeGo, single static binaryTypeScript on Deno (core); recipe steps in TypeScript, Xonsh, Bash or whatever could be executed
AI supportYes, the configuration is designed to be AI-readable and Claude Code is recommended in the README. Also provide skills.Yes (Mimir AI agent for natural language build orchestration)
Maturity6 months, explicitly an experiment in progress3 years

Conclusion

The problem was never the rootfs, it was everything underneath it. Every tool here builds that, and every one of them the same way: metadata and shell mixtures, and a board file you copy once and own forever. Gaia splits those apart. Inert JSON that cannot execute anything, steps as ordinary executables beside it, and a board that overlays the core recipes instead of copying them, so eleven lines describe a Raspberry Pi kernel and upstream fixes keep landing in it. Easy to customize. That separation is also what makes it AI-native: intent an agent can read, steps it can retry alone. Even easier to create our own based distro. Nothing worked that way, so I built the thing that does.

The fair part: 3 years against 9, 10 and 15+. Fewer eyes on it, a recipe format that still moves, 15+ platforms instead of hundreds, and a bus factor of basically me 🤪. Debian is the point and the ceiling, its versions, its footprint, no 20MB image. And the AI part is still an experiment, which is why automatic command execution is off by default.

So that’s why I built Gaia. Tell me where it fits, or where it falls short.

Yeah, sorry, this was a long post:

alt text

But it was needed to clear the confusion around why existing build systems did not meet my needs and how Gaia addresses those gaps.