Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Yoe

[yoe] next generation

Fast tooling and builds. No cross-compiling headaches. Easy to customize/upgrade/debug. One tool for both system engineers and application developers to ship products faster.

[yoe] next generation is a build system (focused on Embedded Linux for now) for teams shipping modern edge products. Components in Go, Rust, Zig, Python, JS/TS, and C/C++ are supported. [yoe] releases often and tracks upstream closely. The configuration language is easily processed by humans and AI. Build on your laptop, on native hardware, or in cloud CI — one integrated tool, same config, same results.

We 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. [yoe] next generation is an experiment in progress — we’re sharing the work in the open as it takes shape.

Note: Not everything in the documentation has been implemented yet as this project is in the early stages.

🎯 Goals

Three north stars guide the experiment — everything else is in service of these:

  1. Drastically improve developer productivity. Shrink the loop between an idea and a running image. Fewer rebuilds, fewer context switches, fewer tools to learn — so engineers spend their time on the product, not on the build.
  2. Easily integrate complex workloads. Modern edge devices ship Go services, Rust agents, Python ML, container workloads, and kernel drivers side by side. Pulling these together should be straightforward, not a custom integration project each time.
  3. Scale to build anything. From tiny Zephyr images to complex AI workloads. From one application to a complete distributed system. One tool, one mental model — the same workflow on a laptop, a build farm, or cloud CI, with caching that keeps up as projects grow.

Is [yoe] Right for You?

[yoe] build is not for everyone. If you are building a mission-critical system that requires bit-for-bit reproducible builds, long-term release freezes, or extensive compliance certification, use the current generation [Yoe Distribution](https://yoedistro.org/ based on Yocto - it is battle-tested for those requirements.

[yoe] build is designed for edge systems that behave more like cloud systems — AI workloads and modern-language applications — and for teams that track upstream closely and prioritize fast iteration over strict reproducibility. If your product ships frequent updates, runs containerized services, or depends heavily on Go/Rust/Python ecosystems, [yoe] may be a better fit.

More fundamentally, [yoe] assumes a small-team problem set, not a scaled-down enterprise one. A startup or ten-person product team doesn’t have smaller versions of an enterprise’s problems — it often has different problems entirely, and adopting tooling built for scale you don’t have imports its operational cost without the payoff (the You Are Not Google point. [yoe] is calibrated for the problems small teams and startups actually have.

🚀 Getting Started

Prerequisites: Linux or macOS with Git and Docker installed. Windows users: install WSL2 and use the Linux binary (Linux x86_64/Docker is the most tested configuration). Claude Code is highly recommended, but not required.

To run a built image you also need QEMU. On Debian/Ubuntu:

sudo apt-get install qemu-system-x86

For KVM acceleration, your user must be in the kvm group. If yoe run reports failed to initialize kvm: Permission denied, add yourself and pick up the new group membership:

sudo usermod -aG kvm $(whoami)
newgrp kvm   # or log out and back in
# Download the yoe binary (Linux x86_64)
curl -L https://github.com/yoebuild/yoe/releases/latest/download/yoe-Linux-x86_64 -o yoe
# For other platforms, download from https://github.com/yoebuild/yoe/releases/latest

chmod +x yoe
mkdir -p ~/bin
mv yoe ~/bin/
# Make sure ~/bin is in your PATH (add to ~/.bashrc or ~/.zshrc if needed)
export PATH="$HOME/bin:$PATH"

# Create a new project
yoe init yoe-test
cd yoe-test

# Start the TUI (see screenshot below)
yoe

# Navigate to the base-image and press 'b' to build

# When build is complete, press 'r' to run (requires `qemu-system-x86_64` installed on your host)

# Log in a user: root, no password

# Power off when finished (inside running image)
poweroff

The TUI user interface:

screenshot

dev-image is another included image with a few more things in it. Press the s key to navigate to the setup menu and select the image.

The / key modifies the unit query to change what units are displayed.

The tab key shifts between the unit, modules, and diagnostics pages.

For more information on the TUI, see the [yoe] tool documentation.

There are also CLI variants of the above commands (build, run, etc.).

What just happened:

  1. yoe init created a project with a PROJECT.star config and a default x86_64 QEMU machine.
  2. On first build, yoe automatically built a Docker container with the toolchain (gcc, make, etc.) and fetched the default unit modules from GitHub.
  3. It built ~10 packages from source (busybox, linux kernel, openssl, etc.) inside the container, each isolated in its own bubblewrap sandbox.
  4. It assembled a bootable disk image from those packages.
  5. yoe run launched the image in QEMU with KVM acceleration.

Everything is in the project directory — no global state, no hidden caches outside the tree.

🔧 Why This Is Possible Now

A decade ago, this combination wasn’t realistic. Several things have changed:

  1. ARM and RISC-V hardware is fast enough to build natively. Modern ARM boards and cloud instances (AWS Graviton, Hetzner CAX) build at full speed. For development, QEMU user-mode emulation runs ARM containers on x86 — no cross-toolchain needed.
  2. Modern languages bring their own package managers. Go, Rust, Zig, and Python already handle dependency resolution, reproducible builds, and caching. [yoe] doesn’t reinvent any of that — application developers use the same Cargo, Go modules, or pip they already know.
  3. AI can guide developers through the system. The hardest part of embedded Linux is knowing what to configure and why. [yoe]’s metadata is structured Starlark — queryable, not buried in shell scripts — so an AI assistant can create units, diagnose build failures, and audit security without the developer memorizing the build system’s quirks.

🧭 Values

  1. The product developer experience is the top priority. Other solutions are often not optimized for developers building products. This includes application developers as well as system engineers. Clear and concise communication is essential when things go wrong. Unintelligible stacktraces are unacceptable.
  2. Be Pragmatic. Leverage what already exists where it makes sense. We don’t have any religion that everything needs to be built from source, or that we all need to build our own toolchains.
  3. Optimized for small teams. [yoe] is a tool for small teams to do big things. There are plenty of enterprise tools (Bazel, Buck2, Maven, etc.); we will use ideas from these tools, but [yoe] aims to be something different.
  4. Scope is not limited to Embedded Linux. Although Embedded Linux is our current focus, a tool like [yoe] could be used for any problem where you pull a lot of pieces together. At its heart, [yoe] is a tool for building complex systems.
  5. Track upstream closely. Modern edge systems are more like the cloud than traditional embedded systems — they are connected, updated regularly, and expected to receive security patches throughout their lifetime. [yoe] assumes you will track upstream releases closely rather than freezing on a version for years. Updating a package should be easy and routine, not a high-risk event that requires a dedicated engineering effort.
  6. Vendor Neutral. [yoe] is a vendor neutral project and welcomes BSPs and other units from any vendor. The goal is to build an integrated ecosystem like Zephyr.

🤖 Why AI-Native

Embedded Linux is hard not because the concepts are complex, but because there are many concepts that interact in non-obvious ways: toolchain flags, dependency ordering, kernel configuration, package splitting, module composition, image assembly, device trees, bootloaders. Traditional build systems manage this complexity through complexity.

[yoe] takes a different approach: Simplify things as much as possible. Starlark units are readable by both humans and AI. The dependency graph is queryable. Build logs are structured. An AI assistant that understands all of this can:

  • Create units from a URL or description/new-unit https://github.com/example/myapp
  • Diagnose build failures by reading logs and the dependency graph — /diagnose openssh
  • Trace why a package is in your image/why libssl
  • Simulate changes before building/what-if remove networkmanager
  • Audit for CVEs and license compliance/cve-check, /license-audit
  • Generate machine definitions from board names/new-machine "Raspberry Pi 5"

Add these skills to your project with yoe skills install (or as a Claude Code plugin via /plugin marketplace add yoebuild/yoe). See AI Skills for installation details and the full catalog of AI-driven workflows.

💡 Inspirations

[yoe] draws selectively from existing systems, taking the best ideas from each while avoiding their respective pain points:

  • Yocto — machine abstraction, image composition, module architecture, OTA integration. Leave behind BitBake, sstate, cross-compilation complexity.
  • Buildroot — the principle that simpler is better. Leave behind monolithic images and full-rebuild-on-config-change.
  • Arch — rolling release, minimal base, PKGBUILD-style simplicity, documentation culture. Leave behind x86-centrism and manual administration.
  • Alpine — apk package manager, busybox, minimal footprint, security defaults. Leave behind lack of BSP support.
  • Nix — content-addressed caching, declarative configuration, hermetic builds, atomic rollback. Leave behind the Nix language and store-path complexity.
  • Google GN — two-phase resolve-then-build model, config propagation through the dependency graph, build introspection commands, label-based target references for composability. Leave behind the C++-specific build model and Ninja generation.
  • Bazel — Starlark as a build configuration language, hermetic sandboxed actions, content-addressed action caching, and remote build execution. Leave behind the monorepo bias, JVM runtime, and BUILD-file verbosity that make Bazel heavy for small teams.

See Comparisons for detailed analysis of how [yoe] relates to each of these (and other) systems, including when you should use them instead.

⚙️ Design

🏗️ A Single Tool

At its heart, [yoe] is a single tool — one Go binary that handles the entire build flow, from fetching sources to assembling bootable images. It exposes three interfaces: AI conversation, an interactive TUI, and a traditional CLI. All three do the same things; use whichever fits the moment.

The tool handles:

  • TUI — run yoe with no arguments for an interactive unit list with inline build status, background builds, search, and quick actions (edit, diagnose, clean).
  • Build orchestration — invoke language-native build tools in the right order, manage caching, assemble outputs. Multiple images and targets live in a single build tree (like Yocto). No global lock or global resource: concurrent yoe invocations run in parallel, which is essential for rapid AI-driven development.
  • Machine/distro configuration — define target boards and distribution profiles in Starlark — Python-like, deterministic, sandboxed.

See The yoe Tool for the full CLI reference, Unit & Configuration Format for the unit and config spec, and Build Languages for the Starlark rationale.

Why Go: single static binary with no runtime dependencies, fast compilation, excellent cross-compilation support (useful for shipping the tool itself), and a strong standard library for file manipulation, process execution, and networking.

🖥️ Workstation-Centric Development

[yoe] is designed first for the developer’s workstation. While yoe runs equally well on an embedded dev kit, an ARM cloud instance, or a CI runner, the development loop is centered on the machine where developers already have their editor, shell, browser, debugger, and Git workflow set up. You should not need to SSH into a Raspberry Pi to get real work done.

That said, the option is there. The selfhost-image for the Raspberry Pi 5 bundles yoe, Go, Docker, git, and the dev-image tool set (helix, yazi, zellij) — flash, boot, and a freshly-imaged RPi5 is a complete native ARM64 build host. See Self-Host on RPi5.

When a build runs much faster on a different native architecture — for example, building ARM64 packages from an x86_64 workstation — those build steps can be dispatched to a native builder (a local ARM board on the network, an AWS Graviton instance, a Hetzner ARM box) without changing the developer’s workflow. This is similar to how CI systems dispatch jobs to remote runners: the developer’s workstation plays the role of the orchestrator, and remote machines act as architecture-specific runners. Orchestration, source edits, project state, the TUI, and the CLI stay on the workstation; only the per-unit build steps that benefit from native execution run elsewhere.

If a remote builder isn’t available — offline, on a plane, or the cloud runner is down — QEMU user-mode emulation is the fallback so builds can always continue locally. It’s slower, but the workflow doesn’t break.

The result: developers stay in the environment they already have tuned, and [yoe] puts the right machine behind each build step.

🚫 No Cross Compilation

Instead of maintaining cross-toolchains, [yoe] targets native builds:

  • QEMU user-mode emulation — build ARM64 or RISC-V images on any x86_64 workstation. The build runs inside a genuine foreign-arch Docker container, transparently emulated via binfmt_misc. One command to set up (yoe container binfmt), then --machine qemu-arm64 just works. ~5-20x slower than native, but fine for iterating on a few packages.
  • Native hardware — build on the target architecture directly (ARM64 dev boards, RISC-V boards).
  • Cloud CI — use native architecture runners (e.g., ARM64 GitHub Actions runners, AWS Graviton, Hetzner ARM boxes) for full-speed CI builds.
  • Per-unit build environment — each unit runs in its own Docker container with bubblewrap sandboxing. Architecture is determined per unit, not globally, and build dependencies don’t pollute the host or leak between units.

This eliminates an entire class of build issues (cross-sysroot management, host contamination, cross-pkg-config, etc.).

* Note, we are not opposed to cross compilation where it makes sense. Go applications cross-compile very easily. The Linux kernel is generally easy to cross-compile, so we may do that. But, cross-compilation of C/C++ components is not required.

📦 Native Language Package Managers

Each language ecosystem manages its own dependencies:

LanguagePackage ManagerLock File
GoGo modulesgo.sum
RustCargoCargo.lock
Pythonpip / uvrequirements.lock
JavaScriptnpm / pnpmpackage-lock.json
ZigZig buildbuild.zig.zon

[yoe] plays nicely with existing language caching infrastructure so builds are fast and repeatable without re-downloading the internet.

🖥️ Kernel and System Image Tooling

While application builds use native language tooling, the system-level pieces still need orchestration:

  • Kernel builds — configure, build, and package kernels for target boards.
  • Root filesystem assembly — combine built artifacts into a bootable image (ext4, squashfs, etc.).
  • Device tree / bootloader management — board-specific configuration.
  • OTA / update support — image-based device management (full image updates, OSTree, BDiff) integrated with update frameworks (RAUC, SWUpdate, etc.). Container workloads on the target device are on the roadmap.

This is where [yoe] tooling (written in Go and Starlark) provides value — similar to what bitbake and wic do in Yocto, but simpler and more opinionated.

📋 Package Management: apk

[yoe] uses apk (Alpine Package Keeper) as its package manager. It is important to distinguish between units and packages — these are separate concepts:

  • Units are build-time definitions (Starlark .star files in the project tree) that describe how to build software. See Unit & Configuration Format.
  • Packages are installable artifacts (.apk files) that units produce. They are what gets installed into root filesystem images and onto devices.

This separation means units are a development/CI concern, while packages are a deployment/device concern. You can build packages once and install them on many devices without needing the unit tree. Rebuilding from source is first class but not required — every package is fully traceable to its unit, with no golden images.

Why apk over apt and dnf:

  • Speed — apk operations are near-instantaneous. Install, remove, and upgrade are measured in milliseconds, not seconds.
  • Simple format — an .apk package is a signed tar.gz with a .PKGINFO metadata file. No complex archive-in-archive wrapping.
  • Small footprint — apk-tools is tiny, appropriate for embedded targets.
  • Active development — apk 3.x adds content-addressed storage and atomic transactions, aligning with [yoe]’s Nix-inspired reproducibility goals.
  • Works with glibc — apk is not tied to musl; it works with any libc. [yoe] runs its own package repositories, not Alpine’s.
  • On-device package management — devices can pull updates from a [yoe] package repository, enabling incremental OTA updates (install only changed packages) alongside full image updates.

The [yoe] build tooling invokes units to produce packages — .apk on the default Alpine base, .deb on the experimental Debian/Ubuntu bases — which are published to a repository. Image assembly then installs those packages into a root filesystem with the base’s native tool (apk on Alpine, mmdebstrap/apt on Debian/Ubuntu).

🧱 Base System

The base userspace depends on the image’s distro. The default — and most mature — base is Alpine-derived: busybox on a musl C library, with busybox’s built-in init as PID 1. [yoe] also builds experimental Debian and Ubuntu bases, which are glibc worlds running systemd as PID 1 (see Yoe and distributions). The choice is per image:

  • C library — musl on the Alpine base (inherited from Alpine’s toolchain); glibc on the Debian/Ubuntu bases, for maximum compatibility with pre-built binaries, vendor blobs, language runtimes (Go, Rust, Python, Node.js), and third-party libraries that assume glibc.
  • busybox (Alpine base) — provides the core userspace utilities (sh, coreutils, etc.) and init in a single small binary, keeping the image minimal while still giving a functional shell environment for debugging and scripting. The Debian/Ubuntu bases ship the full GNU coreutils instead.
  • Init — busybox’s built-in init handles PID 1 on the Alpine base; the Debian/Ubuntu bases run systemd, with its rich service management, integrated journal logging, network management, and device management (udev). The trade-off is size and complexity — which is why the lean busybox path stays the default.

This combination gives a small but fully functional base system that can run real-world services without surprises.

🔒 Reproducibility

[yoe] targets functional equivalence, not bit-for-bit reproducibility. Same inputs produce functionally identical outputs — same behavior, same files, same permissions — but the bytes may differ due to embedded timestamps, archive member ordering, or compiler non-determinism.

This is a deliberate trade-off:

  • Bit-for-bit reproducibility (what Nix aspires to) requires patching upstream build systems to eliminate timestamps (__DATE__, .pyc mtime), enforce file ordering in archives, and strip or fix build IDs. This is enormous effort — Nix still hasn’t fully achieved it after 20 years — and the primary benefit (verifying a binary matches its source by rebuilding) is relevant mainly for high-assurance supply-chain contexts.
  • Functional equivalence gets the practical benefits — reliable caching, hermetic builds, provenance tracking — without the patching burden. Bubblewrap isolation prevents host contamination. Content-addressed input hashing — combining hashes of the unit, its source, and its dependencies — ensures cache hits are reliable. Starlark evaluation is deterministic by design. The remaining non-determinism (timestamps, ordering within packages) doesn’t affect functionality or caching.

The caching model does not depend on output determinism. Cache keys are computed from inputs (unit content, source hash, dependency .apk hashes, build flags), not outputs. If inputs haven’t changed, the cached output is used regardless of whether a fresh build would produce identical bytes.

📚 Documentation

See the main documentation site for more information.

🤝 Contributing

Contributions are welcome — especially BSPs for new boards and units for new packages. AI-assisted contributions are fine; just make sure the result actually works, and keep PRs small and reviewable. Please discuss architectural or significant UI changes before implementing.

💚 Sponsors

[yoe] is supported by:

BEC Systems

📄 License

[yoe] is licensed under the Apache License 2.0.