> 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…
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>
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))