gioser

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 815Rust 56Shell 37Markdown 291229 Definitions

ADR 0020: el experimento CORRIDO — 10 de 12 sobreviven a `-ec`, y el que cae tiene el artefacto BIEN

El diseño es la parte que vale: NO SELLA NADA. Modificar una receta le cambia el ArtifactHash, así
que medirlo a lo bruto habría dejado una docena de duplicados basura en un store que se respalda. En
vez de eso se inyecta `set -e` en cada fase Y un `exit 77` al final de la última: el build siempre
falla, nunca sella, y el veredicto se lee en el código de salida — 77 significa que todas las fases
corrieron enteras bajo set -e. Queda en scripts/farm/exp-ec.sh.

Muestra de 12 recetas ligeras (los pesos pesados quedan fuera por tiempo, y el sesgo va declarado):
10 sobreviven sin tocar nada, 1 se rompe, 1 inconcluso por timeout.

El que se rompe es `e2fsprogs`, y mirarlo de cerca cambia lo que significa: su ./configure aborta con
`external uuid library not found` pese a que la receta pasa --disable-libuuid, y hoy eso se traga
—comprobado: la misma receta sin set -e y con exit 77 LLEGA al 77—. Pero el artefacto que sella está
bien: 149 ficheros con e2fsck, mke2fs, los cuatro fsck.ext* y los mkfs.ext*. O sea que ahí `-ec` no
atrapa un bug: rompe una receta que funciona. Ése es el coste, y es el número que no se podía
adivinar leyendo: ~8%, unas 8 recetas sobre las 96 expuestas.

También se corrige el recuento: 96, no 89. El 89 salía de restar 142−53 dando por hecho que las 53
con `set -e` propio estaban todas dentro de las 142, y no lo estaban.

jaula-preparar suma lo que hizo falta para poder correr todo esto desde adentro: rsync/python3/jq/
cargo enlazados del store, y el cargador de musl, sin el cual python3 y cargo no arrancan en una
imagen glibc.

27ec9caea002e5aa57f6b9ca87de7fdf8eda5b1a · by Sergio · (1 month ago) · parent 6ce34e97d · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

3 files · +129 −16

diff --git a/docs/adr/0020-fases-de-build-sin-set-e.md b/docs/adr/0020-fases-de-build-sin-set-e.md
index 6b42b66d..6ffd469e 100644
--- a/docs/adr/0020-fases-de-build-sin-set-e.md
+++ b/docs/adr/0020-fases-de-build-sin-set-e.md
@@ -1,7 +1,7 @@
 # ADR 0020 — Las fases de build no abortan a la primera: `sh -c` sin `-e`
 
-- **Estado:** PROPUESTO — el hallazgo está MEDIDO y un caso está reparado; la decisión (`-ec` sí o
-  no) queda abierta y depende de un experimento que todavía no se corrió (§Precondiciones).
+- **Estado:** PROPUESTO — hallazgo MEDIDO, un caso reparado y el experimento CORRIDO sobre muestra
+  ligera (§El experimento). Falta la cola pesada antes de adoptar (§Lo que queda por medir).
 - **Fecha:** 2026-09-18
 - **Frontera:** `crates/takana-build/src/sandbox.rs:332,491` (las dos invocaciones de `/bin/sh -c`),
   `crates/takana-build/src/lib.rs:805` (`resolve_phases`, las fases generadas), y las 142 recetas
@@ -80,7 +80,7 @@ Y hay una asimetría que conviene nombrar: los guardianes corren **después** de
 *porque* el shell no aborta. Si mañana se pone `-e`, esas recetas dejan de sellar — no porque el
 artefacto sea peor, sino porque el fallo que hoy se tolera pasa a ser mortal.
 
-## Decisión — ABIERTA
+## Decisión — recomendada, pendiente de la cola pesada
 
 Las tres salidas, con lo que cada una cuesta:
 
@@ -95,21 +95,63 @@ default inseguro, que es lo que causó esto.
 **c) Dejarlo y exigir guardianes.** Es el statu quo, y el statu quo produjo un artefacto con un
 directorio vacío que nadie vio durante un día.
 
-**Recomendación: (a), pero no antes de la medición de abajo.** Cambiar el default a ciegas sobre un
-corpus de 1115 sellados es exactamente el reflejo que este repo evita.
+**Recomendación: (a), y ya no a ciegas.** El experimento de abajo pone el coste en ~8% de las
+expuestas — unas 8 recetas a arreglar sobre 96. Eso es un radio acotado y pagable, no el salto al
+vacío que se temía. Lo que sí queda es hacer la lista COMPLETA antes de tocar `sandbox.rs`.
 
-## Precondiciones — el experimento que falta
+## El experimento, CORRIDO (2026-09-18)
 
-Lo que hay hoy es un **cribado sobre logs**, no una medición del radio de explosión. El número que
-falta es: *¿cuántas de las 142 recetas expuestas dejan de sellar bajo `-ec`?* Y no se puede sacar
-leyendo: hay que reconstruirlas.
+**Diseño: no sella nada, a propósito.** Construir una receta modificada le cambia el `ArtifactHash`,
+así que medirlo a lo bruto habría dejado una docena de duplicados basura en un store que se respalda.
+En vez de eso, a cada receta de la muestra se le inyecta `set -e` al principio de cada fase **y un
+`exit 77` al final de la última**. El veredicto se lee en el código que reporta takana:
 
-    # sobre una muestra de las 89, con el lock tomado una sola vez (CLAUDE.md regla 1)
-    flock -o work/.farm-build.lock bash -c 'for r in <muestra>; do takana --store ./store build "$r"; done'
+- **falló con 77** ⇒ todas las fases corrieron enteras bajo `set -e` ⇒ **sobrevive** a `-ec`
+- **falló con ≠77** ⇒ una orden intermedia falla y hoy se está tragando ⇒ **se rompe** con `-ec`
 
-Con `-ec` puesto y contando los que caen. La muestra sale de las **89 sin `set -e` propio**, no de las
-142: las otras 53 ya corren así y no aportan información. Una muestra de 20 alcanza para decidir; el
-corpus entero es la validación. **Hasta que ese número exista, este ADR no se adopta.**
+Nada llega al store. Las copias viven en `recipes/exp-ec-*.toml` y se barren al terminar.
+
+**Muestra:** 12 de las expuestas sin `set -e` propio, excluyendo los pesos pesados (`clang18`,
+kernels, `mesa`, navegadores) porque no caben en el presupuesto de tiempo. **Sesgo declarado:** la
+muestra es de recetas ligeras.
+
+| resultado | n |
+|---|---|
+| **sobrevive** sin tocar nada | **10** |
+| **se rompe** | **1** (`e2fsprogs`) |
+| inconcluso por tiempo (>7 min) | 1 (`cargo-edit`) |
+
+⚠ El recuento de expuestas sube de 89 a **96**: el 89 salía de restar 142−53 dando por hecho que las
+53 con `set -e` estaban todas dentro de las 142, y no lo estaban. Contadas una por una son 96.
+
+### El único que se rompe, mirado de cerca
+
+`e2fsprogs` muere en su `./configure`, que aborta con `configure: error: external uuid library not
+found` **pese a que la receta pasa `--disable-libuuid`**. Que hoy eso se traga está comprobado: la
+misma receta sin `set -e` y con `exit 77` **llega al 77**, o sea que la fase sigue entera después del
+configure fallido, y el `make` de después es el que decide el estado de salida.
+
+**Y el artefacto que sella está BIEN.** 149 ficheros, 21 M, con `e2fsck`, `mke2fs`, los cuatro
+`fsck.ext*`, los `mkfs.ext*` y `badblocks`. O sea que acá `-ec` **no atrapa un bug: rompe una receta
+que funciona.** Arreglarla es trabajo de receta —entender por qué configure se queja de algo que
+está desactivado— y no un fallo que estuviera escondido.
+
+Ése es justamente el coste que no se podía adivinar leyendo: **1 de 12, ~8%**. Sobre las 96 expuestas
+son **unas 8 recetas** que hay que tocar ANTES de cambiar el default. Es un número chico y acotado,
+que es lo que hacía falta para decidir.
+
+## Lo que queda por medir
+
+El experimento de arriba cubre la muestra ligera. Falta la cola pesada, que se excluyó por tiempo:
+`clang18`, los kernels, `mesa`, los navegadores. Son pocas recetas pero son las más largas, y no hay
+razón para esperar que se comporten distinto — sólo tardan más en decirlo. Se corren igual:
+
+    # reutilizar scripts/farm/exp-ec.sh (el mismo diseño: set -e + exit 77, no sella)
+    # con el lock por receta, NO por tanda: una tanda pesada retendría el lock horas y pararía la cosecha
+
+La validación completa es pasar las 96. Con ~8% de rotura esperada eso son unas 8 recetas a arreglar,
+y conviene tener la lista ENTERA antes de tocar `sandbox.rs`: cambiar el default y arreglar las
+recetas a medida que saltan deja el corpus a medio construir mientras tanto.
 
 ## Consecuencias que se caen solas
 
diff --git a/scripts/farm/exp-ec.sh b/scripts/farm/exp-ec.sh
new file mode 100755
index 00000000..af40c210
--- /dev/null
+++ b/scripts/farm/exp-ec.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+# exp-ec.sh — el experimento del ADR 0020: ¿cuántas recetas dejan de construir si las fases
+# corriesen con `sh -ec` en vez de `sh -c`?
+#
+# ── POR QUÉ NO SELLA NADA, Y POR QUÉ ESO IMPORTA ───────────────────────────────────────────────
+# Modificar una receta le cambia el `ArtifactHash`, así que medir esto a lo bruto dejaría un
+# duplicado basura en el store POR CADA receta que pase — en un store que se respalda. En vez de eso
+# se inyecta `set -e` al principio de cada fase Y un `exit 77` al final de la última, de modo que el
+# build SIEMPRE falla y nunca sella. El veredicto se lee en el código que reporta takana:
+#
+#   · falló con 77   ⇒ las fases corrieron ENTERAS bajo set -e   ⇒ SOBREVIVE a `-ec`
+#   · falló con ≠77  ⇒ una orden intermedia falla y hoy se traga ⇒ SE ROMPE con `-ec`
+#
+# Las copias se escriben en `recipes/exp-ec-*.toml` —tienen que vivir ahí porque takana resuelve las
+# deps RELATIVAS al directorio de la receta— y se barren al terminar.
+#
+# ⚠ El lock se toma POR RECETA, no por tanda. La regla 1 de CLAUDE.md dice lo contrario para las
+# tandas normales, y con razón; acá es al revés a propósito: una tanda de éstas puede durar horas y
+# retener `work/.farm-build.lock` todo ese tiempo pararía la cosecha del latido.
+#
+# Uso:  MUESTRA=/ruta/lista.txt scripts/farm/exp-ec.sh     (una receta por línea, p.ej. recipes/gzip.toml)
+cd "$(dirname "$0")/../.."
+TMP=${TMPDIR:-/tmp}/exp-ec
+REC=$(cd "$(dirname "$0")/../.." && pwd)/recipes
+mkdir -p "$TMP"; OUT="$TMP/resultados.tsv"; : > "$OUT"
+
+while read -r r; do
+  [ -f "$r" ] || continue
+  n=$(basename "$r" .toml)
+  awk '
+    /^(compile|install|configure|check|prepare) = .{3}$/ { print; print "set -e"; inb=1; next }
+    { print }
+  ' "$r" > "$REC/exp-ec-$n.toml"
+  # `exit 77` como última línea de la última fase del fichero
+  last=$(grep -n "^'''$" "$REC/exp-ec-$n.toml" | tail -1 | cut -d: -f1)
+  [ -n "$last" ] && sed -i "${last}i exit 77" "$REC/exp-ec-$n.toml"
+
+  log="$TMP/$n.log"
+  timeout 420 flock -o /work/.farm-build.lock takana build "$REC/exp-ec-$n.toml" > "$log" 2>&1
+  rc=$?
+  if grep -q "exit 77" "$log"; then v="SOBREVIVE"
+  elif [ $rc -eq 124 ]; then v="TIMEOUT"
+  elif grep -qE "build phase falló \(exit [0-9]+\)" "$log"; then v="SE-ROMPE"
+  else v="OTRO"; fi
+  det=$(grep -oE "build phase falló \(exit [0-9]+\): .*" "$log" | head -1 | cut -c1-90)
+  printf '%s\t%s\t%s\n' "$n" "$v" "$det" >> "$OUT"
+  echo "[$v] $n"
+done < ${MUESTRA:?hace falta MUESTRA=<fichero con una receta por línea>}
+
+echo "════ RESUMEN ════"
+for v in SOBREVIVE SE-ROMPE TIMEOUT OTRO; do echo "$v: $(grep -c "	$v	" "$OUT")"; done
+rm -f "$REC"/exp-ec-*.toml
+echo "copias de experimento barridas de recipes/: $(ls "$REC"/exp-ec-*.toml 2>/dev/null | wc -l) quedan"
diff --git a/scripts/servidor/jaula-preparar.sh b/scripts/servidor/jaula-preparar.sh
index 011b0c15..822f7901 100755
--- a/scripts/servidor/jaula-preparar.sh
+++ b/scripts/servidor/jaula-preparar.sh
@@ -81,11 +81,29 @@ echo "  ✓ known_hosts con $(grep -c . "$K" 2>/dev/null || echo 0) entradas"
 BIN=/work/$U/bin
 if [ -d "$BIN" ]; then
   install -d -o "$U" -g "$U" "$UP/usr/bin"
-  for t in takana patch gh hcloud; do
+  for t in takana patch gh hcloud rsync python3 jq cargo; do
     [ -x "$BIN/$t" ] || continue
     ln -sfn "$BIN/$t" "$UP/usr/bin/$t" 2>/dev/null || true
   done
-  echo "  ✓ takana/patch/gh/hcloud enlazados al PATH desde $BIN"
+  ln -sfn "$BIN/python3" "$UP/usr/bin/python" 2>/dev/null || true
+  echo "  ✓ herramientas del store enlazadas al PATH desde $BIN"
+fi
+
+# ── 5. el cargador de musl, para que los artefactos DINÁMICOS arranquen ─────────────────────────
+# No todo lo del store es estático: `python3` y `cargo` son musl DINÁMICOS y piden
+# `/lib/ld-musl-x86_64.so.1`, que una imagen glibc no tiene. Sin esto fallan con un `No such file or
+# directory` que señala al binario y no al cargador, que es lo que despista.
+#
+# ⚠ Y no alcanza con el cargador: `cargo` pide además `libz.so.1` y `libgcc_s.so.1` POR NOMBRE, y en
+# una imagen glibc esos nombres existen — con otro contenido. Coge las de glibc y muere en
+# `Error relocating /lib/libz.so.1: __snprintf_chk: symbol not found` (símbolos de fortify que musl
+# no tiene). Por eso su wrapper exporta LD_LIBRARY_PATH al lab; acá sólo va el cargador.
+LAB=${LAB:-/work/$U/dev-fs/alpine}
+if [ -f "$LAB/lib/ld-musl-x86_64.so.1" ]; then
+  install -D -o "$U" -g "$U" -m 755 "$LAB/lib/ld-musl-x86_64.so.1" "$UP/usr/lib/ld-musl-x86_64.so.1"
+  echo "  ✓ cargador de musl en /usr/lib (lo alcanza /lib, que es symlink a usr/lib)"
+else
+  echo "  · sin lab en $LAB — el cargador de musl no se pone (python3/cargo del store no arrancarán)"
 fi
 
 echo "jaula $INST preparada — lo del manifiesto (paquetes, concesiones) lo pone `takana qorpa provision`"