Getting started
This gets you from a clone of the repo to a built image. It uses the shipped
turing-rk1/forky recipe as the running example, and other boards build the same way
with their own recipe name. Flashing and board-specific notes live on each board’s
page. For the RK1, see Turing RK1.
Which track are you on? This is the shipped-recipe track —
doctorthenbuild, for a recipe that already ships a committed lock (liketuring-rk1/forky). To change what a shipped recipe builds, continue with Adapting a shipped recipe. Bringing up a new board, or authoring a patch, is the longer bring-up track: see Adding a board and Adding a patch.
The build is rootless: it uses no sudo and no loop devices. You only need root
to install host packages and (on some hosts) to enable unprivileged user namespaces
once.
What you need
-
A Linux host, x86_64 or arm64. An x86_64 desktop building the arm64 image is the common case and fully supported, cross-building under
qemu-user. Debian and Ubuntu are the primary targets, and Fedora and Arch work too (doctorknows their package names). macOS can run the read-only commands but cannot build. -
A recent stable Rust toolchain, installed via rustup.
-
A host C compiler and linker. Cargo links the
boot2debbinary with one, and the proc-macro build scripts need it too. On Debian and Ubuntu:sudo apt install build-essentialThis is the only host compiler boot2deb asks for, and it builds the tool rather than anything the tool produces.
doctorcannot report it missing, becausedoctoris the binary that did not build. -
boot2debon yourPATH. From a clone of this repo:cd boot2deb cargo install --locked --path crates/cli # the crate is boot2deb-cli; the binary is boot2deb--lockedbuilds the tool from the dependency versions in the repository’sCargo.lock, which are the ones CI tests and audits. Without it, Cargo ignores that file and takes the newest compatible release of every dependency, the provisioning library included.Every command on this site is written as
boot2deb …, which is also how the tool writes its own hints. Anything it suggests can therefore be pasted back. Developing from a checkout without installing? Prefix each command withcargo run -p boot2deb-cli --. Either way, run it from insideboot2deb/(or pass--root <dir>), since the config root defaults to the current directory. -
Disk and time. A cold build bootstraps a Debian rootfs and, for a board that needs one, compiles a kernel and a bootloader. Budget a few GB of scratch space and tens of minutes the first time. Later builds reuse cached trees. A board that compiles nothing (the C201) is much cheaper: it is a rootfs bootstrap and an image assembly.
Let doctor find what’s missing
Rather than hand-installing a package list, run doctor. It probes for every tool
the build needs and, for anything absent, prints the exact install command for
your distro. You never guess a package name. doctor itself needs nothing
beyond the toolchain above, so it is the first thing to run after cloning:
boot2deb doctor turing-rk1/forky
Run it bare — boot2deb doctor — and it checks the requirements every board shares
(user namespaces and the vendored apt trust anchors) without needing a recipe chosen yet.
It reports your host arch, the two cross answers, and one line per requirement:
host arch : x86_64
target : turing-rk1/forky (arch arm64)
toolchain : cross — the build root carries a toolchain emitting arm64
execution : emulated — needs qemu-user binfmt for arm64 maintainer scripts and sandbox compiles
ok git /usr/bin/git
ok unprivileged user namespaces unshare --map-root-user --map-auto works
MISSING qemu-aarch64-static run target binaries under binfmt — sudo apt install qemu-user-static
...
result : all required host tools present
Run the install lines it reports, then re-run doctor until it prints
all required host tools present. Because the list is generated from the build’s own
requirements, it is always current. This page does not repeat the package names, so
there is nothing here to drift out of date.
The list is short, and that is the design rather than an omission. Every compiler, packaging tool and build dependency a build runs is a package of a provisioned Debian root. Each is resolved from your build’s own mirror list and sha256-pinned in that root’s manifest. It is therefore an input your lock names, not a fact about your machine.
There is no host gcc, no make, no dpkg, no fakeroot in the list, because none of
them is what compiles or archives anything. The C compiler
What you need asks for is not an exception. It links the boot2deb
binary, and no build stage invokes it. What remains is the handful of things no root can
carry:
| Group | What it covers | When |
|---|---|---|
| Provisioned roots | unprivileged user namespaces with a subuid/subgid range — every root is bootstrapped and entered in-process, so this is the whole requirement | always |
| Sources | git, which clones your pinned trees and applies the patch series before there is a root for them to enter | only if the recipe compiles something |
| Build roots | an unprivileged overlay whose upper layer sits on the work dir’s filesystem, which is how each compile root layers a stage’s build dependencies | only if the recipe compiles something |
| Image assembly | tar and cp. No filesystem tooling: the rootfs ext4 is formatted and then scanned back in pure Rust. e2fsck is an optional independent cross-check when present, and the image’s provenance records whether it ran | only if the recipe assembles an image |
| Emulation | qemu-<arch>-static + a registered binfmt handler, so the target’s maintainer scripts run | only if the recipe assembles an image and this host cannot execute target binaries |
| Trying the image | qemu-system-aarch64 / qemu-system-arm, which boot2deb try boots the finished image under. Optional — no build stage invokes it, and doctor reports it as absent rather than missing | only if you run try |
doctor asks only for what your recipe will actually invoke, so the table above
is a superset. doctor turing-rk1/media-accel-forky on an x86_64 host wants every row.
doctor asus-c201/forky wants no git and no overlay at all, because that board
installs Debian’s kernel, boots its own firmware, and so compiles nothing. That is
deliberate: a requirement you do not need is somewhere a requirement you do need can
hide.
Note that the last row’s two conditions are both real, and each one alone would
over-ask. Emulation is about running target binaries, which only the image path does.
The rootfs runs the target’s maintainer scripts, and the media-accel packages compile in
a target-arch sandbox. A bootloader-only build like rk3576-generic/loader compiles in
a host-arch root and executes nothing foreign, so it needs no qemu even though it
builds for arm64. And an arm64 host runs armhf binaries directly (its kernel is built
with CONFIG_COMPAT=y), so building the armhf C201 image there needs no qemu-arm
either. Any x86_64 host building an arm64 or armhf image needs both.
The target-arch sandbox is not a cross-only concern. Packages like ffmpeg-rk and
librga2 are built inside a userland bootstrapped for the target suite, never on your
host. That holds even when your host arch already matches the target. Their runtime
Depends are derived from the libraries present at build time. Building them against
your host’s libraries would stamp your host’s package names and versions into a .deb
bound for a Debian forky image.
That sandbox runs entirely in-process through unprivileged user namespaces, and needs no external sandbox tool. It does run on every host, same-arch included, so those namespaces are a hard requirement even when nothing is cross.
The roots a build provisions
A build stands up as many as four Debian roots, each for one job, each bootstrapped and
entered in-process through unprivileged user namespaces. They live in your work dir and
boot2deb clean --sandbox reclaims them.
| Root | Architecture | What it holds | What runs in it |
|---|---|---|---|
sandbox/cross-<arch>-<suite>-<digest>/ | host’s | a cross toolchain emitting the target’s objects, plus each stage’s build deps layered on (~800 MB) | the kernel, u-boot and out-of-tree module compiles — and the kernel’s own make bindeb-pkg, which packages itself |
sandbox/build-<arch>-<suite>-<digest>/ | target’s | build-essential, dpkg-dev, debhelper | the media-accel .debs (ffmpeg-rk, librga2, MPP) |
sandbox/package-<arch>-<suite>-<digest>/ | host’s | dpkg and xz-utils and nothing else (~130 MB) | archiving the u-boot and kmod .debs, which boot2deb stages itself |
| the rootfs | target’s | your image’s solved package set | the image itself |
The two host-arch roots are host-arch on purpose. Neither compiling a freestanding
kernel nor archiving a staged tree needs to link against the target’s libraries, so
both run natively. That is what keeps a multi-minute kernel build and a
hundred-megabyte xz off qemu-user entirely. The target-arch sandbox is the one that
genuinely cannot be: dpkg-shlibdeps derives each media-accel .deb’s runtime
Depends from the libraries present at build time.
Each root is provisioned for your build’s suite, which is the image’s, or for a
bootloader-only build the board’s declared default. Each publishes a sha256-pinned
manifest of its own packages beside your image. “What compiled this” and “what archived
this” are therefore answered by name and version, rather than by a --version line off
your PATH. See Reproducibility.
There is no fakeroot anywhere in boot2deb, in any root or on your host. Every root maps
you to uid 0, and uid 0 is what a Debian packaging tool actually wants. Your staged tree
is already root:root where dpkg-deb archives it, and dpkg-buildpackage picks no
gain-root command at all. Nothing is faked because nothing needs to be.
The user-namespace check (common blocker on Ubuntu 24.04)
The rootless rootfs bootstrap, the sandbox, and the ext4 image staging all need
unprivileged user namespaces with a subuid/subgid range for your user. Some hosts
disable that by default. doctor tests this by actually creating them (with the
subuid mapping), and if it fails it prints the fix for your host. The usual cases:
- Ubuntu 24.04+ ships an AppArmor restriction on by default:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - Debian with namespaces disabled:
sudo sysctl -w kernel.unprivileged_userns_clone=1 - Either way,
kernel.max_user_namespaces(oruser.max_user_namespaces) must be at least 2. Every launch holds two nested user namespaces, and each is charged against that ceiling. They are the sandbox’s own, and the one its command enters so the kernel locks the sandbox’s mount flags. A host set to1creates one namespace perfectly well and fails every build at launch, sodoctorprobes both. - Your user needs a subuid/subgid range (usually present by default):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
sysctl -w lasts until reboot. Drop the same line in /etc/sysctl.d/ to make it
persist.
On a build that assembles an image for an architecture your host cannot execute,
doctor also checks the qemu-<arch> binfmt handler. It must be registered and
enabled with the F (fix-binary) flag, which the rootfs bootstrap relies on. Installing
qemu-user-static (with binfmt-support / systemd’s binfmt) normally registers this,
and doctor warns if the flag is missing.
The overlay check
Every stage that compiles gets a build root: the shared base plus that stage’s own
build-dependencies, layered on with an unprivileged overlay and discarded afterwards.
doctor probes whether your host can establish one, and prints the directory it probed.
That is why the overlay is a requirement of compiling rather than of every build. A board that installs Debian’s kernel and boots its own firmware layers nothing, and needs only user namespaces. So does a rebuild whose artifacts all restore from the cache, which never stands a build root up at all.
The probe is pointed at the work dir’s filesystem, not at /tmp. That is where the
overlay’s upper layer goes, and the two can answer differently. An unprivileged
overlay records its whiteouts in user.* extended attributes: every on-disk filesystem
(ext4, xfs, btrfs) holds them, but tmpfs only gained them in Linux 6.6. A host with a
tmpfs /tmp on an older kernel would fail a /tmp probe. It would still build fine
with its work dir on disk. If you build with --work-dir, pass the same path to
doctor so it asks about the filesystem you will actually use.
The mount itself needs Linux 5.11 or later, which is where overlay in a user namespace arrived.
Build
With doctor green:
boot2deb build turing-rk1/forky
This resolves the recipe’s committed lock and runs the pipeline end to end. For
turing-rk1/forky that is: compile the kernel and u-boot, bootstrap the Debian rootfs,
and assemble a bootable disk image. A recipe runs only the stages it has.
turing-rk1/media-accel-forky adds the Rockchip media userspace and ffmpeg on top of
those. build asus-c201/forky compiles nothing at all, so it is a rootfs bootstrap
and an image assembly and nothing else.
The build reads only the lock, so it consults no network for its pins and is reproducible
from what is committed. A patch series, where a recipe has one, is fetched automatically
at its pinned commit. That happens when the config root’s sibling ../patches checkout
is not already present, so you never clone it separately. That holds for both patch
axes. The
repo comes from the lock’s own pin, so a u-boot-only recipe such as
rk3576-generic/loader fetches its series the same way a kernel recipe does.
The rootfs bootstrap is content-cached, so a rebuild whose solved package set is
unchanged skips the multi-minute bootstrap. To force a clean rootfs, add
--refresh-rootfs. To build a single stage, pass --stage
(kernel, dtb, kmod, uboot, userspace, ffmpeg, rootfs, image) — see the
CLI reference.
What you get
Artifacts land under the recipe’s work dir, build/turing-rk1/forky/artifacts/:
turing-rk1-forky.img.xz— the compressed bootable image.turing-rk1-forky.provenance.toml— exactly what went into the image: the resolved pins, package count, toolchain identity, the archives the rootfs resolved against, and the first-boot credential.turing-rk1-forky.plan— the package set as an installable document: every version and sha256, plus the state of each archive they came from.boot2deb reproducereplays it to rebuild this exact userland later. See Reproducibility.turing-rk1-forky.rootfs.uapi16— every file the image’s rootfs holds, with its owner, mode and sha256.boot2deb diffcompares two images through it, andboot2deb verify-imagechecks the image against it. See the rootfs file manifest.
Every artifact is named for the whole build point, device and recipe together
(turing-rk1/forky → turing-rk1-forky). Several recipes can therefore share one
--out-dir, and an image copied to a flashing host still says what it is.
The build prints the exact paths on its final lines, including the credential:
compressed : .../build/turing-rk1/forky/artifacts/turing-rk1-forky.img.xz
first-boot pw : <generated> (user debian, expired — change at first login)
cross-manifest : .../build/turing-rk1/forky/artifacts/turing-rk1-forky.cross.pkgs
packaging-manifest: .../build/turing-rk1/forky/artifacts/turing-rk1-forky.packaging.pkgs
provenance : .../build/turing-rk1/forky/artifacts/turing-rk1-forky.provenance.toml
Note the first-boot password down. It is unique per image, shown once here, and stored only in the provenance file. It exists nowhere on the running system in recoverable form. It is also expired, so the first login has to replace it.
If you would rather not transcribe a password at a console, authorize your SSH key in
the recipe instead. ssh debian@<board> then works on the first boot. That, and the
sudo policy the image ships with, are in
The account, sudo, and SSH keys.
Next: flash the image. That step is board-specific — for the RK1, see Turing RK1.