bootstrap: seed como toolchain del sandbox (Lote 3) + decisiones ADR 0008
SeedSpec::toolchain_dir resuelve el dir del compilador dentro de la semilla sellada, listo para que el sandbox lo bindee como /opt/zig; seed_build_config deriva una BuildConfig que cross-compila con el seed pinned, no con el zig del host. Stage 0 sigue siendo ingesta pura: el layout se resuelve al usarlo (zig en la raíz o en el hijo versionado), con error claro si falta o es ambiguo. Decisiones del Lote 4 registradas en ADR 0008 / SDD 11: - coreutils+shell = busybox (balsa desechable; GPLv2 no contamina el final) - init arje diferido (Stage 1 con init mínimo provisional; CRASHED real luego) - layout de semilla: ingesta pura + resolución al usar +5 tests (layouts: raíz / hijo versionado / no-sellada / ambiguo / build_config). Workspace verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1f1fae3508
commit
989e0d2c7f
@@ -14,7 +14,7 @@
|
||||
|
||||
pub mod manifest;
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use hammer_build::download;
|
||||
@@ -83,6 +83,70 @@ impl SeedSpec {
|
||||
pub fn store_name(&self) -> String {
|
||||
format!("seed-{}", self.kind.as_str())
|
||||
}
|
||||
|
||||
/// Directorio del toolchain **dentro de la semilla sellada**, listo para que el sandbox lo
|
||||
/// bindee como `/opt/zig`. Stage 0 sella el árbol del tarball tal cual (ingesta pura, sin
|
||||
/// aplanar): este método lo resuelve al usarlo, localizando el dir que contiene el ejecutable
|
||||
/// `zig` —en la raíz si el tarball lo trae plano, o en el único hijo si trae el típico
|
||||
/// top-level versionado `zig-linux-x86_64-<ver>/`—. Falla si la semilla no está sellada o si
|
||||
/// el ejecutable no aparece (o es ambiguo).
|
||||
pub fn toolchain_dir(&self, store: &Store) -> Result<PathBuf> {
|
||||
let base = store.path_of(&self.seed_hash(), &self.store_name());
|
||||
if !base.is_dir() {
|
||||
return Err(Error::Other(format!(
|
||||
"semilla '{}' no sellada en {}; corre `hammer bootstrap stage0` primero",
|
||||
self.kind.as_str(),
|
||||
base.display()
|
||||
)));
|
||||
}
|
||||
match self.kind {
|
||||
SeedKind::Zig => locate_tool_dir(&base, "zig"),
|
||||
SeedKind::MuslCrossMake => Err(Error::Other(
|
||||
"toolchain_dir para musl-cross-make aún no implementado (escotilla diferida)".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Localiza, dentro de `base`, el directorio que contiene **directamente** el ejecutable `tool`.
|
||||
/// Mira la raíz primero y luego los hijos inmediatos; exige exactamente una coincidencia para no
|
||||
/// elegir a ciegas entre layouts ambiguos.
|
||||
fn locate_tool_dir(base: &Path, tool: &str) -> Result<PathBuf> {
|
||||
if base.join(tool).is_file() {
|
||||
return Ok(base.to_path_buf());
|
||||
}
|
||||
let mut hits = Vec::new();
|
||||
for entry in std::fs::read_dir(base)? {
|
||||
let path = entry?.path();
|
||||
if path.is_dir() && path.join(tool).is_file() {
|
||||
hits.push(path);
|
||||
}
|
||||
}
|
||||
hits.sort();
|
||||
match hits.len() {
|
||||
1 => Ok(hits.pop().unwrap()),
|
||||
0 => Err(Error::Other(format!(
|
||||
"no encontré el ejecutable '{tool}' en la raíz ni en los hijos inmediatos de {}",
|
||||
base.display()
|
||||
))),
|
||||
_ => Err(Error::Other(format!(
|
||||
"ambiguo: '{tool}' aparece en varios directorios de {}: {hits:?}",
|
||||
base.display()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deriva una [`hammer_build::BuildConfig`] desde `base` cuyo `zig_dir` apunta a la **semilla
|
||||
/// sellada** en vez del toolchain del host: así las recetas del Stage 1 cross-compilan con el
|
||||
/// toolchain fijado por hash (`seed_hash`), no con el `.dev-fs/tools/zig` de desarrollo. El resto
|
||||
/// de la config (rootfs, work, cache) se hereda intacto.
|
||||
pub fn seed_build_config(
|
||||
base: &hammer_build::BuildConfig,
|
||||
seed: &SeedSpec,
|
||||
store: &Store,
|
||||
) -> Result<hammer_build::BuildConfig> {
|
||||
let zig_dir = seed.toolchain_dir(store)?;
|
||||
Ok(hammer_build::BuildConfig { zig_dir, ..base.clone() })
|
||||
}
|
||||
|
||||
/// **Stage 0** — ingiere la semilla al store y devuelve su `ArtifactHash`.
|
||||
@@ -309,4 +373,82 @@ mod tests {
|
||||
"el staging debe quedar limpio tras el fallo"
|
||||
);
|
||||
}
|
||||
|
||||
/// Sella una semilla cuyo tarball coloca un `zig` ejecutable en `zig_rel` (relativo a la raíz
|
||||
/// del árbol) y devuelve `(store, seed)` listos para resolver el toolchain.
|
||||
fn seal_seed_with_zig_at(dir: &Path, zig_rel: &str) -> (Store, SeedSpec) {
|
||||
let src = dir.join("src");
|
||||
let zig = src.join(zig_rel);
|
||||
std::fs::create_dir_all(zig.parent().unwrap()).unwrap();
|
||||
std::fs::write(&zig, b"#!/bin/sh\necho fake zig\n").unwrap();
|
||||
let tarball = dir.join("seed.tar.gz");
|
||||
let st = Command::new("tar")
|
||||
.args(["-czf"])
|
||||
.arg(&tarball)
|
||||
.arg("-C")
|
||||
.arg(&src)
|
||||
.arg(".")
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(st.success());
|
||||
let sha = sha256_hex(&std::fs::read(&tarball).unwrap());
|
||||
let store = Store::open(dir.join("store")).unwrap();
|
||||
let seed = spec(&format!("file://{}", tarball.display()), &sha);
|
||||
stage0(&seed, &store).unwrap();
|
||||
(store, seed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_dir_finds_zig_in_versioned_subdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (store, seed) = seal_seed_with_zig_at(tmp.path(), "zig-linux-x86_64-0.13.0/zig");
|
||||
let dir = seed.toolchain_dir(&store).unwrap();
|
||||
assert!(dir.join("zig").is_file(), "el dir resuelto debe contener zig: {}", dir.display());
|
||||
assert!(dir.ends_with("zig-linux-x86_64-0.13.0"), "debe elegir el hijo versionado: {}", dir.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_dir_finds_zig_at_root() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (store, seed) = seal_seed_with_zig_at(tmp.path(), "zig");
|
||||
let dir = seed.toolchain_dir(&store).unwrap();
|
||||
let base = store.path_of(&seed.seed_hash(), &seed.store_name());
|
||||
assert_eq!(dir, base, "zig en la raíz ⇒ el dir es la raíz de la semilla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_dir_errors_when_unsealed() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||
let seed = spec("file:///nope.tar", &"0".repeat(64)); // nunca sellada
|
||||
let err = seed.toolchain_dir(&store).unwrap_err().to_string();
|
||||
assert!(err.contains("no sellada"), "esperaba 'no sellada': {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_dir_errors_when_ambiguous() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
// Dos hijos, cada uno con su `zig`: el resolutor no debe elegir a ciegas.
|
||||
let (store, seed) = seal_seed_with_zig_at(tmp.path(), "a/zig");
|
||||
let base = store.path_of(&seed.seed_hash(), &seed.store_name());
|
||||
// El store sella read-only; relajamos permisos para inyectar el segundo `zig` del test.
|
||||
let b = base.join("b");
|
||||
let _ = std::process::Command::new("chmod").arg("-R").arg("u+w").arg(&base).status();
|
||||
std::fs::create_dir_all(&b).unwrap();
|
||||
std::fs::write(b.join("zig"), b"x").unwrap();
|
||||
let err = seed.toolchain_dir(&store).unwrap_err().to_string();
|
||||
assert!(err.contains("ambiguo"), "esperaba 'ambiguo': {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_build_config_points_zig_dir_at_seed() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (store, seed) = seal_seed_with_zig_at(tmp.path(), "zig-linux-x86_64-0.13.0/zig");
|
||||
let base = hammer_build::BuildConfig::defaults_for_store(store.root());
|
||||
let cfg = seed_build_config(&base, &seed, &store).unwrap();
|
||||
assert_eq!(cfg.zig_dir, seed.toolchain_dir(&store).unwrap());
|
||||
// El resto de la config se hereda sin cambios.
|
||||
assert_eq!(cfg.rootfs, base.rootfs);
|
||||
assert_eq!(cfg.work_root, base.work_root);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -137,14 +137,20 @@ hammer bootstrap stage2 --verify
|
||||
hammer bootstrap --all # las tres + reporte de reproducibilidad # ☐
|
||||
```
|
||||
|
||||
## 6. Decisiones abiertas (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md))
|
||||
## 6. Decisiones (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md))
|
||||
|
||||
- **Semilla primaria:** `zig` vs `musl-cross-make`. Inclinación: `zig` por hermeticidad
|
||||
(ADR 0003), con `musl-cross-make` como escotilla por receta.
|
||||
- **busybox vs toybox** para el userland mínimo (licencia vs cobertura de comandos).
|
||||
- **Particionado/montaje** de `/store` y `/var/lib/hammer` en la imagen destino (track posterior
|
||||
del roadmap).
|
||||
- **Kernel:** importado pinned ahora; from-source después.
|
||||
- **Semilla primaria:** `zig` por hermeticidad (ADR 0003), con `musl-cross-make` como escotilla
|
||||
por receta. ✅
|
||||
- **coreutils+shell = `busybox`** (no toybox): el Stage 1 es una balsa desechable hacia el
|
||||
userland nativo en Rust/Zig; se prioriza compatibilidad de flags y velocidad, y su GPLv2 no
|
||||
contamina el sistema final. ✅
|
||||
- **init `arje` diferido:** Stage 1 con init mínimo provisional; `arje` (y el `CRASHED` real) entran
|
||||
como receta git pinned en un lote posterior. ✅
|
||||
- **Layout de la semilla:** ingesta pura en Stage 0 + resolución del toolchain al usarlo
|
||||
(`SeedSpec::toolchain_dir`, localiza `zig` en raíz o hijo versionado). ✅
|
||||
- **Particionado/montaje** de `/store` y `/var/lib/hammer` en la imagen destino: track posterior
|
||||
del roadmap. ☐
|
||||
- **Kernel:** importado pinned ahora; from-source después. ☐
|
||||
|
||||
## 7. Hecho cuando
|
||||
|
||||
|
||||
@@ -19,8 +19,20 @@ mecanismo de build nuevo):
|
||||
como una *fuente fijada por sha256*, y se sella en el store. El `ArtifactHash` se deriva de
|
||||
la **identidad pinned** (clase + versión + sha256), no de rutas del host. **`zig` es la
|
||||
semilla primaria**; `musl-cross-make` (gcc+musl) es escotilla detrás de la misma interfaz.
|
||||
1. **Stage 1 — userland mínimo:** cross-compilado con Stage 0 (musl, busybox/toybox, el init
|
||||
`arje` [ADR 0007](0007-arje-como-init-propio.md), `hammerd`), ensamblado en un rootfs sellado.
|
||||
1. **Stage 1 — userland mínimo:** cross-compilado con Stage 0 (musl, **busybox**, init, `hammerd`),
|
||||
ensamblado en un rootfs sellado. Decisiones de implementación fijadas al abrir el Lote 4:
|
||||
- **coreutils+shell = `busybox`** (no toybox): el Stage 1 es una *balsa desechable* —se sustituye
|
||||
por el userland multi-call nativo en Rust/Zig más adelante—, así que se optimiza la velocidad
|
||||
y la compatibilidad de flags POSIX en el pipeline (mismo comportamiento que el rootfs Alpine de
|
||||
desarrollo). Su GPLv2 queda *atrapada* en una etapa temprana del manifiesto; no contamina el
|
||||
sistema final mutable.
|
||||
- **init `arje` diferido:** Stage 1 arranca con musl+busybox+`hammerd` y un init mínimo
|
||||
provisional. `arje` ([ADR 0007](0007-arje-como-init-propio.md)) —y con él el `CRASHED` real
|
||||
de la Fase 5— se integra como receta git pinned en un lote posterior, sin retrabajo.
|
||||
- **layout de la semilla:** Stage 0 sella el árbol del tarball **tal cual** (ingesta pura, no
|
||||
aplana); el directorio del toolchain se resuelve **al usarlo** localizando el ejecutable `zig`
|
||||
en la raíz o en su único hijo versionado (`SeedSpec::toolchain_dir`). Stage 0 no adivina
|
||||
layouts y el sandbox recibe un `zig_dir` correcto sea cual sea la forma del tarball.
|
||||
2. **Stage 2 — rebuild nativo:** dentro del rootfs Stage 1, recompilar con las herramientas de
|
||||
Stage 1 y comparar hashes (`stage1` vs `stage1'`). Iguales ⇒ auto-alojado y reproducible.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user