Files
takana/scripts/reconstruir-corpus.sh
T
SergioandClaude Opus 5 4996807db0 corpus: el vigia de disco miraba un solo sistema de ficheros
Media `df` sobre el repo y nada mas. Pero las recetas Rust no gastan solo ahi:
`cargo vendor` copia DESDE `~/.cargo/registry`, que en gioser esta en `/` (74 G)
y no en `/mnt/vvv` (255 G). Hoy eso eran 138 G libres segun el vigia y 19 G de
verdad — luz verde mientras el que se llenaba era el otro, y llenar `/` no
rompe una tanda, rompe la maquina.

Ahora toma el MINIMO de los dos FS (el del repo y el de CARGO_HOME). Si
comparten FS, `df` devuelve lo mismo dos veces y es inocuo.

La purga tambien se queda corta y crece:
- `work/repos/*`, los clones --mirror de gitea de las hub-only. Se re-clonan.
- `$CARGO_HOME/registry` como ULTIMO recurso y CON GUARDA: el act-runner de
  tawasuyu corre como el mismo usuario y comparte ese ~/.cargo. Purgarlo con un
  `cargo` ajeno en vuelo le arranca los ficheros a SU compilador. Se pregunta
  por el codigo de salida de `pgrep`, no por su stdout.

De paso queda dicho en la cabecera que CARGO_HOME se vigila: en gioser el
driver se lanza con el suyo propio dentro del volumen, y asi hammer deja de
gastar `/`.

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

209 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="$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 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"