# SDD 11 — Bootstrap from-scratch (track posterior) > **Estado:** diseño. Abre el *track posterior* del [SDD 10](10-roadmap.md): bajar de "hammer > sobre Alpine" a "hammer sobre sí mismo". No bloquea ninguna Fase 0–6 (ya cerradas); reusa > el laboratorio de build ([SDD 02](02-build-lab.md)) sin retrabajo. ## 1. El problema: el cordón umbilical Hoy el userland que `hammer` muta es el de Alpine (musl + busybox ya cocidos). Eso fue deliberado ([ADR 0002](adr/0002-alpine-first.md)): validar la capa AI-nativa antes de gastar meses en un bootstrap. Validada esa capa, el track posterior responde una sola pregunta: > **¿Puede `hammer` compilar el sistema completo que ejecuta, sin pedirle nada al host más > que un toolchain semilla fijado por hash?** Si la respuesta es sí *y* el resultado es bit-reproducible, hammer deja de heredar la confianza de Alpine y la deriva de su propio [log de transparencia](09-trust-model.md). ## 2. Principios (heredados, no nuevos) - **Todo es una receta sellada.** Cada pieza del bootstrap es un `Recipe` ([SDD 02 §1](02-build-lab.md)) cuyo artefacto se direcciona por hash en el `Store`. El bootstrap es un *subgrafo* del grafo de dependencias que ya construye el lab, no un mecanismo aparte. - **Nada del host entra al hash.** El único insumo externo es el **toolchain semilla**, y entra como un artefacto importado con su sha256 **fijado** ([ADR 0006](adr/0006-pinned-commits.md)), igual que un tarball upstream. El host aporta un kernel para correr procesos y nada más. - **`zig cc` es la semilla primaria** ([ADR 0003](adr/0003-zig-cc-builder.md)): un binario hermético = compilador C/C++ + musl + headers, cross al target. Escotilla `musl-cross-make` (gcc+musl) detrás de la misma interfaz de receta (`compiler = "gcc"`) para los paquetes con gcc-ismos. ## 3. Las tres etapas ``` host (sólo kernel + seed pinned) │ ┌─────────────▼─────────────┐ │ Stage 0 — toolchain semilla│ zig (o musl-cross-make) ingerido al store, │ sellado por hash │ fijado por sha256. Cross → x86_64-linux-musl. └─────────────┬─────────────┘ │ artifact_hash(seed) ← dependencia de build de todo lo demás ┌─────────────▼─────────────┐ │ Stage 1 — userland mínimo │ musl, busybox/toybox, init arje, hammerd, │ cross-compilado │ cross-compilados CON Stage 0. Sale un rootfs sellado. └─────────────┬─────────────┘ │ rootfs_hash(stage1) ┌─────────────▼─────────────┐ │ Stage 2 — rebuild nativo │ DENTRO del rootfs Stage 1 (bwrap/chroot/VM), │ y verificación │ recompila toolchain+userland con las herramientas └────────────────────────────┘ de Stage 1, NO las del host. Compara hashes. ``` ### Stage 0 — Toolchain semilla Modelamos el toolchain semilla como una **fuente fijada**, no como un build: un `Source` de tipo tarball con `sha256` pinned (`zig--linux-x86_64.tar.xz`, o el tarball de `musl-cross-make`). `hammer-bootstrap` lo descarga (fuera del sandbox, como `hammer_build::download` ya hace para `patch_url`/`content_url`), verifica el sha256 **antes** de sellarlo, y lo deposita en el store con un `artifact_hash` derivado de ese sha256. A partir de ahí el resto del bootstrap depende del *hash del store*, no de la ruta del host. Salida: `seed_hash` (artefacto del toolchain en el store). ### Stage 1 — Userland mínimo Un conjunto de recetas cuyo `[deps].build` incluye `seed_hash`. El conjunto mínimo que arranca y se administra solo: | Pieza | Por qué | Compilador | |---|---|---| | `musl` | libc del target | zig cc (estático) | | `busybox` o `toybox` | coreutils + shell | zig cc (estático) | | `arje` | init: PID 1 + supervisión ([ADR 0007](adr/0007-arje-como-init-propio.md)) | toolchain Rust | | `hammerd` | diario + bus de agente + watcher, **servicio supervisado por arje** | toolchain Rust | Todo se enlaza estático o con rpath controlado (sin depender del loader del host). El resultado se ensambla en un **rootfs** (árbol FHS) que a su vez se sella como un artefacto del store (`stage1_hash`). El init **no se escribe de cero**: se adopta `arje` ([ADR 0007](adr/0007-arje-como-init-propio.md)), cuyo PID 1 con supervisión real entrega el `CRASHED` real que la Fase 5 dejó diferido — `hammerd` corre como servicio bajo arje. > El **kernel** no es el foco de este hito: se importa pinned (como la semilla) para poder > correr el rootfs. Construirlo desde fuente es un sub-ítem posterior, ortogonal al > auto-alojamiento del userland. ### Stage 2 — Rebuild nativo y verificación El corte del cordón. Dentro del rootfs Stage 1 (primero vía `bwrap`/`chroot`, luego en la VM destino), se reconstruyen Stage 0' y Stage 1' usando **sólo** las herramientas de Stage 1. Se compara: ``` hash(stage1) vs hash(stage1') ``` - **Iguales** ⇒ el sistema se compila a sí mismo bit a bit: reproducible y auto-alojado. Hito cumplido. - **Distintos** ⇒ hay no-determinismo (timestamps, paths embebidos, orden de enlace). Se caza y se elimina; es exactamente el trabajo que [SDD 09](09-trust-model.md) §2 exige. ## 4. Manifiesto de bootstrap (embrión del log de transparencia) ✅ Cada etapa anota una línea `(stage, recipe_hash, artifact_hash, seed_hash, ts)` en un `bootstrap.json` versionado (en la raíz del store). Ese manifiesto **es** la semilla del log de transparencia ([SDD 09 §4](09-trust-model.md)): un tercero reproduce el bootstrap y verifica que sus hashes coinciden con los publicados, sin confiar en el binario del autor. Implementado en `hammer-bootstrap::manifest` (`BootstrapManifest` / `StageEntry`): append-only e **idempotente** (de-dup por `(stage, artifact_hash)`), escritura atómica, y `ts` que **no** entra en ningún hash (dos reproducciones del mismo bootstrap difieren a lo sumo en ese campo). Stage 0 ya anota su línea: `recipe_hash = None` (la semilla se ingiere, no se compila) y `artifact_hash == seed_hash` (la semilla es a la vez insumo y producto). **`hammer bootstrap manifest`** imprime el `bootstrap.json` actual: es la superficie de **publicación** del log: el autor lo comparte y un tercero reproduce el bootstrap y compara sus hashes contra estas líneas, sin confiar en el binario del autor ([SDD 09 §4](09-trust-model.md)). ## 5. Interfaz Nuevo crate `hammer-bootstrap` (orquestador) sobre `hammer-build` + `hammer-core::Store`. No introduce mecanismo nuevo de build: encadena recetas y persiste el manifiesto. ```rust // hammer-bootstrap pub fn stage0(seed: &SeedSpec, store: &Store) -> Result; // ✅ toolchain semilla pub fn stage1(spec: &Stage1Spec, cfg: &BuildConfig, store: &Store) -> Result; // ◑ musl+busybox+hammerd; init arje ☐ pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result; // ✅ ancla+verifica; rebuild in-rootfs en VM ✓ REPRODUCIBLE pub fn all(seed, recipes_dir, base_cfg, store) -> Result; // ✅ encadena stage0→1→2 + manifiesto ``` `all` encadena las tres etapas del host en una corrida (stage0 ingiere la semilla, stage1 construye y sella el rootfs, stage2 ancla la referencia) y devuelve los tres hashes + el `BootstrapManifest` poblado. El veredicto `✓ REPRODUCIBLE` **no** sale de aquí: el rebuild nativo que lo decide corre dentro del rootfs (en la VM, `scripts/selfhost-verify.sh`); `all` deja la referencia anclada lista para que ese rebuild la compare con `stage2 --verify`. Idempotente (compone las idempotencias de cada etapa). `stage1` cross-compila `musl` + `busybox` + `hammerd` con la semilla (no el zig del host), ensambla un rootfs FHS por hardlink y lo sella; el `RootfsHash` se deriva del **contenido** (hashes de componentes + init), no del árbol en disco, así que es reproducible. El PID 1 provisional es el `init` de busybox vía `/etc/inittab`, que arranca `hammerd` como servicio respawn. `hammerd` es una receta Cargo (repo pinned + deps vendoreadas en el fetch para build `--offline`). `arje` ([ADR 0007](adr/0007-arje-como-init-propio.md)) ya tiene su **receta puente** `recipes/arje-zero.toml` (Cargo, fuente = monorepo tawasuyu pinned al commit de la migración del CAS a BLAKE3, `-p arje-zero`): prueba que el lab construye el init, pero todavía **no** es PID 1. El paso "init real" —arje-zero como PID 1 del rootfs con su seed card, `arje-bus` y hammerd como Card de servicio supervisada, que entrega el `CRASHED` real— está diseñado en el [SDD 12](12-init-real.md) y **validado en QEMU** (ver [runbook](runbooks/stage1-vm-boot.md)). `stage2` ancla el **content-hash** del rootfs de Stage 1 (`ArtifactHash::of_tree` — los bytes reales, no el hash input-addressed del store) como la referencia a reproducir, y la anota en el manifiesto. `verify_against` la compara con un rebuild `stage1'` producido **dentro** del rootfs (en la VM, recompilando con sólo las herramientas de Stage 1): iguales ⇒ auto-alojado bit a bit. El determinismo necesario lo dan las rutas fijas (`/src`) + `SOURCE_DATE_EPOCH` del sandbox + `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1` (impuesto por el sandbox tras cazar el no-determinismo del codegen paralelo de rustc en `arje-zero`). **El rebuild in-rootfs ya se ejecutó end-to-end**: `KVM=1 MEM=24576 ./scripts/selfhost-verify.sh` reconstruyó los 4/4 dentro del rootfs y su `of_tree(stage1') = b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1` (ver [runbook](runbooks/stage1-vm-boot.md) §8). `SeedSpec { kind, version, url, sha256 }` es la identidad pinned de la semilla; `seed_hash()` deriva el `ArtifactHash` de `(kind, version, sha256)` — no del `url` ni del host, así que cualquier espejo del mismo tarball produce el mismo artefacto. CLI (implementado lo de Stage 0; el resto pendiente): ``` hammer bootstrap stage0 --url URL --sha256 HEX --version V [--seed zig|musl-cross-make] # ✅ hammer bootstrap stage1 --seed-hash HASH [--seed zig] [--recipes DIR] # ✅ booteado en QEMU hammer bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH] # ✅ ancla+verifica; rebuild en VM ✓ REPRODUCIBLE hammer bootstrap all --url URL --sha256 HEX --version V [--seed zig] [--recipes DIR] # ✅ las tres etapas + manifiesto hammer bootstrap manifest # imprime el bootstrap.json (log de transparencia, §4) # ✅ ``` ## 6. Decisiones (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md)) - **Semilla primaria:** `zig` por hermeticidad (ADR 0003), con `musl-cross-make` como escotilla por receta. ✅ - **coreutils+shell = `busybox`** (no toybox): el Stage 1 es una balsa desechable hacia el userland nativo en Rust/Zig; se prioriza compatibilidad de flags y velocidad, y su GPLv2 no contamina el sistema final. ✅ - **init `arje` diferido:** Stage 1 con init mínimo provisional; `arje` (y el `CRASHED` real) entran como receta git pinned en un lote posterior. ✅ - **Layout de la semilla:** ingesta pura en Stage 0 + resolución del toolchain al usarlo (`SeedSpec::toolchain_dir`, localiza `zig` en raíz o hijo versionado). ✅ - **Particionado/montaje** de `/store` y `/var/lib/hammer` en la imagen destino: ✅ (Etapa B3). `scripts/disk-image.sh` arma una imagen GPT con 3 particiones ext4 dedicadas — `/dev/vda1`→`/`, `/dev/vda2`→`/store` (CAS inmutable), `/dev/vda3`→`/var/lib/hammer` (estado mutable). El kernel monta vda1; un wrapper `/sbin/init` monta vda2/vda3 y hace `exec` del init real (arje-zero). Consecuencia que hubo que resolver: con `/store` en su propia partición, el sellado del store cruza filesystems y `rename(2)` da EXDEV ⇒ `Store::seal` cae a copia-a-staging dentro del store + rename store-interno (atómico). Verificado in-VM: rebuild ✓ REPRODUCIBLE. - **Kernel:** importado pinned ahora; from-source después. ☐ ## 7. Auto-alojamiento: el builder rootfs (camino a Stage 2 pleno) La verificación de reproducibilidad de Stage 2 está operativa, probada y **ejecutada end-to-end**: `of_tree` (content-hash de bytes) + `verify_against`, los 4 componentes reconstruyen **bit-idéntico**, y el **rebuild *dentro* del rootfs** ya corrió en la VM dando `✓ REPRODUCIBLE` (variante (a) abajo; ver [runbook §8](runbooks/stage1-vm-boot.md)). El obstáculo que hubo que resolver para llegar ahí: > El Stage 1 que booteamos es un **runtime** (musl+busybox+hammerd+arje-zero): **no trae compilador** > (ni `zig`, ni `make`/autotools, ni `cargo`/rust, ni `linux-headers`, ni `bwrap`). No puede > reconstruir nada. El rebuild in-rootfs exige un **builder rootfs**: Stage 1 + el toolchain adentro. ### 7.1 Qué necesita el builder rootfs Para correr `hammer bootstrap stage1` **dentro** de sí mismo: el binario `hammer` (estático), las recetas, la **semilla** (`zig`, ya un artefacto sellado), `make`+autotools, `cargo`+rust, `linux-headers`, y `bwrap` (el lab anida un sandbox ⇒ userns sin privilegios dentro del chroot/VM). ### 7.2 Dos niveles de pureza (fasable) - **(a) Builder pragmático** — el toolchain entra al rootfs **desde Alpine** (apk), no construido por hammer. Demuestra el **mecanismo** (rebuild in-rootfs + `of_tree(stage1)==of_tree(stage1')`) y cierra la reproducibilidad *end-to-end*, sin ser aún auto-alojamiento puro. - **(b) Auto-alojamiento puro** — el toolchain lo **construye hammer desde fuente** (recetas para make/autotools, y el gran tramo: rust/llvm o un rustc bootstrappeado). Es el end-state; el más caro (construir rust desde cero). Se llega **incrementalmente**, reemplazando una a una las piezas Alpine por componentes hammer, con Stage 2 verificando cada paso. - **Pieza 1 — GNU make `4.4.1`** (`recipes/make.toml`): arranque de (b). Build estático musl con `zig cc` (mismo camino que `grep`: tarball release con `configure` → AutoconfReady), sellado en `b3:fbad44ac…` y **reproducible bit-a-bit** (dos builds en stores distintos ⇒ árbol idéntico). Es la herramienta base de toda receta autotools. **Swap implementado:** `BuilderSpec.swaps` (`ToolchainSwap { name, artifact, rel_path }`) monta el binario sellado sobre el path Alpine en `/toolchain` (estático musl ⇒ sin shim del loader) y lo ancla en el **hash lógico** del builder (`swaps_digest`, ordenado por nombre): la procedencia deja de ser "todo Alpine" y se vuelve auditable para el log de transparencia. CLI: `hammer bootstrap builder --swap make=[:rel]` (repetible; `rel_path` por defecto `usr/bin/`). En el host: pura `b3:8a370f5d…` vs swapped `b3:e7e2282c…`, y `/toolchain/usr/bin/make` queda hardlinkeado al artefacto `fbad44ac…`. El `selfhost-verify.sh` lo expone opt-in con `SWAP_MAKE=1` (construye make y lo swapea). **Validado en host:** con make de hammer los 4/4 reproducen `of_tree=9adefb82…` (idéntico al baseline determinista). - **Pieza 2 — busybox `1.36.1`** (ya es componente de Stage 1, `recipes/busybox.toml`): provee el `sh`+coreutils que el build usa del `/toolchain`. El `/toolchain` de Alpine es **mixto** — busybox para `sh/sed/grep/awk/tar/find` (symlinks → `/bin/busybox`) y GNU coreutils para `cp/mkdir/install`. El swap monta el busybox estático de hammer sobre `/toolchain/bin/busybox` (`--swap busybox=:bin/busybox`; sin código nuevo — el mecanismo `--swap` ya soporta `rel_path`): los applets busybox-backed pasan a usar hammer-busybox, coreutils GNU intacto. `SWAP_BUSYBOX=1` en el verify. **Validado en host:** con make+busybox de hammer los 4/4 (incl. busybox compilándose a sí mismo con hammer-busybox de shell) reproducen `of_tree=9adefb82…`. - **Determinismo (transversal):** `codegen-units=1` NO bastaba — el backend paralelo de rustc/LLVM (ThinLTO) divergía ~62 KB en `arje-zero` a >1 CPU. Fix: `CARGO_BUILD_JOBS=1` en el sandbox serializa el jobserver ⇒ reproducible e independiente del nº de CPUs (SDD 09 §2). El baseline reproducible verdadero es `of_tree=9adefb82…` (el viejo `0039b2b9…` era un build paralelo no-fiable). - **✅ VARIANTE (b) CERRADA** (ver [runbook self-hosting-toolchain.md](runbooks/self-hosting-toolchain.md)): se completaron todas las piezas en-camino — make, busybox, **coreutils, bwrap, linux-headers** y el gran tramo **rust/cargo 1.91.1** (cadena purista mrustc→1.90→1.91.0→1.91.1, LLVM 20.1.8 externo reusado; ver [`scripts/rust-frontier/`](../scripts/rust-frontier/README.md)). El **capstone** corrió los 6 swaps juntos in-VM (`SWAP_{MAKE,BUSYBOX,COREUTILS,BWRAP,LINUX_HEADERS,RUST}=1`) dando `✓ REPRODUCIBLE` con `of_tree(stage1')=7fa6cb4e…` bit a bit. Dos anclajes de `of_tree`: `9adefb82…` (rustc Alpine, los 5 tools lo mantienen) y `7fa6cb4e…` (hammer-rust, auto-consistente — el compilador emite los bytes). binutils desde fuente (`recipes/binutils.toml`, 2.45.1) cierra la provenance pero es **inerte** al `of_tree` (zig provee as/ld; verificado por bisección). No quedan piezas del toolchain por de-Alpinizar. ### 7.3 Enganche con lo ya hecho `stage2` ya ancla `of_tree(stage1)`; el builder produce `stage1'` y `verify_against` emite el veredicto. El determinismo necesario está cubierto (paths fijos `/src`, `SOURCE_DATE_EPOCH`, locks deterministas). El sub-ítem era: **ensamblar el builder** (variante de `assemble_rootfs` que hidrata el toolchain) y **correr el rebuild anidado** en la VM. ### 7.4 Estado: builder ensamblado + rebuild in-VM ejecutado ✅ ✓ REPRODUCIBLE El **ensamblado** del builder está implementado (`hammer_bootstrap::builder_rootfs`, variante a): ``` hammer bootstrap builder --stage1 --seed-hash \ [--toolchain .dev-fs/alpine] [--toolchain-tag alpine-3.23.4] \ [--hammer-bin ] [--ref-content of_tree(stage1)] \ [--work-cache work/] [--out work/builder-rootfs] ``` Sobre el rootfs de Stage 1 (hidratado como base) monta: `/toolchain` (el rootfs Alpine, el sandbox de build), `/store` con la **semilla replicada** (el `hammer` de adentro resuelve zig por hash), `/usr/bin/hammer` + `/etc/hammer/recipes`, y el driver **`/usr/bin/rebuild-stage1`** que lee `/etc/hammer/rebuild.env` (`SEED_HASH`/`SEED_KIND`/`REF_CONTENT`), apunta `HAMMER_ROOTFS=/toolchain`, corre `hammer bootstrap stage1` y compara `stage1'` contra la referencia con `stage2 --verify`. El builder **no se sella** (su `/toolchain` Alpine no es content-addressed); se ensambla en un `out_dir` para empaquetar como initramfs. Su identidad sí es reproducible: un `ArtifactHash` *lógico* de sus insumos (stage1 + semilla + recetas + binario + tag del toolchain + el driver), anotado en el manifiesto (línea stage 2). Validado contra el store real (Stage 1 `73d7a9be…` + semilla `3ce721ec…` ⇒ builder `81dad3d9…`, 1.2 GB con el toolchain). **El paso operacional ya se ejecutó:** `scripts/selfhost-verify.sh` empaqueta el builder, lo bootea con KVM y corre `rebuild-stage1` no-interactivo; in-VM reconstruyó los 4/4 (arje-zero cu=1 en ~27 min) y `of_tree(stage1') = b3:0039b2b9…` igualó la referencia ⇒ `✓ REPRODUCIBLE: stage1' == stage1`. Lo que queda es la variante (b), el auto-alojamiento *puro* (toolchain construido por hammer desde fuente, §7.2). **Arrancada:** la pieza 1, GNU make `4.4.1` (`recipes/make.toml`), ya se construye desde fuente con el lab — estática, reproducible bit-a-bit (§7.2b). Ver [runbook §8](runbooks/stage1-vm-boot.md). ## 8. Hecho cuando `hammer bootstrap --all` produce un rootfs que, ejecutado en la VM destino, **reconstruye su propio toolchain y userland con hashes idénticos a los publicados**, sin que ninguna herramienta del host entre al resultado. Ese día Alpine deja de ser una dependencia y pasa a ser, a lo sumo, una conveniencia de desarrollo. **Estado (2026-06-12):** el **mecanismo** está demostrado end-to-end con la variante (a) — el rootfs reconstruye su userland in-VM con `of_tree` idéntico (`✓ REPRODUCIBLE`, §7.4). Falta sólo la variante (b): que el **toolchain** dentro del builder lo construya hammer desde fuente (no provenga de Alpine), reemplazando una a una las piezas con Stage 2 verificando cada paso. Ese es el corte pleno del cordón.