Files
sergioandClaude Opus 5 d70098ade5 GTK3: decisión del usuario — NO entra; gnome-session, gsd y gdm quedan CERRADAS por escrito
Decidido el 2026-08-08. Las tres recetas dependen de GTK3 (gnome-settings-daemon además de
gtk+-x11-3.0) y quedan cerradas, **no «en deuda»**. La diferencia no es semántica: una deuda
invita a reintentarla en cada informe y en cada ciclo de granja; una decisión se respeta.

POR QUÉ ES RAZONABLE Y NO UNA RENDICIÓN:
· GNOME funciona sin esto — el camino vivo es mutter → gnome-shell arrancado por arje, y
  ninguno de los dos depende de gnome-session (su cierre es mutter+gjs+gobject-introspection+
  gnome-desktop).
· Traer GTK3 sería una torre de C muerta —y para waybar además gtkmm, sus bindings de C++—
  por un gestor de sesión y un login manager que este escritorio no necesita.
· La función está cubierta: la sesión la levanta arje y la barra de estado es `yambar`, C puro,
  sellado anoche en el frente wlr.
Se reabre si alguien trae GTK3 por otro motivo con peso propio (una app gráfica que lo exija).

EL MARCADOR VIVE EN LA RECETA, NO EN UNA LISTA APARTE. `build-farm.sh` salta las recetas cuya
cabecera dice «CERRADA POR DECISIÓN». Podría haber hecho un fichero de exclusiones, pero una
lista y una cabecera se desincronizan solas: así, quien lee el porqué y quien decide saltarla
miran EL MISMO TEXTO. Y el drenador lo dice en su salida, para que la decisión sea visible en
vez de silenciosa.

Con esto la granja deja de quemar CPU en tres recetas que nadie va a arreglar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:31:08 -04:00

153 lines
9.7 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="${HAMMER:-$ROOT/target/release/hammer}"
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 `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/"