diff --git a/docs/state/sonames.txt b/docs/state/sonames.txt new file mode 100644 index 00000000..e864ee9e --- /dev/null +++ b/docs/state/sonames.txt @@ -0,0 +1,11 @@ +== escritorio-mirada 41 nodos · 136 sonames provistos · 1 sin proveedor + FALTA libperl.so ← lo piden: perl +== escritorio-kde 306 nodos · 1387 sonames provistos · 1 sin proveedor + FALTA libperl.so ← lo piden: perl +== escritorio-gnome 190 nodos · 517 sonames provistos · 1 sin proveedor + FALTA libperl.so ← lo piden: perl +== escritorio-cosmic 161 nodos · 390 sonames provistos · 1 sin proveedor + FALTA libperl.so ← lo piden: perl +== escritorio-sway 201 nodos · 402 sonames provistos · 2 sin proveedor + FALTA libc.so.6 ← lo piden: go + FALTA libperl.so ← lo piden: perl diff --git a/recipes/incoming-gnome/sqlite-shared.toml b/recipes/incoming-gnome/sqlite-shared.toml index d5229d51..40c3e6a3 100644 --- a/recipes/incoming-gnome/sqlite-shared.toml +++ b/recipes/incoming-gnome/sqlite-shared.toml @@ -44,4 +44,12 @@ compile = 'make -j"$(nproc)"' install = "make install DESTDIR=/out" [deps] +# ✅ DECIDIDA 2026-09-06, y la decisión cambió porque cambió el cálculo. La verruga de arriba dejaba +# dos salidas sin elegir, y el argumento contra la primera era «la imagen carga una librería más sólo +# para que ande un comando». Ya no es cierto: `python3` sale con el MISMO `NEEDED libreadline.so.8` +# y es raíz de los cuatro perfiles, así que `readline-shared` entra en la clausura igual. El coste +# marginal de arreglar además el `sqlite3` de la CLI es CERO. +# `runtime` no entra en el ArtifactHash ⇒ no re-hashea, y los 6 dependientes que menciona la nota de +# arriba no se tocan. Esa era la otra mitad del precio y tampoco se paga. +runtime = ["readline-shared"] build = ["make", "pkgconf"] diff --git a/recipes/python3.toml b/recipes/python3.toml index a90cd041..1aff5d6c 100644 --- a/recipes/python3.toml +++ b/recipes/python3.toml @@ -37,6 +37,16 @@ link = "dynamic" zig_version = "0.13.0" [deps] +# ⚠ DEP DE EJECUCIÓN (2026-09-06). Este artefacto sale con `NEEDED libreadline.so.8` y hasta hoy +# ningún artefacto del cierre publicaba ese soname: la `readline` del catálogo es `--disable-shared`. +# En el rootfs hidratado se resolvía contra el sysroot Alpine DEL LAB — que en la máquina de +# desarrollo está y en la distro no. Lo venía reportando `scripts/vigia-sonames.py` en los cuatro +# perfiles. `runtime` NO entra en el ArtifactHash, así que esto NO re-hashea: cambia la CLAUSURA. +# Las CUATRO que `python3` pide por soname y que el cierre no publicaba (lo reporta +# `scripts/vigia-sonames.py`): readline para el REPL, y sqlite/lzma/bz2 para los módulos `sqlite3`, +# `lzma` y `bz2` de la stdlib, que se cargan por `dlopen` y por eso no rompen el arranque sino un +# `import` — un fallo que aparece lejos y no menciona a python. +runtime = ["readline-shared", "sqlite-shared", "xz-shared", "bzip2-shared"] # `libffi` NO es opcional aunque el build no falle sin él: sin libffi el `configure` de CPython # construye un intérprete SIN el módulo `_ctypes`, y lo hace en SILENCIO — sella un python3 que # arranca perfectamente y revienta más tarde, en otra receta, con diff --git a/scripts/audit-needed.sh b/scripts/audit-needed.sh deleted file mode 100755 index 0cbb5193..00000000 --- a/scripts/audit-needed.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# audit-needed.sh — ¿hay artefactos que piden una librería que NADIE del corpus provee? -# -# ── POR QUÉ EXISTE ───────────────────────────────────────────────────────────────────────────── -# Encontrado el 2026-09-06 corriendo el navegador, no leyendo: `atuq` moría con decenas de -# «Error relocating: _ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found». El binario sellado de -# firefox declara -# -# NEEDED libstdc++.so.6 NEEDED libgcc_s.so.1 NEEDED libc.so -# -# y las dos primeras NO EXISTEN en el store ni en ningún cierre: sólo en `.dev-fs/alpine`, o sea en -# el LAB. Es exactamente la lección de `quitar-dep-no-apaga-funcion`: el proyecto la encuentra en el -# sysroot del lab y el artefacto sella con un NEEDED que nadie provee. -# -# LO QUE LO HACE CARO ES QUE NINGUNA MÉTRICA LO VE. `build-state.json` dice `sealed`, -# `verificar-repro.sh` dice que REPRODUCE bit a bit, los guardianes de la receta pasan — y el -# binario no arranca en la distro. El verde responde «¿construye?», nunca «¿corre?». -# -# ── QUÉ MIDE, EXACTAMENTE ────────────────────────────────────────────────────────────────────── -# Índice de sonames PROVISTOS = todo `*.so*` que haya en el store. Para cada ELF ejecutable del -# store, sus `NEEDED` menos lo provisto. Lo que sobra es una dep de ejecución colgante. -# -# NO mira el lab a propósito: si mirara `.dev-fs/alpine` diría que todo está bien, que es justo la -# ilusión que causó el problema. El lab es donde se CONSTRUYE, no donde se corre. -# -# Uso: scripts/audit-needed.sh [--store DIR] (sale 1 si hay colgantes) -set -uo pipefail -STORE="${STORE:-./store}" -[ "${1:-}" = "--store" ] && STORE="$2" -[ -d "$STORE" ] || { echo "no existe $STORE" >&2; exit 2; } - -tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT - -# 1. Qué provee el corpus. Sólo el basename: es lo que el loader busca. -find "$STORE" -maxdepth 5 -name '*.so' -o -maxdepth 5 -name '*.so.*' 2>/dev/null \ - | sed 's|.*/||' | sort -u > "$tmp/provisto" - -echo "== sonames provistos por el store: $(wc -l < "$tmp/provisto")" - -# 2. Qué pide cada artefacto. -# -# ⚠ DOS CORRECCIONES SOBRE LA PRIMERA VERSIÓN, las dos por sub-reportar (2026-09-06): -# · Miraba sólo ejecutables y `head -12` por artefacto. El `NEEDED` que faltaba de verdad estaba -# en una LIBRERÍA (`libmozsandbox.so` pedía `libnspr4.so`), así que el audit daba «6 recetas» -# cuando el artefacto de firefox solo declara 30 sonames. Ahora mira TODOS los ELF. -# · No contaba lo que el propio artefacto TRAE. Firefox bundlea su nspr/nss y las encuentra por el -# `LD_LIBRARY_PATH` que fija su lanzador; contarlas como colgantes sería un falso positivo. -colgantes=0 -for d in "$STORE"/*/; do - d=${d%/}; nombre=$(basename "$d"); receta=${nombre#*-} - # lo que este artefacto se provee a sí mismo - find "$d" \( -name '*.so' -o -name '*.so.*' \) 2>/dev/null | sed 's|.*/||' | sort -u > "$tmp/propio" - falta="" - while IFS= read -r f; do - n=$(readelf -d "$f" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p') - for so in $n; do - grep -qxF "$so" "$tmp/provisto" && continue - grep -qxF "$so" "$tmp/propio" && continue - case "$so" in libc.so|ld-musl*) continue ;; esac # la libc la pone el sistema base - case " $falta " in *" $so "*) ;; *) falta="$falta $so" ;; esac - done - done < <(find "$d" -type f \( -name '*.so' -o -name '*.so.*' -o -perm -u+x \) 2>/dev/null) - [ -n "$falta" ] && { printf ' ✗ %-24s pide y nadie provee:%s\n' "$receta" "$falta"; colgantes=$((colgantes+1)); } -done - -echo "════════════════════════════════════════════════════════════" -if [ "$colgantes" -eq 0 ]; then - echo " ✅ sin deps de ejecución colgantes" -else - echo " ⚠ $colgantes artefacto(s) con deps de ejecución COLGANTES" - echo " no arrancan fuera del lab, por más que sellen y reproduzcan" -fi -exit $([ "$colgantes" -eq 0 ] && echo 0 || echo 1) diff --git a/scripts/build-state.py b/scripts/build-state.py index af33caf8..63bf7601 100755 --- a/scripts/build-state.py +++ b/scripts/build-state.py @@ -124,7 +124,15 @@ def load_recipes(): version=d.get("version"), link=b.get("link"), compiler=b.get("compiler"), - deps=d.get("deps", {}).get("build", []), + # ⚠ BUILD **+ RUNTIME** (2026-09-06). Antes sólo `build`, y por eso una dep de + # EJECUCIÓN declarada en la receta no entraba en la clausura ni en el rootfs + # hidratado: `firefox` declaraba `runtime = ["gcc-libs"]` y el grafo seguía sin + # conocerla, así que la imagen se armaba sin `libstdc++.so.6` y el navegador no + # arrancaba. `deps.runtime` existe en el esquema de hammer desde siempre; lo que + # faltaba era que ESTE contador lo mirara. Un campo que nadie lee es un campo que + # miente. La unión es la definición de clausura: para CORRER hacen falta las dos. + deps=sorted(set(d.get("deps", {}).get("build", [])) + | set(d.get("deps", {}).get("runtime", []))), cls=classify(n, d), queue=queue, # `foreign = true`: la receta SELLA bytes ajenos ya compilados, no construye desde diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index 81790e3c..97c98247 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -277,6 +277,20 @@ if [ -x "$HAMMER" ]; then # la granja entera. `nice -n 19` porque la prioridad la tiene construir, no auditar. AUDIT_SELLO="work/.static-audit.sello" if [ ! -f "$AUDIT_SELLO" ] || [ -n "$(find "$AUDIT_SELLO" -mtime +0 2>/dev/null)" ]; then + # ── VIGÍA DE SONAMES (2026-09-06) ──────────────────────────────────────────────────────────── + # `vigia-sonames.py` existía desde antes y contesta la pregunta que el grafo NO contesta: no + # «¿está sellado?» sino «¿arranca?». Encuentra los `NEEDED` que ningún artefacto del cierre + # publica y que el rootfs hidratado resolvía contra el sysroot del LAB. + # + # NADIE LO CORRÍA. Por eso `libstdc++.so.6` —que rompía el navegador en los CUATRO perfiles— + # estuvo en su salida sin que nadie lo leyera, y se redescubrió el 2026-09-06 arrancando `atuq` + # a mano. Un vigía que hay que acordarse de invocar no se distingue de no tenerlo: es el mismo + # argumento que `scripts/test-atuq-politica.py` hace sobre los guardianes. + # Deja fichero de estado para que la salida quede en el repo y se vea si empeora entre cosechas. + python3 scripts/vigia-sonames.py > docs/state/sonames.txt 2>&1 \ + && echo " sonames.txt ✓ $(grep -c 'FALTA' docs/state/sonames.txt) hueco(s) sin proveedor" \ + || echo " ⚠ vigia-sonames.py falló" + echo " static-audit: puerta diaria abierta, barriendo el store (~8 min, nice 19)…" nice -n 19 scripts/static-audit.sh > work/.static-audit.salida 2>&1 audit_rc=$? @@ -308,7 +322,7 @@ fi # 4. Commitear SÓLO el estado firme (el avance que el humano sigue). Recetas nuevas que YO autoré se # commitean aparte, a mano, para no meter autoría a medias en un commit de cron. if [ "${NO_COMMIT:-}" != "1" ]; then - git add docs/state/build-state.json docs/state/build-state-kde.json docs/state/build-state-gnome.json docs/state/build-state-cosmic.json docs/state/build-state-wlr.json docs/state/drenaje.json docs/state/fuentes-vigia.json docs/state/kernel-contract.txt docs/state/static-audit.txt 2>/dev/null || true + git add docs/state/sonames.txt docs/state/build-state.json docs/state/build-state-kde.json docs/state/build-state-gnome.json docs/state/build-state-cosmic.json docs/state/build-state-wlr.json docs/state/drenaje.json docs/state/fuentes-vigia.json docs/state/kernel-contract.txt docs/state/static-audit.txt 2>/dev/null || true if ! git diff --cached --quiet 2>/dev/null; then git commit -q -m "estado: cosecha granja $(ts) — avance del árbol KDE" 2>/dev/null \ && { git push -q origin main 2>/dev/null && echo "==> estado commiteado+pusheado" \ diff --git a/scripts/yupana.py b/scripts/yupana.py index 0e957079..ed0eedeb 100755 --- a/scripts/yupana.py +++ b/scripts/yupana.py @@ -90,7 +90,16 @@ def cargar_colas(): def _deps(f): try: - return tomllib.load(open(f, "rb")).get("deps", {}).get("build", []) or [] + d = tomllib.load(open(f, "rb")).get("deps", {}) + # ⚠ BUILD **+ RUNTIME** (2026-09-06). Leía sólo `build`, y por eso una dep de EJECUCIÓN no + # existía para NINGUNA herramienta que use yupana —el vigía de sonames, la membresía de + # perfiles, el grafo de estado—. Medido: `firefox` declaraba `runtime = ["gcc-libs"]` y el + # cierre de las cuatro imágenes seguía sin `libstdc++.so.6`, así que el navegador no + # arrancaba y el vigía lo seguía reportando como hueco después de haberlo arreglado. + # `deps.runtime` está en el esquema de hammer desde siempre; lo que faltaba era que ALGUIEN + # lo leyera. Un campo que nadie lee es un campo que miente. + # La unión es la definición de cierre: para CORRER hacen falta las dos. + return sorted(set(d.get("build", []) or []) | set(d.get("runtime", []) or [])) except Exception: return []