From 98f0b2d228ffebb06941dfc1f436167ab40d8f21 Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 19 Jun 2026 19:11:56 -0400 Subject: [PATCH] bootstrap: orquestador `all` + export del manifiesto (Etapa A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cierra la Etapa A del camino a la distro (SDD 11 §5): - `hammer_bootstrap::all(seed, recipes_dir, base_cfg, store) -> AllReport` encadena stage0→stage1→stage2 en una corrida del host y deja el `bootstrap.json` poblado. El veredicto ✓ REPRODUCIBLE lo sigue sellando el rebuild in-VM (selfhost-verify.sh); `all` ancla la referencia para comparar. - CLI: `hammer bootstrap all --url … --sha256 … --version …` y `hammer bootstrap manifest` (imprime el log de transparencia, §4). - Refactor: `parse_seed_kind` factoriza el match de `--seed` (estaba duplicado en 4 sitios del despacho). - hammer-build: arregla el test stale `detect_cargo` — la fase Cargo evolucionó a `RF=…`+`-C link-self-contained=no` condicional, pero la aserción esperaba la forma vieja `RUSTFLAGS="-C linker=…`. Restaura el workspace en verde (43/43). Tests: cargo test --workspace verde. Co-Authored-By: Claude Opus 4.8 --- crates/hammer-bootstrap/src/lib.rs | 42 ++++++++++++++++ crates/hammer-build/src/lib.rs | 5 +- crates/hammer-cli/src/main.rs | 81 ++++++++++++++++++++++++------ docs/11-bootstrap.md | 16 ++++-- 4 files changed, 125 insertions(+), 19 deletions(-) diff --git a/crates/hammer-bootstrap/src/lib.rs b/crates/hammer-bootstrap/src/lib.rs index b3f545bf..c5c420fc 100644 --- a/crates/hammer-bootstrap/src/lib.rs +++ b/crates/hammer-bootstrap/src/lib.rs @@ -469,6 +469,48 @@ pub fn verify_against(report: &VerifyReport, rebuilt_content: ArtifactHash) -> R } } +/// Reporte de [`all`]: los tres hashes de las etapas + el manifiesto ya poblado. El veredicto de +/// auto-alojamiento NO va aquí (es `RebuildPending` en el host): el rebuild nativo que lo decide corre +/// **dentro** del rootfs, en la VM destino (`scripts/selfhost-verify.sh`, runbook §8). +#[derive(Debug, Clone)] +pub struct AllReport { + pub seed_hash: ArtifactHash, + pub stage1_rootfs: RootfsHash, + pub stage2: VerifyReport, + pub manifest: BootstrapManifest, +} + +/// **`bootstrap --all`** ([SDD 11 §5](../../docs/11-bootstrap.md)) — encadena las tres etapas en una +/// sola corrida del host: ingiere la semilla (Stage 0), construye y sella el rootfs del userland mínimo +/// (Stage 1) y ancla su content-hash como referencia de reproducibilidad (Stage 2). Devuelve los tres +/// hashes y el [`BootstrapManifest`] (`bootstrap.json`) resultante: el embrión del log de transparencia +/// ([SDD 11 §4](../../docs/11-bootstrap.md)), listo para publicar. +/// +/// El **corte del cordón** (el veredicto `✓ REPRODUCIBLE`) no se emite aquí: el rebuild nativo que lo +/// decide corre con las herramientas de **dentro** del rootfs (en la VM), no las del host. `all` deja la +/// referencia anclada (`stage2.stage1_content`) lista para que ese rebuild la compare vía `stage2 +/// --verify` (ver [`verify_against`] y `scripts/selfhost-verify.sh`). +/// +/// Idempotente componiendo idempotencias: cada etapa no rehace lo ya sellado ni duplica su línea de +/// manifiesto, así que re-correr `all` sobre un store ya poblado sólo recolecta los hashes. +pub fn all( + seed: &SeedSpec, + recipes_dir: &Path, + base_cfg: &hammer_build::BuildConfig, + store: &Store, +) -> Result { + let seed_hash = stage0(seed, store)?; + let stage1_spec = Stage1Spec { + seed_hash: seed_hash.clone(), + seed_kind: seed.kind, + recipes_dir: recipes_dir.to_path_buf(), + }; + let stage1_rootfs = stage1(&stage1_spec, base_cfg, store)?; + let stage2 = stage2(&stage1_rootfs, store)?; + let manifest = BootstrapManifest::load(store)?; + Ok(AllReport { seed_hash, stage1_rootfs, stage2, manifest }) +} + // ── Builder rootfs: el camino al rebuild in-rootfs (Stage 2 pleno, SDD 11 §7) ──────────────────── // // El Stage 1 que booteamos es un *runtime* (musl+busybox+hammerd+arje-zero): no trae compilador, no diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index 31346510..633d8507 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -503,7 +503,10 @@ commit = "deadbeef" assert!(c.contains("-- -C target-feature=+crt-static"), "crt-static scoped tras -- ({c})"); assert!(c.contains("relocation-model=static"), "no-PIE: ET_EXEC sin interpreter ({c})"); assert!(c.contains("CC=\"$PWD/.hammer-zig-cc\""), "CC = wrapper para cc-rs ({c})"); - assert!(c.contains("RUSTFLAGS=\"-C linker=$PWD/.hammer-zig-cc"), "linker por RUSTFLAGS ({c})"); + // El linker se arma en `RF` (que además gana `-C link-self-contained=no` cuando el host no es + // Alpine) y se aplica vía `RUSTFLAGS="$RF"`. + assert!(c.contains("RF=\"-C linker=$PWD/.hammer-zig-cc"), "linker en RF ({c})"); + assert!(c.contains("RUSTFLAGS=\"$RF\""), "RF aplicado por RUSTFLAGS ({c})"); let i = p.install.unwrap(); assert!(i.contains("/out/usr/bin"), "{i}"); assert!(i.contains("find target/release"), "nativo instala de target/release ({i})"); diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs index f91571b8..0341972f 100644 --- a/crates/hammer-cli/src/main.rs +++ b/crates/hammer-cli/src/main.rs @@ -315,6 +315,38 @@ enum BootstrapCmd { #[arg(long, default_value = "work/builder-rootfs")] out: String, }, + /// [Stage 0+1+2] Encadena las tres etapas (semilla → rootfs → referencia anclada) en una corrida + /// y deja el `bootstrap.json` poblado (SDD 11 §5). El veredicto `✓ REPRODUCIBLE` lo sella el + /// rebuild nativo dentro del rootfs (VM, `scripts/selfhost-verify.sh`), no esta corrida del host. + All { + /// URL del tarball del toolchain semilla (cualquier esquema de curl; `file://` para offline). + #[arg(long)] + url: String, + /// sha256 hex del tarball, fijado. Se verifica antes de sellar. + #[arg(long)] + sha256: String, + /// Versión de la semilla (entra en la identidad/hash del artefacto). + #[arg(long)] + version: String, + /// Clase de semilla. + #[arg(long, default_value = "zig")] + seed: String, + /// Directorio de recetas (`musl.toml`, `busybox.toml`, `hammerd.toml`, `arje-zero.toml`). + #[arg(long, default_value = "recipes")] + recipes: String, + }, + /// Imprime el `bootstrap.json` del store: el log de transparencia (SDD 11 §4). Un tercero + /// reproduce el bootstrap y verifica que sus hashes coinciden con los de estas líneas. + Manifest, +} + +/// Parsea la clase de semilla del flag `--seed`. Único punto de verdad para todas las subcomandos. +fn parse_seed_kind(s: &str) -> anyhow::Result { + match s { + "zig" => Ok(hammer_bootstrap::SeedKind::Zig), + "musl-cross-make" => Ok(hammer_bootstrap::SeedKind::MuslCrossMake), + other => anyhow::bail!("--seed debe ser 'zig' o 'musl-cross-make', no '{other}'"), + } } fn print_event(ev: &hammer_journal::MutationEvent, format: &str) { @@ -521,22 +553,14 @@ fn main() -> anyhow::Result<()> { } Cmd::Bootstrap { sub } => match sub { BootstrapCmd::Stage0 { url, sha256, version, seed } => { - let kind = match seed.as_str() { - "zig" => hammer_bootstrap::SeedKind::Zig, - "musl-cross-make" => hammer_bootstrap::SeedKind::MuslCrossMake, - other => anyhow::bail!("--seed debe ser 'zig' o 'musl-cross-make', no '{other}'"), - }; + let kind = parse_seed_kind(&seed)?; let store = hammer_core::Store::open(&cli.store)?; let spec = hammer_bootstrap::SeedSpec { kind, version, url, sha256 }; let hash = hammer_bootstrap::stage0(&spec, &store)?; println!("{hash}"); } BootstrapCmd::Stage1 { seed_hash, seed, recipes } => { - let seed_kind = match seed.as_str() { - "zig" => hammer_bootstrap::SeedKind::Zig, - "musl-cross-make" => hammer_bootstrap::SeedKind::MuslCrossMake, - other => anyhow::bail!("--seed debe ser 'zig' o 'musl-cross-make', no '{other}'"), - }; + let seed_kind = parse_seed_kind(&seed)?; let store = hammer_core::Store::open(&cli.store)?; let base_cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root()); let spec = hammer_bootstrap::Stage1Spec { @@ -585,11 +609,7 @@ fn main() -> anyhow::Result<()> { swaps, out, } => { - let seed_kind = match seed.as_str() { - "zig" => hammer_bootstrap::SeedKind::Zig, - "musl-cross-make" => hammer_bootstrap::SeedKind::MuslCrossMake, - other => anyhow::bail!("--seed debe ser 'zig' o 'musl-cross-make', no '{other}'"), - }; + let seed_kind = parse_seed_kind(&seed)?; let store = hammer_core::Store::open(&cli.store)?; let swaps = swaps.iter().map(|s| parse_swap(s)).collect::>>()?; let spec = hammer_bootstrap::BuilderSpec { @@ -613,6 +633,37 @@ fn main() -> anyhow::Result<()> { "siguiente: empaquetar como initramfs y bootear en la VM; adentro corre `rebuild-stage1` (runbook §8c)" ); } + BootstrapCmd::All { url, sha256, version, seed, recipes } => { + let kind = parse_seed_kind(&seed)?; + let store = hammer_core::Store::open(&cli.store)?; + let base_cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root()); + let spec = hammer_bootstrap::SeedSpec { kind, version, url, sha256 }; + let report = hammer_bootstrap::all( + &spec, + std::path::Path::new(&recipes), + &base_cfg, + &store, + )?; + println!("Stage 0 · semilla {}", report.seed_hash); + println!("Stage 1 · rootfs {}", report.stage1_rootfs); + println!("Stage 2 · referencia {}", report.stage2.stage1_content); + println!(); + println!( + "bootstrap.json: {} línea(s) anotada(s) (`hammer bootstrap manifest` para verlo)", + report.manifest.entries.len() + ); + println!( + "auto-alojamiento: el rebuild nativo que sella ✓ REPRODUCIBLE corre dentro del rootfs (VM):" + ); + println!( + " ./scripts/selfhost-verify.sh # reconstruye stage1' y lo compara con la referencia de arriba" + ); + } + BootstrapCmd::Manifest => { + let store = hammer_core::Store::open(&cli.store)?; + let manifest = hammer_bootstrap::BootstrapManifest::load(&store)?; + println!("{}", serde_json::to_string_pretty(&manifest)?); + } }, } Ok(()) diff --git a/docs/11-bootstrap.md b/docs/11-bootstrap.md index d5386c70..eb70d6da 100644 --- a/docs/11-bootstrap.md +++ b/docs/11-bootstrap.md @@ -109,7 +109,10 @@ Implementado en `hammer-bootstrap::manifest` (`BootstrapManifest` / `StageEntry` **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). +`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 @@ -122,9 +125,15 @@ pub fn stage0(seed: &SeedSpec, store: &Store) -> Result; 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: &SeedSpec, store: &Store) -> Result; // ☐ +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 @@ -158,7 +167,8 @@ 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 # las tres + reporte de reproducibilidad # ☐ +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))