From f6e690d9946a0c0e08b291eb70256dc54c5695e9 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 5 Sep 2026 17:11:23 +0000 Subject: [PATCH] =?UTF-8?q?vigia-imagen:=20un=20m=C3=B3dulo=20QML=20regist?= =?UTF-8?q?rado=20en=20C++=20no=20est=C3=A1=20=C2=ABsin=20instalar=C2=BB?= =?UTF-8?q?=20=E2=80=94=20se=20comprueba,=20no=20se=20recuerda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los DOS únicos ✗ que reportaba este vigía eran falsos, y de la misma clase: un `import` 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. Buscarlo en el sistema de ficheros da ✗ con la imagen perfecta. Los dos, verificados uno por uno antes de tocar el script: · `org.kde.kscreenlocker` ← `usr/lib64/libexec/kscreenlocker_greet`. Y valía la pena mirarlo: los que lo importan son `lockscreen/LockScreenUi.qml` y `MainBlock.qml` de plasma-desktop, o sea la pantalla de bloqueo de verdad. Si hubiera faltado no era un diálogo de ajustes. · `org.kde.newstuff.core` ← `usr/lib/qt6/qml/org/kde/newstuff/libnewstuffqmlplugin.so`. Antes esto se tapaba anotando el módulo a mano en `QML_EN_RUNTIME`. `registrado_en_binario()` lo contesta ahora con evidencia —el URI queda como literal en `.rodata`, así que se busca en los ELF del cierre— y además NOMBRA al registrador, que es lo que permite auditar el veredicto sin repetirlo. Mira primero los artefactos cuyo nombre comparte una palabra con el URI, que es donde está casi siempre, y sólo si no aparece recorre el resto. ⇒ la lista escrita a mano SE BORRA ENTERA. Al vaciarla, los cuatro módulos que tenía se resolvieron solos, cada uno nombrando su binario (plasmashell, kwin_wayland). Y salió gratis un hallazgo que ninguna lista podía dar: `org.kde.kwin.effect` no se importa en NINGÚN cierre — era una entrada muerta, y nadie tenía cómo saberlo. Los cinco perfiles quedan en ✓ de qml, sin un solo ✗ en todo el informe. La regla detrás: un guardián que grita en falso no se endurece, se ignora. Cuando el 100% de sus hallazgos son falsos positivos, el trabajo no es anotarlos — es enseñarle a preguntar bien. --- scripts/vigia-imagen.py | 86 ++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 14 deletions(-) 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"