Nix-free seed (stage0)
Goal: produce the first elpe binary (Rust backend elpe-server)
without Nix, from a content-hash-pinned toolchain. After stage0, elpe
rebuilds itself (build.ml → Rust.rust, toolchain from Ubuntu, crates
pinned by SHA256 in Cargo.lock). Full design + inventory:
../../../elpe.roadmap/project_nixfree_seed.md.
The one insight
Elpe already compiles its own Rust backend without Nix (build.ml,
elpe/lib/rust/rust.ml). rustc/cargo come from Ubuntu (rust.ml:661,663),
crates from crates.io pinned by SHA256 (rust.ml:8-17). The ONLY thing Nix
uniquely produces is the first elpe binary needed to run build.ml — a
chicken-and-egg. This directory pins the stage0 toolchain by hash so that
first binary can be built without Nix.
Trust roots (all content hashes)
| File | Pins | Status |
|---|---|---|
toolchain.lock | rustc/cargo/gcc/clang/libc6-dev/protoc (Ubuntu .deb, SHA256) | filled + verified (resolute 26.04 LTS, rustc-1.93; all 6 .debs fetched & hash-checked) |
../../src/ubuntu-archive-keyring.asc | Ubuntu archive PGP signing key — the InRelease trust root, embedded in the binary (src/pgp.rs) | pinned (fingerprint F6ECB376…, replaces the former sq binary; sq.lock now obsolete) |
crates.lock | 323 crate SHA256 — pointer to Cargo.lock (source of truth) | derivable, run extract-hashes.sh |
elpe.SHA256SUMS | pre-built seed elpe binary (option 1) | TODO — after reference build |
Usage
Two ways to run stage0. Both need a real build (never in an agent fork).
A. Automated — pristine VM (recommended, push-button)
test-stage0-vm.sh boots a pinned, pristine Ubuntu in QEMU and runs the
whole stage0 there, so the “clean box” milestone is a repeatable test, not a
manual procedure. It also gives the daemon the root/namespaces it needs (no host
sudo — unblocks §9.9).
# On any KVM-capable Linux host with qemu + cloud-image-utils + pijul:
./test-stage0-vm.sh
What it does: fetch+verify the pinned cloud image (vm.lock, SHA256) → pijul clone the repo into an empty dir → boot QEMU headless with that clone mounted
read-only over 9p and a writable results share → cloud-init runs
guest-stage0.sh (provisions a cc + OCaml/dune/opam from the same signed
resolute suite, then calls bootstrap-seed.sh) → the host reads back
RESULT/rc/console.log and asserts success. The pinned base image is booted
with -snapshot, so it is never mutated.
B. Manual — directly on a clean Ubuntu box
./extract-hashes.sh # derive crates.lock from Cargo.lock (no build)
./bootstrap-seed.sh # fetch+verify pinned toolchain, build stage0, close loop
toolchain.lock is already filled & verified against Ubuntu resolute 26.04
LTS (PGP InRelease → Packages.xz → .deb sha256), matching rust.ml
(rust_version=1.93, ~release:"resolute"). To refresh for a newer suite,
re-run the fetch+verify and update the hashes/URLs + rust_version. (sq.lock
is obsolete — PGP verify is now in-process, see src/pgp.rs.)
bootstrap-seed.sh refuses to run while any lock still has TODO. It builds
the backend elped (hash-pinned cargo), then — if the frontend’s
opam deps are present — the frontend elpe, then runs Phase B (elpe build build.ml) twice and asserts the rebuilt daemon is bit-for-bit
reproducible (§6.3/§9.10 — the real closure test; note the cargo-built seed
hash and the elpe-built hash differ legitimately). Without the opam deps it
still builds the backend seed and reports PARTIAL.
Lock files
| File | Pins |
|---|---|
toolchain.lock | rustc/cargo/protoc + gcc/clang/libc6-dev metapackages (SHA256) |
vm.lock | pristine Ubuntu cloud image for the VM test (SHA256 + signed SHA256SUMS) |
crates.lock | pointer to Cargo.lock (source of truth for 323 crate SHA256) |
elpe.SHA256SUMS | reproducible Phase-B elped hash (written on PASS) |
sq.lock | obsolete (PGP verify now in-process, src/pgp.rs) |
Why not just cargo?
We use cargo build only for stage0 (to make the first elpe without a
pre-existing elpe). cargo is never used as the compiler afterwards: elpe’s
rust.ml calls rustc directly (rust.ml:348,360); cargo is only used for
cargo metadata --offline (rust.ml:168). So the cargo path here is the
bootstrap floor, not the steady state.