#!/bin/sh # traer-artefactos.sh — trae del worker los artefactos VIGENTES que al hub le faltan, por receta. # # ══ POR QUÉ EXISTE ═════════════════════════════════════════════════════════════════════════════ # La cosecha **ya no baja el store** (es lo que evita que el disco del hub se llene), así que lo que # el worker sella se queda allá hasta que alguien lo pida. Y el hub SÍ lo necesita: es donde se # arman las imágenes y donde `cierre-perfiles.py` mide la clausura. # # Lo que había era todo o nada: `farm-sync.sh` baja el store ENTERO y `takana mirror pull` quiere una # RUTA local (no sabe de ssh). Traer tres artefactos era copiar a mano un `rsync` con cuatro # precauciones que se olvidan — y las olvidé una vez cada una antes de escribir esto. # # ══ LAS CUATRO PRECAUCIONES, TODAS APRENDIDAS A GOLPES ═════════════════════════════════════════ # 1. **`-H`**, que `-a` NO incluye. El store es content-addressed y `.dmerge` hardlinkea: sin # `-H` cada enlace viaja y se escribe como copia entera, y el tamaño del destino no se parece # al que informa `du` en el origen (medido: 25 G de origen llenaron 95 G de destino). # 2. **Excluir `.dmerge`**: es caché pura que takana reconstruye sola. Copiarla no aporta un # artefacto y es la parte más pesada del árbol. # 3. **Aterrizar por `.incoming-*` y después `mv`.** Un directorio de artefacto a medias es # EXACTAMENTE un cache-hit envenenado (regla 3 del CLAUDE.md): `build` mira presencia, así que # un árbol vacío o truncado se da por bueno para siempre. El `mv` dentro del mismo filesystem # es atómico. # 4. **No pisar lo que ya está.** `mv A B` con B existente lo ANIDA (B/A), y entonces el # artefacto queda con un nivel de más: presente, con contenido, y mintiendo. # # ══ QUÉ CUENTA COMO VIGENTE ════════════════════════════════════════════════════════════════════ # El hash lo calcula `takana hash` sobre la receta DEL HUB, que es la fuente de verdad del # contrato. No se lee el nombre del directorio del worker ni su manifiesto: si el worker selló otra # cosa, lo que queremos saber es justamente que no coincide, no copiarla. # # Uso: scripts/farm/traer-artefactos.sh ... # scripts/farm/traer-artefactos.sh --lista fichero-con-una-receta-por-linea # Env: WORKER (def dev.gioser.net) · SSH_KEY (def ~/.ssh/id_ed25519) · STORE (def /store) # REMOTE_STORE (def /opt/takana/store) · TAKANA (def ./target/release/takana) set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" WORKER="${WORKER:-dev.gioser.net}" SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}" STORE="${STORE:-/store}" REMOTE_STORE="${REMOTE_STORE:-/opt/takana/store}" TAKANA="${TAKANA:-$ROOT/target/release/takana}" SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=20" [ -x "$TAKANA" ] || { echo "no encuentro el binario en $TAKANA" >&2; exit 1; } recetas="" if [ "${1:-}" = "--lista" ]; then [ -r "${2:-}" ] || { echo "no puedo leer la lista ${2:-}" >&2; exit 2; } recetas=$(grep -vE '^\s*(#|$)' "$2") else [ $# -gt 0 ] || { sed -n '/^# Uso:/,/^# Env:/p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2; } recetas="$*" fi # Resolver nombre → ruta de receta. Una receta puede vivir en el corpus o en una cola, y acá eso # importa: pedir `recipes/.toml` para algo que está en `incoming-kde` sale # `io: No such file or directory`, que no se lee como «está en otro sitio». Lo aprendí perdiendo # seis builds de una tanda de 22. ruta_de() { case "$1" in *.toml) [ -f "$1" ] && echo "$1"; return ;; esac [ -f "recipes/$1.toml" ] && { echo "recipes/$1.toml"; return; } for d in recipes/incoming-*; do [ -f "$d/$1.toml" ] && { echo "$d/$1.toml"; return; } done return 1 } ya=0; traidos=0; faltan=0; vacios=0 for r in $recetas; do f=$(ruta_de "$r") || { printf '%-26s ⚠ no encuentro la receta (ni en el corpus ni en las colas)\n' "$r"; faltan=$((faltan+1)); continue; } n=$(sed -n 's/^name[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$f" | head -1) [ -n "$n" ] || n=$(basename "$f" .toml) h=$("$TAKANA" --store "$STORE" hash "$f" 2>/dev/null | tail -1 | sed 's/^b3://') case "$h" in ''|*[!0-9a-f]*) printf '%-26s ⚠ no pude calcular el hash\n' "$r"; faltan=$((faltan+1)); continue ;; esac par="$h-$n" if [ -d "$STORE/$par" ]; then printf '%-26s ya está en el hub\n' "$n"; ya=$((ya+1)); continue; fi tmp="$STORE/.incoming-$par" rm -rf "$tmp" if ! rsync -azH --exclude '/.dmerge' -e "$SSH" "root@$WORKER:$REMOTE_STORE/$par/" "$tmp/" 2>/dev/null; then printf '%-26s ⚠ no está en el worker (o el rsync falló): %s\n' "$n" "$h" rm -rf "$tmp"; faltan=$((faltan+1)); continue fi c=$(find "$tmp" ! -type d 2>/dev/null | wc -l) if [ "$c" -eq 0 ]; then printf '%-26s ✗ llegó VACÍO — no lo sello (sería un cache-hit envenenado)\n' "$n" rm -rf "$tmp"; vacios=$((vacios+1)); continue fi mv "$tmp" "$STORE/$par" printf '%-26s ✓ %s entradas · %s\n' "$n" "$c" "$(du -sh "$STORE/$par" 2>/dev/null | cut -f1)" traidos=$((traidos+1)) done echo "== ya estaban $ya · traídos $traidos · sin traer $faltan · vacíos $vacios" # Un vacío NO es lo mismo que un ausente y por eso se cuentan aparte: el ausente falla ruidosamente # la próxima vez que alguien lo pida; el vacío, si se hubiera sellado, se daría por bueno para # siempre. Sale ≠0 si algo quedó sin traer, para que un llamador en un guion se entere. [ "$faltan" -eq 0 ] && [ "$vacios" -eq 0 ]