Files
takana/scripts/reconstruir-corpus.sh
T
SergioandClaude Opus 5 b653d39550 corpus: ORDEN se puede imponer desde fuera — 17 recetas sin reconstruir 780
El fichero de orden estaba fijo en la linea 69 y lo regeneraba el bloque python
en cada corrida, asi que no habia forma de pasarle una lista concreta. Con el
grafo ya corregido quedan 17 recetas que cierran las SEIS imagenes (base, cli,
sway, mirada, cosmic, gnome) y ninguna esta bloqueada; correr el corpus entero
para llegar a ellas es absurdo.

Ahora `ORDEN_IMPUESTO=1 ORDEN=<fichero>` usa la lista del que llama y hereda
gratis lo que hace util a este script: el flock de la regla 1, el vigia de disco
de dos sistemas de ficheros y el log por receta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xd5DComN9Nu47TBpYMBHBn
2026-08-23 18:28:20 +00:00

218 lines
12 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 40) umbral de purga · ABORT_GB (def 15) aborta antes de llenar el disco
# CARGO_HOME (def ~/.cargo) — se VIGILA su sistema de ficheros además del del repo
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
# ── EL VIGÍA MIRA DOS SISTEMAS DE FICHEROS, NO UNO ──────────────────────────────────────────────
# Medía sólo el FS del repo, y las recetas Rust no gastan sólo ahí: `cargo vendor` copia DESDE
# `~/.cargo/registry`, que en gioser está en `/` (74 G) y no en `/mnt/vvv` (255 G). El 2026-08-20 eso
# era 138 G libres en el repo y 19 G en `/`, o sea que el vigía habría dado luz verde mientras el que
# se llenaba de verdad era el otro — y llenar `/` no rompe una tanda, rompe la máquina. Se toma el
# MÍNIMO de los dos. Si comparten FS, `df` devuelve lo mismo dos veces y esto es inocuo.
CARGO_H="${CARGO_HOME:-$HOME/.cargo}"
avail_gb() { df -BG --output=avail "$1" 2>/dev/null | tail -1 | tr -dc 0-9; }
libre_gb() {
local a b
a=$(avail_gb "$ROOT"); b=$(avail_gb "$CARGO_H")
[ -z "${a:-}" ] && a=999999
[ -z "${b:-}" ] && b=999999
[ "$a" -le "$b" ] && echo "$a" || echo "$b"
}
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 se puede IMPONER desde fuera (2026-08-23): `ORDEN=work/faltan-perfiles.txt` corre una lista
# concreta en vez del corpus entero, y hereda gratis el flock, el vigía de disco y el log de acá.
# Nació de tener 17 recetas sueltas —las que cierran las seis imágenes— y ninguna forma de pasarlas
# sin reconstruir 780. Si el fichero ya existe y no está vacío, NO se regenera: la lista del que
# llama manda. Admite tanto rutas `recipes/…/x.toml` como nombres a secas.
ORDEN="${ORDEN:-$ROOT/work/reconstruccion-orden.txt}"
if [ -s "$ORDEN" ] && [ -n "${ORDEN_IMPUESTO:-}" ]; then
echo " orden IMPUESTO desde $ORDEN ($(grep -c . "$ORDEN") entradas) — no regenero"
else
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
fi
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 y work/repos" | tee -a "$LOG"
rm -rf work/sources/* 2>/dev/null
# `work/repos` son los clones `--mirror` de gitea de las recetas hub-only. Se re-clonan; el coste
# es red, no trabajo perdido.
rm -rf work/repos/* 2>/dev/null
# El registry de cargo es lo ÚLTIMO, y va con guarda: el act-runner de tawasuyu corre como el
# mismo usuario y comparte este `~/.cargo`. Purgarlo con un `cargo` ajeno en vuelo le arranca los
# ficheros a SU compilador — el mismo accidente del kernel a medias, sólo que en el vecino. Se
# pregunta por el CÓDIGO DE SALIDA de `pgrep`, nunca por su stdout.
if [ "$(libre_gb)" -lt "$MIN_LIBRE_GB" ]; then
if pgrep -x cargo >/dev/null 2>&1 || pgrep -x rustc >/dev/null 2>&1; then
echo " sigue corto, pero hay cargo/rustc ajeno en vuelo ⇒ NO toco $CARGO_H/registry" | tee -a "$LOG"
else
echo " sigue corto ⇒ purgo $CARGO_H/registry (caché, se re-descarga)" | tee -a "$LOG"
rm -rf "$CARGO_H"/registry/cache/* "$CARGO_H"/registry/src/* 2>/dev/null
fi
fi
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"