From beb1c2a597cfd1ab149e0eaf1d64c7f64b1e1844 Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 11 Sep 2026 22:53:29 +0000 Subject: [PATCH] =?UTF-8?q?auditar-raices:=20el=20guardi=C3=A1n=20de=20la?= =?UTF-8?q?=20ra=C3=ADz=20sucia=20s=C3=B3lo=20ve=C3=ADa=20los=20perfiles?= =?UTF-8?q?=20=E2=80=94=20y=20hay=20649=20recetas=20fuera?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hydrate-profile.py` ya tenía el bloque «quién ensucia la RAÍZ del rootfs», y es bueno: mira por ARTEFACTO y no sobre el árbol fundido, porque en el fundido el nombre del culpable ya se perdió. Pero sólo corre al hidratar un perfil ⇒ **sólo ve lo que alguna imagen declara**. Hoy hay **649 recetas que no alcanza ninguna imagen**, y una fase `install` que se equivoca de destino en una de ésas es invisible hasta el día que alguien la declare. Lo destapó `qdrant`: selló con **69 M** de cabeceras de protobuf bajo `/src`, y no está en ningún perfil ⇒ ningún guardián lo habría visto. Lo encontré mirando el árbol del artefacto a mano antes de promoverlo, que es justo lo que no se puede dejar a que alguien se acuerde. `--auditar-raices` hace la misma pregunta sobre el STORE ENTERO sin hidratar nada, reutilizando el mismo `FHS_RAIZ` (una sola definición de «qué puede ir en la raíz», no dos que se desincronizan). Y dos tablas, porque un barrido que canta tres cosas de las cuales dos son correctas se deja de leer: · `RAIZ_POR_CONTRATO` — `seed-zig` (el toolchain ES el artefacto) y los rootfs (`store`/`ente` son suyos por diseño). Se imprimen como ⊘ con el motivo y no cuentan. · `RAIZ_DEUDA_DECIDIDA` — `perl` y sus 945 páginas nroff en `/`. Es suciedad REAL pero su arreglo está decidido EN CONTRA por ahora (una línea, 305 rebuilds, va con el próximo bump). Se imprime como contexto y **no hace fallar**: si fallara siempre, un ofensor NUEVO se perdería entre el ruido del viejo — que es exactamente cómo se muere un guardián. Probado con los dos controles, no sólo con el que da verde: sobre un store de juguete con una suciedad inventada sale **1** y la nombra; quitándola sale **0**. Sobre el store real: 0 ofensores nuevos sobre 3 artefactos conocidos. --- scripts/hydrate-profile.py | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/scripts/hydrate-profile.py b/scripts/hydrate-profile.py index a49d0c98..cf2710ba 100755 --- a/scripts/hydrate-profile.py +++ b/scripts/hydrate-profile.py @@ -47,10 +47,83 @@ def opcion(nombre, defecto=None): return defecto +# Lo que SÍ puede aparecer en la raíz de ciertos artefactos, con el motivo al lado. Sin esta tabla +# el barrido canta tres cosas cada vez y dos son correctas ⇒ se deja de leer, que es como se muere un +# guardián. La clave es el nombre de receta y el valor `(motivo, patrón de lo permitido)`. +RAIZ_POR_CONTRATO = { + "seed-zig": ("el toolchain zig ES el artefacto: su directorio versionado va en la raíz", + r"^zig-"), + "product-rootfs": ("es un ROOTFS, no un paquete: `store` y `ente` son suyos por diseño", + r"^(store|ente)$"), + "stage1-rootfs": ("es un ROOTFS, no un paquete: `ente` es suyo por diseño", + r"^(store|ente)$"), +} + +# Suciedad REAL, medida, y cuyo arreglo está DECIDIDO en contra por ahora. Se imprime como contexto y +# NO hace fallar: si fallara siempre, un ofensor NUEVO se perdería entre el ruido de uno viejo. +RAIZ_DEUDA_DECIDIDA = { + "perl": "945 páginas nroff en `/` porque su Configure eligió man1dir=' '. El arreglo es UNA " + "línea y cuesta 305 rebuilds ⇒ va con el próximo bump. Ver recipes/perl.toml.", +} + + +def auditar_raices(store): + """`--auditar-raices`: la misma pregunta que el bloque «quién ensucia la RAÍZ», pero sobre el + STORE ENTERO en vez de sobre el cierre de un perfil, y sin hidratar nada. + + ⚠ **POR QUÉ HACÍA FALTA**: la comprobación de abajo sólo ve los artefactos que un perfil + proyecta, y hoy hay **649 recetas que no alcanza ninguna imagen** (catálogo, no distro). Una fase + `install` que se equivoca de destino en una de ésas es invisible hasta el día que alguien la + declare en un perfil — y entonces ensucia la raíz del rootfs sin que se sepa quién fue. + + Lo destapó `qdrant` (2026-09-11): selló con **69 M** de cabeceras de protobuf bajo `/src`, porque + el sandbox exporta `DESTDIR=/out` GLOBAL y el `make install` ANIDADO de `protobuf-src` lo hereda. + No está en ningún perfil ⇒ ningún guardián lo habría visto. Barrido el store entero tras + 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): + continue + 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)) + print("== raíces sucias en el STORE ENTERO ==") + nuevos = [] + for n, nombre, extra in sorted(filas, reverse=True): + muestra = ", ".join(extra[:3]) + (", …" if n > 3 else "") + contrato = RAIZ_POR_CONTRATO.get(nombre) + if contrato and all(_re.match(contrato[1], e) for e in extra): + print(" ⊘ %-22s %4d (%s) — %s" % (nombre, n, muestra, contrato[0])) + continue + if nombre in RAIZ_DEUDA_DECIDIDA: + print(" • %-22s %4d (%s)" % (nombre, n, muestra)) + print(" DEUDA DECIDIDA: %s" % RAIZ_DEUDA_DECIDIDA[nombre]) + continue + print(" ✗ %-22s %4d (%s)" % (nombre, n, muestra)) + 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") + return 0 + print("\n ✗ %d ofensor(es) NUEVO(s): %s\n" + " Un `install` que se equivoca de destino se lee, en el rootfs fundido, como si fuera\n" + " parte del sistema." % (len(nuevos), ", ".join(nuevos))) + return 1 + + def main(): libres = [a for i, a in enumerate(sys.argv[1:], 1) if not a.startswith("--") and not (sys.argv[i - 1].startswith("--") and sys.argv[i - 1] in FLAGS_CON_VALOR)] + if "--auditar-raices" in sys.argv: + sys.exit(auditar_raices(opcion("--store", + os.environ.get("HAMMER_STORE", str(ROOT / "store"))))) if not libres: sys.exit(__doc__ or "uso: scripts/hydrate-profile.py [--into DIR]") perfil = libres[0]