boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit 34d1a969385623fc738ff411b2c83b926bcfdef6
parent 4732be263077c6e3cf4b956bd9e963948c5ef797
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 18 Jul 2026 09:51:29 -0700

boot7: assemble verified static toolchain

Diffstat:
MMakefile | 48+++++++++++++++++++++++++++++++++++-------------
MREADME.md | 29++++++++++++++++++++---------
Mboot/boot.sh | 5+++--
Aboot/boot7.sh | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mboot/lib-arch.sh | 2+-
Mbootprep/boot5-enumerate.sh | 5+++++
Mdocs/TOUR.md | 32++++++++++++++++++++++++++++----
Mtests/README.md | 2+-
Mtests/seed-accept.sh | 2+-
Mtools/mkrelease.sh | 23+++++++++++++++++------
Mtools/release.sh | 14+++++++++++---
Mtools/release/README.md | 32+++++++++++++++++++++++---------
Mtools/release/verify.sh | 53+++++++++++++++++++++++++++++++++++++++++++++++++----
13 files changed, 299 insertions(+), 53 deletions(-)

diff --git a/Makefile b/Makefile @@ -5,7 +5,7 @@ # and the rule body invokes the right bootN.sh (or prep-src.sh / # prep-musl.sh) script with the appropriate ARCH and DRIVER. # -# Output layout: build/<arch>/<driver>/boot{0..6}/<artifacts>. +# Output layout: build/<arch>/<driver>/boot{0..7}/<artifacts>. # <arch> ∈ {aarch64, amd64, riscv64} # <driver> ∈ {podman, seed} # @@ -15,7 +15,7 @@ # make build/riscv64/podman/boot1/M1pp # only prep-src + boot0 + boot1 # # Convenience entrypoints (default ARCH=aarch64, DRIVER=podman): -# make all build boot6 kernel for ARCH × DRIVER +# make all build boot6 kernel + boot7 toolchain # make test SUITE=<name> run a test suite (see SUITE list at bottom) # make image build the per-arch container image used by tests # make cloc line counts for the bootstrap sources @@ -64,18 +64,21 @@ OUT_DIR := build/$(ARCH)/$(DRIVER) # ── Top-level targets ──────────────────────────────────────────────────── -all: build/$(ARCH)/$(DRIVER)/boot6/$(KERNEL_NAME_$(ARCH)) +all: build/$(ARCH)/$(DRIVER)/boot6/$(KERNEL_NAME_$(ARCH)) \ + build/$(ARCH)/$(DRIVER)/boot7/toolchain/MANIFEST.sha256 # Prepare the canonical source tree (prep-src + prep-musl) for ARCH. src: build/$(ARCH)/src/musl/.stamp help: @echo 'Targets (default ARCH=$(ARCH) DRIVER=$(DRIVER)):' - @echo ' make all build boot6 kernel' + @echo ' make all build boot6 kernel + boot7 toolchain' @echo ' make src prep canonical src/ tree (incl. musl)' @echo ' make package quick: package boot2-<arch>.tar.gz from current build' @echo ' make release validated: build + input/tar/output proofs, mint to dist/' - @echo ' make build/<arch>/<driver>/boot6/<kn> full chain (kn = Image | kernel.elf)' + @echo ' make build/<arch>/<driver>/boot6/<kn> kernel branch (kn = Image | kernel.elf)' + @echo ' make build/<arch>/<driver>/boot7/toolchain/MANIFEST.sha256' + @echo ' assemble final static toolchain' @echo ' make build/<arch>/<driver>/bootN/<file> any single artifact' @echo ' make test SUITE=<suite> test suite (NAMES=<filter> optional)' @echo ' make image per-arch tests container image' @@ -86,13 +89,12 @@ clean: rm -rf build/ # `package`: package a per-arch tarball from the current build tree. -# Depends on the full chain (boot5 musl + boot6 kernel) so -# OUTPUT_MANIFEST.txt has real artifacts to hash. boot5 is not on the -# `all` dep path (boot6 doesn't link musl), so list it explicitly here. +# Depends on both terminal branches: the boot6 kernel and boot7's installed +# toolchain (which pulls in boot5 musl), so every manifest entry exists. # Lands at build/<arch>/release/boot2-<arch>.tar.gz. Fast: no # reproducibility or verify check. -package: build/$(ARCH)/$(DRIVER)/boot5/libc.a \ - build/$(ARCH)/$(DRIVER)/boot6/$(KERNEL_NAME_$(ARCH)) +package: build/$(ARCH)/$(DRIVER)/boot6/$(KERNEL_NAME_$(ARCH)) \ + build/$(ARCH)/$(DRIVER)/boot7/toolchain/MANIFEST.sha256 DRIVER=$(DRIVER) tools/mkrelease.sh $(ARCH) # `release`: the validated path. Builds + packages once, regenerates @@ -102,7 +104,7 @@ package: build/$(ARCH)/$(DRIVER)/boot5/libc.a \ release: DRIVER=$(DRIVER) tools/release.sh $(ARCH) -# ── prep-src + boot0..boot6 chain (rules per arch × driver) ────────────── +# ── prep-src + boot0..boot7 chain (rules per arch × driver) ────────────── # # The .stamp files are the make-rule pegs. Each rule lists its real # outputs as additional targets so single-binary builds work — `make @@ -116,7 +118,9 @@ release: PREP_SRC_COMMON_SRCS := \ bootprep/prep-src.sh boot/lib-arch.sh \ bootprep/stage1-flatten.sh bootprep/libc-flatten.sh \ - bootprep/boot4-gen-runscm.sh bootprep/boot6-gen-runscm.sh \ + bootprep/boot4-gen-runscm.sh \ + bootprep/boot5-enumerate.sh bootprep/boot5-gen-runscm.sh \ + bootprep/boot6-gen-runscm.sh \ bootprep/assets/boot3-run.scm bootprep/assets/boot-hello.c \ M1pp/M1pp.P1 hex2pp/hex2pp.P1 \ P1/P1.M1pp P1/P1pp.P1pp \ @@ -170,7 +174,7 @@ endef $(foreach a,$(ALL_ARCHES),$(eval $(call PREP_RULES,$a))) -# Per-(arch, driver) boot0..boot6 rules. +# Per-(arch, driver) boot0..boot7 rules. # # Each stage's .stamp is the recipe peg; each real artifact is declared # as a target depending on the .stamp with an empty recipe (`;`). We @@ -257,6 +261,24 @@ build/$1/$2/boot6/.stamp: \ @touch $$@ build/$1/$2/boot6/$$(KERNEL_NAME_$1): build/$1/$2/boot6/.stamp ; + +# boot7: install the final compiler + musl static sysroot in one tree. +# This is a deterministic host-side assembly stage for either DRIVER; all +# copied binaries were already produced through that driver's boot stages. +build/$1/$2/boot7/.stamp: \ + build/$1/$2/boot4/.stamp build/$1/$2/boot5/.stamp \ + build/$1/src/.stamp \ + boot/boot7.sh boot/lib-arch.sh + DRIVER=$2 boot/boot7.sh $1 + @touch $$@ + +build/$1/$2/boot7/toolchain/bin/tcc \ +build/$1/$2/boot7/toolchain/lib/libc.a \ +build/$1/$2/boot7/toolchain/lib/libtcc1.a \ +build/$1/$2/boot7/toolchain/lib/crt1.o \ +build/$1/$2/boot7/toolchain/lib/crti.o \ +build/$1/$2/boot7/toolchain/lib/crtn.o \ +build/$1/$2/boot7/toolchain/MANIFEST.sha256: build/$1/$2/boot7/.stamp ; endef $(foreach a,$(ALL_ARCHES),$(foreach d,$(ALL_DRIVERS),$(eval $(call BOOT_CHAIN_RULES,$a,$d)))) diff --git a/README.md b/README.md @@ -65,6 +65,10 @@ A series walking through the chain, one rung at a time: ;; ── boot6.sh ── seed-kernel ─────────────────────────────────────────── ;; tcc links the seed-kernel ELF (Image on aarch64). That kernel is the ;; runtime for DRIVER=seed re-runs, closing the bootstrap loop. + +;; ── boot7.sh ── installed static toolchain ────────────────────────────────── +;; Assemble tcc2 + libtcc1.a + musl libc/crt + public headers under one +;; relocatable toolchain/{bin,lib,include}/ tree and hash every file. ``` For a stage-by-stage walk-through of what is built, what becomes @@ -134,7 +138,7 @@ files crossed by the chain, and the order in which to read them, is in * **5 minutes** — the chain pseudocode above, plus [docs/TOUR.md §0 "Map"](docs/TOUR.md). * **An hour** — [docs/TOUR.md](docs/TOUR.md) end to end, then skim - [boot/boot0.sh](boot/boot0.sh) … [boot/boot6.sh](boot/boot6.sh). + [boot/boot0.sh](boot/boot0.sh) … [boot/boot7.sh](boot/boot7.sh). * **A day** — the component specs in dependency order: [docs/P1.md](docs/P1.md), [docs/M1PP.md](docs/M1PP.md), [docs/HEX2pp.md](docs/HEX2pp.md), [docs/LIBP1PP.md](docs/LIBP1PP.md), @@ -147,7 +151,8 @@ files crossed by the chain, and the order in which to read them, is in `DRIVER={podman,seed} × ARCH={aarch64,amd64,riscv64}` -`DRIVER` selects the runtime that executes each `bootN` stage: +`DRIVER` selects the runtime that executes the compiling `bootN` stages; +boot7 is a host-side installation step over that driver's verified outputs: * **podman** (default) — each stage runs in a container with access only to its input binaries and sources. @@ -174,24 +179,29 @@ Or via path-based Make targets — outputs are the targets, so deps walk the chain back to source-prep: ```sh -make build/aarch64/podman/boot6/Image # full chain +make build/aarch64/podman/boot6/Image # kernel branch make build/amd64/podman/boot6/kernel.elf +make build/aarch64/podman/boot7/toolchain/MANIFEST.sha256 make build/riscv64/podman/boot1/M1pp # only prep-src + boot0 + boot1 -make all ARCH=aarch64 DRIVER=podman # convenience: boot6 kernel +make all ARCH=aarch64 DRIVER=podman # kernel + installed toolchain make help # target list ``` -Per-stage outputs land at `build/<arch>/<driver>/boot{0..6}/`; the +Per-stage outputs land at `build/<arch>/<driver>/boot{0..7}/`; the canonical generated source tree (used by every stage) is at `build/<arch>/src/`. +The final static toolchain is assembled at +`build/<arch>/<driver>/boot7/toolchain/`, with `bin/`, `lib/`, and +`include/` subdirectories plus a complete `MANIFEST.sha256`. + ## Reproducible releases The validated release path performs one clean build, regenerates the canonical source tree and compares its input manifest, repackages the sealed payload and asserts byte-identical tar output, then rebuilds boot0 through -boot6 from the extracted archive and compares every key output against its -SHA-256 manifest: +boot7 from the extracted archive, compares every key stage output, and +checks every installed toolchain file against its SHA-256 manifest: ```sh ARCH=aarch64 # or amd64, riscv64 @@ -205,11 +215,12 @@ results are: * `dist/boot2-$ARCH.tar.gz` — self-contained source and verification bundle * `dist/boot2-$ARCH.tar.gz.sha256` — archive digest * `dist/boot2-$ARCH.outputs.sha256` — expected hashes for all stage artifacts +* `dist/boot2-$ARCH.toolchain.sha256` — every final toolchain file and hash * `dist/boot2-$ARCH.tar.gz.provenance` — revision, driver, host, and timestamp Artifact hashes are release-specific and are generated from the validated -build; they are not hard-coded in this README. The final artifact is the -`boot6/Image` row for aarch64 and `boot6/kernel.elf` for amd64/riscv64. +build; they are not hard-coded in this README. The boot6 kernel closes the +execution loop; boot7 is the consumable static C toolchain. To verify and reproduce a minted release on macOS (use `sha256sum` instead of `shasum -a 256` on Linux): diff --git a/boot/boot.sh b/boot/boot.sh @@ -1,12 +1,12 @@ #!/bin/sh -## boot.sh — drive boot0 → boot6 end-to-end under one driver. +## boot.sh — drive boot0 → boot7 end-to-end under one driver. set -eu case "${1:-}" in -h|--help) cat <<'EOF' -boot.sh — drive boot0 → boot6 end-to-end under one driver. +boot.sh — drive boot0 → boot7 end-to-end under one driver. Usage: boot/boot.sh <aarch64|amd64|riscv64> @@ -52,3 +52,4 @@ rm -rf build/$ARCH/$DRIVER ./boot/boot4.sh $ARCH ./boot/boot5.sh $ARCH ./boot/boot6.sh $ARCH +./boot/boot7.sh $ARCH diff --git a/boot/boot7.sh b/boot/boot7.sh @@ -0,0 +1,105 @@ +#!/bin/sh +## boot7.sh — assemble the final static C toolchain sysroot. +## +## This stage performs no compilation. It installs the verified boot4/boot5 +## products and the canonical musl headers into one relocatable directory: +## +## build/$ARCH/$DRIVER/boot7/toolchain/ +## bin/tcc boot4's fixed-point compiler (tcc2) +## lib/{libc.a,libtcc1.a} boot5 musl + boot4 compiler runtime +## lib/crt{1,i,n}.o boot5 startup objects +## include/ installed public musl headers +## include/tcc/stdarg-bridge.h +## MANIFEST.sha256 hash of every installed file except itself +## +## boot7 is deliberately host-side for both DRIVER values: it only copies +## already-verified artifacts. Slash-containing paths work in both execution +## transports, but assembling an installed tree does not gain anything from +## hundreds of guest-side copy operations. +## +## Usage: boot/boot7.sh <arch> +## <arch> ∈ {aarch64, amd64, riscv64} for either DRIVER (default podman). + +set -eu + +. boot/lib-arch.sh +bootlib_init boot7 "${1:-}" +require_src + +OUT=build/$ARCH/$DRIVER/boot7 +STAGE=build/$ARCH/$DRIVER/.boot7-stage +export OUT STAGE + +BOOT4=build/$ARCH/$DRIVER/boot4 +BOOT5=build/$ARCH/$DRIVER/boot5 +MUSL=build/$ARCH/src/src/musl +BRIDGE=build/$ARCH/src/src/tcc/stdarg-bridge.h + +for f in \ + "$BOOT4/tcc2" "$BOOT4/libtcc1.a" \ + "$BOOT5/libc.a" "$BOOT5/crt1.o" "$BOOT5/crti.o" "$BOOT5/crtn.o" \ + "$MUSL/obj/include/bits/alltypes.h" \ + "$MUSL/obj/include/bits/syscall.h" \ + "$BRIDGE" +do + require_file "$f" +done + +rm -rf "$STAGE" +mkdir -p "$STAGE/toolchain/bin" "$STAGE/toolchain/lib" \ + "$STAGE/toolchain/include/tcc" +TOOLCHAIN=$STAGE/toolchain + +# Final built binaries and libraries. boot4's mes-libc/crt1 are bootstrap +# internals; the installed libc/crt objects are boot5's musl products. +cp "$BOOT4/tcc2" "$TOOLCHAIN/bin/tcc" +cp "$BOOT4/libtcc1.a" "$TOOLCHAIN/lib/libtcc1.a" +cp "$BOOT5/libc.a" "$TOOLCHAIN/lib/libc.a" +cp "$BOOT5/crt1.o" "$TOOLCHAIN/lib/crt1.o" +cp "$BOOT5/crti.o" "$TOOLCHAIN/lib/crti.o" +cp "$BOOT5/crtn.o" "$TOOLCHAIN/lib/crtn.o" + +# Install public musl headers with the same precedence as musl's +# install-headers target: generic bits, arch bits, then generated bits. +# The general include/ tree is disjoint from the installed bits/ overlay. +copy_headers() { + _src=$1; _prefix=$2 + ( cd "$_src" && find . -type f -name '*.h' | LC_ALL=C sort ) | + while IFS= read -r _rel; do + _rel=${_rel#./} + _dst=$TOOLCHAIN/include/$_prefix$_rel + mkdir -p "$(dirname "$_dst")" + cp "$_src/$_rel" "$_dst" + done +} + +copy_headers "$MUSL/include" "" +copy_headers "$MUSL/arch/generic/bits" "bits/" +copy_headers "$MUSL/arch/$MUSL_ARCH/bits" "bits/" +copy_headers "$MUSL/obj/include/bits" "bits/" +cp "$BRIDGE" "$TOOLCHAIN/include/tcc/stdarg-bridge.h" + +chmod 0755 "$TOOLCHAIN/bin/tcc" +find "$TOOLCHAIN/lib" "$TOOLCHAIN/include" -type f -exec chmod 0644 {} + + +if command -v sha256sum >/dev/null 2>&1; then + sha256() { sha256sum "$1" | awk '{print $1}'; } +else + sha256() { shasum -a 256 "$1" | awk '{print $1}'; } +fi + +: > "$TOOLCHAIN/MANIFEST.sha256" +( cd "$TOOLCHAIN" && find . -type f ! -name MANIFEST.sha256 | LC_ALL=C sort ) | +while IFS= read -r rel; do + rel=${rel#./} + printf '%s %s\n' "$(sha256 "$TOOLCHAIN/$rel")" "$rel" \ + >> "$TOOLCHAIN/MANIFEST.sha256" +done +chmod 0644 "$TOOLCHAIN/MANIFEST.sha256" + +rm -rf "$OUT" +mkdir -p "$OUT" +mv "$TOOLCHAIN" "$OUT/toolchain" + +count=$(wc -l < "$OUT/toolchain/MANIFEST.sha256" | tr -d ' ') +echo "[$BOOT_TAG] OK -> $OUT/toolchain ($count files)" diff --git a/boot/lib-arch.sh b/boot/lib-arch.sh @@ -1,5 +1,5 @@ # lib-arch.sh — single source for arch + driver setup shared by -# boot/boot.sh, boot/boot{0..6}.sh, lib-pipeline.sh, lib-runscm.sh. +# boot/boot.sh, boot/boot{0..7}.sh, lib-pipeline.sh, lib-runscm.sh. # # Public entry points (call in this order from a bootN.sh): # diff --git a/bootprep/boot5-enumerate.sh b/bootprep/boot5-enumerate.sh @@ -15,6 +15,11 @@ ## Usage: bootprep/boot5-enumerate.sh <arch> set -eu +# Source enumeration becomes archive member order, so it must not inherit +# the host's locale collation (for example, whether '_' sorts before '.'). +LC_ALL=C +export LC_ALL + [ "$#" -eq 1 ] || { echo "usage: $0 <arch>" >&2; exit 2; } ARCH=$1 diff --git a/docs/TOUR.md b/docs/TOUR.md @@ -10,7 +10,7 @@ together. ## §0. Map -The chain is a sequence of seven shell scripts under [boot/](../boot). +The chain is a sequence of eight shell scripts under [boot/](../boot). Each `bootN.sh` produces one or two binaries from the binaries the prior stages produced, plus source from the canonical `build/<arch>/src/` tree (prepared once by `bootprep/prep-src.sh`). @@ -24,10 +24,12 @@ prior stages produced, plus source from the canonical | 4 | [boot4.sh](../boot/boot4.sh) | `tcc1`, `tcc2`, `libc.a`, `libtcc1.a` | tcc self-host, byte-identical fixed point `tcc1 == tcc2`, minimal libc | | 5 | [boot5.sh](../boot/boot5.sh) | `libc.a`, `crt{1,i,n}.o` | musl-1.2.5 built by the self-hosted tcc | | 6 | [boot6.sh](../boot/boot6.sh) | `Image` (aarch64) / `kernel.elf` | a minimal kernel that can host the chain (`DRIVER=seed`) | +| 7 | [boot7.sh](../boot/boot7.sh) | `toolchain/{bin,lib,include}` | final compiler and static sysroot installed in one tree | Drivers (`DRIVER=podman` default, `DRIVER=seed` for the loop pass) only -change *where* each stage executes; the inputs, outputs, and shell -scripts are identical. +change where compiling stages execute; their inputs, outputs, and shell +scripts are identical. Boot7 is the same host-side installation step over +either driver's already-built artifacts. ## §1. boot0 — from a hex seed to a macro assembler @@ -280,7 +282,29 @@ stage inside the kernel that boot6 just produced. console / fs / trap dispatcher headers. The full OS contract is [docs/OS.md](OS.md). -## §8. The loop +## §8. boot7 — install the final static toolchain + +**You arrive with**: fixed-point `tcc2` and `libtcc1.a` from boot4, +musl `libc.a` and `crt{1,i,n}.o` from boot5, and the canonical generated +musl headers. + +**boot7 builds**: no new machine code. It installs those verified files +into a relocatable `toolchain/` tree with conventional `bin/`, `lib/`, and +`include/` directories. The public header overlay follows musl's own +`install-headers` precedence: generic `bits/`, architecture-specific +`bits/`, then generated `alltypes.h` and `syscall.h`. + +`bin/tcc` is the unwrapped `tcc2` ELF. No shell wrapper or other unbuilt +executable is added; callers pass the installed include, CRT, and library +paths directly to TCC. A separate `MANIFEST.sha256` records every installed +file. + +**Trust extension**: none. This is a deterministic assembly boundary that +turns the proof-oriented stage outputs into the consumable product of the +chain. It runs host-side for either driver because it only copies artifacts +already produced and compared under Podman or the seed kernel. + +## §9. The loop The chain has now produced a kernel. The kernel is enough to run any of the earlier stages: under `DRIVER=seed`, each `bootN.sh` packs its diff --git a/tests/README.md b/tests/README.md @@ -106,5 +106,5 @@ tests/seed-accept.sh boot5 # boot5 byte-eq vs podman ARCH is fixed to `aarch64` since that's the only seed-driver-complete arch today. Prereq for every mode: `./boot/boot.sh aarch64` has run -under the default `DRIVER=podman` so `build/aarch64/podman/boot{0..6}/` +under the default `DRIVER=podman` so `build/aarch64/podman/boot{0..7}/` is populated. diff --git a/tests/seed-accept.sh b/tests/seed-accept.sh @@ -19,7 +19,7 @@ ## hello vs the podman reference. ## ## All three modes target ARCH=aarch64 (the only seed-driver-complete -## arch today). Prereq for every mode: build/aarch64/podman/boot{0..6}/ +## arch today). Prereq for every mode: build/aarch64/podman/boot{0..7}/ ## populated via `./boot/boot.sh aarch64` (default DRIVER=podman), ## including boot6/Image as the seed kernel. ## diff --git a/tools/mkrelease.sh b/tools/mkrelease.sh @@ -2,24 +2,25 @@ ## mkrelease.sh — package a per-arch boot2 release tarball. ## ## A release tarball is a self-contained bundle that lets anyone -## reproduce the full boot0..boot6 chain off the bundled inputs and +## reproduce the full boot0..boot7 chain off the bundled inputs and ## byte-compare the outputs against a hash manifest. Layout: ## ## boot2-<arch>[-<rev>].tar.gz ## boot2-<arch>[-<rev>]/ ## README.md extract + run instructions -## verify.sh drives boot0..boot6 + diffs OUTPUT_MANIFEST +## verify.sh drives boot0..boot7 + diffs manifests ## INPUT_MANIFEST.txt sha256 of every input under src/ + boot/ ## OUTPUT_MANIFEST.txt sha256 of expected per-stage artifacts -## (driver-agnostic; the project's seed-accept -## harness verifies podman vs seed equivalence) +## TOOLCHAIN_MANIFEST.txt sha256 of every installed boot7 file +## Both output manifests are driver-agnostic; the project's +## seed-accept harness verifies podman vs seed equivalence. ## src/ the sealed source tree (from ## build/<arch>/src/, produced by prep-src.sh) -## boot/ boot0..boot6 stage drivers + libs + +## boot/ boot0..boot7 stage drivers + libs + ## containers/Containerfile.* ## ## The output manifest is generated from the current build outputs in -## build/<arch>/<driver>/boot{0..6}/. mkrelease.sh does NOT rebuild; +## build/<arch>/<driver>/boot{0..7}/. mkrelease.sh does NOT rebuild; ## prereqs (`make all ARCH=<arch>`) must already have run. ## ## Usage: tools/mkrelease.sh <arch> @@ -67,6 +68,7 @@ TARBALL=$REL_DIR/$NAME.tar.gz [ -d "$SRC_TREE" ] || { echo "[mkrelease] missing $SRC_TREE — run bootprep/prep-src.sh $ARCH" >&2; exit 1; } [ -f "$BUILD_TREE/boot6/$KERNEL_NAME" ] || { echo "[mkrelease] missing $BUILD_TREE/boot6/$KERNEL_NAME — run 'make all ARCH=$ARCH DRIVER=$DRIVER'" >&2; exit 1; } +[ -f "$BUILD_TREE/boot7/toolchain/MANIFEST.sha256" ] || { echo "[mkrelease] missing $BUILD_TREE/boot7/toolchain/MANIFEST.sha256 — run 'make all ARCH=$ARCH DRIVER=$DRIVER'" >&2; exit 1; } # Portable sha256. Use sha256sum if present; else shasum -a 256. if command -v sha256sum >/dev/null 2>&1; then @@ -133,6 +135,7 @@ boot5/crti.o boot5/crtn.o boot5/hello boot6/$KERNEL_NAME +boot7/toolchain/MANIFEST.sha256 EOF } @@ -160,6 +163,13 @@ fi n_out=$(wc -l < "$OUTMAN" | tr -d ' ') # ── (6) tarball — deterministic ────────────────────────────────────── +# Keep detailed installed-tree hashes next to the concise per-stage +# manifest. verify.sh compares this with boot7's regenerated manifest, +# then checks every installed file named by it. +TOOLMAN=$STAGING/TOOLCHAIN_MANIFEST.txt +cp "$BUILD_TREE/boot7/toolchain/MANIFEST.sha256" "$TOOLMAN" +n_tool=$(wc -l < "$TOOLMAN" | tr -d ' ') + echo "[mkrelease] tar -> $TARBALL" TARBALL_ABS=$PWD/$TARBALL tools/release/tar-payload.sh "$STAGING" "$TARBALL_ABS" @@ -174,3 +184,4 @@ echo "[mkrelease] tarball : $TARBALL ($bytes bytes)" echo "[mkrelease] sha256 : $TAR_SHA" echo "[mkrelease] inputs : $n_in files" echo "[mkrelease] outputs : $n_out artifacts" +echo "[mkrelease] toolchain: $n_tool files" diff --git a/tools/release.sh b/tools/release.sh @@ -3,15 +3,16 @@ ## ## Releases are critical, so the path to minting one is paranoid: ## -## 1. Clean-build and package prep-src → boot0..boot6 once. +## 1. Clean-build and package prep-src → boot0..boot7 once. ## 2. Extract the archive into a fresh directory under $HOME. ## 3. Clean-regenerate the checkout's canonical src/ tree and compare ## its full boot/ + src/ hash manifest with the bundled ## INPUT_MANIFEST.txt. ## 4. Repackage the extracted sealed payload and assert that its tarball ## is byte-identical to the original. -## 5. Run the bundled verify.sh. It rebuilds boot0..boot6 from only the -## sealed inputs and compares every artifact with OUTPUT_MANIFEST.txt, +## 5. Run the bundled verify.sh. It rebuilds boot0..boot7 from only the +## sealed inputs, compares stage artifacts with OUTPUT_MANIFEST.txt, +## and checks the complete installed tree with TOOLCHAIN_MANIFEST.txt, ## proving both build determinism and archive completeness. ## 6. Promote the validated tarball to dist/<name>.tar.gz with a ## sha256 sidecar. dist/ is the only directory mkrelease.sh / @@ -97,8 +98,10 @@ tar xzf "$ARCHIVE" -C "$VERIFY_BASE" SEALED=$VERIFY_BASE/$NAME INPUT_MANIFEST=$SEALED/INPUT_MANIFEST.txt OUTPUT_MANIFEST=$SEALED/OUTPUT_MANIFEST.txt +TOOLCHAIN_MANIFEST=$SEALED/TOOLCHAIN_MANIFEST.txt [ -f "$INPUT_MANIFEST" ] || { log "FAIL: archive has no INPUT_MANIFEST.txt"; exit 1; } [ -f "$OUTPUT_MANIFEST" ] || { log "FAIL: archive has no OUTPUT_MANIFEST.txt"; exit 1; } +[ -f "$TOOLCHAIN_MANIFEST" ] || { log "FAIL: archive has no TOOLCHAIN_MANIFEST.txt"; exit 1; } # Independently regenerate canonical inputs from the checkout. This is # cheap compared with boot3 and proves prep-src produces the sealed tree. @@ -143,6 +146,7 @@ if [ "$DRIVER" = seed ]; then fi OUTPUT_MANIFEST_SHA=$(sha256 "$OUTPUT_MANIFEST") +TOOLCHAIN_MANIFEST_SHA=$(sha256 "$TOOLCHAIN_MANIFEST") # Promote to dist/ — the only directory we treat as "publishable". hr "mint" @@ -150,6 +154,7 @@ mkdir -p "$DIST" cp "$ARCHIVE" "$DIST/$NAME.tar.gz" printf '%s %s\n' "$SHA_A" "$NAME.tar.gz" > "$DIST/$NAME.tar.gz.sha256" cp "$OUTPUT_MANIFEST" "$DIST/$NAME.outputs.sha256" +cp "$TOOLCHAIN_MANIFEST" "$DIST/$NAME.toolchain.sha256" # Provenance sidecar — keeps git rev / build host / driver / timestamp # outside the tarball so they don't perturb the content hash. @@ -158,6 +163,8 @@ cp "$OUTPUT_MANIFEST" "$DIST/$NAME.outputs.sha256" printf 'sha256: %s\n' "$SHA_A" printf 'outputs: %s.outputs.sha256\n' "$NAME" printf 'outputs_sha256: %s\n' "$OUTPUT_MANIFEST_SHA" + printf 'toolchain: %s.toolchain.sha256\n' "$NAME" + printf 'toolchain_sha256: %s\n' "$TOOLCHAIN_MANIFEST_SHA" printf 'arch: %s\n' "$ARCH" printf 'driver: %s\n' "$DRIVER" printf 'git_rev: %s%s\n' "$REV" "$DIRTY" @@ -174,3 +181,4 @@ log "MINTED in ${elapsed}s" log " tarball : $DIST/$NAME.tar.gz ($bytes bytes)" log " sha256 : $SHA_A" log " outputs : $DIST/$NAME.outputs.sha256" +log " toolchain: $DIST/$NAME.toolchain.sha256" diff --git a/tools/release/README.md b/tools/release/README.md @@ -2,15 +2,17 @@ This tarball is a self-contained input bundle for the boot2 bootstrap chain on `@ARCH@`. Extract it, run `./verify.sh`, and the chain will be -re-run from boot0 through boot6 using only the bundled inputs. The -outputs are sha256-compared against `OUTPUT_MANIFEST.txt`. +re-run from boot0 through boot7 using only the bundled inputs. Stage +outputs are sha256-compared against `OUTPUT_MANIFEST.txt`; every file in +the final installed toolchain is checked against `TOOLCHAIN_MANIFEST.txt`. The tarball is content-addressed: its sha256 reflects only the bundled inputs + manifests. Provenance (the git rev it was built from, build date, build host arch) lives in the sidecar `<tarball>.provenance` file in the same directory as the tarball, not inside it. Validated releases also publish `<name>.outputs.sha256`, an external copy of the -artifact hashes bundled as `OUTPUT_MANIFEST.txt`. +artifact hashes bundled as `OUTPUT_MANIFEST.txt`, and +`<name>.toolchain.sha256`, the detailed installed-tree hashes. ## Contents @@ -20,13 +22,14 @@ boot2-@ARCH@/ ├── verify.sh build + diff driver ├── INPUT_MANIFEST.txt sha256 of every file under src/ + boot/ ├── OUTPUT_MANIFEST.txt sha256 of expected per-stage artifacts +├── TOOLCHAIN_MANIFEST.txt sha256 of every file installed by boot7 ├── src/ sealed source tree (canonical inputs) │ ├── bin/hex0-seed vendored ELF seed (the only opaque input) │ ├── src/ all textual sources: P1, M1pp, hex2pp, │ │ scheme1, cc, tcc, mes-libc, musl, kernel │ └── run/ run.scm files driving boot3..boot6 -└── boot/ boot0..boot6 stage drivers + libs - ├── boot{0..6}.sh +└── boot/ boot0..boot7 stage drivers + libs + ├── boot{0..7}.sh ├── lib-{arch,pipeline,runscm}.sh └── containers/ Containerfile.{busybox,empty} (DRIVER=podman) ``` @@ -66,19 +69,30 @@ DRIVER=seed ./verify.sh 1. Stages `src/` into `build/@ARCH@/src/` (the layout the boot stage scripts expect). -2. Runs `boot/boot0.sh` … `boot/boot6.sh` in order under the selected +2. Runs `boot/boot0.sh` … `boot/boot7.sh` in order under the selected driver. -3. Hashes each artifact listed in `OUTPUT_MANIFEST.txt` and prints - `OK` / `DIFFER` / `MISSING` per row. +3. Hashes each artifact listed in `OUTPUT_MANIFEST.txt`, then checks the + complete installed `boot7/toolchain/` tree against + `TOOLCHAIN_MANIFEST.txt`. Exit status is 0 iff every artifact matches. +After verification, the ready-to-use static toolchain is at: + +```text +build/@ARCH@/<driver>/boot7/toolchain/ +├── bin/tcc +├── lib/{libc.a,libtcc1.a,crt1.o,crti.o,crtn.o} +├── include/ +└── MANIFEST.sha256 +``` + ## Drivers | DRIVER | runtime | prereqs | | ---------- | ----------------------------------------------- | ---------------------------------- | | `podman` | each stage runs in a minimal container | `podman` (rootless ok), `qemu-user-static` for cross-arch | -| `seed` | each stage runs inside `seed-kernel` under qemu | one prior `DRIVER=podman` pass to mint the boot6 kernel; `qemu-system-@ARCH@` | +| `seed` | boot0..6 run in `seed-kernel`; boot7 installs their outputs | one prior `DRIVER=podman` pass to mint the boot6 kernel; `qemu-system-@ARCH@` | The output manifest is **driver-agnostic**: the same artifacts must hash identically under both drivers. The upstream repo's diff --git a/tools/release/verify.sh b/tools/release/verify.sh @@ -1,6 +1,6 @@ #!/bin/sh -## verify.sh — drive boot0..boot6 off the bundled inputs and compare -## the outputs against OUTPUT_MANIFEST.txt. +## verify.sh — drive boot0..boot7 off the bundled inputs and compare +## the outputs against OUTPUT_MANIFEST.txt and TOOLCHAIN_MANIFEST.txt. ## ## This script ships inside a boot2-@ARCH@ release tarball. It ## stages the sealed src/ tree at build/@ARCH@/src/ (the layout every @@ -76,10 +76,10 @@ if [ ! -d "build/$ARCH/src" ]; then cp -R src "build/$ARCH/src" fi -# ── (2) drive boot0..boot6 ──────────────────────────────────────────── +# ── (2) drive boot0..boot7 ──────────────────────────────────────────────── if [ "$CHECK_ONLY" = 0 ]; then export DRIVER - for s in 0 1 2 3 4 5 6; do + for s in 0 1 2 3 4 5 6 7; do echo "[verify] boot$s" ./boot/boot$s.sh "$ARCH" done @@ -121,4 +121,49 @@ if [ "$fail" -gt 0 ]; then echo "[verify] FAIL" >&2 exit 1 fi + +# ── (4) verify the installed toolchain tree ──────────────────────────────── +TOOLCHAIN=$BUILD_TREE/boot7/toolchain +GENERATED_TOOLMAN=$TOOLCHAIN/MANIFEST.sha256 +if [ ! -f "$GENERATED_TOOLMAN" ]; then + echo "[verify] FAIL: missing $GENERATED_TOOLMAN" >&2 + exit 1 +fi +if ! cmp -s TOOLCHAIN_MANIFEST.txt "$GENERATED_TOOLMAN"; then + echo "[verify] FAIL: boot7 manifest differs from TOOLCHAIN_MANIFEST.txt" >&2 + diff -u TOOLCHAIN_MANIFEST.txt "$GENERATED_TOOLMAN" || true + exit 1 +fi + +echo "[verify] checking installed boot7 toolchain files" +tool_ok=0 +tool_fail=0 +while IFS= read -r line; do + case "$line" in + ''|\#*) continue ;; + esac + want=$(printf '%s' "$line" | awk '{print $1}') + rel=$(printf '%s' "$line" | awk '{print $2}') + f=$TOOLCHAIN/$rel + if [ ! -f "$f" ]; then + printf 'MISSING toolchain/%s\n' "$rel" + tool_fail=$((tool_fail + 1)) + continue + fi + got=$(sha256 "$f") + if [ "$got" = "$want" ]; then + tool_ok=$((tool_ok + 1)) + else + printf 'DIFFER toolchain/%s\n' "$rel" + printf ' want %s\n' "$want" + printf ' got %s\n' "$got" + tool_fail=$((tool_fail + 1)) + fi +done < TOOLCHAIN_MANIFEST.txt + +echo "[verify] toolchain ok=$tool_ok differ-or-missing=$tool_fail" +if [ "$tool_fail" -gt 0 ]; then + echo "[verify] FAIL" >&2 + exit 1 +fi echo "[verify] PASS"