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…
⎇ e0d9752e196f0f145cdfa43269d4121e10330381
scripts/fuentes/fuentes-vigia.py
History
Who touched what
raw
ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad
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())