#!/usr/bin/env python3 """atribuir-fallos.py — dice QUÉ receta EMITE cada error, no cuál lo sufre. El driver marca FALLA en la receta que invocó, pero `hammer build` construye deps recursivamente: si una dep muere, la FALLA sale a nombre del dependiente. El 2026-08-27 siete recetas (xdg-desktop-portal, gnome-desktop, gjs, gdm, gnome-session, gnome-settings-daemon, gnome-shell) figuraban como rotas y ninguna lo estaba: los 1978 errores los emitía `gdk-pixbuf`. Los tiempos de 2-10 s son la firma — demasiado rápido para ser un build propio. Método: quitar ANSI, partir el log por los marcadores `hammer_build: fetch … name=X` y atribuir cada línea de error al último `fetch` anterior. Uso: scripts/atribuir-fallos.py [work/reconstruccion-detalle.log] [--desde 2026-08-27T03:] """ import re, sys, bisect, collections PATRONES = [ ("PIC", r"recompile with -fPIC"), ("_ssl", r"No module named '_ssl'"), ("modulo", r"ModuleNotFoundError: No module named"), ("ninja", r"ninja: subcommand failed"), ("FAILED", r"^FAILED:"), ("fase", r"Error: build phase falló"), ("undef", r"undefined (?:reference|symbol)"), ("nospace", r"No space left on device"), ] log = sys.argv[1] if len(sys.argv) > 1 and not sys.argv[1].startswith("--") else "work/reconstruccion-detalle.log" desde = None if "--desde" in sys.argv: desde = sys.argv[sys.argv.index("--desde") + 1] raw = re.sub(r"\x1b\[[0-9;]*m", "", open(log, encoding="utf-8", errors="replace").read()) lineas = raw.splitlines() if desde: ini = next((j for j, l in enumerate(lineas) if desde in l), 0) lineas = lineas[ini:] marcas = [(i, m.group(1)) for i, l in enumerate(lineas) # Las DOS formas del target de tracing (ADR 0016): el target es el module_path!, o sea # el nombre del crate, que pasó de `hammer_build` a `takana_build` en la etapa 4. Hay que # aceptar las dos porque los logs VIEJOS que ya están en disco dicen la forma vieja — y # porque este script no falla si no casa: atribuye cero fallos y se ve igual que un log # sin problemas. if (m := re.search(r"(?:hammer|takana)_build: fetch hash=\S+ name=(\S+)", l))] if not marcas: print("sin marcadores `fetch` en la ventana — ¿log equivocado o --desde muy tarde?") sys.exit(1) idx = [i for i, _ in marcas] print(f"{len(marcas)} builds arrancados en la ventana\n") tabla = collections.defaultdict(collections.Counter) for etiqueta, pat in PATRONES: rx = re.compile(pat) for i, l in enumerate(lineas): if rx.search(l): k = bisect.bisect_right(idx, i) - 1 tabla[marcas[k][1] if k >= 0 else "(antes de la ventana)"][etiqueta] += 1 if not tabla: print("ningún error reconocido") else: print(f"{'receta que EMITE':32} errores") for receta, c in sorted(tabla.items(), key=lambda kv: -sum(kv[1].values())): det = " ".join(f"{k}={v}" for k, v in c.most_common()) print(f" {receta:30} {det}")