La tanda del 2026-08-13 ABORTO con 7 G libres despues de haber purgado a los 25: una sola receta se comio >18 G entre purga y purga. Son los cargo vendor de las recetas Rust grandes, que sueltan varios GB cada una. Con 40/15 la purga ocurre ANTES de que una receta gorda agote lo que queda, en vez de justo despues. El guardian de aborto hizo su trabajo —paro sin llenar el disco y sin corromper nada— pero llegar a el significa perder la tanda; el objetivo es no llegar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
179 lines
9.7 KiB
Bash
Executable File
179 lines
9.7 KiB
Bash
Executable File
#!/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/<dep>-<sha>`. 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}"
|
||
# 25/10 SE QUEDÓ CORTO: el 2026-08-13 la tanda abortó con 7 G tras purgar a los 25 — o sea que UNA
|
||
# sola receta se comió >18 G entre purga y purga. Son los `cargo vendor` de las recetas Rust grandes,
|
||
# que sueltan varios GB cada una. Con 40/15 hay margen para que la purga ocurra ANTES de que una
|
||
# receta gorda agote lo que queda, en vez de justo después.
|
||
MIN_LIBRE_GB="${MIN_LIBRE_GB:-40}"
|
||
ABORT_GB="${ABORT_GB:-15}"
|
||
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"
|
||
|
||
# ── PARTICIÓN ENTRE WORKERS: SHARD=i/N ──────────────────────────────────────────────────────────
|
||
# Con un worker el corpus son ~7 días (medido: ~9 min/receta sobre 1161). Repartirlo es la única
|
||
# forma de bajarlo, y no hace falta planificador central: cada worker construye SU parte y las deps
|
||
# que le falten se las construye `hammer build` solo. Al cosechar, todo converge en el mismo store
|
||
# porque las direcciones coinciden — eso está PROBADO por el paso 4 de `farm-lab-sync.sh`, no
|
||
# supuesto.
|
||
#
|
||
# El reparto es por ÍNDICE (i-1, i-1+N, i-1+2N…), no por bloques contiguos: el orden ya está por
|
||
# objetivo, así que bloques contiguos le darían a un worker todo el tramo GUI —el más lento— y a
|
||
# otro sólo hojas rápidas. Intercalando, los cuatro avanzan por el mismo terreno a la vez.
|
||
#
|
||
# ⚠ HAY TRABAJO DUPLICADO y es deliberado: si a dos workers les tocan recetas que cuelgan de
|
||
# `qtbase`, los dos lo construyen. Sale a cuenta igual frente a serializar, pero no esperes
|
||
# aceleración lineal — con 4 workers no son 4×, son ~3×.
|
||
if [ -n "${SHARD:-}" ]; then
|
||
SH_I="${SHARD%%/*}"; SH_N="${SHARD##*/}"
|
||
case "$SH_I$SH_N" in *[!0-9]*|"") echo "SHARD debe ser i/N (ej. 2/4)" >&2; exit 1;; esac
|
||
[ "$SH_I" -ge 1 ] && [ "$SH_I" -le "$SH_N" ] || { echo "SHARD fuera de rango: $SHARD" >&2; exit 1; }
|
||
SUB=(); k=0
|
||
for f in "${RECETAS[@]}"; do
|
||
[ $(( k % SH_N )) -eq $(( SH_I - 1 )) ] && SUB+=("$f")
|
||
k=$((k + 1))
|
||
done
|
||
RECETAS=("${SUB[@]}")
|
||
LOG="${LOG%.log}-$SH_I-de-$SH_N.log" # un log por shard, o se pisan entre workers
|
||
echo "== SHARD $SH_I/$SH_N ⇒ ${#RECETAS[@]} recetas de este worker" | tee -a "$LOG"
|
||
fi
|
||
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"
|