BQ2 is a small Linux system that rebuilds itself from readable source. It began as a way to make bootable disk images. It grew into a sandbox, a private, closed-off area, where anyone running Linux can write Crystal code, managed with Fossil and Git.
Where it began
The project started in June 2025 to make QCOW2 disk images. QCOW2 is the disk format used by QEMU, a program that emulates a whole computer. The image boots through EFI. EFI is the standard way a computer's firmware, the small program built into it, starts an operating system. The first images, in July 2025, were built with Buildroot, an existing tool for building small Linux systems.
An image is only trustworthy if you can rebuild it. Rebuilding it needs the compilers that built it, and those compilers must be rebuilt too. So the work turned to the toolchain: the compilers and tools that turn source code into programs. BQ2 now builds its own compilers from source, starting from a seed. A seed is a ready-made root filesystem, the full set of files a Linux system runs from, with just enough tools to start the build. The first seed was Alpine Linux's minirootfs. BQ2 now builds its own seed, and that seed builds the next one.
The project has three goals. Crystal rebuilds itself inside the system. Fossil rebuilds itself there and serves repositories. And the system ships as a disk image that boots through EFI and is ready to host Fossil on a server. That first aim, a bootable image, is still the third goal.
Why yet another Linux distribution?
BQ2 isn't trying to be a better distribution. It asks a narrower question: can one person read, rebuild and check a system's whole toolchain? Distributions build with many languages: shell scripts, Make, package recipes. Linux From Scratch is a book of shell commands that a person types by hand. BQ2's build is one program, written in Crystal, that can be read from start to end. Each rebuild is checked byte for byte against the last.
Every mind is bounded. Every extra syntax a reader must learn before following the build is another barrier.
What it's made of
- Crystal, for the build itself. One program, bq2, does every job: planning, building, entering the sandbox, even fetching files.
- LLVM and Clang, the compilers, and lld, the linker: the tool that joins compiled pieces into one program.
- musl, the C library: the code every C program calls. And BusyBox, the basic Unix commands in one program.
- Fossil and Git, for version control. Fossil also serves repositories on the web.
- Smaller pieces, such as SQLite, CMake and zlib.
All of these come from upstream sources: the original projects' own releases, not copies changed by another distribution. There are 22 in all. Each is pinned, fixed to one version. Each should also have a checksum, a fingerprint of the file that proves it hasn't changed. Two still lack one; adding them is in progress.
How it's shaped
A build is a plan: a list of phases, each a list of steps. A step's recipe is the set of instructions for building one package. Each phase runs inside its own root filesystem. The builder writes the plan to a JSON file. The runner reads that file, runs each step, and records which steps are done, so a stopped build can resume.
Why JSON? The plan is written outside the sandbox and read inside it, so it has to live in a file. Crystal's standard library reads and writes JSON with no extra code. Inside the program, the plan is plain Crystal structures. One question is still open: should the plan stay a JSON file, or become Crystal code itself? It was never decided. The project's stated direction is one syntax for both code and data.
Why its own sandbox tools? A user namespace is a Linux feature that lets an ordinary user act as root inside a sandbox, without sudo. BQ2 enters its sandbox with its own Crystal code instead of tools such as bubblewrap or Docker. The one recorded reason is in the code: it gives a way in without sudo. It also follows the project's rule of Crystal over shell tools, so the build stays in one language. Whether bubblewrap was weighed and rejected was never recorded.
Running it on any Linux machine
The point is this: if you run Linux on a compatible processor with the right kernel settings, you can unpack a seed, start it in its own sandbox, and write any Crystal code there, managed with Fossil and Git. The x86_64 seed is 106 MB compressed. It holds Crystal, shards, Clang, Fossil and Git.
It needs:
- an x86_64 or aarch64 processor: 64-bit Intel/AMD or ARM;
- unprivileged user namespaces, that is, ones made without root. Kernel settings are switches an administrator sets in the running Linux kernel. The two that matter are kernel.unprivileged_userns_clone=1 and kernel.apparmor_restrict_unprivileged_userns=0, on kernels that have them;
- a /dev whose device files can be bind-mounted, that is, made visible at a second place.
One limit today: the bq2 inside the seed only runs with the seed's own C library. So you enter the seed with a bq2 built on the host, and that needs Crystal on the host. Starting a seed with nothing installed on the host hasn't been shown yet.
How it's built
Work moves in small steps. Changes to sources and recipes are collected, then proven by one full rebuild on each processor, checked byte for byte. The Status page (https://ollama.openbeagle.org/bootstrap-qcow2/wiki?name=Status) says where things stand.
What success looks like
- From its seed, BQ2 rebuilds itself byte for byte on x86_64 and on aarch64. x86_64: done (https://ollama.openbeagle.org/bootstrap-qcow2/forumpost/9db587576c). aarch64: not yet; two builds still differ.
- Its image boots, in QEMU and on a board, and rebuilds itself there. Not yet shown for the current seed.
- Every source it builds from is a Fossil repository on this server: readable, patchable, and ready for newer versions or Crystal rewrites. In progress: one trial, bdwgc, works locally (https://ollama.openbeagle.org/bootstrap-qcow2/forumpost/29d6a59fe6).
- A seed builds fast enough to work with. Each speed change is measured against the last build, and the tests and the byte-for-byte check still pass. In progress: an x86_64 seed takes 3 h 32 min, and building LLVM three times is 2 h 43 min of that (https://ollama.openbeagle.org/bootstrap-qcow2/forumpost/e12047277c).
- Its non-Crystal pieces shrink over time, one measured step at a time. Not yet measured.
- It is the whole toolchain for Glint and cleave, and it runs on Glint on a board like BeagleBadge. Partly: Glint already builds with BQ2's tools alone (https://ollama.openbeagle.org/glint/doc/trunk/README.md). Running on Glint: not yet.
At the edges of this picture: BQ2 could one day define the workspaces that OCX jobs run in. OCX is the system that hands this program's jobs to machines. That is a direction, not a plan.
The virtues behind it
Like every project in this program, BQ2 follows OCX's four virtues:
- Clearly stated: anyone can tell what each part does. The build is one Crystal program, and the plan is a file anyone can open.
- Loosely coupled: each part can be swapped without breaking the rest. The seed can be swapped, and Alpine stays available for anyone starting from nothing.
- Demonstrably verified: every claim is shown working, not just stated. A rebuild isn't done until it matches the last one byte for byte.
- Parsimoniously bounded: it does no more than it needs to. Each package must justify its place, and the non-Crystal pieces are meant to shrink.
Every mind is bounded, so the build reads in one language. And many eyes outlast one: code shared where many people already work, such as Linux, LLVM and Crystal, stays better supported than code kept alone. So BQ2 builds from those upstream projects instead of forking them. Their sources are mirrored here only so anyone can read and patch them.
Build reproducible QCOW2 and chroot images with Crystal-first tooling. The sysroot builder targets aarch64 by default, caches upstream source tarballs, and stages a chroot that can rebuild the sysroot inside itself using a Crystal coordinator. Alpine’s 3.23.2 minirootfs is the current bootstrap seed, but the builder is designed so the starting rootfs, architecture, and package set remain swappable once a self-hosted rootfs is available.
Project plan and architecture direction
The objective of this project is to create a Crystal runtime environment that can be booted via an EFI executable in a QCOW2 disk image, and that can regenerate itself from source entirely within the image. Crystal is chosen for its human-readable syntax, high-level expressiveness, and ability to compile into low-level, minimal-overhead executables with strong performance characteristics.
Human-readability is the most important outcome, with source-based reproducibility an absolute imperative. The project starts with a minimal, bootable environment and iteratively reduces binary dependencies while expanding the amount of the toolchain implemented in Crystal. Any new or retained dependency must be justifiable in terms of clarity, auditability, and its role in the migration toward a single comprehensible syntax. The intent is to progressively remove unnecessary code and to refactor the remaining code into smaller, clearer, and more accessible components. Applying best-available human-readability metrics to guide these improvements is encouraged.
This project acknowledges that prior generations achieved significant functionality with modest resources and far fewer layers. Today’s baseline depends on large, complex systems such as Linux and LLVM. That is an acceptable starting point, but the long-term direction is to migrate toward a system where both code and data are expressed in a single, consistent syntax, and where the runtime becomes comprehensible end-to-end.
During the interim, reliance on externally-authored and compiled tools (for example bash, make, cmake, or clang) should be minimized. Whenever practical, their responsibilities should be migrated into Crystal through statically or dynamically linked function calls, and the necessary data should be extracted programmatically from upstream sources. Any newly generated imperative or declarative operations must be expressed in Crystal, with APIs and macros used to improve clarity rather than convenience.
Installation
- Install Crystal 1.18.2 or newer.
- Run
shards install(no postinstall steps are required).
Usage
Build the CLI
shards build
./bin/bq2 --install # creates symlinks
Build the directory layout and write the plan
$ bin/sysroot-builder
Prepared sysroot workspace at data/sysroot
Wrote build plan at data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json
$ tree data/sysroot/seed-rootfs/
data/sysroot/seed-rootfs/
├── bq2-rootfs
│ ├── var
│ │ └── lib
│ │ └── sysroot-build-plan.json
│ └── workspace
└── opt
└── sysroot
7 directories, 1 file
To validate self-hosting with the published 0.3.3 seed rootfs tarball, generate an alternate plan with:
$ bin/sysroot-builder --seed bq2-rootfs-0.3.3
Build the target rootfs
Just build everything
If you just try to build everything, it should take about an hour on a really fast machine right now.
$ bin/sysroot-runner
####
$ bin/sysroot-status
plan_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json
state_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-state.json
report_dir=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/workspace/sysroot-build-reports
next_phase=host-setup
next_step=download-sources
Run a single phase
$ bin/sysroot-runner --phase host-setup
2026-02-23T14:41:25.685363Z INFO - Finished prefetch-shards
2026-02-23T14:41:25.700043Z INFO - All build steps completed
2026-02-23T14:41:25.700046Z INFO - Completed phase host-setup
Run individual steps
$ bin/sysroot-runner --phase sysroot-from-seed --package seed-resolv-conf
2026-02-23T15:12:35.841817Z INFO - Running plan /home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json with overrides /home/ubuntu/worksp
ace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-overrides.json (namespace=Host)
2026-02-23T15:12:35.842025Z INFO - Entering namespace seed for phase sysroot-from-seed
2026-02-23T15:12:35.843592Z INFO - Executing phase sysroot-from-seed (namespace=seed)
2026-02-23T15:12:35.843595Z INFO - **** Build a self-contained sysroot using seed rootfs tools. ****
2026-02-23T15:12:35.843597Z INFO - Executing 1 build steps
2026-02-23T15:12:35.843602Z INFO - Building seed-resolv-conf in (phase=sysroot-from-seed)
2026-02-23T15:12:35.843624Z INFO - Starting write-file build for seed-resolv-conf in (no chdir) (cpus=48)
2026-02-23T15:12:35.843696Z INFO - Finished seed-resolv-conf
2026-02-23T15:12:35.843820Z INFO - All build steps completed
2026-02-23T15:12:35.843822Z INFO - Completed phase sysroot-from-seed
Execute a command in the new namespace
$ bin/sysroot-namespace
Entering namespace with rootfs=data/sysroot/seed-rootfs
Bind mounts:
Command: /bin/sh --login
Executing command: /bin/sh --login
ip-172-31-1-184:/#
Continue where you left off
$ bin/sysroot-status
plan_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json
state_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-state.json
report_dir=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/workspace/sysroot-build-reports
current_phase=sysroot-from-seed
next_phase=sysroot-from-seed
next_step=m4
$ bin/sysroot-runner
On failure
$ bin/sysroot-status
plan_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json
state_path=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-state.json
report_dir=/home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/workspace/sysroot-build-reports
current_phase=sysroot-from-seed
next_phase=sysroot-from-seed
next_step=llvm-project-stage2
last_failure=sysroot-from-seed/llvm-project-stage2
last_failure_report=/bq2-rootfs/workspace/sysroot-build-reports/20260223T154034.392Z-sysroot_from_alpine-llvm_project_stage2-d64a9325.json
Change the plan without starting over
$ shards build
Dependencies are satisfied
Building: bq2
$ bin/sysroot-builder-overrides
Wrote build plan overrides to /home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-overrides.json
Overrides phases=4
$ bin/sysroot-runner
2026-02-23T18:20:15.075526Z INFO - Applying build plan overrides from /home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-overrides.json
2026-02-23T18:20:15.077316Z INFO - Running plan /home/ubuntu/workspace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-plan.json with overrides /home/ubuntu/worksp
ace/bootstrap-qcow2/data/sysroot/seed-rootfs/bq2-rootfs/var/lib/sysroot-build-overrides.json (namespace=Host)
2026-02-23T18:20:15.077641Z INFO - Entering namespace seed for phase system-from-sysroot
2026-02-23T18:20:15.079295Z INFO - Executing phase system-from-sysroot (namespace=seed)
2026-02-23T18:20:15.079298Z INFO - **** Rebuild sysroot packages into /usr inside the new rootfs (prefix-free). ****
2026-02-23T18:20:15.079304Z INFO - Executing 17 build steps
...
2026-02-23T22:17:34.701593Z INFO - **** Strip the sysroot prefix and emit a prefix-free rootfs tarball. ****
2026-02-23T22:17:34.701595Z INFO - Executing 1 build steps
2026-02-23T22:17:34.701601Z INFO - Building rootfs-tarball in / (phase=finalize-rootfs)
2026-02-23T22:17:34.701624Z INFO - Starting tarball build for rootfs-tarball in / (cpus=48)
2026-02-23T22:17:34.701671Z INFO - Running in /: tar -czf /workspace/bq2-rootfs-0.3.3.tar.gz --exclude=var/lib --exclude=var/lib/** --exclude=workspace --exclude=worksp
ace/** --exclude=work --exclude=work/** --exclude=proc --exclude=proc/** --exclude=sys --exclude=sys/** --exclude=dev --exclude=dev/** --exclude=run --exclude=run/** --ex
clude=tmp --exclude=tmp/** --exclude=.bq2-rootfs -C / .
2026-02-23T22:18:32.049728Z INFO - Finished in 57.348s (exit=0): tar -czf /workspace/bq2-rootfs-0.3.3.tar.gz --exclude=var/lib --exclude=var/lib/** --exclude=workspace
--exclude=workspace/** --exclude=work --exclude=work/** --exclude=proc --exclude=proc/** --exclude=sys --exclude=sys/** --exclude=dev --exclude=dev/** --exclude=run --exc
lude=run/** --exclude=tmp --exclude=tmp/** --exclude=.bq2-rootfs -C / .
2026-02-23T22:18:32.049737Z INFO - Finished rootfs-tarball
2026-02-23T22:18:32.050064Z INFO - All build steps completed
2026-02-23T22:18:32.050085Z INFO - Completed phase finalize-rootfs in 57.348s
2026-02-23T22:18:32.051877Z INFO - Completed sysroot run in 57.352s
Build an EFI application from Crystal
Use the efi-app-builder command to emit a .efi binary by cross-compiling to a Windows COFF object and linking it as efi_application:
./bin/bq2 efi-app-builder --input src/hello-efi.cr --output out/hello-efi.efi --arch aarch64
Supported architectures are aarch64 and x86_64. Use --keep-object to retain the intermediate .obj file for linker/debug inspection.
Busybox-style CLI (bq2)
The single executable (bin/bq2) dispatches subcommands by argv[0] or the first argument. Symlinks in bin/ mirror the subcommands (create them with ./bin/bq2 --install).
shards build
./bin/bq2 --install
# Build the sysroot tarball
./bin/sysroot-builder --output sysroot.tar.gz
# Or via the main binary:
./bin/bq2 sysroot-builder --output sysroot.tar.gz
# Enter the sysroot namespace
./bin/sysroot-namespace --rootfs data/sysroot/rootfs -- /bin/sh
# Or:
./bin/bq2 sysroot-namespace --rootfs data/sysroot/rootfs -- /bin/sh
# Inside the sysroot, build the CLI from staged source and run the plan
cd /workspace/bootstrap-qcow2-master
shards build
./bin/bq2 --install
./bin/bq2 sysroot-runner
# Run the rootfs validation phase (installs into /workspace/rootfs as a staging DESTDIR)
./bin/bq2 sysroot-runner --phase rootfs-from-sysroot
# Or run every phase in order:
./bin/bq2 sysroot-runner --phase all
# Default (no args): show resume status + help (use sysroot --resume to continue a build)
./bin/bq2
Iterating (overrides + state)
During iterative, in-container debugging, treat the plan JSON (/var/lib/sysroot-build-plan.json) as immutable and apply changes via:
/var/lib/sysroot-build-overrides.jsonfor runtime-only tweaks to flags/env/install paths/var/lib/sysroot-build-state.jsonfor bookmark/progress tracking (auto-updated bysysroot-runner)/workspace/sysroot-build-reports/*.jsonfor failure reports (lives under the workspace, not var/lib, so it stays writable under a mapped uid; seeSysrootBuildState#report_dir)
The rootfs-from-sysroot phase uses DESTDIR=/workspace/rootfs to assemble a candidate rootfs tree without changing the running sysroot; that staged tree is what later gets validated (and eventually entered via pivot_root) once the sysroot toolchain is stable.
After a full successful round, back-port overrides into src/sysroot_builder.cr, delete overrides/state, and retry from scratch to validate reproducibility.
This is intended for clean, sudo-less development workflows, not as a security boundary. The namespace runner still has access to host files that are reachable from the sysroot rootfs, and it requires kernel support for unprivileged namespaces to work at all. If your kernel disables user namespaces, enable the setting explicitly.
Resuming sysroot workflows (host)
bq2 sysroot resumes by default. Use ./bin/bq2 sysroot --no-resume to restart from
scratch, or ./bin/bq2 sysroot --resume to be explicit about resuming. The resume logic
selects the earliest incomplete stage in the following order:
plan-write(workspace/plan missing)sysroot-runner(plan present, state missing or in-progress)
The host-setup phase (download/extract/populate seed) runs inside sysroot-runner.
If a state file exists but its plan digest does not match the current plan, the resume logic
refuses to guess and requires a manual cleanup or rerun from scratch. Running ./bin/bq2
with no arguments now prints the resume decision before the help output; it provides more
host-side context than bin/sysroot-status, which only inspects a state file.
Development
- Format Crystal code with
crystal tool format. - Run specs with
crystal spec. - CI: GitHub Actions (
.github/workflows/ci.yml) runs format + specs on push/PR; triggerable from the Actions tab. - API docs are published via GitHub Pages after each push to
main: https://jkridner.github.io/bootstrap-qcow2/
Contributing
- Fork it (https://github.com/embedconsult/bootstrap-qcow2/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Jason Kridner - creator and maintainer