An immutable, Rust-first Linux distribution — built entirely from source, from a tiny auditable seed all the way up to the running image.
td boots onto a read-only, content-addressed root. Every package lives
at its own /td/store/<hash>-name path, and the system you run is exactly
the artifact graph that was built — from the stage0-posix seed on up. No
host compiler and no downloaded binary ends up in the image you boot, and
nothing in the root is mutable.
- Immutable — the root filesystem is a read-only erofs image.
/binis a pure symlink farm into/td/store; there is no/usror/sbin./varis persistent Btrfs state;/runand/tmpare volatile tmpfs. - Built from source — bootstrapped from the tiny
stage0-posixseed through an iterative GCC/glibc ladder and a source-built Rust toolchain. Host/bin,/usr, and ambientPATHnever enter a build. (The one downloaded trust root — the pinned Rust bootstrap snapshot — is rebuilt from source and never reaches the final image.) - Rust-first userland — the core file and text tools are Rust uutils, and the boot/login/shell path is td's own static Rust multicalls. No third-party multicall is shipped: the image packs nothing td did not build from source.
- Content-addressed — store paths, offline builds, and fixed-output
sources verified by SHA-256. Deployment updates are verified into a hidden
directory, flushed, atomically published by manifest hash, and activated
with a retained verified previous deployment. Once a fallback exists, new
deployments receive three durable boot attempts; the first deployment is
trusted because it has nowhere to roll back. A healthy target acknowledges
its exact deployment, while an exhausted candidate automatically rolls back.
A corrupt current selector is durably repaired to its verified previous
deployment, and an explicit
td-boot rollbackremains available. Update transactions and boot selection are serialized per block device through unmount; verified read-only recovery remains available when a writable bookkeeping transaction is unavailable and confirms health without attempting to mutate that state. Invalid bookkeeping ownership or modes are not repaired automatically: verified recovery remains bootable, but a root operator must inspect and repair the state before normal update acknowledgement resumes.
- A Linux x86-64 host with unprivileged user namespaces enabled — the
build sandbox is rootless, so no
sudoand no host/tddirectory is needed (the store is assembled inside the sandbox). - Rust 1.95 or newer (
cargoandrustc) to build td's control-plane tools. - QEMU (
qemu-system-x86_64) to boot the image.
The examples use td-recipe-eval, shorthand for cargo run --release --manifest-path recipes/Cargo.toml --bin td-recipe-eval --.
Build the system and boot it under QEMU:
./start./start is the repository-root convenience wrapper for
td-recipe-eval run system-x86-64. ./news and ./mail run the
checkout's two applications on this host instead, unjailed and as you,
under your Wayland session, with td's fetch service started for each
launch (td-builder host-run, APPLICATIONS.md §X.7); they need cargo
and a C compiler on PATH (or TD_CC_HOME) and nothing else of the
toolchain, and crates.io reachable the first time, for td-net's
dependencies.
It boots a selector initramfs, verifies the current deployment on a persistent
Btrfs volume, kexecs that deployment, loop-mounts its read-only EROFS root,
starts the software Wayland compositor and visible td-native demo, and
auto-logs you in as tester on the serial console. Type exit (or Ctrl-D) to
power off; Ctrl-A X force-quits QEMU. The private test volume lasts for this
interactive session and is discarded when QEMU exits; the headless
qemu-boot-system check first proves a pending deployment can acknowledge and
remain attempt-free, then recreates the volume, fails a candidate before the
health target for three boots, preserves /var through that reused-volume
sequence, and proves automatic rollback on the next boot. An explicitly
read-only disk pass exercises selector-side bookkeeping recovery; a separate
fixture proves corrupted-current fallback.
./build-qcow./build-qcow is the wrapper for td-recipe-eval bundle. It builds the same
system ./start boots and writes dist/td-vm-x86-64/: the kernel, the
selector initramfs, a qcow2 Btrfs volume holding the signed deployment, a
SHA256SUMS, a README, and a POSIX-sh start launcher rendered from the very
QEMU profile ./start uses. Someone with QEMU and no checkout unpacks it and
runs ./start, and the guest writes go to a throwaway overlay so the image
keeps matching its checksum.
The bundle is a convenience for people evaluating td, not a distribution
channel: nothing in the recipe graph names it, no build fetches it, and td
updates remain a git pull and a rebuild.
Published bundles also carry the checkout's committed Git history on the disk,
at /run/td-volume/td/source/repository.bundle inside the guest. The adjacent
revision file names the exported commit. ./build-qcow requires a clean,
complete Git checkout and checks that its commit remains unchanged through
publication. First boot clones this source offline into ~/src/td as the
login user. New checkouts track main at https://github.com/timmydo/td.git;
--source-origin and --source-branch select other upstream settings when
building a bundle. Existing checkouts retain their edits and Git settings.
Create a private installation in a new directory whose parent exists:
./build-qcow --installation --out "$HOME/td-installation"
TD_VM_MEMORY=12288 "$HOME/td-installation/start"This gives the guest 12 GiB, matching the native update regression's memory allocation. The launcher's default 2 GiB is for booting and exploring the system. A first build reconstructs the bootstrap graph and can take many hours; subsequent updates reuse the retained build cache.
This launcher keeps guest writes in the installation disk across boots. Each installation generates its own signing key, retained in root-only state outside the recipe graph. Keep the installation directory private: copying its disk also copies that identity. Create another installation for a separate identity. The default evaluation bundle discards its signing key and cannot install locally built updates.
Once first boot has initialized the checkout, run inside td:
cd ~/src/td
git pull --ff-only
./updateThe updater builds with the installed toolchain and the source-bootstrap
graph. The first build fetches declared sources and builds the graph locally;
later builds reuse local outputs. When installation is requested, press
Ctrl+Alt+Escape, then I. Review the deployment ID and press Enter to approve,
or Escape to cancel. The authorized operation signs with this installation's
key and installs the successor. Restart explicitly to boot it; the previous
deployment remains available for recovery. Use ./update build to stop after
building without requesting installation.
The checkout, build cache and other user state persist across updates and rollback. The source bundle is companion data outside the signed deployment; it does not authorize installation. See td-update/DESIGN.md for the source, build and installation contract and native VM regression.
/td/store/<hash>-<name>/ every package, content-addressed and read-only
/bin symlink farm → /td/store (td multicalls + uutils)
/etc generated, deployment-owned, immutable
/var persistent writable Btrfs @var subvolume
/run /tmp volatile writable tmpfs
/home /root symlinks into /var
A whole distribution is one Rust recipe. recipes/src/recipes/system-x86-64.rs
composes the kernel, td's own multicalls, and uutils into a boot selector plus a deployment
bundle containing {bzImage, initramfs.cpio, root.erofs, manifest}. Edit its
SYSTEM constant to tailor the hostname, users, auto-login, login shell, and
applet set, then td-recipe-eval run again.
Individual packages are recipes too — declarative Rust, no shell:
pub fn recipe() -> Recipe {
Recipe::rust("fd", "10.2.0")
.source_pin(SourcePin::new(
"fd-source",
"https://static.crates.io/crates/fd-find/fd-find-10.2.0.crate",
"de08defa195af894cc295a43bfc65ba28903e492fd5f32f7a24bf75eafd9bf34",
"fd-find-10.2.0.crate",
))
.bins(&["fd"])
.no_default_features()
.features(&["completions"])
}td-recipe-eval list # list every recipe
td-recipe-eval build-run <name> # build one recipe into /td/store
td-recipe-eval qemu-boot-session # one offline graphical session (pass/fail)
td-recipe-eval qemu-boot-system # install/update/recovery boot sequence
td-recipe-eval qemu-install # disposable offline ISO installation testTo compose a retained optical/USB ISO from prepared boot files and optional
payloads, use td-recipe-eval compose-iso OUTPUT KERNEL INITRAMFS [ISO-NAME=FILE ...]. See the media contract for its
input and publication rules. The command assembles bytes; the complete live
installer profile and physical-device installation flow are still in progress.
To boot a retained ISO interactively with a disposable destination disk:
./test-iso path/to/installer.iso
./test-iso path/to/installer.iso --usbThe first command attaches the ISO as optical media; --usb tests the same
image as USB mass storage. QEMU opens the ISO read-only and creates a private
16 GiB sparse destination. After the first VM exits, type boot to start the
destination alone with fresh UEFI variables, or press Enter to stop. Inspect both
sessions manually; QEMU exit status alone does not prove installation worked.
The disk is removed when the command exits normally. Scratch files default to
target/; set TMPDIR to choose another host filesystem with room for the
disk's written contents. After Ctrl-C, remove any leftover target/td-test-iso-*
directory from that run. The current compose-iso command does not yet produce
a graphical installer image.
Set TD_QEMU_ACCEL=tcg to force software emulation when KVM is available.
GPL-3.0-or-later. See COPYING.