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:
@@ -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
|
||||
|
||||
@@ -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})");
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user