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>
This commit is contained in:
2026-06-19 19:11:56 -04:00
co-authored by Claude Opus 4.8
parent b37f11b666
commit 98f0b2d228
4 changed files with 125 additions and 19 deletions
+42
View File
@@ -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
+4 -1
View File
@@ -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})");
+66 -15
View File
@@ -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(())