Skip to content

Latest commit

 

History

1,744 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

td

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.

Highlights

  • Immutable — the root filesystem is a read-only erofs image. /bin is a pure symlink farm into /td/store; there is no /usr or /sbin. /var is persistent Btrfs state; /run and /tmp are volatile tmpfs.
  • Built from source — bootstrapped from the tiny stage0-posix seed through an iterative GCC/glibc ladder and a source-built Rust toolchain. Host /bin, /usr, and ambient PATH never 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 rollback remains 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.

Requirements

  • A Linux x86-64 host with unprivileged user namespaces enabled — the build sandbox is rootless, so no sudo and no host /td directory is needed (the store is assembled inside the sandbox).
  • Rust 1.95 or newer (cargo and rustc) to build td's control-plane tools.
  • QEMU (qemu-system-x86_64) to boot the image.

Try it

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.

Hand it to somebody who has not built td

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

Keep a VM and update it from within td

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
./update

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

Filesystem layout

/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

Defining the system

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"])
}

Building

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 test

To 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 --usb

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

License

GPL-3.0-or-later. See COPYING.

About

Timmy's Distro

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages