#!/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="${HAMMER:-$ROOT/target/release/hammer}" PROMOTE="${PROMOTE:-1}" JOBS="${JOBS:-$(( $(nproc) - 2 ))}"; [ "$JOBS" -ge 1 ] || JOBS=1 FARM="$ROOT/work/farm" 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; } 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/- + out/- 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/-/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/-/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 `hammer 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 grep -qiE "Some other Meson process|already using this build directory|Directory not empty|No such file or directory|gdkconfig\.h" "$log" 2>/dev/null || continue 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/"