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 1411Shell 141Markdown 85Rust 842918 Definiciones

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>

989e0d2c7f5c0e44b358b0985c61da84cfef06dd · por Sergio · (hace 4 meses) · padre 1f1fae350 · ver el árbol en este commit

Delta semántico · 9

3 archivos · +171 −11

diff --git a/crates/hammer-bootstrap/src/lib.rs b/crates/hammer-bootstrap/src/lib.rs
index a2064b68..d3f8b994 100644
--- a/crates/hammer-bootstrap/src/lib.rs
+++ b/crates/hammer-bootstrap/src/lib.rs
@@ -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);
+    }
 }
diff --git a/docs/11-bootstrap.md b/docs/11-bootstrap.md
index 15f7c641..9272077e 100644
--- a/docs/11-bootstrap.md
+++ b/docs/11-bootstrap.md
@@ -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))
-
-- **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.
+## 6. Decisiones (fijadas en [ADR 0008](adr/0008-bootstrap-stages.md))
+
+- **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
 
diff --git a/docs/adr/0008-bootstrap-stages.md b/docs/adr/0008-bootstrap-stages.md
index a3a4e15e..180e3312 100644
--- a/docs/adr/0008-bootstrap-stages.md
+++ b/docs/adr/0008-bootstrap-stages.md
@@ -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.