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.
167 lines
11 KiB
Bash
Executable File
167 lines
11 KiB
Bash
Executable File
#!/bin/sh
|
|
# build-farm.sh — Etapa G: GRANJA de build paralela sobre la COLA DE STAGING (recipes/incoming/).
|
|
# El import escala (import-batch ~95% yield, cientos en minutos); el COMPILE es el cuello de botella
|
|
# (1-7 min c/u) pero es embarrassingly-parallel + cacheable por contenido (el store dedup global). Esta
|
|
# es la pieza que convierte "decenas por sesión serial" en "drenar la cola en paralelo": construye N a
|
|
# la vez, y lo que CONSTRUYE+sella lo PROMUEVE incoming/->recipes/ y lo publica al repo firmado; lo que
|
|
# falla queda staged con su motivo (MSRV, dep faltante, …) para revisión.
|
|
#
|
|
# Dos fases: (1) BUILD en paralelo (lo caro, worker pool de $JOBS) — escribe status por paquete; (2)
|
|
# PROMOTE+PUBLISH en SERIE (el index.json del repo es mutable compartido ⇒ no se paraleliza) + firma 1 vez.
|
|
# Idempotente: una receta ya sellada sale por cache-hit; re-correr sólo reintenta lo que falta.
|
|
#
|
|
# Uso: JOBS=4 scripts/build-farm.sh # drena recipes/incoming/*.toml
|
|
# JOBS=6 QUEUE=recipes/incoming scripts/build-farm.sh
|
|
# Env: JOBS (def: nproc-2), QUEUE (def recipes/incoming), STORE (def ./store), REPO (def dist/repo),
|
|
# KEY (clave de firma; def dist/keys/release.ed25519), DISTRO (def dev), HAMMER (cmd del cli),
|
|
# PROMOTE (def 1; =0 sólo construye+mide, no toca recipes/ ni el repo).
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
|
|
QUEUE="${QUEUE:-recipes/incoming}"
|
|
STORE="${STORE:-./store}"
|
|
REPO="${REPO:-$ROOT/dist/repo}"
|
|
KEY="${KEY:-$ROOT/dist/keys/release.ed25519}"
|
|
DISTRO="${DISTRO:-dev}"
|
|
HAMMER="${TAKANA:-${HAMMER:-$ROOT/target/release/takana}}"
|
|
PROMOTE="${PROMOTE:-1}"
|
|
JOBS="${JOBS:-$(( $(nproc) - 2 ))}"; [ "$JOBS" -ge 1 ] || JOBS=1
|
|
|
|
# Un subdir POR COLA: el worker-loop corre varias colas en serie por ciclo y con un dir compartido
|
|
# la cola siguiente PISA los logs/status de la anterior (imposible diagnosticar fallos de la fase go
|
|
# tras la fase clib). basename de $QUEUE ⇒ work/farm/incoming-go/{log,status}, etc.
|
|
FARM="$ROOT/work/farm/$(basename "$QUEUE")"
|
|
rm -rf "$FARM"; mkdir -p "$FARM/log" "$FARM/status"
|
|
|
|
queue=$(ls "$QUEUE"/*.toml 2>/dev/null || true)
|
|
[ -n "$queue" ] || { echo "cola vacía: $QUEUE/*.toml"; exit 0; }
|
|
|
|
# ── NO REHACER LO QUE EL HUB YA SELLÓ ──────────────────────────────────────────────────────────
|
|
# `farm-sync.sh` sincroniza el store en UN SOLO SENTIDO: sube el código EXCLUYENDO /store y baja el
|
|
# store del worker. O sea que el worker nunca recibe los artefactos que se sellan en el hub — y por
|
|
# tanto reintenta cada ciclo, y vuelve a fallar, trabajo que YA ESTÁ HECHO. Medido el 2026-08-07:
|
|
# de 20 «fallos» de incoming-gnome, 17 correspondían a recetas con artefacto vigente en el hub.
|
|
# No eran marcadores rancios (el status se borra en cada ciclo): era CPU quemada de verdad.
|
|
#
|
|
# Mandarle los artefactos costaría gigabytes por un enlace de 8 Mbps. Mandarle la LISTA cuesta unos
|
|
# kilobytes: `work/farm-sellados.txt` son los nombres `<hash>-<paquete>` del store del hub. Si el
|
|
# hash vigente de una receta está ahí, el hub ya la tiene y el worker se la salta.
|
|
SELLADOS="${SELLADOS:-work/farm-sellados.txt}"
|
|
if [ -s "$SELLADOS" ]; then
|
|
saltadas=0; cerradas=0; filtrada=""
|
|
for f in $queue; do
|
|
# ⚠ EL NOMBRE DEL ARTEFACTO ES EL CAMPO `name` DE LA RECETA, NO EL DEL FICHERO. Son distintos en
|
|
# 17 recetas del catálogo — el arquetipo es `recipes/incoming-kde/qtbase.toml`, cuyo `name` es
|
|
# `qt6-qtbase`, y el artefacto en el store se llama `<hash>-qt6-qtbase`. Usar el basename daba
|
|
# «no sellada» para las 17 y las habría mandado a reconstruir: exactamente el desperdicio que
|
|
# este filtro existe para evitar. (Lo tenía mal en la primera versión de este bloque.)
|
|
# ── RECETAS CERRADAS POR DECISIÓN: el marcador vive EN LA RECETA ─────────────────────────────
|
|
# Una receta puede estar sin sellar porque falta trabajo (deuda) o porque se decidió NO hacerla
|
|
# (alcance). La diferencia importa: la deuda invita a reintentar en cada ciclo; la decisión hay
|
|
# que respetarla, o la granja quema CPU para siempre en algo que nadie va a arreglar.
|
|
# El marcador va en la CABECERA DE LA RECETA, no en una lista aparte, justo para que no puedan
|
|
# desincronizarse: quien lee el porqué y quien decide saltarla miran el mismo texto.
|
|
if grep -q "CERRADA POR DECISIÓN" "$f" 2>/dev/null; then
|
|
cerradas=$((cerradas+1)); continue
|
|
fi
|
|
n=$(awk -F'"' '/^name[[:space:]]*=/{print $2; exit}' "$f")
|
|
[ -n "$n" ] || n=$(basename "$f" .toml)
|
|
H=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null | tail -1 | sed 's/^b3://')
|
|
if [ -n "$H" ] && grep -qx "$H-$n" "$SELLADOS"; then saltadas=$((saltadas+1)); continue; fi
|
|
filtrada="$filtrada $f"
|
|
done
|
|
[ "$cerradas" -gt 0 ] && echo " ($cerradas CERRADAS por decisión — ver la cabecera de cada receta)"
|
|
[ "$saltadas" -gt 0 ] && echo " (saltadas $saltadas ya selladas en el hub — ver la nota del sync unidireccional)"
|
|
queue="$filtrada"
|
|
[ -n "$(echo $queue)" ] || { echo "nada que hacer en $QUEUE: todo sellado en el hub"; exit 0; }
|
|
fi
|
|
n_total=$(printf '%s\n' "$queue" | wc -l | tr -d ' ')
|
|
echo "==> GRANJA: $n_total recetas en $QUEUE, $JOBS workers en paralelo"
|
|
|
|
# ── Fase 1: BUILD en paralelo. Cada worker construye una receta y deja su status (.ok con hash | .fail
|
|
# con motivo). El store sella atómico y cada RECETA usa sources/<name>-<sha> + out/<hash>-<name> propios.
|
|
# CAVEAT: dos recetas DISTINTAS que comparten una dep transitiva NO sellada (p.ej. gtk4 bajo libadwaita
|
|
# y gtksourceview) la construyen en el MISMO sources/<dep>-<sha>/output ⇒ chocan (meson/tar). Eso lo
|
|
# concilia la Fase 1b (reintento serial). `xargs -P` es el pool.
|
|
printf '%s\n' "$queue" | xargs -P"$JOBS" -I '{}' sh -c '
|
|
f="$1"; n=$(basename "$f" .toml); log="'"$FARM"'/log/$n.log"
|
|
H=$("'"$HAMMER"'" --store "'"$STORE"'" build "$f" 2>"$log" | tail -1 || true)
|
|
if [ "${H#b3:}" != "$H" ] && [ -n "$H" ]; then
|
|
echo "$H" > "'"$FARM"'/status/$n.ok"; printf " ✓ %-18s %s\n" "$n" "$H"
|
|
else
|
|
r=$(grep -oiE "requires rustc [0-9.]+|cannot find -l[a-z0-9]+|could not find .[a-z0-9_-]+.|error\[E[0-9]+\]|not found|unsupported|no such file" "$log" | head -1)
|
|
echo "${r:-fallo (ver log)}" > "'"$FARM"'/status/$n.fail"; printf " ✗ %-18s %s\n" "$n" "${r:-fallo}"
|
|
fi
|
|
' _ '{}'
|
|
|
|
# ── Fase 1b: REINTENTO SERIAL de fallos por COLISIÓN de concurrencia. El pool paralelo puede hacer que
|
|
# dos recetas construyan la MISMA dep transitiva compartida (gtk4/cairo/pango bajo varios consumidores)
|
|
# a la vez en sources/<dep>-<sha>/output ⇒ meson/tar chocan ("Some other Meson process is already using
|
|
# this build directory", "Directory not empty", "No such file"). Tras la Fase 1 la dep ya la selló el
|
|
# ganador de la carrera ⇒ reconstruir el fallo EN SERIE ahora cache-hitea la dep y compila sin colisión.
|
|
# Un solo pase basta: cada `takana build` arrastra sus deps, así que aunque el orden no sea topológico,
|
|
# la dep se construye/sella dentro del propio reintento. Sólo se reintenta lo que tiene FIRMA de colisión
|
|
# (no MSRV/dep-faltante reales, que re-fallarían y sólo gastarían tiempo).
|
|
for s in "$FARM"/status/*.fail; do
|
|
[ -f "$s" ] || continue
|
|
n=$(basename "$s" .fail); log="$FARM/log/$n.log"; f="$QUEUE/$n.toml"
|
|
[ -f "$f" ] || continue
|
|
# ── `File name too long` TAMBIÉN es colisión (2026-08-09) ────────────────────────────────────
|
|
# `vulkan-loader` fallaba con «io: File name too long (os error 36)» en TODOS los ciclos de la
|
|
# granja y sella sin una queja ejecutado en serie — comprobado a mano en el worker. La firma no
|
|
# estaba en esta lista, así que nunca se reintentaba: fallaba, se contaba como deuda, y al ciclo
|
|
# siguiente volvía a fallar igual. Un fallo que se repite idéntico cada vez no es un fallo
|
|
# intermitente, es uno que nadie está reintentando.
|
|
if ! grep -qiE "Some other Meson process|already using this build directory|Directory not empty|No such file or directory|gdkconfig\.h|File name too long|os error 36" "$log" 2>/dev/null; then
|
|
# ── Y LO QUE NO SE REINTENTA, SE DICE ────────────────────────────────────────────────────────
|
|
# El defecto de fondo no era la firma que faltaba sino que faltarla fuese MUDO: la lista sólo
|
|
# puede crecer si alguien se entera de que se quedó corta. Sin esta línea, la única señal de que
|
|
# una receta es reintentable era que a alguien se le ocurriera correrla a mano.
|
|
printf " · %-18s no reintentado (firma no reconocida: %s)\n" "$n" \
|
|
"$(head -c 120 "$log" 2>/dev/null | tr '\n' ' ' | tr -s ' ')"
|
|
continue
|
|
fi
|
|
H=$("$HAMMER" --store "$STORE" build "$f" 2>"$log" | tail -1 || true)
|
|
if [ "${H#b3:}" != "$H" ] && [ -n "$H" ]; then
|
|
rm -f "$s"; echo "$H" > "$FARM/status/$n.ok"; printf " ✓ (reintento serial) %-18s %s\n" "$n" "$H"
|
|
fi
|
|
done
|
|
|
|
# ── Fase 2: PROMOTE + PUBLISH en serie (índice del repo = recurso mutable compartido).
|
|
ok=$(ls "$FARM"/status/*.ok 2>/dev/null | wc -l | tr -d ' ')
|
|
fail=$(ls "$FARM"/status/*.fail 2>/dev/null | wc -l | tr -d ' ')
|
|
echo "==> BUILD-YIELD: $ok/$n_total construyen ($fail fallan)"
|
|
|
|
if [ "$PROMOTE" = 1 ] && [ "$ok" -gt 0 ]; then
|
|
echo "==> promoviendo + publicando $ok al repo firmado…"
|
|
for s in "$FARM"/status/*.ok; do
|
|
n=$(basename "$s" .ok)
|
|
# GUARDA DE COLISIÓN: si ya existe una receta canónica en recipes/ y la cola trae
|
|
# OTRA variante con el mismo nombre, NO la promovemos (mv sobrescribiría lo canónico:
|
|
# p.ej. un `curl` mínimo-para-appstream pisando el curl 8.20 full de la que dependen tools).
|
|
# Queda staged en la cola para triage manual; tampoco re-publicamos sobre lo canónico.
|
|
if [ -f "$QUEUE/$n.toml" ] && [ -f "recipes/$n.toml" ]; then
|
|
printf " = %s (ya existe recipes/%s.toml canónica — NO promovido, queda en %s)\n" "$n" "$n" "$QUEUE"
|
|
continue
|
|
fi
|
|
if [ -f "$QUEUE/$n.toml" ]; then
|
|
# mover la receta Y sus patches referenciados (viven junto al .toml en la cola);
|
|
# si no, `pack` no los encuentra en recipes/ y falla toda receta parcheada.
|
|
for p in $(grep -oE '"[^"]+\.patch"' "$QUEUE/$n.toml" | tr -d '"'); do
|
|
[ -f "$QUEUE/$p" ] && mv -f "$QUEUE/$p" recipes/
|
|
done
|
|
mv "$QUEUE/$n.toml" recipes/
|
|
fi
|
|
"$HAMMER" --store "$STORE" pack "recipes/$n.toml" --repo "$REPO" --distro-version "$DISTRO" --build >/dev/null 2>&1 \
|
|
&& printf " + %s\n" "$n" || printf " ! %s (pack falló)\n" "$n"
|
|
done
|
|
"$HAMMER" repo sign --repo "$REPO" --key "$KEY" >/dev/null 2>&1 && echo "==> release re-firmado"
|
|
fi
|
|
|
|
# ── Reporte de fallos (motivo por paquete) para triage.
|
|
if [ "$fail" -gt 0 ]; then
|
|
echo "==> FALLARON ($fail) — quedan staged en $QUEUE:"
|
|
for s in "$FARM"/status/*.fail; do printf " ✗ %-18s %s\n" "$(basename "$s" .fail)" "$(cat "$s")"; done
|
|
fi
|
|
echo "==> granja: $ok promovidos, $fail staged. logs en $FARM/log/"
|