Va solo y ANTES de cambiar ninguna invocación, a propósito. El worker compila desde fuente (farm-worker-loop.sh) y la siembra excluye /target: si primero cambiara las llamadas a `takana` y después el build, habría una ventana en la que el worker sincroniza scripts nuevos y sigue teniendo sólo el binario viejo — y eso no falla ruidosamente, deja de cosechar en silencio. Con los dos emitidos, cualquier orden de sincronización queda sano.
154 lines
6.6 KiB
Bash
Executable File
154 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env python3
|
|
"""why-differs-barrido — pasa `hammer why-differs` por TODO el store y agrega las causas.
|
|
|
|
Para qué: el store guarda varios sellados por paquete (recetas que cambiaron a lo largo del tiempo).
|
|
Comparar dos sellados del mismo nombre responde una pregunta que hasta ahora nadie hacía a escala:
|
|
**¿por qué difieren?** Y sobre todo separa los dos casos que se confunden a ojo:
|
|
|
|
· `.hammer/recipe.toml` DIFIERE ⇒ son recetas distintas. Que el artefacto cambie es lo esperado.
|
|
· `.hammer/recipe.toml` IDÉNTICO ⇒ misma receta, distinto artefacto: **sospechoso**.
|
|
|
|
CUIDADO CON EL SEGUNDO CASO (y por qué este script no grita "no-reproducción"): el artefacto sólo
|
|
guarda su `recipe.toml`, NO los hashes de sus dependencias — pero el ArtifactHash SÍ los incluye.
|
|
Dos sellados con el mismo `recipe.toml` pueden diferir **legítimamente** porque una dep cambió
|
|
(glib nuevo ⇒ gtk4 se re-sella con contenido distinto). "Mismo recipe.toml, artefacto distinto" es
|
|
un SUPERCONJUNTO de las no-reproducciones, no una prueba.
|
|
|
|
Lo que sí se puede afirmar mirando la CAUSA, y por eso el informe separa dos grupos:
|
|
|
|
· **no-determinismo probable** — todas las divergencias son metadatos que no dependen de las
|
|
deps: MTIME de gzip, cabecera `ar`, ruta de build embebida, o secciones ELF puramente
|
|
informativas (`.debug_*`, `.comment`, `.note.gnu.build-id`, tablas de símbolos). Una dep
|
|
distinta no explica que cambie SÓLO eso.
|
|
· **puede ser una dep** — difiere `.text`/`.data`/`.rodata` o hay ficheros presentes/ausentes.
|
|
Indistinguible de "la dependencia cambió" sin reconstruir. Reconstruir es la única prueba.
|
|
|
|
Es el "multiplicador de granja" del SDD 17 §1.2: debuggear una no-reproducción deja de ser artesanal.
|
|
|
|
Uso: scripts/why-differs-barrido.sh # barre todo el store
|
|
LIMITE=50 scripts/why-differs-barrido.sh # sólo los primeros 50 paquetes
|
|
JSON=informe.json scripts/why-differs-barrido.sh
|
|
"""
|
|
import collections
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
|
|
RAIZ = pathlib.Path(__file__).resolve().parent.parent
|
|
STORE = RAIZ / "store"
|
|
HAMMER = RAIZ / "target/release/hammer"
|
|
LIMITE = int(os.environ.get("LIMITE", "0"))
|
|
SALIDA_JSON = os.environ.get("JSON")
|
|
# Un artefacto puede pesar GB (qt6, kernels): hashearlo entero para el barrido no aporta.
|
|
TIMEOUT = int(os.environ.get("TIMEOUT", "120"))
|
|
|
|
|
|
def sellados_por_nombre():
|
|
"""{nombre: [rutas…]} — el nombre es lo que sigue al hash en `<hash>-<nombre>`."""
|
|
m = collections.defaultdict(list)
|
|
for d in STORE.iterdir():
|
|
if not d.is_dir():
|
|
continue
|
|
_, sep, nombre = d.name.partition("-")
|
|
if sep:
|
|
m[nombre].append(d)
|
|
return {n: sorted(v, key=lambda p: p.stat().st_mtime) for n, v in m.items() if len(v) >= 2}
|
|
|
|
|
|
# Secciones ELF que son metadatos: su contenido no cambia porque una dependencia cambie.
|
|
SECCIONES_META = (".debug", ".zdebug", ".comment", ".note", ".symtab", ".strtab", ".shstrtab")
|
|
# Causas que, por sí solas, no las explica un cambio de dependencia.
|
|
CAUSAS_META = {"gzip_mtime", "ar_cabecera", "ruta_de_build"}
|
|
|
|
|
|
def solo_metadatos(d):
|
|
"""¿Esta divergencia es explicable SÓLO por no-determinismo del build?"""
|
|
if d["causa"] in CAUSAS_META:
|
|
return True
|
|
if d["causa"] != "elf_secciones":
|
|
return False
|
|
return all(s.split(" ")[0].startswith(SECCIONES_META) for s in d["secciones"])
|
|
|
|
|
|
def main():
|
|
if not HAMMER.exists():
|
|
sys.exit(f"falta {HAMMER} (cargo build --release --bin takana --bin hammer)")
|
|
|
|
grupos = sellados_por_nombre()
|
|
nombres = sorted(grupos)
|
|
if LIMITE:
|
|
nombres = nombres[:LIMITE]
|
|
print(f"== barrido why-differs: {len(nombres)} paquetes con ≥2 sellados\n")
|
|
|
|
causas = collections.Counter()
|
|
no_determinismo = [] # misma receta y sólo metadatos ⇒ el build no es determinista
|
|
quiza_dep = [] # misma receta pero difiere código ⇒ pudo cambiar una dependencia
|
|
recetas_distintas = 0
|
|
errores = []
|
|
|
|
for i, nombre in enumerate(nombres, 1):
|
|
a, b = grupos[nombre][-2], grupos[nombre][-1] # los dos más recientes
|
|
try:
|
|
r = subprocess.run(
|
|
[str(HAMMER), "why-differs", str(a), str(b), "--json"],
|
|
capture_output=True, text=True, timeout=TIMEOUT,
|
|
)
|
|
informe = json.loads(r.stdout)
|
|
except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
|
|
errores.append((nombre, type(e).__name__))
|
|
continue
|
|
|
|
divs = informe["divergencias"]
|
|
if not divs:
|
|
continue
|
|
receta_cambio = any(d["ruta"] == ".hammer/recipe.toml" for d in divs)
|
|
for d in divs:
|
|
causas[d["causa"]] += 1
|
|
if receta_cambio:
|
|
recetas_distintas += 1
|
|
elif all(solo_metadatos(d) for d in divs):
|
|
no_determinismo.append((nombre, a.name[:12], b.name[:12], divs))
|
|
else:
|
|
quiza_dep.append((nombre, a.name[:12], b.name[:12], divs))
|
|
if i % 50 == 0:
|
|
print(f" … {i}/{len(nombres)}")
|
|
|
|
print(f"\n── recetas distintas (divergencia esperada): {recetas_distintas}")
|
|
print(f"── misma receta, SÓLO metadatos difieren (no-determinismo probable): {len(no_determinismo)}")
|
|
print(f"── misma receta, difiere código/ficheros (puede ser una dep — no concluyente): {len(quiza_dep)}")
|
|
if errores:
|
|
print(f"── no analizados: {len(errores)} ({errores[:3]}…)")
|
|
|
|
print("\n── causas agregadas:")
|
|
for c, n in causas.most_common():
|
|
print(f" {c:22s} {n}")
|
|
|
|
if no_determinismo:
|
|
print("\n══ NO-DETERMINISMO PROBABLE (misma receta; sólo difieren metadatos que las deps")
|
|
print(" no explican — reconstruir para confirmar):")
|
|
for nombre, ha, hb, divs in no_determinismo:
|
|
print(f"\n {nombre} [{ha}… vs {hb}…] {len(divs)} divergencia(s)")
|
|
for d in divs[:4]:
|
|
print(f" · {d['ruta']}: {d['explicacion'].splitlines()[0][:150]}")
|
|
if d.get("remedio"):
|
|
print(f" → {d['remedio']}")
|
|
|
|
if SALIDA_JSON:
|
|
pathlib.Path(SALIDA_JSON).write_text(json.dumps({
|
|
"causas": dict(causas),
|
|
"recetas_distintas": recetas_distintas,
|
|
"no_determinismo": [
|
|
{"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in no_determinismo
|
|
],
|
|
"quiza_dep": [
|
|
{"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in quiza_dep
|
|
],
|
|
}, indent=2, ensure_ascii=False))
|
|
print(f"\n informe → {SALIDA_JSON}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|