gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1184Shell 70Rust 62Markdown 501503 Sutichaykuna

vigía de sonames: KDE, GNOME y COSMIC tenían la MISMA fuga al lab que sway ya había documentado

Verificando que mpv anduviera en las cuatro imágenes salió esto: `libEGL.so.1` (mesa) pide
`libexpat.so.1` y `libwayland-client.so.0` pide `libffi.so.8`, las recetas canónicas de expat y
libffi son `--disable-shared`, y ningún artefacto del cierre publica esos SONAME ⇒ los tres rootfs
los resolvían contra el **sysroot Alpine DEL LAB**.

Es peor que una dep faltante: el lab NO entra en `hash_inputs`, así que el store no puede notarlo —
el artefacto se sella, el perfil reporta 100%, y la imagen sólo arranca en una máquina con Alpine
debajo. El perfil de sway ya tenía escrito exactamente este párrafo desde 2026-08-26; lo que faltaba
era el instrumento para ver que las otras tres estaban igual.

Arreglo (gratis, sin rebuild: las dos recetas ya estaban selladas por sway): `expat-shared` y
`libffi-shared` pasan a raíces de los tres perfiles. Van de raíces y no de `[deps]` por la misma
razón que las fuentes y el XKB — son data de RUNTIME y ninguna arista de build las alcanza.

`scripts/vigia-sonames.py` es el guardián que sale del punto ciego: recorre los NEEDED de todo el
cierre de cada imagen contra los SONAME que ese mismo cierre publica. Dos decisiones de diseño:
  · keying por PAR `(cola, nombre)` vía yupana, NO por nombre — `build-state.json` colapsa los
    nombres que viven en dos colas y su campo `perfiles` puede quedar vacío para una receta que sí
    está en la imagen.
  · imprime SIEMPRE quién pide cada soname, porque el cierre incluye herramientas de build
    (python3, cmake, perl, go) que la hidratación no instala: sin esa columna el informe no se tría.

Después del arreglo, `libexpat.so.1` y `libffi.so.8` desaparecen de los tres. Lo que queda son
hallazgos REALES que no son de este commit y quedan anotados:
  · kde:    karchive pide libbz2.so.1 y liblzma.so.5
  · gnome:  spidermonkey pide libstdc++.so.6 y libgcc_s.so.1; libadwaita pide liblzma.so.5;
            freetype-shared pide libbz2.so.1; sqlite-shared pide libreadline.so.8
  · cosmic: llvm18 pide libgcc_s.so.1
  · los tres: `pw-top` de pipewire enlaza `libncursesw.so.6` DEL LAB — y la receta afirma en un
    comentario que sin la dep «meson saltea pw-top». Es falso desde al menos el 2026-08-29:
    `dependency('ncursesw')` lo encuentra igual en el sysroot del lab. El comentario dice una cosa
    y el binario otra.

f9da495b8c95c960bc3ecd78783a9731085a1396 · ruwaq Sergio · (1 killa ñawpaq) · tayta d8778e4dd · kay commitpi sach'ata qhaway

Semántico sayk'uy · 4

scripts/vigia-sonames.py

2 qillqakuna · +162 −0

diff --git a/docs/state/targets.toml b/docs/state/targets.toml
index 653c3a4a..ba6a668e 100644
--- a/docs/state/targets.toml
+++ b/docs/state/targets.toml
@@ -78,6 +78,18 @@ paquetes = [
   # `-Dpipewire-alsa=disabled` en las pipewire (⇒ no suena con PipeWire vivo), sin Lua (⇒ sin OSC)
   # y sin vaapi (⇒ sin decodificación por hardware).
   "mpv",
+  # ── LAS `.so` QUE mesa Y wayland PIDEN EN RUNTIME Y NINGÚN ARTEFACTO DEL CIERRE PROVEE ────────
+  # Medido el 2026-09-03 recorriendo los NEEDED de todo el cierre de esta imagen contra los SONAME
+  # que ese mismo cierre publica: `libEGL.so.1` pide `libexpat.so.1` y `libwayland-client.so.0` pide
+  # `libffi.so.8`, y las recetas canónicas de expat y libffi son `--disable-shared`. O sea que el
+  # rootfs hidratado los resolvía contra el **sysroot Alpine DEL LAB**.
+  # Es peor que una dep faltante: el lab NO entra en `hash_inputs`, así que la fuga es invisible para
+  # el store — el artefacto se da por bueno y sólo arranca en una máquina con Alpine debajo.
+  # No es un descubrimiento nuevo: es EXACTAMENTE lo que el perfil `escritorio-sway` ya documentó
+  # para `sway`, sólo que ahí se encontró a mano al armar la imagen y estas tres nunca se revisaron.
+  # Van de raíces (y no de `[deps]`) por lo mismo que las fuentes y el XKB: son data de RUNTIME y
+  # nadie las declara como dep de BUILD, así que la clausura no puede alcanzarlas por ninguna arista.
+  "expat-shared", "libffi-shared",
 ]
 
 [perfil.escritorio-gnome]
@@ -118,6 +130,18 @@ paquetes = [
   # `-Dpipewire-alsa=disabled` en las pipewire (⇒ no suena con PipeWire vivo), sin Lua (⇒ sin OSC)
   # y sin vaapi (⇒ sin decodificación por hardware).
   "mpv",
+  # ── LAS `.so` QUE mesa Y wayland PIDEN EN RUNTIME Y NINGÚN ARTEFACTO DEL CIERRE PROVEE ────────
+  # Medido el 2026-09-03 recorriendo los NEEDED de todo el cierre de esta imagen contra los SONAME
+  # que ese mismo cierre publica: `libEGL.so.1` pide `libexpat.so.1` y `libwayland-client.so.0` pide
+  # `libffi.so.8`, y las recetas canónicas de expat y libffi son `--disable-shared`. O sea que el
+  # rootfs hidratado los resolvía contra el **sysroot Alpine DEL LAB**.
+  # Es peor que una dep faltante: el lab NO entra en `hash_inputs`, así que la fuga es invisible para
+  # el store — el artefacto se da por bueno y sólo arranca en una máquina con Alpine debajo.
+  # No es un descubrimiento nuevo: es EXACTAMENTE lo que el perfil `escritorio-sway` ya documentó
+  # para `sway`, sólo que ahí se encontró a mano al armar la imagen y estas tres nunca se revisaron.
+  # Van de raíces (y no de `[deps]`) por lo mismo que las fuentes y el XKB: son data de RUNTIME y
+  # nadie las declara como dep de BUILD, así que la clausura no puede alcanzarlas por ninguna arista.
+  "expat-shared", "libffi-shared",
 ]
 
 [perfil.escritorio-cosmic]
@@ -199,6 +223,18 @@ paquetes = [
   # `-Dpipewire-alsa=disabled` en las pipewire (⇒ no suena con PipeWire vivo), sin Lua (⇒ sin OSC)
   # y sin vaapi (⇒ sin decodificación por hardware).
   "mpv",
+  # ── LAS `.so` QUE mesa Y wayland PIDEN EN RUNTIME Y NINGÚN ARTEFACTO DEL CIERRE PROVEE ────────
+  # Medido el 2026-09-03 recorriendo los NEEDED de todo el cierre de esta imagen contra los SONAME
+  # que ese mismo cierre publica: `libEGL.so.1` pide `libexpat.so.1` y `libwayland-client.so.0` pide
+  # `libffi.so.8`, y las recetas canónicas de expat y libffi son `--disable-shared`. O sea que el
+  # rootfs hidratado los resolvía contra el **sysroot Alpine DEL LAB**.
+  # Es peor que una dep faltante: el lab NO entra en `hash_inputs`, así que la fuga es invisible para
+  # el store — el artefacto se da por bueno y sólo arranca en una máquina con Alpine debajo.
+  # No es un descubrimiento nuevo: es EXACTAMENTE lo que el perfil `escritorio-sway` ya documentó
+  # para `sway`, sólo que ahí se encontró a mano al armar la imagen y estas tres nunca se revisaron.
+  # Van de raíces (y no de `[deps]`) por lo mismo que las fuentes y el XKB: son data de RUNTIME y
+  # nadie las declara como dep de BUILD, así que la clausura no puede alcanzarlas por ninguna arista.
+  "expat-shared", "libffi-shared",
 ]
 
 [perfil.escritorio-sway]
diff --git a/scripts/vigia-sonames.py b/scripts/vigia-sonames.py
new file mode 100755
index 00000000..5c2adbce
--- /dev/null
+++ b/scripts/vigia-sonames.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# vigia-sonames.py — ¿qué `.so` pide una imagen que NINGÚN artefacto de su cierre provee?
+#
+# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════
+# El grafo de estado responde «¿está sellado?». No responde «¿arranca?». Y hay una fuga que pasa
+# entera por debajo del grafo:
+#
+#   un binario `link = "dynamic"` sale con `NEEDED libffi.so.8`; la receta canónica de libffi es
+#   `--disable-shared`; ningún artefacto del cierre publica ese SONAME; y el rootfs hidratado lo
+#   resuelve contra el **sysroot Alpine DEL LAB**.
+#
+# El artefacto se sella, el perfil reporta 100%, y la imagen sólo arranca en una máquina que tenga
+# Alpine debajo. Es PEOR que una dep faltante, porque el lab no entra en `hash_inputs`: el store no
+# puede ni notarlo. Es el mismo patrón que [[artefacto-vacio-envenena-cache]] — algo que se lee como
+# presencia y no lo es.
+#
+# Se descubrió DOS VECES a mano y nunca hubo instrumento: la primera al armar la imagen de sway
+# (zlib/expat/libffi, ver el comentario del perfil `escritorio-sway` en targets.toml), y la segunda
+# el 2026-09-03 al verificar que mpv anduviera — donde resultó que KDE, GNOME y COSMIC tenían la
+# MISMA fuga desde siempre, con mesa y wayland pidiendo expat y libffi. Este script es el guardián
+# que sale de ese punto ciego: la regla del repo es que cada uno se convierte en uno.
+#
+# ══ POR QUÉ NO SE PUEDE LEER DE build-state.json ═══════════════════════════════════════════════
+# Porque el JSON colapsa los nodos POR NOMBRE y hay nombres que viven en dos colas: su campo
+# `perfiles` puede quedar vacío para una receta que sí está en la imagen (por la otra cola). La
+# membresía honesta se identifica por PAR `(cola, nombre)` y de eso sabe yupana, que resuelve
+# sibling-first igual que el sandbox. Misma lección que el bug de libdrm: QUÉ NODOS PUNTÚO es una
+# decisión de vista; QUIÉN ME CONSUME —y a qué imagen pertenezco— es un hecho.
+#
+# ══ CÓMO LEER LA SALIDA, que tiene ruido legítimo ══════════════════════════════════════════════
+# El cierre de un perfil incluye sus HERRAMIENTAS DE BUILD (python3, cmake, perl, go…), y la
+# hidratación de la imagen NO las instala: va por lista explícita. Sus NEEDED aparecen igual acá y
+# **no son un bug de la imagen**. Lo accionable es lo que pide un componente de RUNTIME — mesa,
+# wayland, el compositor, una app. Por eso se imprime siempre QUIÉN pide cada soname: sin esa
+# columna el informe no se puede triar.
+#
+#   uso:  python3 scripts/vigia-sonames.py [perfil...] [--fallar]
+#         --fallar  → exit 1 si algo falta (para cron/CI)
+import os, sys, glob, subprocess
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
+import yupana
+import targets as targets_mod
+
+ROOT = yupana.ROOT
+STORE = os.environ.get("HAMMER_STORE", str(ROOT / "store"))
+HAMMER = str(ROOT / "target/release/hammer")
+
+_cache = {}
+def artefacto(cola, nombre):
+    """Directorio del artefacto de (cola,nombre) en el store, o None."""
+    if (cola, nombre) in _cache:
+        return _cache[(cola, nombre)]
+    receta = ROOT / ("recipes/%s.toml" % nombre if cola == "corpus"
+                     else "recipes/%s/%s.toml" % (cola, nombre))
+    d = None
+    if receta.exists():
+        r = subprocess.run([HAMMER, "--store", STORE, "hash", str(receta)],
+                           capture_output=True, text=True)
+        h = r.stdout.strip().replace("b3:", "")
+        if len(h) == 64:
+            cand = glob.glob(os.path.join(STORE, h + "-*"))
+            d = cand[0] if cand else None
+    _cache[(cola, nombre)] = d
+    return d
+
+def elf(path):
+    """(NEEDED[], SONAME[]) de un ELF, o None si no lo es."""
+    try:
+        out = subprocess.run(["readelf", "-d", path], capture_output=True,
+                             text=True, timeout=30).stdout
+    except Exception:
+        return None
+    if "(NEEDED)" not in out and "(SONAME)" not in out:
+        return None
+    tag = lambda t: [l.split("[")[1].split("]")[0] for l in out.splitlines() if t in l]
+    return tag("(NEEDED)"), tag("(SONAME)")
+
+def escanear(nodos):
+    provee, pide = set(), {}
+    for (cola, nombre) in sorted(nodos):
+        d = artefacto(cola, nombre)
+        if not d:
+            continue
+        for raiz, _, ficheros in os.walk(d):
+            for fn in ficheros:
+                p = os.path.join(raiz, fn)
+                if os.path.islink(p):
+                    # un symlink `libfoo.so.1 -> libfoo.so.1.2.3` TAMBIÉN provee ese nombre
+                    if ".so" in fn:
+                        provee.add(fn)
+                    continue
+                if ".so" not in fn and not raiz.endswith(("/bin", "/sbin", "/libexec")):
+                    continue
+                r = elf(p)
+                if not r:
+                    continue
+                need, son = r
+                provee.update(son)
+                if son:
+                    provee.add(fn)
+                for n in need:
+                    pide.setdefault(n, set()).add(nombre)
+    return provee, pide
+
+def main():
+    args = [a for a in sys.argv[1:] if not a.startswith("--")]
+    fallar = "--fallar" in sys.argv
+    colas = yupana.cargar_colas()
+    perfiles = targets_mod.load()
+    memb = yupana.membresia(colas, perfiles)
+    quiero = args or [p for p in perfiles if p.startswith("escritorio")]
+    huecos = 0
+    for perfil in quiero:
+        nodos = {n for n, ps in memb.items() if perfil in ps and n[0] != "wanted"}
+        provee, pide = escanear(nodos)
+        # `libc.so` es el intérprete musl: lo pone el rootfs base, no una receta del cierre.
+        faltan = {s: v for s, v in pide.items() if s not in provee and s != "libc.so"}
+        print("== %-20s %3d nodos · %4d sonames provistos · %2d sin proveedor"
+              % (perfil, len(nodos), len(provee), len(faltan)))
+        for s, quienes in sorted(faltan.items()):
+            print("   FALTA %-24s ← lo piden: %s" % (s, ", ".join(sorted(quienes))))
+        huecos += len(faltan)
+    return 1 if (fallar and huecos) else 0
+
+if __name__ == "__main__":
+    sys.exit(main())