#!/usr/bin/env bash # reconstruir-corpus.sh — reconstruye TODO lo que esté en deuda, en serie y de forma reanudable. # # ── CUÁNDO SE USA ─────────────────────────────────────────────────────────────────────────────── # Cuando algo mueve el `ArtifactHash` de todo el corpus a la vez. Hoy eso es UNA cosa: cambiar la # imagen del lab (`scripts/lab-image.sh --crear` + pinear el sha nuevo), porque desde el 2026-08-10 # el toolchain entra en `hash_inputs`. No es un accidente que sea caro: ése es el precio explícito # de cambiar de compilador, y antes se pagaba sin enterarse (dos labs sellaban bytes distintos en # la misma dirección). # # ── POR QUÉ EN SERIE, Y NO ES NEGOCIABLE ──────────────────────────────────────────────────────── # `hammer build` comparte `work/sources/-`. Dos builds que compartan una dep se pisan el # árbol y queda ROTO PARA SIEMPRE (ADR 0012, sin decidir; ~93 de 205 recetas KDE murieron así al # invalidar libdrm). Todo el run va bajo UN `flock work/.farm-build.lock`, el mismo que toman los # scripts de la granja, así que también nos serializa con ellos y con cualquier otro agente. # # ── REANUDABLE POR CONSTRUCCIÓN ───────────────────────────────────────────────────────────────── # No lleva estado propio: la pregunta "¿esto ya está?" se la hace al store con `hammer hash --check` # (~2 ms, sin construir). Matarlo y relanzarlo continúa donde iba. Un run entero sobre un corpus ya # construido son unos segundos de puros --check. # # ── EL DISCO ES EL LÍMITE REAL, NO LA CPU ─────────────────────────────────────────────────────── # Cada sellado añade un artefacto y cada build deja su árbol en `work/sources/`. Entre receta y # receta —con NADA corriendo, que es cuando es trivialmente seguro— se purgan los árboles si el # disco baja del umbral. Purgar CON un build en vuelo le arranca los ficheros al compilador: pasó # el 2026-08-10 y se llevó por delante un kernel a medias. # # Uso: scripts/reconstruir-corpus.sh [--seco] # Env: MIN_LIBRE_GB (def 25) umbral de purga · ABORT_GB (def 10) aborta antes de llenar el disco set -uo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" HAMMER="${HAMMER:-$ROOT/target/release/hammer}" STORE="${STORE:-./store}" LOCK="$ROOT/work/.farm-build.lock" LOG="${LOG:-$ROOT/work/reconstruccion.log}" MIN_LIBRE_GB="${MIN_LIBRE_GB:-25}" ABORT_GB="${ABORT_GB:-10}" SECO=0; [ "${1:-}" = "--seco" ] && SECO=1 libre_gb() { df -BG --output=avail "$ROOT" 2>/dev/null | tail -1 | tr -dc 0-9; } ts() { date -u +%Y-%m-%dT%H:%M:%SZ; } # EL ORDEN IMPORTA, aunque las deps se resuelvan solas. `hammer build` construye recursivamente lo # que le falta, así que cualquier orden termina — pero no cualquier orden es útil a mitad de camino. # Hay 1186 ficheros de receta y sólo ~440 alcanzan alguna imagen; el resto son catálogo y colas de # staging. Construyendo por orden alfabético, tras un día de máquina no habría NI UNA imagen # completa. Se ordena por objetivo: primero las clausuras de los perfiles de `targets.toml` # (base, cli, escritorio-*), después todo lo demás. Así cada perfil que cierra es utilizable ya. ORDEN="$ROOT/work/reconstruccion-orden.txt" python3 - "$ORDEN" <<'PY' import json, os, sys, subprocess # Las raíces salen de `targets.toml` vía su expansor oficial (scripts/targets.py), NO de los nodos # de build-state: ese grafo es el corpus ENTERO, así que usarlo marcaba 1161 de 1161 como # prioritarias y el orden no ordenaba nada. `targets.py` da lo que una imagen pide por nombre. PERFILES = ["base", "cli", "escritorio-mirada", "escritorio-sway", "escritorio-cosmic", "escritorio-kde", "escritorio-gnome"] prioritarios, orden_perfil = set(), [] for p in PERFILES: r = subprocess.run(["python3", "scripts/targets.py", p], capture_output=True, text=True) if r.returncode != 0: continue nombres = [l.strip() for l in r.stdout.split() if l.strip()] orden_perfil.append((p, len(nombres))) prioritarios.update(nombres) # La CLAUSURA (las libs de las que cuelgan las raíces) se saca del grafo siguiendo las aristas. d = json.load(open("docs/state/build-state.json")) nodos = d.get("nodes", {}) pila, vistos = list(prioritarios), set() while pila: x = pila.pop() if x in vistos: continue vistos.add(x) pila += nodos.get(x, {}).get("deps", []) prioritarios = vistos # os.walk y no glob: `recipes/**/*.toml` se dejaba 25 ficheros fuera y no se nota mirando el total. # # `.deferred/` FUERA: son recetas APARCADAS por diseño y no construyen sueltas — sus `deps.build` # resuelven contra el directorio hermano, que ahí no existe, así que fallan al instante con # «no pude cargar la dep de build 'zlib' de 'git' (recipes/incoming/.deferred/zlib.toml)». # Son 25 y cada una gasta un intento. Intentarlas no es sólo inútil: mete 25 FALLA en el log que # parecen roturas del corpus y esconden las de verdad. todas = [] for dirpath, _, files in os.walk("recipes"): if ".deferred" in dirpath.split(os.sep): continue todas += [os.path.join(dirpath, f) for f in files if f.endswith(".toml")] todas.sort() # El nombre del artefacto es el campo `name`, no el del fichero (17 recetas difieren), pero para # ORDENAR alcanza el basename: equivocarse acá sólo cambia la prioridad, nunca la corrección. pri = [f for f in todas if os.path.basename(f)[:-5] in prioritarios] resto = [f for f in todas if os.path.basename(f)[:-5] not in prioritarios] with open(sys.argv[1], "w") as fh: fh.write("\n".join(pri + resto) + "\n") print(f" orden: {len(pri)} de perfil primero, {len(resto)} de catálogo después") PY mapfile -t RECETAS < "$ORDEN" echo "== $(ts) reconstrucción: ${#RECETAS[@]} recetas · libre $(libre_gb) G" | tee -a "$LOG" if [ "$SECO" = "1" ]; then pend=0 for f in "${RECETAS[@]}"; do "$HAMMER" --store "$STORE" hash "$f" --check >/dev/null 2>&1 || pend=$((pend+1)) done echo " en deuda: $pend de ${#RECETAS[@]}" | tee -a "$LOG" exit 0 fi exec 9>"$LOCK" echo "== esperando el lock de build…" | tee -a "$LOG" flock 9 echo "== $(ts) lock tomado" | tee -a "$LOG" ok=0; falla=0; cache=0; i=0 for f in "${RECETAS[@]}"; do i=$((i+1)) n="$(basename "$f" .toml)" if "$HAMMER" --store "$STORE" hash "$f" --check >/dev/null 2>&1; then cache=$((cache+1)); continue fi l=$(libre_gb) if [ "${l:-0}" -lt "$ABORT_GB" ]; then echo "!! $(ts) ABORTO: quedan ${l} G (< ${ABORT_GB}). Sellado $ok, fallo $falla." | tee -a "$LOG" exit 1 fi if [ "${l:-0}" -lt "$MIN_LIBRE_GB" ]; then # Seguro por construcción: acá NO hay ningún build corriendo (somos el único, y en serie). echo " [$(ts)] libre ${l} G < ${MIN_LIBRE_GB} ⇒ purgo work/sources" | tee -a "$LOG" rm -rf work/sources/* 2>/dev/null echo " libre tras purgar: $(libre_gb) G" | tee -a "$LOG" fi t0=$(date +%s) if "$HAMMER" --store "$STORE" build "$f" >>"$ROOT/work/reconstruccion-detalle.log" 2>&1; then ok=$((ok+1)); estado=OK else falla=$((falla+1)); estado=FALLA fi printf '%s [%4d/%4d] %-6s %-40s %4ds libre %sG\n' \ "$(ts)" "$i" "${#RECETAS[@]}" "$estado" "$n" "$(( $(date +%s) - t0 ))" "$(libre_gb)" | tee -a "$LOG" done echo "== $(ts) FIN · sellado $ok · fallo $falla · ya-estaba $cache · libre $(libre_gb) G" | tee -a "$LOG"