diff --git a/scripts/vigia-imagen.py b/scripts/vigia-imagen.py index 544054ae..6550d997 100644 --- a/scripts/vigia-imagen.py +++ b/scripts/vigia-imagen.py @@ -41,8 +41,9 @@ # ══ RUIDO LEGÍTIMO, para poder triar ═══════════════════════════════════════════════════════════ # Hay módulos QML que NO se instalan en disco porque el binario los REGISTRA en runtime # (`org.kde.plasma.shell` lo registra plasmashell; los `Qt*` los trae el propio Qt en otra ruta). -# Están en QML_EN_RUNTIME y se excluyen; la lista se amplía con lo que se demuestre, no con lo que -# se suponga. Todo lo demás que salga acá es accionable. +# No se excluyen por una lista escrita a mano —eso envejece y no dice por qué— sino comprobando que +# el URI esté como literal en un ELF del cierre: ver `registrado_en_binario()`. Se reportan aparte, +# nombrando al registrador. Todo lo demás que salga acá es accionable. # # uso: python3 scripts/vigia-imagen.py [perfil...] [--fail] [--qml] # --fail → exit 1 si algún invariante falla; exit 2 si NO SE PUDO medir (nodos del cierre @@ -72,13 +73,10 @@ EXCEPCIONES = { "promover adwaita o breeze al corpus — decisión, no olvido", } -# Módulos QML que NADIE instala en disco y aun así resuelven: los registra un binario en runtime. -QML_EN_RUNTIME = { - "org.kde.plasma.shell", # plasmashell los registra al arrancar el shell - "org.kde.plasma.shell.panel", - "org.kde.kwin", # kwin_wayland registra su propia API de efectos - "org.kde.kwin.effect", -} +# (Acá vivía `QML_EN_RUNTIME`, una lista escrita a mano de módulos que nadie instala en disco. La +# reemplaza `registrado_en_binario()`, que contesta lo mismo con evidencia y además NOMBRA al +# registrador. Al vaciarla, los cuatro que tenía se resolvieron solos —y `org.kde.kwin.effect` ni +# siquiera se importa en ningún cierre: era una entrada muerta que nadie podía saber que sobraba.) _cache = {} def artefacto(cola, nombre): @@ -104,16 +102,62 @@ ICONO_EXT = (".png", ".svg", ".svgz", ".xpm") FUENTE_EXT = (".ttf", ".otf", ".ttc", ".pfb", ".pcf", ".pcf.gz") +def registrado_en_binario(uri, dirs): + """¿Algún ELF del cierre lleva `uri` como literal? Entonces lo registra en C++ y NO falta. + + ── POR QUÉ HACE FALTA ────────────────────────────────────────────────────────────────────── + Un `import org.kde.kscreenlocker` no siempre se satisface con un directorio y su `qmldir`. El + greeter de la pantalla de bloqueo hace `qmlRegisterUncreatableType( + "org.kde.kscreenlocker", …)`: el módulo existe SÓLO dentro del proceso que carga ese `.qml`, y + en disco no hay nada que instalar. Buscar el módulo en el sistema de ficheros da ✗ y la imagen + está perfecta. + + Medido el 2026-09-05: los DOS únicos ✗ de qml que reportaba este vigía eran de esta clase + —`org.kde.kscreenlocker` en `libexec/kscreenlocker_greet` y `org.kde.newstuff.core` en + `libnewstuffqmlplugin.so`— o sea que el 100% de sus hallazgos eran falsos. Y un guardián que + grita en falso no se endurece: se ignora. Antes esto se tapaba anotando el módulo a mano en + una lista escrita a mano, que envejece y no dice por qué. + + El literal queda en `.rodata` del binario, así que la pregunta se contesta con evidencia. Se + miran primero los artefactos cuyo nombre comparte una palabra con el URI (`kscreenlocker` → + `kscreenlocker`), que es donde está casi siempre, y sólo si no aparece se recorre el resto. + """ + aguja = uri.encode() + tokens = [t for t in re.split(r"[.\-_]", uri.lower()) if len(t) > 3] + + def primero_los_parecidos(par): + return 0 if any(t in par[0].lower() for t in tokens) else 1 + + for nombre, d in sorted(dirs.items(), key=primero_los_parecidos): + for raiz, _, ficheros in os.walk(d): + for f in ficheros: + ruta = os.path.join(raiz, f) + try: + if os.path.getsize(ruta) > 256 * 1024 * 1024: + continue + with open(ruta, "rb") as fh: + if fh.read(4) != b"\x7fELF": + continue + fh.seek(0) + if aguja in fh.read(): + return "%s (%s)" % (os.path.relpath(ruta, d), nombre) + except OSError: + continue + return None + + def escanear(nodos): """Recorre los artefactos del cierre una sola vez y junta los cinco hechos.""" temas, cursores, fuentes, terminales = {}, {}, 0, {} qml_modulos, qml_imports = set(), {} sin_artefacto = [] + dirs = {} for (cola, nombre) in sorted(nodos): d = artefacto(cola, nombre) if not d: sin_artefacto.append(nombre) continue + dirs[nombre] = d # ── temas de iconos y de cursores ─────────────────────────────────────────────────────── base = os.path.join(d, "usr/share/icons") if os.path.isdir(base): @@ -175,11 +219,19 @@ def escanear(nodos): continue if re.search(r"^Categories=.*TerminalEmulator", txt, re.M): terminales.setdefault(f[:-8], nombre) - faltan_qml = {m: v for m, v in qml_imports.items() - if m not in qml_modulos and m not in QML_EN_RUNTIME + candidatos = {m: v for m, v in qml_imports.items() + if m not in qml_modulos and not m.startswith(("QtQuick", "QtQml", "QtCore", "QtTest", "Qt5", "Qt.")) and m != "Qt"} - return temas, cursores, fuentes, terminales, faltan_qml, sin_artefacto + # Último filtro, y el que decide: un módulo registrado en C++ no está «sin instalar». + faltan_qml, en_binario = {}, {} + for m, v in candidatos.items(): + donde = registrado_en_binario(m, dirs) + if donde: + en_binario[m] = donde + else: + faltan_qml[m] = v + return temas, cursores, fuentes, terminales, faltan_qml, sin_artefacto, en_binario def main(): @@ -196,7 +248,7 @@ def main(): nodos = {n for n, ps in memb.items() if perfil in ps and n[0] != "wanted"} if not nodos: continue - temas, cursores, fuentes, terminales, faltan_qml, sin_art = escanear(nodos) + temas, cursores, fuentes, terminales, faltan_qml, sin_art, en_binario = escanear(nodos) reales = {t: v for t, v in temas.items() if t != "hicolor"} print("== %-20s %3d nodos del cierre" % (perfil, len(nodos))) parcial = bool(sin_art) @@ -237,9 +289,15 @@ def main(): linea(bool(terminales), "terminal", ", ".join("%s (%s)" % (k, v) for k, v in sorted(terminales.items())) or "NINGÚN emulador de terminal — no se puede salir de un fallo") + nota = "" + if en_binario: + nota = " (%d registrado(s) en C++, no en disco)" % len(en_binario) linea(not faltan_qml, "qml", - "todos los import resuelven" if not faltan_qml + ("todos los import resuelven" + nota) if not faltan_qml else "%d módulo(s) importado(s) y NO instalado(s)" % len(faltan_qml)) + if en_binario and detalle_qml: + for m, donde in sorted(en_binario.items()): + print(" · %-38s lo registra: %s" % (m, donde)) if faltan_qml and (detalle_qml or True): for m, quienes in sorted(faltan_qml.items()): print(" FALTA %-34s ← lo importa: %s"