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 1569Shell 203Rust 109Markdown 993728 Definitions

sandbox: forzar archivos `ar` deterministas — dos builds sellaban bytes distintos

CÓMO SE VIO. Construir zlib en gioser y en el worker nuevo dio el MISMO ArtifactHash
y BYTES DISTINTOS. `hammer why-differs` lo nombró exacto: 13 entradas idénticas, 1
diverge — `usr/lib/libz.a`, cabecera `ar` del miembro `/` (la tabla de símbolos),
mtime 1787326910 vs 1787950875. Es la hora de pared de cada build.

CAUSA, AISLADA POR BISECCIÓN EN EL LAB (no por lectura de código):
  · `zig ar` (llvm-ar)          → mtime 0   ✅
  · `ranlib` del rootfs Alpine  → mtime 0   ✅ (Alpine compila binutils con
                                              --enable-deterministic-archives)
  · `ar` y `ranlib` de NUESTRO recipes/binutils.toml → hora de pared  ❌
  · los mismos con `-D`         → mtime 0   ✅
La receta de binutils no pasa --enable-deterministic-archives, así que toda receta
que lo materialice como build-dep y llame a ar/ranlib hereda la hora.

Y el comentario de SOURCE_DATE_EPOCH en este mismo fichero AFIRMABA cubrir `ar`.
Era falso: binutils no lee esa variable. Corregido, porque una suposición escrita
en un comentario es la que impide que alguien vuelva a medir.

ARREGLO. Wrappers `ar`/`ranlib` en /usr/local/bin (que gana al /usr/bin del PATH)
que anteponen `-D`. Mismo patrón que ya usa el sandbox para arreglar esta CLASE de
bug sin re-hashear: -mcpu=baseline, CARGO_PROFILE_RELEASE_CODEGEN_UNITS,
CARGO_BUILD_JOBS. `-D` antepuesto y no como modificador de la cadena de operación,
porque la operación la pone el llamador en "$@" y no se puede reescribir.
Verificado: la secuencia exacta de zlib (`ar rc` + `ranlib`) da mtime 0 y el índice
del archivo sigue siendo legible (`ar t`, `nm -s`).

POR QUÉ AQUÍ Y NO EN LA RECETA. Arreglar recipes/binutils.toml es la cura de raíz,
pero `yupana radio binutils` da 421 dependientes transitivos y 365 sellados a deuda,
en las siete imágenes — desproporcionado frente a los 9 artefactos realmente
contaminados (medidos barriendo los 185 artefactos con .a del store: elfutils,
elfutils-libdw ×4, zlib, musl-fts, musl-obstack, argp-standalone). La cura de raíz
se agenda para el próximo rebuild del corpus que ya haya que hacer por otro motivo.

PENDIENTE Y DICHO EN VOZ ALTA: esto cambia los BYTES sin cambiar la DIRECCIÓN de
esos 9. Hay que re-sellarlos a propósito — es el mismo peligro que la huella del lab
existe para eliminar, y no se cierra solo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4

7106ec0645541893031ef4da42bc63b41f4c53ff · by Sergio · (1 month ago) · parent 853853d91 · browse the tree at this commit

Semantic delta · 3

crates/hammer-build/src/sandbox.rs

1 files · +54 −1

diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index 73922584..b9bca334 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -426,7 +426,11 @@ impl Sandbox {
             ("LC_ALL", "C"),
             ("LANG", "C"),
             // Reproducibilidad (pre-Stage 2): fija los timestamps que muchas herramientas embeben
-            // (tar, ar, gzip, __DATE__/__TIME__ vía SOURCE_DATE_EPOCH). Valor fijo y arbitrario
+            // (tar, gzip, __DATE__/__TIME__ vía SOURCE_DATE_EPOCH). Valor fijo y arbitrario
+            // ⚠ NO cubre `ar`/`ranlib`: binutils NO lee SOURCE_DATE_EPOCH — la determinismo de
+            // archivos se decide en `configure` (`--enable-deterministic-archives`) o por opción
+            // `-D` en cada invocación. Este comentario decía que sí lo cubría y era FALSO; lo
+            // desmintió comparar dos builds de zlib (ver AR_WRAPPER/RANLIB_WRAPPER abajo).
             // (1970-01-01 00:00:01). Las rutas ya son deterministas: el source se bindea en `/src`
             // (constante entre rebuilds, sea cual sea el work dir del host). Ver SDD 11 / runbook.
             ("SOURCE_DATE_EPOCH", "1"),
@@ -529,6 +533,33 @@ pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> hammer_core::Result<()> {
 pub const CC_WRAPPER: &str = "hammer-zig-cc";
 pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
 
+/// Herramientas de archivo que se interponen en `/usr/local/bin` para forzar **modo determinista**.
+///
+/// ── POR QUÉ (medido 2026-08-28) ─────────────────────────────────────────────────────────────
+/// Construir `zlib` en dos máquinas dio el MISMO `ArtifactHash` y **bytes distintos**:
+/// `usr/lib/libz.a` divergía sólo en la cabecera `ar` del miembro `/` (la tabla de símbolos),
+/// con la hora de pared del build. `hammer why-differs` lo nombró exacto.
+///
+/// La causa NO es `zig ar` (llvm-ar sella mtime 0) ni el binutils del rootfs Alpine (que Alpine
+/// compila con `--enable-deterministic-archives`): es **nuestro propio `recipes/binutils.toml`**,
+/// que no pasa ese flag. Toda receta que lo materialice como build-dep y llame a `ar`/`ranlib`
+/// —directamente o vía el `$(RANLIB)` de su Makefile— hereda la hora de pared.
+///
+/// ── POR QUÉ AQUÍ Y NO EN LA RECETA ──────────────────────────────────────────────────────────
+/// Arreglar `recipes/binutils.toml` es la cura de raíz, pero su radio son **421 recetas
+/// transitivas y 365 sellados a deuda** (medido con `yupana radio binutils`) — desproporcionado
+/// frente a los 9 artefactos realmente contaminados. El sandbox es donde este proyecto ya
+/// arregla esta CLASE de bug sin re-hashear: `-mcpu=baseline`, `CARGO_PROFILE_RELEASE_CODEGEN_UNITS`
+/// y `CARGO_BUILD_JOBS` son exactamente el mismo patrón. La cura de raíz queda pendiente para
+/// cuando toque un rebuild del corpus por otro motivo (SDD 23), no como campaña propia.
+///
+/// ⚠ CONSECUENCIA QUE HAY QUE EJECUTAR, NO SÓLO DECLARAR: esto cambia los BYTES sin cambiar la
+/// DIRECCIÓN de los 9 artefactos ya contaminados. Un artefacto viejo y uno nuevo son distintos en
+/// el mismo hash — el peligro exacto que la huella del lab existe para eliminar. Por eso la
+/// adopción incluye **re-sellar esos 9 a propósito**, no esperar a que caduquen solos.
+pub const AR_WRAPPER: &str = "ar";
+pub const RANLIB_WRAPPER: &str = "ranlib";
+
 /// Materializa en el rootfs dos wrappers de linker-driver (`hammer-zig-cc`/`-cxx`) que envuelven a
 /// `zig cc`/`zig c++`. Su ÚNICO efecto es QUITAR `-static` cuando el link REQUIERE enlace dinámico.
 ///
@@ -594,6 +625,28 @@ fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
             hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
         })?;
     }
+
+    // Archivadores en modo determinista. Ver la doc de AR_WRAPPER.
+    //
+    // `-D` ANTEPUESTO y no como modificador de la cadena de operación (`Drc`): las dos formas
+    // funcionan en binutils (verificado), pero anteponerlo es lo único que compone con una línea
+    // de comando ajena — el llamador pone su propia operación (`rc`, `t`, `x`…) en "$@" y no la
+    // podemos reescribir. `-D` es inocuo en las operaciones de lectura.
+    //
+    // Ruta ABSOLUTA `/usr/bin/<tool>` a propósito: el wrapper vive en `/usr/local/bin`, que va
+    // ANTES en el PATH, así que un `exec ar` se llamaría a sí mismo para siempre.
+    for tool in [AR_WRAPPER, RANLIB_WRAPPER] {
+        let path = bindir.join(tool);
+        let body = format!(
+            "#!/bin/sh\n             # Interpuesto por hammer: fuerza archivos deterministas (sin hora de pared en la\n             # cabecera `ar`). Sin esto dos builds idénticos sellan bytes distintos. Ver\n             # sandbox.rs::AR_WRAPPER.\n             [ -x /usr/bin/{tool} ] || exit 127\n             exec /usr/bin/{tool} -D \"$@\"\n"
+        );
+        std::fs::write(&path, body).map_err(|e| {
+            hammer_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display()))
+        })?;
+        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
+            hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
+        })?;
+    }
     Ok(())
 }