auditar-raices: el guardián de la raíz sucia sólo veía los perfiles — y hay 649 recetas fuera
`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.
This commit is contained in:
@@ -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 <perfil> [--into DIR]")
|
||||
perfil = libres[0]
|
||||
|
||||
Reference in New Issue
Block a user