Files
takana/scripts/reconstruir-corpus.sh
T
SergioandClaude Opus 5 3848e5c945 corpus: SHARD=i/N para repartir la reconstruccion entre workers
Con un worker el corpus son ~7 dias (medido: ~9 min/receta sobre 1161).
Repartirlo es la unica 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 lo PRUEBA el paso 4 de
farm-lab-sync.sh, no se supone.

El reparto es por INDICE (i-1, i-1+N, ...) y no por bloques contiguos: el
orden ya esta por objetivo, asi que bloques contiguos le darian a un worker
todo el tramo GUI —el mas lento— y a otro solo hojas rapidas. Intercalando,
todos avanzan por el mismo terreno a la vez.

⚠ Hay trabajo DUPLICADO y es deliberado: dos workers con recetas que cuelgan
de qtbase lo construyen los dos. Sale a cuenta frente a serializar, pero con
4 workers no son 4x, son ~3x.

Un log por shard, o se pisan. Verificado: los 4 shards suman 1161 exactas y
rechaza 5/4 y `abc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:45:41 +00:00

175 lines
9.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}"
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"
# ── 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"