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>
118 lines
4.5 KiB
Bash
Executable File
118 lines
4.5 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 = 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()
|