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 1518Shell 194Rust 108Markdown 983677 Definitions

⎇ e0d9752e196f0f145cdfa43269d4121e10330381 scripts/fuentes/fuentes-vigia.py Python · 82 lines · 4.1 KB History Who touched what raw

ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad 45b95f78b · Sergio ·

1#!/usr/bin/env python3
2"""Comprueba que las URLs de fuentes de las recetas siguen vivas. Ver ADR 0013 §3.
3
4No descarga: pide cabeceras. Para `git` no hay HEAD que valga, así que usa `git ls-remote
5--exit-code`, que es la pregunta equivalente («¿este remoto sigue ahí y responde?»).
6
7Escribe `docs/state/fuentes-vigia.json`. NO falla si hay URLs muertas: su trabajo es INFORMAR.
8Un vigía que aborta el latido en la primera URL caída deja de correr y se vuelve inútil justo
9cuando más hace falta — que es cuando hay varias caídas.
10"""
11import json, os, re, subprocess, sys, glob
12from concurrent.futures import ThreadPoolExecutor
13
14ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15os.chdir(ROOT)
16LIMITE = int(os.environ.get("LIMITE", "0"))
17JOBS = int(os.environ.get("JOBS", "12"))
18TIMEOUT = int(os.environ.get("TIMEOUT", "25"))
19
20def fuentes():
21    out = []
22    for f in sorted(glob.glob("recipes/**/*.toml", recursive=True)):
23        s = open(f, errors="ignore").read()
24        nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1]
25        t = re.search(r'^\s*tarball\s*=\s*"([^"]+)"', s, re.M)
26        g = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M)
27        if t:   out.append({"receta": nombre, "fichero": f, "tipo": "tarball", "url": t.group(1)})
28        elif g: out.append({"receta": nombre, "fichero": f, "tipo": "git", "url": g.group(1)})
29    return out
30
31def revisa(e):
32    url, tipo = e["url"], e["tipo"]
33    try:
34        if tipo == "tarball":
35            # -I pide cabeceras; algunos servidores no admiten HEAD y responden 405 -> se reintenta
36            # con -r 0-0 (un rango de UN byte), que es un GET mínimo y no descarga el tarball.
37            r = subprocess.run(["curl", "-sIL", "-o", "/dev/null", "-w", "%{http_code}",
38                                "--connect-timeout", str(TIMEOUT), "--max-time", str(TIMEOUT * 2), url],
39                               capture_output=True, text=True, timeout=TIMEOUT * 3)
40            code = (r.stdout or "").strip()[-3:]
41            if code in ("405", "501", "403"):
42                r = subprocess.run(["curl", "-sL", "-r", "0-0", "-o", "/dev/null", "-w", "%{http_code}",
43                                    "--connect-timeout", str(TIMEOUT), "--max-time", str(TIMEOUT * 2), url],
44                                   capture_output=True, text=True, timeout=TIMEOUT * 3)
45                code = (r.stdout or "").strip()[-3:]
46            viva = code.startswith("2") or code.startswith("3")
47            return {**e, "codigo": code or "000", "viva": viva}
48        else:
49            r = subprocess.run(["git", "ls-remote", "--exit-code", url, "HEAD"],
50                               capture_output=True, text=True, timeout=TIMEOUT * 2)
51            return {**e, "codigo": f"git:{r.returncode}", "viva": r.returncode == 0}
52    except Exception as ex:
53        return {**e, "codigo": f"error:{type(ex).__name__}", "viva": False}
54
55def main():
56    fs = fuentes()
57    if LIMITE:
58        fs = fs[:LIMITE]
59    print(f"== vigía de fuentes: {len(fs)} recetas ({sum(1 for f in fs if f['tipo']=='tarball')} tarball, "
60          f"{sum(1 for f in fs if f['tipo']=='git')} git), {JOBS} en paralelo", flush=True)
61    with ThreadPoolExecutor(max_workers=JOBS) as ex:
62        res = list(ex.map(revisa, fs))
63    muertas = [r for r in res if not r["viva"]]
64    # El informe se ordena por receta para que el diff de git sea legible entre corridas.
65    inf = {
66        "schema": "hammer-fuentes-vigia/1",
67        "total": len(res),
68        "vivas": len(res) - len(muertas),
69        "muertas": len(muertas),
70        "detalle_muertas": sorted(muertas, key=lambda r: r["receta"]),
71    }
72    os.makedirs("docs/state", exist_ok=True)
73    with open("docs/state/fuentes-vigia.json", "w") as fh:
74        json.dump(inf, fh, indent=1, ensure_ascii=False)
75        fh.write("\n")
76    print(f"== vivas {inf['vivas']} / {inf['total']}   MUERTAS {inf['muertas']}")
77    for m in inf["detalle_muertas"][:40]:
78        print(f"   {m['codigo']:>12}  {m['receta']:<24} {m['url'][:70]}")
79    # Exit 0 SIEMPRE: informar, no abortar el latido. Ver la docstring.
80    return 0
81
82sys.exit(main())