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 1561Shell 203Rust 109Markdown 993728 Definitions

⎇ c5a27d9b4c8d4f53a5562e11df7102121da77a20 scripts/busybox-vigia.py Python · 137 lines · 5.9 KB History Who touched what raw

busybox recortado: 401 -> 277 applets, con vigía y cinco indultos medidos c5a27d9b4 · Sergio ·

1#!/usr/bin/env python3
2"""Vigía del recorte de busybox (docs/plan-botar-busybox.md, paso 1).
3
4Falla si alguna receta, script o card del repo INVOCA un applet que la receta de busybox
5ya no construye. Sin esto el recorte se deshace solo: nada falla al quitar un applet —
6el fallo aparece en boot, en otra máquina, semanas después.
7
8    scripts/busybox-vigia.py            # exit 1 si hay invocaciones de applets retirados
9    scripts/busybox-vigia.py --list     # imprime los applets retirados y sale 0
10
11La lista de retirados se LEE de recipes/busybox.toml (los `# CONFIG_X is not set` que el
12recorte inyecta), cruzada con el mapa applet->símbolo de docs/state/busybox-applets.tsv.
13Así no hay dos listas que se desincronicen.
14"""
15import re, sys, pathlib, json
16
17RAIZ = pathlib.Path(__file__).resolve().parent.parent
18EXT = {'.sh', '.rs', '.toml', '.py'}
19SALTAR = ('target/', '.git/', 'store/', 'work/', 'docs/')
20
21# Un applet en posición de COMANDO: inicio de línea, o tras | ; & ` $( exec sudo && ||
22PRE = r'(?:^|[|;&`]|\$\(|\bexec\s+|\bsudo\s+|&&\s*|\|\|\s*)[ \t]*'
23POST = r'(?=[ \t;|&)`"\']|$)'
24
25
26def simbolos_retirados():
27    """Los símbolos Kconfig que la receta apaga: la lista RETIRADOS de su fase configure,
28    más cualquier `# CONFIG_X is not set` suelto (el recorte de `tc`, por ejemplo)."""
29    txt = (RAIZ / 'recipes' / 'busybox.toml').read_text()
30    syms = set(re.findall(r'#\s*CONFIG_([A-Z0-9_]+)\s+is\s+not\s+set', txt))
31    for lista in re.findall(r"RETIRADOS='([^']*)'", txt):
32        syms |= set(lista.split())
33    return syms
34
35
36def applets_retirados():
37    """Applets cuyo símbolo Kconfig la receta apaga.
38
39    El mapa applet->símbolo NO se deriva por mayúsculas: sale de la columna
40    `simbolo_kconfig` del TSV, extraída de las líneas `//applet:` del fuente de busybox.
41    Derivarlo estaría MAL en 6 casos medidos: `[`->TEST1, `[[`->TEST2, arch->BB_ARCH,
42    nbd-client->NBDCLIENT, sh->SH_IS_ASH, sysctl->BB_SYSCTL."""
43    syms = simbolos_retirados()
44    if not syms:
45        return set()
46    tsv = RAIZ / 'docs' / 'state' / 'busybox-applets.tsv'
47    filas = [l.split('\t') for l in tsv.read_text().splitlines()[1:] if l.strip()]
48    return {f[0] for f in filas if len(f) > 5 and f[5] in syms}
49
50
51# Palabras del shell, no applets invocables: `[` y `[[` aparecen en todo script bash del HUB
52# (que corre con el bash del hub, no con busybox) y no son llamadas a un applet.
53NO_VIGILAR = {'[', '[['}
54
55
56# Una cadena de Rust/Python sólo se mira si PARECE una línea de comandos. Sin esto, los literales
57# sueltos ("init" como nombre de capacidad, "tree" como variable) ahogan al vigía: 91 hits, 0 reales.
58PARECE_COMANDO = re.compile(r'(?:\bsh\s+-c\b|/bin/|/usr/bin/|/sbin/|&&|\|\||\$\()')
59
60
61def sin_comentarios(txt, suf):
62    """Quita comentarios y, en los lenguajes que no son shell, deja sólo las cadenas que
63    parecen una línea de comandos. Es el 90 % del ruido."""
64    if suf in ('.sh', '.toml'):
65        return re.sub(r'(?m)(?<!\\)#.*$', '', txt)
66    if suf in ('.rs', '.py'):
67        if suf == '.rs':
68            txt = re.sub(r'(?m)//.*$', '', txt)
69        else:
70            txt = re.sub(r'(?m)(?<!\\)#.*$', '', txt)
71        cad = re.findall(r'"((?:[^"\\\n]|\\.)*)"', txt) + re.findall(r"'((?:[^'\\\n]|\\.)*)'", txt)
72        return '\n'.join(c for c in cad if PARECE_COMANDO.search(c))
73    return txt
74
75
76def funciones_definidas(txt):
77    """Nombres definidos como función de shell EN ESE fichero: `ts() {`, `function ts {`."""
78    return set(re.findall(r'(?m)^\s*(?:function\s+)?([A-Za-z_][A-Za-z0-9_-]*)\s*\(\)\s*\{', txt)) | \
79           set(re.findall(r'(?m)^\s*function\s+([A-Za-z_][A-Za-z0-9_-]*)\s*\{', txt))
80
81
82def main():
83    retirados = applets_retirados() - NO_VIGILAR
84    if '--list' in sys.argv:
85        print('\n'.join(sorted(retirados)) or '(la receta no retira ningún applet)')
86        return 0
87    if not retirados:
88        print('busybox-vigia: la receta no retira ningún applet — nada que vigilar')
89        return 0
90    rx = re.compile(PRE + '(' + '|'.join(re.escape(a) for a in
91                    sorted(retirados, key=len, reverse=True)) + ')' + POST, re.M)
92    fallos = []
93    for f in RAIZ.rglob('*'):
94        if not f.is_file() or f.suffix not in EXT:
95            continue
96        rel = str(f.relative_to(RAIZ))
97        if any(rel.startswith(s) or ('/' + s) in rel for s in SALTAR):
98            continue
99        try:
100            crudo = f.read_text(errors='ignore')
101        except OSError:
102            continue
103        if len(crudo) > 900_000:
104            continue
105        propias = funciones_definidas(crudo)
106        txt = sin_comentarios(crudo, f.suffix)
107        lineas = None
108        for m in rx.finditer(txt):
109            ap = m.group(1)
110            if ap in propias:       # una función local del script, no el applet
111                continue
112            antes, despues = txt[:m.start(1)].rstrip(), txt[m.end(1):m.end(1) + 1]
113            # Patrón de `case`, no invocación: `proc|sys|dev|init) continue ;;`
114            if antes.endswith('|') and despues == ')':
115                continue
116            # Comprobación explícita de existencia: quien escribe `command -v X` ya tolera que falte.
117            ini = txt.rfind('\n', 0, m.start(1)) + 1
118            fin = txt.find('\n', m.end(1))
119            if re.search(r'command\s+-v\s+' + re.escape(ap) + r'\b', txt[ini:fin if fin > 0 else len(txt)]):
120                continue
121            if lineas is None:
122                lineas = txt.splitlines()
123            n = txt[:m.start()].count('\n') + 1
124            fallos.append((rel, n, ap, lineas[n - 1].strip()[:100]))
125    print(f'busybox-vigia: {len(retirados)} applets retirados de la receta')
126    if not fallos:
127        print('busybox-vigia: OK — nadie los invoca')
128        return 0
129    print(f'busybox-vigia: FALLO — {len(fallos)} invocaciones de applets retirados\n')
130    for rel, n, ap, linea in fallos:
131        print(f'  {rel}:{n}  [{ap}]  {linea}')
132    print('\nO se indulta el applet en la receta, o se migra el llamador.')
133    return 1
134
135
136if __name__ == '__main__':
137    sys.exit(main())