tawasuyu

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 1373Shell 116Rust 74Markdown 641915 Sutichaykuna

⎇ e49545e0a7eff5f10c1d90e8cb54036becedf255 scripts/provee.py Python · 203 siq'ikuna · 8.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

takana etapa 3b: las 49 invocaciones pasan a ./target/release/takana 8730aad34 · Sergio ·

1#!/usr/bin/env python3
2# provee.py — ¿QUÉ publica de verdad el catálogo, y desde dónde se alcanza?
3#
4# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════
5# El triaje de apps de `docs/plan-apps-usuario-final.md` falló TRES veces, cada vez por una pregunta
6# distinta, y cada vez la anterior daba verde:
7#
8#   1. «¿existe la receta en el disco?»  — la pregunta original. Contaba `makedepends` de Alpine
9#      contra `recipes/*.toml`.
10#   2. «¿la ALCANZA el consumidor?»      — wf-recorder daba 0/0 y grababa MUDO: sus dos backends de
11#      audio existían, pero en colas hermanas, que una receta del corpus no ve.
12#   3. «¿la VARIANTE sellada publica la ABI?» — imv: `mesa` existe, es alcanzable desde cualquier
13#      lado, y aun así no sirve. Las tres variantes van `-Dglx=disabled -Dglvnd=false` ⇒ publican
14#      `libEGL.so.1` y `libGLESv2.so.2` y **ningún `libGL.so` ni `gl.pc`**. La trampa fina: mesa SÍ
15#      instala `/usr/include/GL/gl.h`, así que la app compila entera y muere recién al LIGAR.
16#
17# Las tres son la misma equivocación con distinto disfraz: preguntarle al CATÁLOGO lo que sólo sabe
18# el ARTEFACTO. Este script pregunta lo único que importa —**¿quién publica este `.pc` / esta
19# librería, y desde qué colas se alcanza?**— mirando los artefactos sellados, no las recetas.
20#
21# Es hermano de `vigia-sonames.py`, que resuelve la mitad de RUNTIME («qué `.so` pide una imagen que
22# nadie provee»). Éste resuelve la mitad de BUILD, que es la que se paga antes: qué puede pedir una
23# receta nueva sin que el configure muera.
24#
25# ══ LA REGLA DE ALCANCE, QUE ES LO QUE HACE ÚTIL AL INFORME ════════════════════════════════════
26# Una receta resuelve **sibling-first**: primero su propia cola, después el catálogo PADRE, y
27# NUNCA una cola hermana. Entonces lo que publica `corpus` lo ve todo el mundo, y lo que publica
28# `incoming-kde` sólo lo ven las recetas de `incoming-kde`. Por eso la columna que importa no es
29# «¿existe?» sino «desde qué colas se puede pedir», y por eso `exiv2` y `librsvg` —que existen— no
30# le sirven a una app del corpus.
31#
32#   uso:  scripts/provee.py gl.pc libGL.so opengl          # ¿quién publica esto? ¿desde dónde?
33#         scripts/provee.py --listar [cola]                # inventario completo de una cola
34#         scripts/provee.py --desde corpus luajit.pc ...   # exit 1 si algo NO se alcanza desde ahí
35#
36# El nombre se busca flojo a propósito: `gl`, `gl.pc`, `libGL.so` y `libGL.so.1` encuentran lo mismo.
37# Un triaje se hace con el nombre que trae el APKBUILD, que no siempre es el del fichero.
38import os, re, sys, glob, json, subprocess
39
40sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
41import yupana
42
43ROOT = yupana.ROOT
44STORE = os.environ.get("HAMMER_STORE", str(ROOT / "store"))
45HAMMER = str(ROOT / "target/release/takana")
46CACHE = ROOT / "work/provee-cache.json"
47
48
49def receta(cola, nombre):
50    return ROOT / ("recipes/%s.toml" % nombre if cola == "corpus"
51                   else "recipes/%s/%s.toml" % (cola, nombre))
52
53
54def artefacto(cola, nombre):
55    """Directorio del artefacto sellado de (cola,nombre), o None si no está en el store."""
56    f = receta(cola, nombre)
57    if not f.exists():
58        return None
59    r = subprocess.run([HAMMER, "--store", STORE, "hash", str(f)],
60                       capture_output=True, text=True)
61    h = r.stdout.strip().replace("b3:", "")
62    if len(h) != 64:
63        return None
64    cand = glob.glob(os.path.join(STORE, h + "-*"))
65    return cand[0] if cand else None
66
67
68def escanear_uno(d):
69    """(pc[], libs[]) publicados por un artefacto. `libs` incluye `.a` y `.so*`: un `.pc` no es la
70    única forma de pedir una librería — `cc.find_library('gif')` no mira pkg-config."""
71    pc, libs = set(), set()
72    for raiz, _, ficheros in os.walk(d):
73        en_pkgconfig = raiz.endswith("/pkgconfig")
74        for fn in ficheros:
75            if en_pkgconfig and fn.endswith(".pc"):
76                pc.add(fn)
77            elif fn.startswith("lib") and (fn.endswith(".a") or ".so" in fn):
78                libs.add(fn)
79    return sorted(pc), sorted(libs)
80
81
82def inventario(refrescar=False):
83    """{cola: {nombre: {"pc": [...], "libs": [...]}}} sobre TODO el catálogo sellado.
84
85    Cachea en `work/provee-cache.json` con la clave = ArtifactHash, porque el barrido son ~1100
86    invocaciones de `hammer hash` y una receta que no cambió publica lo mismo que ayer."""
87    cache = {}
88    if CACHE.exists() and not refrescar:
89        try:
90            cache = json.load(open(CACHE))
91        except Exception:
92            cache = {}
93    colas = yupana.cargar_colas()
94    inv, nuevo = {}, {}
95    for cola, nombres in sorted(colas.items()):
96        for nombre in sorted(nombres):
97            d = artefacto(cola, nombre)
98            if not d:
99                continue
100            clave = os.path.basename(d)
101            if clave in cache:
102                pc, libs = cache[clave]["pc"], cache[clave]["libs"]
103            else:
104                pc, libs = escanear_uno(d)
105            nuevo[clave] = {"pc": list(pc), "libs": list(libs)}
106            if pc or libs:
107                inv.setdefault(cola, {})[nombre] = {"pc": pc, "libs": libs}
108    CACHE.parent.mkdir(parents=True, exist_ok=True)
109    json.dump(nuevo, open(CACHE, "w"))
110    return inv
111
112
113def _base(n):
114    """`libGL.so.1` → `gl`; `librsvg-2.0.pc` → `rsvg`; `wayland-client.pc` → `wayland-client`.
115    Quita la extensión, el `lib` de adelante y el sufijo de VERSIÓN — que es lo que hace que
116    `librsvg` no encuentre a `librsvg-2.0.pc` si uno compara a lo bruto."""
117    n = n.lower()
118    n = n.split(".so")[0] if ".so" in n else re.sub(r"\.(pc|a)$", "", n)
119    n = re.sub(r"^lib", "", n)
120    return n
121
122
123def _casa(consulta, fichero):
124    """`gl`, `gl.pc`, `libGL.so` y `libGL.so.1` tienen que encontrar lo mismo, y `librsvg` tiene que
125    encontrar a `librsvg-2.0.pc`: el nombre que trae un APKBUILD casi nunca lleva la versión."""
126    q, f = _base(consulta), _base(fichero)
127    if q == f:
128        return True
129    # sufijo de versión: `rsvg-2.0` ~ `rsvg`, `tiff-4` ~ `tiff`, `xkbcommon-x11` NO ~ `xkbcommon`
130    return bool(re.fullmatch(re.escape(q) + r"[-_]?[0-9][0-9.]*", f))
131
132
133def buscar(inv, consulta):
134    """{(cola, nombre): [ficheros]} que publican algo que case con `consulta`. Agrupado por receta
135    a propósito: `mesa` publica cuatro ficheros de EGL y repetir la receta cuatro veces convierte
136    el informe en ruido justo cuando hay que leerlo rápido."""
137    hits = {}
138    for cola, recetas in inv.items():
139        for nombre, pub in recetas.items():
140            casan = [fn for fn in pub["pc"] + pub["libs"] if _casa(consulta, fn)]
141            if casan:
142                hits[(cola, nombre)] = sorted(casan)
143    return dict(sorted(hits.items()))
144
145
146def alcance(colas_que_publican):
147    """Colas desde las que se puede PEDIR, dado quién lo publica. Sibling-first: `corpus` lo ve
148    todo el mundo; una `incoming-<x>` sólo se ve a sí misma."""
149    todas = set(yupana.cargar_colas())
150    if "corpus" in colas_que_publican:
151        return sorted(todas)
152    return sorted(colas_que_publican)
153
154
155def main():
156    argv = sys.argv[1:]
157    refrescar = "--refrescar" in argv
158    argv = [a for a in argv if a != "--refrescar"]
159
160    if argv and argv[0] == "--listar":
161        inv = inventario(refrescar)
162        quiero = argv[1:] or sorted(inv)
163        for cola in quiero:
164            recetas = inv.get(cola, {})
165            npc = sum(len(v["pc"]) for v in recetas.values())
166            print("== %-18s %3d recetas publican algo · %4d .pc" % (cola, len(recetas), npc))
167            for nombre, pub in sorted(recetas.items()):
168                if pub["pc"]:
169                    print("   %-28s %s" % (nombre, " ".join(pub["pc"])))
170        return 0
171
172    desde = None
173    if argv and argv[0] == "--desde":
174        desde, argv = argv[1], argv[2:]
175    if not argv:
176        print(__doc__ or "uso: scripts/provee.py <nombre>... | --listar [cola] | --desde <cola> <nombre>...")
177        return 2
178
179    inv = inventario(refrescar)
180    huecos = 0
181    for consulta in argv:
182        hits = buscar(inv, consulta)
183        if not hits:
184            print("%-22s ✗ NADIE LO PUBLICA en ninguna cola" % consulta)
185            huecos += 1
186            continue
187        visible = alcance({c for c, _ in hits})
188        marca = ""
189        if desde:
190            ok = desde in visible
191            marca = "  ⇒ desde %s: %s" % (desde, "SÍ" if ok else "NO ALCANZABLE")
192            if not ok:
193                huecos += 1
194        print("%-22s ✓ %s%s" % (consulta, ", ".join("%s/%s" % k for k in hits), marca))
195        for (c, n), ficheros in hits.items():
196            print("      %-22s %s" % ("%s/%s" % (c, n), " ".join(ficheros)))
197        if not desde:
198            print("      alcanzable desde: %s" % ", ".join(visible))
199    return 1 if (desde and huecos) else 0
200
201
202if __name__ == "__main__":
203    sys.exit(main())