diff --git a/recipes/bzip2.toml b/recipes/bzip2.toml index c2189da2..af6b5467 100644 --- a/recipes/bzip2.toml +++ b/recipes/bzip2.toml @@ -28,4 +28,26 @@ build = ["binutils", "make"] # jq/parted/shadow/procps-ng, que sí linkean con libtool); bzip2 usa Makefile crudo, así que el # flag llega tal cual al compilador → "error: Unknown Clang option: '-all-static'". compile = 'make CC="$CC" LDFLAGS="-static -no-pie"' -install = 'make install PREFIX=/out/usr CC="$CC"' +# ⚠ **LOS CUATRO SYMLINKS SALEN ROTOS SI NO SE REHACEN, Y EL ARTEFACTO NO LO DICE.** El `install` +# del Makefile de bzip2 los crea así: +# +# cd $(PREFIX)/bin; ln -s -f $(PREFIX)/bin/bzdiff bzcmp +# +# — con `$(PREFIX)` DENTRO del destino. Como bzip2 **no soporta `DESTDIR`** (por eso acá va +# `PREFIX=/out/usr`, el mismo truco que valkey), el destino que queda horneado es +# `/out/usr/bin/bzdiff`: la ruta del SANDBOX. Al hidratar, `/usr/bin/bzcmp` apunta a un `/out` que no +# existe en el sistema real ⇒ `bzcmp`, `bzegrep`, `bzfgrep` y `bzless` no funcionan. +# +# Nada lo delata: el artefacto tiene contenido, `bzip2` y `bzgrep` corren, el audit de enlace +# estático pasa y la receta REPRODUCE. Se ve buscando en el store symlinks que no resuelven dentro de +# su propio artefacto (2026-09-12; los únicos dos casos del corpus eran éste y uno legítimo de KDE, +# que apunta a otro artefacto del MISMO perfil). +# +# Se rehacen RELATIVOS, que es lo correcto para un artefacto direccionado por hash: un enlace +# relativo sigue valiendo esté el árbol montado donde esté. +install = ''' +make install PREFIX=/out/usr CC="$CC" +for par in bzcmp:bzdiff bzegrep:bzgrep bzfgrep:bzgrep bzless:bzmore; do + ln -sf "${par#*:}" "/out/usr/bin/${par%%:*}" +done +''' diff --git a/scripts/hydrate-profile.py b/scripts/hydrate-profile.py index cf2710ba..8f081373 100755 --- a/scripts/hydrate-profile.py +++ b/scripts/hydrate-profile.py @@ -82,17 +82,84 @@ def auditar_raices(store): arreglarlo: los únicos que quedan son `perl` (deuda medida y decidida, ver `recipes/perl.toml`) y los tres rootfs/seed, que ponen ahí lo suyo por contrato.""" import re as _re - filas = [] - for d in sorted(glob.glob(os.path.join(store, "*"))): - m = _re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d)) - if not m or not os.path.isdir(d): + import json as _json + + # ⚠ **SÓLO EL ARTEFACTO VIGENTE DE CADA RECETA.** El store guarda TODOS los sellados históricos + # (bzip2 tenía dos al arreglarlo), así que auditarlos todos acusa a recetas YA SANAS por un + # sellado viejo — el mismo sobre-reporte que `static-audit.sh` tuvo que quitarse de encima + # («auditábamos el más reciente por mtime»). Un guardián que grita sobre lo ya arreglado se + # empieza a ignorar. + # El hash vigente no se recalcula con `takana hash` receta a receta (1150 procesos): sale de + # los grafos de estado, que es donde el repo YA lo tiene escrito, y cubre a la vez el corpus y + # las cinco colas. + vigentes = set() + for g in glob.glob(str(ROOT / "docs/state/build-state*.json")): + try: + for nodo in _json.load(open(g)).get("nodes", {}).values(): + h = nodo.get("hash") + if h: + vigentes.add(h[3:] if h.startswith("b3:") else h) + except (OSError, ValueError): continue + + filas = [] + mirados = 0 + for d in sorted(glob.glob(os.path.join(store, "*"))): + m = _re.match(r"^([0-9a-f]{64})-(.+)$", os.path.basename(d)) + if not m or not os.path.isdir(d) or (vigentes and m.group(1) not in vigentes): + continue + mirados += 1 try: extra = sorted(e for e in os.listdir(d) if e not in FHS_RAIZ) except OSError: continue if extra: - filas.append((len(extra), m.group(1), extra)) + filas.append((len(extra), m.group(2), extra)) + # ── SEGUNDA PREGUNTA, LA MISMA FAMILIA: symlinks que apuntan a una raíz que NO ES EL FHS ───── + # Un `install` se puede equivocar de destino de dos maneras. Una deja ficheros donde no van (lo + # de arriba). La otra deja **enlaces** con la ruta del SANDBOX horneada dentro: + # + # usr/bin/bzcmp -> /out/usr/bin/bzdiff + # + # El Makefile de bzip2 crea sus symlinks con `ln -s $(PREFIX)/bin/bzdiff`, y como bzip2 no + # soporta `DESTDIR` la receta pasa `PREFIX=/out/usr` ⇒ el `/out` queda DENTRO del destino. Al + # hidratar, esos cuatro enlaces apuntan a un directorio que en el sistema real no existe. + # + # ⚠ Nada lo delata: el artefacto tiene contenido, los binarios de verdad corren, el audit de + # enlace estático pasa y la receta REPRODUCE. Cinco verdes sobre cuatro comandos rotos. + # + # **La regla es independiente del perfil, y por eso se puede aplicar al store entero**: un enlace + # a otro ARTEFACTO es normal y correcto (`kinfocenter -> /usr/bin/systemsettings`: los dos están + # en `escritorio-kde` y en el rootfs fundido resuelve). Lo que NUNCA puede estar bien es un + # destino absoluto cuya PRIMERA componente no sea del FHS — `/out`, `/src`, `/tmp`: ésas son + # rutas del lab, no del sistema que se instala. + malos_enlaces = [] + for d in sorted(glob.glob(os.path.join(store, "*"))): + m = _re.match(r"^([0-9a-f]{64})-(.+)$", os.path.basename(d)) + if not m or not os.path.isdir(d) or (vigentes and m.group(1) not in vigentes): + continue + for root, dirs, files in os.walk(d): + for e in files + dirs: + p_ = os.path.join(root, e) + if not os.path.islink(p_): + continue + t = os.readlink(p_) + if not t.startswith("/"): + continue # relativo: siempre correcto en un CAS + raiz = t.lstrip("/").split("/", 1)[0] + if raiz not in FHS_RAIZ: + malos_enlaces.append((m.group(2), os.path.relpath(p_, d), t)) + + # ⚠ **UN BARRIDO QUE NO MIRÓ NADA TIENE QUE DECIRLO.** Filtrar por hash vigente significa que un + # `--store` que no case con los grafos (otra máquina, otro checkout, un store de prueba) deja + # CERO artefactos auditados — y sin esta guarda eso se imprimiría como «✓ 0 ofensores», que es + # una respuesta falsa con forma de respuesta. Es la regla de siempre: un ausente falla ruidoso. + if vigentes and mirados == 0: + print("!! 0 artefactos auditados: ninguno de los %d hashes vigentes está en `%s`.\n" + " ¿`--store` apunta a otro sitio, o los grafos están sin regenerar?" + % (len(vigentes), store)) + return 2 + print("== raíces sucias en el STORE ENTERO ==") nuevos = [] for n, nombre, extra in sorted(filas, reverse=True): @@ -107,6 +174,15 @@ def auditar_raices(store): continue print(" ✗ %-22s %4d (%s)" % (nombre, n, muestra)) nuevos.append(nombre) + print("\n== symlinks con una raíz que NO es del FHS (ruta del lab horneada) ==") + if not malos_enlaces: + print(" ✓ ninguno") + else: + for nombre, origen, destino in malos_enlaces: + print(" ✗ %-18s %s -> %s" % (nombre, origen, destino)) + if nombre not in nuevos: + nuevos.append(nombre) + if not nuevos: print("\n ✓ 0 ofensores NUEVOS sobre %d artefactos." % len(filas) if filas else "\n ✓ ningún artefacto pone nada fuera del FHS en su raíz")