For teams building edge products in Go, Rust, Zig, Python, etc. who need Linux on ARM/RISC-V without the complexity of Yocto.
We have maintained the Yoe Distribution for many years
which is a Yocto based Embedded Linux distribution designed for product
development. [yoe] is a fresh take on building software for edge devices. Your
application is written in modern language. Your target is embedded/edge
hardware. You need a minimal Linux image with your app, the right kernel, and
nothing else. You shouldn't need to learn complex build systems, manage
cross-toolchains, or read a thousand-page manual to get there.
One Go binary. Readable config files. AI that understands the system and how to modify/debug it. Build ARM images on your x86 laptop, on native hardware, or in cloud CI β same tool, same config, same results.
Note: Not everything in the documentation has been implemented yet as this project is in the early stages.
[yoe] 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 Yocto β
it is battle-tested for those requirements.
[yoe] is designed for edge systems that behave more like cloud systems: AI
workloads, applications written in modern languages, systems that track upstream
closely, and teams that 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.
Prerequisites: Linux or macOS with Git and Docker (or Podman) 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.
# Download the yoe binary
curl -L https://github.com/YoeDistro/yoe/releases/latest/download/yoe-$(uname -s)-$(uname -m) -o yoe
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.
# Log in a user: root, no password
# Power off when finished (inside running image)
poweroffThere are also CLI variants of the above commands (build, run, etc.).
dev-image is another included image with a few more things in it.
What just happened:
yoe initcreated a project with aPROJECT.starconfig and a default x86_64 QEMU machine.- On first build,
yoeautomatically built a Docker container with the toolchain (gcc, make, etc.) and fetched theunits-coremodule from GitHub. - It built ~10 packages from source (busybox, linux kernel, openssl, etc.) inside the container, each isolated in its own bubblewrap sandbox.
- It assembled a bootable disk image from those packages.
yoe runlaunched the image in QEMU with KVM acceleration.
Everything is in the project directory β no global state, no hidden caches outside the tree.
Build ARM64 images on an x86_64 host using QEMU user-mode emulation:
# One-time setup: register QEMU user-mode emulation
yoe container binfmt
# Build for ARM64
yoe build base-image --machine qemu-arm64
# Run it
yoe run base-image --machine qemu-arm64(or run the above in TUI be selecting machine in setup first)
No cross-compilation toolchain needed β the build runs inside a genuine ARM64 Docker container, transparently emulated by the host kernel.
Existing embedded Linux build systems (Yocto, Buildroot) were designed in a world where ARM hardware was slow, applications were written in C, and developers configured everything by hand. Three things have changed:
-
ARM and RISC-V hardware is fast. Modern ARM boards and cloud instances (AWS Graviton, Hetzner CAX) build at speeds that make cross-compilation unnecessary for most workloads. For development, QEMU user-mode emulation lets you build ARM images on x86 without a cross-toolchain β slower, but correct and simple.
-
Applications are moving to modern languages. Go, Rust, Zig, and Python have their own dependency management, reproducible builds, and caching. The elaborate cross-compilation and sysroot machinery in traditional build systems was designed for C/C++ β wrapping Go modules or Cargo in BitBake recipes adds friction without proportional benefit.
-
AI changes the interface. The hardest part of embedded Linux is knowing what to configure and how. An AI assistant that understands the build system can guide developers through unit creation, debug build failures, and audit security β without requiring them to memorize a build system's quirks. But this only works if the build metadata is structured and queryable, not buried in shell scripts and environment variables.
[yoe] is built for this new world: native builds, language-native package
managers, structured Starlark metadata, and AI as a first-class interface.
- Leverage existing infrastructure. Docker containers already provide working toolchains, and language-native package managers (Go modules, pip, npm, Cargo) already solve dependency resolution and caching β there is no need to rebuild the world or reimplement what these ecosystems provide.
- Aggressive caching. Cache at the developer, team, and global levels to avoid rebuilds whenever possible.
- Custom containers per unit and task. There is no one-size-fits-all container for an entire build. Units can specify the host container environment they need.
- No intermediate formats. Avoid generating shell scripts or other intermediate artifacts when possible. Intermediate formats complicate debugging β when something fails, you should be looking at the code you wrote, not machine-generated output.
- One tool for all levels. The tool should be fast and simple enough to be used for both system software development and application development. Generating SDKs is a waste of time if everyone can use the same tool.
- 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.
Traditional embedded build systems maintain a sharp boundary between "building the OS" and "developing applications." The OS team produces an SDK β a frozen snapshot of the sysroot, toolchain, and headers β and hands it to application developers. From that point on, the two worlds drift: the SDK ages, libraries diverge, and "it works on my machine" becomes "it works with my SDK version."
[yoe] eliminates this boundary by recognizing that there are distinct kinds of
build dependencies, and they should be managed differently:
- Host tools (compilers, build utilities, code generators) β these come from
Docker containers. Every unit can specify its own container, so one team's
toolchain requirements don't constrain another. A kernel unit can use a
minimal C toolchain container. A Go application can use the official
golang:1.23image. A Rust service can pin a specific Rust nightly. - Library dependencies (headers, shared libraries your code links against) β
these come from a shared sysroot populated by apk packages. Each unit produces
an apk package when it builds; that package is either built locally or pulled
from a cache (team-level or global). Before a unit builds, its declared
dependencies are installed from these packages into the sysroot β the same way
apt install libssl-devpopulates/usr/includeand/usr/libon a Debian system. Most developers never build OpenSSL themselves; they pull the cached package and get the headers and libraries they need in seconds. - Language-native dependencies (Go modules, npm packages, Cargo crates, pip
packages) β these are managed by the language's own package manager, not the
sysroot. A Go unit runs
go buildand Go fetches its own modules. A Node unit runsnpm install. Cargo handles Rust crates. These ecosystems already solve dependency resolution, caching, and reproducibility β[yoe]doesn't reimplement any of that. The container provides the language runtime (Go compiler, Node, rustc), and the language's package manager handles the rest. When a language unit also needs a C library (e.g., a Rust crate linking against libssl via cgo or FFI), that C library comes from the sysroot as usual.
Caching is symmetric at the unit level. Every unit β regardless of language β produces an apk package that is cached and shared across developers, CI, and build machines. Most people never rebuild a unit; they pull the cached apk.
The difference shows up when you do rebuild: a C unit finds its dependencies
already in the sysroot (from other units' cached apks), while a Rust unit has
Cargo recompile its crate dependencies using its local cache. This is fine β the
person rebuilding a Rust unit is the developer actively working on it, and their
local Cargo cache handles repeat builds. Go builds so fast it does not matter.
Some ecosystems go further: PyPI distributes pre-compiled wheels globally, so
pip install pulls binaries for most packages without compiling anything.
[yoe] doesn't need to replicate what these ecosystems already provide.
Native builds unlock existing package ecosystems. This is especially clear
with Python. In traditional cross-compilation systems like Yocto or Buildroot,
PyPI wheels are useless β pip runs on the x86_64 host but the target is ARM, so
pre-compiled aarch64 wheels can't be installed. Instead, every Python package
needs a custom recipe that cross-compiles C extensions against the target
sysroot, effectively reimplementing pip. In [yoe], pip runs inside a
native-arch container (real ARM64 or QEMU-emulated), so pip install numpy just
downloads the aarch64 wheel from PyPI and unpacks it β no compilation, no
custom recipe. The same advantage applies to any language ecosystem that
distributes pre-built binaries by architecture.
Note, there are risks with safety or mission-critical systems of using packages from a compromised global package system. We could force building of Python packages in some cases or verify the binaries via a hash mechanism. This point is for developers, we should be able to leverage all the conveniences modern language ecosystems provide.
Containers provide the tools to build. The sysroot provides C/C++ libraries
to link against. Language-native package managers handle everything else. For
any given unit, the developer, the system team, and CI all use the same
container β that's how you stay in sync. A new developer clones the repo, runs
yoe build, and gets working build environments pulled automatically.
Docker containers are already the standard way teams manage development
environments. [yoe] leans into this rather than inventing a parallel universe
of SDKs.
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"
See AI Skills for the full catalog of AI-driven workflows.
- AI-native β structured metadata (Starlark), queryable dependency graphs, and AI skills as first-class interfaces. See AI Skills.
- Three interfaces β AI conversation, interactive TUI, and traditional CLI. All three do the same things; use whichever fits the moment.
- Developer-focused β first-class support for application development, not just system integration. Good tooling for kernel, applications, and BSPs (similar to Yocto's scope, but simpler).
- Simple β one Go binary, one language (Starlark), one package format (apk)
- Easy to get started β AI guides you through project setup, unit creation, and image configuration
- Tooling written in Go β single static binary, no runtime dependencies, TUI built with Bubble Tea, fast enough, trivial to distribute
- Build dependencies isolated with bubblewrap β no host dependency pollution
- Easy BSP support β support for many boards, inclusive of hardware ecosystem
- Multiple images/targets in a single build tree (like Yocto)
- Rebuilding from source is first class, but not required β fully traceable, no golden images
- Modern languages (Go, Rust, Zig, Python, JavaScript) β uses native language package managers, caches packages where possible
- Cross compilation is optional β native builds via QEMU user-mode emulation or real ARM/RISC-V hardware. Build environment is per-unit, not global β each unit runs in its own isolated container. For some toolchains, like Go, which cross compile easily, then it makes sense to cross compile. For some C/C++ packages, it may make sense to still cross compile them if it is easy and fast, but this is optional. For fussy/difficult packages, we can avoid cross- compilation altogether when necessary.
- Starlark for units and build rules β Python-like, deterministic, sandboxed (see Build Languages)
- 64-bit only β x86, ARM, RISC-V
- Granular packaging (like Yocto/Debian) β one unit can produce multiple
sub-packages (
-dev,-doc,-dbg, custom splits) - Composable modules β pull in units/packages using GitHub URLs; vendor BSP,
product, and core modules compose through Starlark
load()function calls - Image-based device management β full image updates, OSTree, BDiff. Container workloads on the target device are on the roadmap β running application containers on edge hardware is a natural fit for systems that already track upstream closely.
- Parallel β no global lock or global resource, support running concurrent
versions of
yoeconcurrently. This is essential for rapid development using AI.
- AI Skills β AI-driven workflows for unit creation, build debugging, security auditing, and more
- The
yoeTool β CLI reference for building, imaging, and flashing - Unit & Configuration Format β Starlark unit and configuration spec
- Build Environment β bootstrap, host tools, and build isolation
- SDK Management β development environments, container-based SDK, pre-built binary packages
- Comparisons β how
[yoe]relates to Yocto, Buildroot, Alpine, Arch, and NixOS - Build Languages β analysis of Starlark, CUE, Nix, and other embeddable languages for unit definitions
- Units Roadmap β existing units and what's needed for a complete base system
[yoe] draws selectively from five 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 musl and 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.
See Comparisons for detailed analysis of how [yoe]
relates to each of these systems, including when you should use them instead.
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-arm64just 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.
This eliminates an entire class of build issues (sysroot management, host contamination, cross-pkg-config, etc.).
Each language ecosystem manages its own dependencies:
| Language | Package Manager | Lock File |
|---|---|---|
| Go | Go modules | go.sum |
| Rust | Cargo | Cargo.lock |
| Python | pip / uv | requirements.lock |
| JavaScript | npm / pnpm | package-lock.json |
| Zig | Zig build | build.zig.zon |
[yoe] provides caching infrastructure (a shared module proxy for Go, a
registry mirror for Cargo/npm, etc.) so builds are fast and repeatable without
re-downloading the internet.
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 β integration with update frameworks (RAUC, SWUpdate, etc.).
This is where [yoe] tooling (written in Go) provides value β similar to what
bitbake and wic do in Yocto, but simpler and more opinionated.
The [yoe] CLI tool handles:
- TUI β run
yoewith 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. See
The
yoeTool for the full CLI reference. - Machine/distro configuration β define target boards and distribution profiles in Starlark. See Unit & Configuration Format for the full specification.
Why Go:
- Single static binary β no runtime dependencies, trivial to distribute.
- Fast compilation and execution.
- Excellent cross-compilation support (ironic, but useful for the tool itself).
- Strong standard library for file manipulation, process execution, and networking.
[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
.starfiles in the project tree) that describe how to build software. See Unit & Configuration Format. - Packages are installable artifacts (
.apkfiles) 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.
Why apk over pacman or opkg:
- Speed β apk operations are near-instantaneous. Install, remove, and upgrade are measured in milliseconds, not seconds.
- Simple format β an
.apkpackage is a signed tar.gz with a.PKGINFOmetadata 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 .apk packages, which are
published to a repository. Image assembly then uses apk to install packages
into a root filesystem, just as Alpine does.
Currently running musl busybox/init for simplicity, but hope to eventually move to glibc/systemd.
The base userspace is glibc + busybox + systemd:
- glibc β the standard C library. Maximizes compatibility with pre-built binaries, language runtimes (Go, Rust, Python, Node.js), and third-party libraries. musl is lighter but introduces subtle compatibility issues that aren't worth fighting in a system that already includes systemd.
- busybox β provides the core userspace utilities (sh, coreutils, etc.) in a single small binary. Keeps the base image minimal while still having a functional shell environment for debugging and scripting.
- systemd β the init system and service manager. Despite its size, systemd
is the pragmatic choice:
- Well-understood by developers and ops teams.
- Rich ecosystem of unit files for common services.
- Built-in support for journal logging, network management, device management (udev), and container integration.
- Required or assumed by many modern Linux components.
This combination gives a small but fully functional base system that can run real-world services without surprises.
[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__,.pycmtime), 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 (unit
- source + dependency hashes) 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.