Skip to content

YoeDistro/yoe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

466 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Yoe

[yoe] Next Generation

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.

Is [yoe] Right for You?

[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.

πŸš€ Getting Started

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)
poweroff

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

image

dev-image is another included image with a few more things in it.

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 units-core module 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.

Cross-Architecture Builds

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.

πŸ”§ Motivation

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:

  1. 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.

  2. 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.

  3. 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.

🧭 Principles

  1. 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.
  2. Aggressive caching. Cache at the developer, team, and global levels to avoid rebuilds whenever possible.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

🐳 Build Dependencies and Caching

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.23 image. 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-dev populates /usr/include and /usr/lib on 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 build and Go fetches its own modules. A Node unit runs npm 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.

πŸ€– 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"

See AI Skills for the full catalog of AI-driven workflows.

🎯 Design Priorities

  • 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 yoe concurrently. This is essential for rapid development using AI.

πŸ“š Documentation

  • AI Skills β€” AI-driven workflows for unit creation, build debugging, security auditing, and more
  • The yoe Tool β€” 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

πŸ’‘ Inspirations

[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.

βš™οΈ Design Principles

🚫 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.

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

πŸ“¦ Native Language Package Managers

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.

πŸ–₯️ 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 β€” 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.

πŸ—οΈ Go-Based Tooling

The [yoe] CLI 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. See The yoe Tool 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.

πŸ“‹ 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.

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 .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 .apk packages, which are published to a repository. Image assembly then uses apk to install packages into a root filesystem, just as Alpine does.

🧱 Base System

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.

πŸ”’ 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 (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.

About

Yoe Next Generation

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages