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 1572Shell 203Rust 109Markdown 993728 Definiciones

Caché persistente de zig + bootstrap reproducible del .dev-fs

- `BuildConfig.cache_root` (override `HAMMER_CACHE`): bindea `/cache` al sandbox
  y exporta `ZIG_GLOBAL_CACHE_DIR=/cache/zig`. Evita ~30s de recompilación de
  musl en cada build.
- `scripts/bootstrap-devfs.sh`: idempotente, descarga+verifica alpine-minirootfs
  3.23.4 y zig 0.16.0 con sha256 fijo, instala build tools en el rootfs vía apk
  bajo bwrap.
- `make_tree_read_only`: solo archivos regulares pierden `w`; los directorios
  conservan 0o755 para no estorbar GC ni `rm -rf` administrativo. La
  inmutabilidad estricta del store se delega al mount RO de la distro propia.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

e53877a53209cf15e3cd682fa97b16048e6ffc62 · por Sergio · (hace 4 meses) · padre 736e73e2e · ver el árbol en este commit

Delta semántico · 10

crates/hammer-build/src/config.rs

crates/hammer-build/src/lib.rs

  • cambió por dentro fn build pub

crates/hammer-build/src/sandbox.rs

crates/hammer-build/tests/end_to_end_autotools.rs

crates/hammer-build/tests/end_to_end_hello.rs

crates/hammer-core/src/store.rs

7 archivos · +181 −11

diff --git a/crates/hammer-build/src/config.rs b/crates/hammer-build/src/config.rs
index 17e25751..6cda2eb6 100644
--- a/crates/hammer-build/src/config.rs
+++ b/crates/hammer-build/src/config.rs
@@ -16,12 +16,15 @@ pub struct BuildConfig {
     /// patch+compile, y `out` por build). Debe estar en el mismo filesystem que el store
     /// para que el `seal` final sea un rename atómico.
     pub work_root: PathBuf,
+    /// Directorio host persistente que se bindea como `/cache` en el sandbox; alberga la
+    /// caché global de `zig` (objetos de musl, libc, etc.) entre builds. `None` desactiva.
+    pub cache_root: Option<PathBuf>,
 }
 
 impl BuildConfig {
     /// Defaults relativos al store: `.dev-fs` y `work/` como hermanos del store.
     /// `store_root = /…/store` → rootfs = `/…/.dev-fs/alpine`, zig = `/…/.dev-fs/tools/zig`,
-    /// work = `/…/work`.
+    /// work = `/…/work`, cache = `/…/.dev-fs/cache`.
     pub fn defaults_for_store(store_root: &Path) -> Self {
         let project_root = store_root
             .parent()
@@ -31,10 +34,12 @@ impl BuildConfig {
             rootfs: project_root.join(".dev-fs/alpine"),
             zig_dir: project_root.join(".dev-fs/tools/zig"),
             work_root: project_root.join("work"),
+            cache_root: Some(project_root.join(".dev-fs/cache")),
         }
     }
 
-    /// Lee overrides desde entorno (`HAMMER_ROOTFS`, `HAMMER_ZIG`, `HAMMER_WORK`).
+    /// Lee overrides desde entorno (`HAMMER_ROOTFS`, `HAMMER_ZIG`, `HAMMER_WORK`,
+    /// `HAMMER_CACHE`). `HAMMER_CACHE=""` (vacío) desactiva la caché.
     pub fn from_env_or_defaults(store_root: &Path) -> Self {
         let mut cfg = Self::defaults_for_store(store_root);
         if let Ok(v) = std::env::var("HAMMER_ROOTFS") {
@@ -46,6 +51,9 @@ impl BuildConfig {
         if let Ok(v) = std::env::var("HAMMER_WORK") {
             cfg.work_root = PathBuf::from(v);
         }
+        if let Ok(v) = std::env::var("HAMMER_CACHE") {
+            cfg.cache_root = if v.is_empty() { None } else { Some(PathBuf::from(v)) };
+        }
         cfg
     }
 }
diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index 3c1de99b..51933b90 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -55,11 +55,15 @@ pub fn build(
         // monolítico que pide nuestra estrategia de hidratación primaria.
         env.push(("LDFLAGS".into(), "-static".into()));
     }
+    if let Some(cache) = &cfg.cache_root {
+        std::fs::create_dir_all(cache.join("zig"))?;
+    }
     let sb = Sandbox {
         rootfs: cfg.rootfs.clone(),
         zig_dir: cfg.zig_dir.clone(),
         src_dir: src_tree.clone(),
         out_dir: out_dir.clone(),
+        cache_dir: cfg.cache_root.clone(),
         env,
     };
 
diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index 90fa9548..e96151c7 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -17,6 +17,9 @@ pub struct Sandbox {
     pub zig_dir: PathBuf,
     pub src_dir: PathBuf,
     pub out_dir: PathBuf,
+    /// Si Some, se bindea RW como `/cache` y se setea `ZIG_GLOBAL_CACHE_DIR=/cache/zig`.
+    /// Persistir esta caché entre builds evita que `zig cc` recompile musl cada vez (~30s).
+    pub cache_dir: Option<PathBuf>,
     /// Vars de entorno extra inyectadas además del set por defecto.
     pub env: Vec<(String, String)>,
 }
@@ -70,8 +73,14 @@ impl Sandbox {
             "--die-with-parent".into(),
         ];
 
+        if let Some(cache) = &self.cache_dir {
+            args.push("--bind".into());
+            args.push(cache.display().to_string());
+            args.push("/cache".into());
+        }
+
         // Entorno mínimo y predecible.
-        let defaults: Vec<(&str, &str)> = vec![
+        let mut defaults: Vec<(&str, &str)> = vec![
             ("PATH", "/opt/zig:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"),
             ("HOME", "/tmp"),
             ("DESTDIR", "/out"),
@@ -82,6 +91,10 @@ impl Sandbox {
             ("LC_ALL", "C"),
             ("LANG", "C"),
         ];
+        if self.cache_dir.is_some() {
+            defaults.push(("ZIG_GLOBAL_CACHE_DIR", "/cache/zig"));
+            defaults.push(("ZIG_LOCAL_CACHE_DIR", "/cache/zig"));
+        }
         for (k, v) in defaults {
             args.push("--setenv".into());
             args.push(k.into());
diff --git a/crates/hammer-build/tests/end_to_end_autotools.rs b/crates/hammer-build/tests/end_to_end_autotools.rs
index 1380b14a..3c8bb14c 100644
--- a/crates/hammer-build/tests/end_to_end_autotools.rs
+++ b/crates/hammer-build/tests/end_to_end_autotools.rs
@@ -106,6 +106,7 @@ fn build_autotools_hello_end_to_end() {
         rootfs: project.join(".dev-fs/alpine"),
         zig_dir: project.join(".dev-fs/tools/zig"),
         work_root: tempfile::tempdir().unwrap().keep(),
+        cache_root: Some(project.join(".dev-fs/cache")),
     };
     if skip_if_no_layout(&cfg) {
         return;
diff --git a/crates/hammer-build/tests/end_to_end_hello.rs b/crates/hammer-build/tests/end_to_end_hello.rs
index aa718923..efe2f5b9 100644
--- a/crates/hammer-build/tests/end_to_end_hello.rs
+++ b/crates/hammer-build/tests/end_to_end_hello.rs
@@ -114,6 +114,9 @@ fn build_hello_end_to_end() {
         rootfs: rootfs.clone(),
         zig_dir: zig_dir.clone(),
         work_root,
+        // Reusa la caché global del dev — la primera corrida tras `cargo clean` paga
+        // ~30s recompilando musl; las siguientes terminan en segundos.
+        cache_root: Some(project.join(".dev-fs/cache")),
     };
     if skip_if_no_layout(&cfg) {
         return;
diff --git a/crates/hammer-core/src/store.rs b/crates/hammer-core/src/store.rs
index 1f4f7e14..629b25e9 100644
--- a/crates/hammer-core/src/store.rs
+++ b/crates/hammer-core/src/store.rs
@@ -105,8 +105,11 @@ impl Store {
     }
 }
 
-/// Hace todo el árbol read-only. Archivos pierden `w` para todos los usuarios; directorios
-/// mantienen permisos suficientes para listarse y atravesarse, pero pierden `w`.
+/// Quita los bits de escritura de cada archivo regular del árbol. Los directorios mantienen
+/// sus permisos originales (típicamente 0o755): eso es suficiente para garantizar
+/// "contenido inmutable" sin estorbar al GC o a operaciones administrativas como `rm -rf`.
+/// La inmutabilidad estricta del store entero (impedir crear archivos *nuevos* dentro de un
+/// dir sellado) se aplicará vía mount read-only en el track de distro propia.
 fn make_tree_read_only(root: &Path) -> crate::Result<()> {
     use std::os::unix::fs::PermissionsExt;
 
@@ -114,18 +117,13 @@ fn make_tree_read_only(root: &Path) -> crate::Result<()> {
         let meta = std::fs::symlink_metadata(path)?;
         let ft = meta.file_type();
         if ft.is_symlink() {
-            // No tocamos symlinks (chmod sigue el link en muchas plataformas; no merece la pena).
             return Ok(());
         }
         if ft.is_dir() {
             for entry in std::fs::read_dir(path)? {
                 walk(&entry?.path())?;
             }
-            // Quitar bits de escritura del directorio (lectura+ejec sí, para poder atravesar).
-            let mut perms = meta.permissions();
-            perms.set_mode(perms.mode() & !0o222);
-            std::fs::set_permissions(path, perms)?;
-        } else {
+        } else if ft.is_file() {
             let mut perms = meta.permissions();
             perms.set_mode(perms.mode() & !0o222);
             std::fs::set_permissions(path, perms)?;
diff --git a/scripts/bootstrap-devfs.sh b/scripts/bootstrap-devfs.sh
new file mode 100755
index 00000000..9494918b
--- /dev/null
+++ b/scripts/bootstrap-devfs.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+# bootstrap-devfs.sh — Prepara .dev-fs/ con todo lo que hammer-build necesita:
+#   1. Rootfs Alpine minirootfs como base hermética del sandbox.
+#   2. Compilador zig oficial bajo .dev-fs/tools/zig (symlink versionado).
+#   3. Build tools (make, autoconf, automake, m4, patch, coreutils, libtool,
+#      pkgconf, bash) instaladas dentro del rootfs con apk.
+#   4. Directorios de caché y work para builds posteriores.
+#
+# Idempotente: cada paso se salta si ya hizo. Reinvocar tras un cambio de
+# versión actualiza in-place.
+#
+# Variables (override por entorno):
+#   ALPINE_BRANCH  rama de Alpine (default v3.23)
+#   ALPINE_VER     versión exacta del tarball (default 3.23.4)
+#   ALPINE_SHA256  sha256 del tarball (verificado obligatorio)
+#   ZIG_VER        versión de zig (default 0.16.0)
+#   ZIG_SHA256     sha256 del tarball zig
+#
+# Uso:
+#   ./scripts/bootstrap-devfs.sh                  # bootstrap completo
+#   ./scripts/bootstrap-devfs.sh --skip-apk       # sólo rootfs + zig
+#   ./scripts/bootstrap-devfs.sh --force-zig      # re-descarga zig
+
+set -euo pipefail
+
+ALPINE_BRANCH="${ALPINE_BRANCH:-v3.23}"
+ALPINE_VER="${ALPINE_VER:-3.23.4}"
+ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}"
+ZIG_VER="${ZIG_VER:-0.16.0}"
+ZIG_SHA256="${ZIG_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}"
+
+SKIP_APK=0
+FORCE_ZIG=0
+FORCE_ROOTFS=0
+for arg in "$@"; do
+    case "$arg" in
+        --skip-apk)    SKIP_APK=1 ;;
+        --force-zig)   FORCE_ZIG=1 ;;
+        --force-rootfs) FORCE_ROOTFS=1 ;;
+        -h|--help)
+            sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
+            exit 0 ;;
+        *)
+            echo "argumento desconocido: $arg" >&2
+            exit 1 ;;
+    esac
+done
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+DEVFS="$REPO_ROOT/.dev-fs"
+ROOTFS="$DEVFS/alpine"
+TOOLS="$DEVFS/tools"
+ZIG_DIR="$TOOLS/zig-x86_64-linux-$ZIG_VER"
+ZIG_LINK="$TOOLS/zig"
+CACHE="$DEVFS/cache"
+WORK="$REPO_ROOT/work"
+
+log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
+ok()  { printf '\033[1;32m✓\033[0m  %s\n' "$*"; }
+
+mkdir -p "$DEVFS" "$TOOLS" "$CACHE" "$WORK"
+
+# --- 1. Rootfs Alpine ---------------------------------------------------------
+if [[ $FORCE_ROOTFS -eq 1 ]] && [[ -d "$ROOTFS" ]]; then
+    log "rootfs: --force-rootfs ⇒ borro $ROOTFS"
+    rm -rf "$ROOTFS"
+fi
+
+if [[ -f "$ROOTFS/bin/busybox" ]]; then
+    ok "rootfs Alpine ya presente en $ROOTFS"
+else
+    log "rootfs: descargando alpine-minirootfs $ALPINE_VER"
+    tar="$DEVFS/alpine-minirootfs-$ALPINE_VER.tar.gz"
+    url="https://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/releases/x86_64/alpine-minirootfs-$ALPINE_VER-x86_64.tar.gz"
+    curl -fsSL "$url" -o "$tar"
+    echo "$ALPINE_SHA256  $tar" | sha256sum -c -
+    mkdir -p "$ROOTFS"
+    tar -xzf "$tar" -C "$ROOTFS"
+    rm -f "$tar"
+    ok "rootfs Alpine $ALPINE_VER extraído"
+fi
+
+# --- 2. Compilador zig --------------------------------------------------------
+if [[ $FORCE_ZIG -eq 1 ]] && [[ -e "$ZIG_DIR" ]]; then
+    log "zig: --force-zig ⇒ borro $ZIG_DIR"
+    rm -rf "$ZIG_DIR"
+fi
+
+if [[ -x "$ZIG_LINK/zig" ]]; then
+    ok "zig $ZIG_VER ya presente en $ZIG_LINK"
+else
+    log "zig: descargando $ZIG_VER"
+    tar="$TOOLS/zig.tar.xz"
+    url="https://ziglang.org/download/$ZIG_VER/zig-x86_64-linux-$ZIG_VER.tar.xz"
+    curl -fsSL "$url" -o "$tar"
+    echo "$ZIG_SHA256  $tar" | sha256sum -c -
+    tar -xJf "$tar" -C "$TOOLS"
+    rm -f "$tar"
+    ln -sfn "zig-x86_64-linux-$ZIG_VER" "$ZIG_LINK"
+    ok "zig $ZIG_VER instalado, symlink $ZIG_LINK"
+fi
+
+# --- 3. Build tools en el rootfs (apk add) ------------------------------------
+if [[ $SKIP_APK -eq 1 ]]; then
+    log "apk: --skip-apk ⇒ saltando build tools"
+else
+    NEEDED="make autoconf automake m4 patch coreutils libtool pkgconf bash"
+    missing=""
+    for pkg in $NEEDED; do
+        # apk info -e devuelve el paquete si está instalado, vacío si no.
+        if ! bwrap --bind "$ROOTFS" / --proc /proc --dev /dev --unshare-all \
+              --setenv PATH /usr/bin:/usr/sbin:/bin:/sbin \
+              /sbin/apk info -e "$pkg" >/dev/null 2>&1; then
+            missing="$missing $pkg"
+        fi
+    done
+    if [[ -n "$missing" ]]; then
+        log "apk: instalando paquetes faltantes:$missing"
+        bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \
+            --ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \
+            /sbin/apk update >/dev/null
+        bwrap --bind "$ROOTFS" / --proc /proc --dev /dev \
+            --ro-bind /etc/resolv.conf /etc/resolv.conf --share-net \
+            /sbin/apk add $missing
+    fi
+    ok "build tools presentes en el rootfs ($NEEDED)"
+fi
+
+# --- 4. Resumen ---------------------------------------------------------------
+cat <<EOF
+
+bootstrap-devfs.sh — OK
+  rootfs:  $ROOTFS
+  zig:     $ZIG_LINK ($("$ZIG_LINK/zig" version))
+  cache:   $CACHE
+  work:    $WORK
+
+Variables que la CLI ya recoge por defecto (override con env vars):
+  HAMMER_ROOTFS  = $ROOTFS
+  HAMMER_ZIG     = $ZIG_LINK
+  HAMMER_CACHE   = $CACHE        # vacío ("") desactiva la caché
+  HAMMER_WORK    = $WORK
+EOF