Hay una familia de recetas que no son programas: son SUBCOMANDOS. `protoc-gen-go` es un plugin que
ejecuta `protoc`; `cargo-audit` existe para escribirse `cargo audit`; `kubectl-tree` lo despacha
`kubectl`. Están selladas, tienen contenido, resuelven sus sonames, el grafo las cuenta y
`verificar-repro` dice que reproducen. **Cuatro indicadores en verde sobre binarios que no se pueden
usar**, porque el driver que los invoca no está en el catálogo.
Ninguna métrica existente puede verlo: la arista «me ejecuta aquél» NO es una dep de build, así que
no existe en el grafo. Lo descubrí por accidente — `protoc-gen-go` llevaba meses sellado y el
catálogo no tenía `protoc`; lo delató ir a escribir la receta de un servicio gRPC y preguntarme con
qué se generan los stubs. Eso es exactamente un punto ciego, y acá va su guardián.
Lo que mide hoy:
✗ falta `cargo` ⇒ 44 recetas INERTES (cargo-audit, cargo-nextest, cargo-deny, …)
✗ falta `kubectl` ⇒ 2 recetas INERTES (kubectl-neat, kubectl-tree)
TOTAL: 46. No es deuda de BUILD —construyen y reproducen— es deuda de CATÁLOGO.
Dos decisiones de diseño que son la diferencia entre un vigía que se lee y uno que se ignora:
· **Comprueba el BINARIO, no el nombre de la receta.** El driver de `protoc-gen-*` es `protoc`, que
lo publica la receta `protobuf`. Preguntar «¿existe recipes/protoc.toml?» habría seguido diciendo
«no» DESPUÉS de cerrarlo, y un guardián que grita cuando ya está arreglado se empieza a ignorar.
· **La tabla de drivers es explícita, no una heurística sobre guiones.** `git-cliff` es subcomando de
git; `gettext-tiny` no es subcomando de `gettext`. Adivinar por el guión da falsos positivos, y un
vigía con falsos positivos no se lee.
Y trae `--autoprueba` con los dos controles, porque un guardián que nunca falló no se sabe si sirve:
NEGATIVO (escondo `git`, que sí está ⇒ tiene que cantar `git-`) y POSITIVO (sin tocar nada, no debe
cantar sobre los drivers presentes). Los dos pasan.
⚠ Una trampa medida escribiéndolo, dentro del propio script: `glob('store/*-go')` casa por SUFIJO y
matchea `…-protoc-gen-go`, o sea que contestaba que `go` estaba sellado cuando no lo estaba. El
nombre de receta se saca con una regex ANCLADA sobre el basename.
159 lines
8.0 KiB
Python
Executable File
159 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# vigia-subcomandos.py — ¿hay herramientas SELLADAS que no sirven porque falta su DRIVER?
|
|
#
|
|
# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════
|
|
# `build-state.py` contesta «¿está sellado?», `vigia-sonames.py` contesta «¿algún `.so` que alguien
|
|
# pide no lo provee nadie?» y `vigia-imagen.py` contesta «¿la imagen se puede USAR?». Ninguno
|
|
# contesta ésta: **¿este binario sellado sirve para algo por sí solo?**
|
|
#
|
|
# Hay una familia entera de herramientas que NO son programas: son SUBCOMANDOS. `protoc-gen-go` es un
|
|
# plugin que `protoc` ejecuta; `cargo-audit` existe para escribirse `cargo audit`; `kubectl-tree` lo
|
|
# despacha `kubectl`. Sellados, no vacíos, sin NEEDED colgante, reproducibles — y **inertes**, porque
|
|
# el driver que los invoca no está en el catálogo.
|
|
#
|
|
# ⚠ **Ningún indicador existente puede verlo.** El artefacto tiene contenido (regla 3 ✓), resuelve
|
|
# sus sonames (vigia-sonames ✓), el grafo lo cuenta como `sealed` (✓) y `verificar-repro` dice que
|
|
# REPRODUCE (✓). Cuatro verdes sobre algo que no se puede usar. La arista «me ejecuta aquél» no es
|
|
# una dep de build, así que no existe en el grafo: se descubre sólo cuando alguien va a usarlo.
|
|
#
|
|
# ══ CÓMO LO ENCONTRÉ (2026-09-11), que es la razón de que este fichero exista ═══════════════════
|
|
# `recipes/protoc-gen-go.toml` llevaba meses sellado y el catálogo NO tenía `protoc`. Un plugin de
|
|
# protoc sin protoc no hace absolutamente nada. No lo delató ninguna métrica: lo delató ir a escribir
|
|
# la receta de un servicio gRPC y preguntarse con qué se generan los stubs.
|
|
#
|
|
# ══ QUÉ MIDE, Y POR QUÉ ASÍ ════════════════════════════════════════════════════════════════════
|
|
# Para cada prefijo de la tabla DRIVERS, junta las recetas que empiezan con él y comprueba que el
|
|
# **BINARIO** del driver exista dentro de algún artefacto sellado.
|
|
#
|
|
# · Se comprueba el BINARIO y no el nombre de la receta a propósito: el driver de `protoc-gen-*` es
|
|
# `protoc`, que lo publica la receta `protobuf`. Preguntar «¿existe recipes/protoc.toml?» habría
|
|
# contestado «no» incluso después de cerrarlo, y un vigía que grita cuando ya está arreglado se
|
|
# empieza a ignorar — que es cómo se muere un guardián ([[guardianes-el-ruido-los-mata]]).
|
|
# · La tabla es EXPLÍCITA y no una heurística sobre guiones. `git-cliff` es subcomando de git, pero
|
|
# `gettext-tiny` no es subcomando de `gettext`. Adivinar por el guión da falsos positivos, y un
|
|
# vigía con falsos positivos no se lee.
|
|
#
|
|
# ══ AUTOPRUEBA ═════════════════════════════════════════════════════════════════════════════════
|
|
# `--autoprueba` corre los dos controles que hacen falta para creerle a un guardián: uno NEGATIVO
|
|
# (esconder un driver que sí está ⇒ TIENE que saltar) y uno POSITIVO (sin tocar nada, los drivers
|
|
# presentes NO deben aparecer). Un guardián que nunca falló no se sabe si sirve.
|
|
#
|
|
# Uso: scripts/vigia-subcomandos.py [--autoprueba]
|
|
# Sale: 0 si no hay inertes · 1 si los hay · 2 si la autoprueba falla
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# prefijo de receta → (binario del driver, quién se supone que lo publica, para el mensaje)
|
|
DRIVERS = {
|
|
"cargo-": ("cargo", "el toolchain de Rust"),
|
|
"kubectl-": ("kubectl", "una receta kubectl"),
|
|
"git-": ("git", "recipes/git.toml"),
|
|
"helm-": ("helm", "recipes/helm.toml"),
|
|
"protoc-gen-": ("protoc", "recipes/protobuf.toml"),
|
|
"docker-": ("docker", "una receta docker"),
|
|
}
|
|
|
|
|
|
def artefactos():
|
|
"""Todos los directorios de artefacto del store, por nombre de receta.
|
|
|
|
⚠ El nombre se saca con una EXPRESIÓN ANCLADA, no con `glob('store/*-<n>')`: ese glob hace
|
|
coincidencia por sufijo, así que `*-go` casa con `…-protoc-gen-go` y contesta que `go` está
|
|
sellado cuando no lo está. Me pasó midiendo esto mismo."""
|
|
out = {}
|
|
for d in glob.glob(os.path.join(RAIZ, "store", "*")):
|
|
m = re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d))
|
|
if m and os.path.isdir(d):
|
|
out.setdefault(m.group(1), []).append(d)
|
|
return out
|
|
|
|
|
|
def provee_binario(arts, nombre_bin, ocultar=()):
|
|
"""¿Algún artefacto sellado trae un ejecutable llamado `nombre_bin`?"""
|
|
for receta, dirs in arts.items():
|
|
if receta in ocultar:
|
|
continue
|
|
for d in dirs:
|
|
for sub in ("bin", "sbin", "usr/bin", "usr/sbin", "usr/local/bin"):
|
|
p = os.path.join(d, sub, nombre_bin)
|
|
if os.path.exists(p):
|
|
return receta
|
|
return None
|
|
|
|
|
|
def barrido(arts, ocultar=()):
|
|
"""→ lista de (prefijo, binario, quien_deberia, [recetas inertes])."""
|
|
recetas = set()
|
|
for patron in ("recipes/*.toml", "recipes/incoming*/*.toml"):
|
|
for f in glob.glob(os.path.join(RAIZ, patron)):
|
|
recetas.add(os.path.basename(f)[:-5])
|
|
hallazgos = []
|
|
for prefijo, (binario, quien) in sorted(DRIVERS.items()):
|
|
subs = sorted(r for r in recetas if r.startswith(prefijo) and r != prefijo.rstrip("-"))
|
|
if not subs:
|
|
continue
|
|
if provee_binario(arts, binario, ocultar) is None:
|
|
hallazgos.append((prefijo, binario, quien, subs))
|
|
return hallazgos
|
|
|
|
|
|
def autoprueba():
|
|
"""Control NEGATIVO + control POSITIVO. Sin los dos, no se sabe si el vigía mide algo."""
|
|
arts = artefactos()
|
|
ok = True
|
|
|
|
# NEGATIVO: escondemos `git`, que SÍ está y tiene subcomandos ⇒ el vigía tiene que verlo.
|
|
quien_git = provee_binario(arts, "git")
|
|
if quien_git is None:
|
|
print(" ⚠ control negativo NO APLICABLE: `git` no está sellado en esta máquina")
|
|
else:
|
|
roto = [p for p, _b, _q, _s in barrido(arts, ocultar={quien_git})]
|
|
if "git-" in roto:
|
|
print(f" ✓ control NEGATIVO: escondiendo `{quien_git}`, el vigía canta `git-` ")
|
|
else:
|
|
print(f" ✗ control NEGATIVO FALLÓ: escondí `{quien_git}` y el vigía no dijo nada")
|
|
ok = False
|
|
|
|
# POSITIVO: sin tocar nada, los drivers que SÍ están no deben aparecer. Un vigía que grita
|
|
# sobre todo es igual de inútil que uno que nunca grita.
|
|
presentes = [p for p, (b, _q) in DRIVERS.items() if provee_binario(arts, b)]
|
|
ruidosos = [p for p, _b, _q, _s in barrido(arts) if p in presentes]
|
|
if ruidosos:
|
|
print(f" ✗ control POSITIVO FALLÓ: canta sobre drivers presentes: {ruidosos}")
|
|
ok = False
|
|
else:
|
|
print(f" ✓ control POSITIVO: no canta sobre los {len(presentes)} drivers presentes "
|
|
f"({', '.join(p.rstrip('-') for p in presentes)})")
|
|
return ok
|
|
|
|
|
|
def main():
|
|
if "--autoprueba" in sys.argv:
|
|
print("══ AUTOPRUEBA DEL VIGÍA ══")
|
|
sys.exit(0 if autoprueba() else 2)
|
|
|
|
arts = artefactos()
|
|
hallazgos = barrido(arts)
|
|
print("══ SUBCOMANDOS SIN DRIVER ══")
|
|
if not hallazgos:
|
|
print(" ✓ 0 herramientas inertes: todos los drivers de la tabla están en el catálogo")
|
|
sys.exit(0)
|
|
total = 0
|
|
for prefijo, binario, quien, subs in hallazgos:
|
|
total += len(subs)
|
|
print(f"\n ✗ falta el binario `{binario}` ({quien})")
|
|
print(f" ⇒ {len(subs)} recetas selladas quedan INERTES:")
|
|
for i in range(0, len(subs), 6):
|
|
print(" " + " ".join(subs[i:i + 6]))
|
|
print(f"\n TOTAL: {total} herramientas selladas que no se pueden invocar.")
|
|
print(" No es deuda de BUILD (construyen y reproducen): es deuda de CATÁLOGO.")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|