tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1184Shell 71Rust 62Markdown 501503 Definiciones

bootstrap: orquestador `all` + export del manifiesto (Etapa A)

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 <noreply@anthropic.com>

98f0b2d228ffebb06941dfc1f436167ab40d8f21 · por sergio · (hace 4 meses) · padre b37f11b66 · ver el árbol en este commit

Delta semántico · 6

crates/hammer-bootstrap/src/lib.rs

crates/hammer-build/src/lib.rs

crates/hammer-cli/src/main.rs

4 archivos · +125 −19

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<AllReport> {
+    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<hammer_bootstrap::SeedKind> {
+    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::<anyhow::Result<Vec<_>>>()?;
                 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<ArtifactHash>;
 pub fn stage1(spec: &Stage1Spec, cfg: &BuildConfig, store: &Store)
     -> Result<RootfsHash>;                                                       // ◑ musl+busybox+hammerd; init arje ☐
 pub fn stage2(stage1: &RootfsHash, store: &Store) -> Result<VerifyReport>;        // ✅ ancla+verifica; rebuild in-rootfs en VM ✓ REPRODUCIBLE
-pub fn all(seed: &SeedSpec, store: &Store) -> Result<BootstrapManifest>;          // ☐
+pub fn all(seed, recipes_dir, base_cfg, store) -> Result<AllReport>;              // ✅ 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))