Files
takana/scripts/reconstruir-corpus.sh
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

236 lines
14 KiB
Bash
Executable File
Raw Permalink 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 ────────────────────────────────────────────────────────
# `takana 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 `takana 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="${TAKANA:-${HAMMER:-$ROOT/target/release/takana}}"
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.
#
# ── 2026-08-27: MIRA DÓNDE SE GASTA, NO DÓNDE ESTÁ EL REPO ──────────────────────────────────────
# Medir `$ROOT` dejó de ser correcto al mudar el store al volumen `harkaq-cosecha`. `$ROOT` es
# /mnt/vvv, que takana COMPARTE con tawasuyu, y tawasuyu repuebla su target a ~62 G/h: el 27/08 se
# comió los 92 G que se le habían liberado en 70 minutos y el vigía abortó la tanda por un disco
# que takana ya ni usaba. Un vigía que mide el disco de OTRO no protege la tanda, la mata.
# Ahora se miden los tres sitios donde takana SÍ crece —store, árboles de fuentes y CARGO_HOME—
# y se toma el mínimo. Si comparten FS, `df` repite el número y esto es inocuo.
CARGO_H="${CARGO_HOME:-$HOME/.cargo}"
# ── GOPATH TAMBIÉN CRECE, y llenó `/` hasta 0 BYTES el 2026-08-28 ───────────────────────────────
# El vigía miraba store/sources/CARGO_HOME pero NO la caché de módulos de Go. Esa noche pasó de
# 498 MB a **25 G** construyendo las recetas Go y dejó `/dev/sda2` al 100% — sin espacio ni para el
# resto de la máquina (gitea, caddy). Los builds Go morían con
# `write /home/sergio/go/pkg/mod/cache/download/…: no space left on device`, que NO se parece a un
# fallo de disco del build porque la ruta no es del repo. Se movió a `/mnt/cosecha/gopath` (symlink,
# igual que ~/.cargo) y se añade acá para que el vigía lo vea aunque alguien lo devuelva a `/`.
GO_MODCACHE="${GOMODCACHE:-$(go env GOMODCACHE 2>/dev/null || echo "$HOME/go/pkg/mod")}"
avail_gb() { df -BG --output=avail "$1" 2>/dev/null | tail -1 | tr -dc 0-9; }
libre_gb() {
local m=999999 v d
for d in "$STORE" "$ROOT/work/sources" "$CARGO_H" "$GO_MODCACHE"; do
[ -e "$d" ] || continue
v=$(avail_gb "$d"); [ -z "${v:-}" ] && continue
[ "$v" -lt "$m" ] && m=$v
done
echo "$m"
}
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
# EL ORDEN IMPORTA, aunque las deps se resuelvan solas. `takana 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 `takana 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"