cierre #2 del SDD 17: hammer why-differs — el diffoscope propio
Cuando un artefacto no reproduce, el store sólo sabe decir "el hash no coincide" y el resto es trabajo artesanal. Esto responde POR QUÉ, en términos de la CAUSA y no del byte: - gzip con MTIME embebido (bytes 4..8) → remedio: `gzip -n` - cabecera `ar` de un `.a` (mtime/uid/gid) → remedio: modo determinista (`ar D`) - secciones ELF, con lectura experta: sólo `.comment` ⇒ otra versión de compilador; sólo `.debug_*` ⇒ rutas de build; sólo `.symtab`/`.dynsym` ⇒ orden de símbolos (código idéntico); sólo build-id ⇒ residuo, no causa raíz. En un `.a` dice QUÉ MIEMBRO difiere. - ruta del árbol de build embebida, texto (línea que difiere), y bytes como último recurso. Y sobre todo trae la EVIDENCIA, no sólo la hipótesis: para las secciones de texto extrae las cadenas que están en un ELF y no en el otro. Caso real que lo motivó (alsa-lib): la interpretación decía "típicamente rutas de build" y la evidencia mostró `/src/target/release/build/libsodium-sys-<hash-cargo>/out/…`. Sin la cadena era una corazonada; reproducir eso a mano cuesta varios readelf, la herramienta lo da en 40ms. Descenso, no comparación total: sólo baja donde los hashes difieren (el cruce con format/ reconcile del SDD 17). Sin dependencias externas — parsers gzip/ar/ELF propios, como manda el ADR 0004: un diffoscope de verdad se apoya en medio mundo de binarios ajenos. `--json` para el bucle agéntico; exit 0 si reproduce, 1 si diverge (encadenable en scripts). `scripts/why-differs-barrido.sh` lo pasa por todo el store y separa los dos casos que se confunden a ojo: recipe.toml distinto (divergencia esperada) vs recipe.toml IDÉNTICO y artefacto distinto (no-reproducción a investigar). 5 tests nuevos; los 142 de hammer-core siguen en verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/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 = NO-REPRODUCCIÓN REAL.**
|
||||
Eso es un bug de reproducibilidad, y el informe ya dice la causa (gzip mtime, cabecera ar,
|
||||
sección ELF, ruta de build embebida…).
|
||||
|
||||
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}
|
||||
|
||||
|
||||
def main():
|
||||
if not HAMMER.exists():
|
||||
sys.exit(f"falta {HAMMER} (cargo build --release --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_reproducen = [] # misma receta, distinto artefacto ⇒ bug real
|
||||
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
|
||||
else:
|
||||
no_reproducen.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, artefacto distinto (NO REPRODUCE): {len(no_reproducen)}")
|
||||
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_reproducen:
|
||||
print("\n══ NO-REPRODUCCIONES (misma receta ⇒ el artefacto debería ser idéntico):")
|
||||
for nombre, ha, hb, divs in no_reproducen:
|
||||
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_reproducen": [
|
||||
{"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in no_reproducen
|
||||
],
|
||||
}, indent=2, ensure_ascii=False))
|
||||
print(f"\n informe → {SALIDA_JSON}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user