#!/usr/bin/env bash # lab-image.sh — empaqueta y publica el ROOTFS YA BOOTSTRAPEADO como imagen pineada por sha256. # # ── POR QUÉ EXISTE ────────────────────────────────────────────────────────────────────────────── # Desde el 2026-08-10 el toolchain del lab entra en `hash_inputs` (ver `takana-core/src/lab.rs`): # dos máquinas con distinto `rustc` sellan en direcciones distintas. Eso cierra un agujero real # —antes sellaban bytes distintos en la MISMA dirección— pero abre uno operativo: # # Los repos del rootfs apuntan a Alpine **edge**, que es RODANTE. Dos máquinas que corran # `bootstrap-devfs.sh` en fechas distintas resuelven toolchains distintos ⇒ **no comparten NI UN # artefacto**. Ni cosecha de granja, ni `mirror pull` entre hubs, ni cache-hit. Cada worker # efímero nacería con un lab propio y reconstruiría todo desde cero. # # El `apk add` contra edge es, por diseño, irrepetible. Así que el rootfs deja de resolverse y pasa # a **traerse**: una imagen construida UNA vez, pineada por sha256 y compartida por todas las # máquinas — exactamente el trato que ya reciben `alpine-minirootfs` y `zig` en bootstrap-devfs.sh. # # ── EL TAR ES DETERMINISTA A PROPÓSITO ────────────────────────────────────────────────────────── # `--sort=name` + `--mtime=@1` + `--owner=0 --group=0 --numeric-owner`: sin esto el sha256 depende # del orden del directorio y del uid de quien empaqueta, así que dos empaquetados del MISMO rootfs # darían imágenes distintas y no habría forma de verificar que una imagen es la que dice ser. # Con esto, `--crear` sobre el mismo rootfs reproduce el mismo sha, y eso es comprobable. # Ojo: el uid 0 del tar es cosmético — quien extrae sin ser root se queda como dueño igual, y el # sandbox usa `--tmp-overlay /`, así que la propiedad de los ficheros del rootfs no influye. # # Uso: # scripts/lab-image.sh --crear # empaqueta .dev-fs/alpine, imprime el sha256 y lo sube al box # scripts/lab-image.sh --sha # sólo calcula el sha del rootfs actual (no sube nada) # scripts/lab-image.sh --traer # baja la imagen pineada y reemplaza .dev-fs/alpine set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" DEVFS="$ROOT/.dev-fs" ROOTFS="$DEVFS/alpine" # El sha de la imagen VIGENTE. Es el contrato compartido: todas las máquinas deben tener ésta. # Se actualiza a mano tras un `--crear`, igual que ALPINE_SHA256 y ZIG_SHA256 en bootstrap-devfs.sh. LAB_IMAGE_SHA256="${LAB_IMAGE_SHA256:-PENDIENTE}" SB_USER="${SB_USER:-u647150}" SB_HOST="${SB_HOST:-u647150.your-storagebox.de}" SB_PORT="${SB_PORT:-23}" KEY="${KEY:-$HOME/.ssh/github5}" SSH_CMD="ssh -4 -p $SB_PORT -i $KEY -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30" REMOTO="hammer/lab" log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } # ── POR QUÉ SE EXCLUYE `var/log/apk.log` (2026-09-05) ─────────────────────────────────────────── # El tar es determinista a propósito (ver la cabecera), pero `apk` escribe en ese log una línea con # la FECHA de cada operación. O sea: dos rootfs con exactamente los mismos paquetes, los mismos # ficheros, los mismos modos y los mismos hardlinks empaquetan a shas DISTINTOS, sólo porque las # instalaciones ocurrieron en momentos distintos. Eso rompe justo lo que este fichero existe para # dar: si el sha no se puede reproducir desde un rootfs equivalente, no verifica nada. # # Medido el día que se añadió `lld` al lab: hub y worker quedaron con listas de ficheros IDÉNTICAS # (9550 entradas), metadatos idénticos (modo, nlink, destino de symlink: 0 diferencias) y **una sola # diferencia de contenido en todo el rootfs**: `var/log/apk.log`. Con esa exclusión los dos labs # empaquetan igual y se puede DEMOSTRAR que la granja está sincronizada sin reemplazarle el rootfs a # un worker que está construyendo. # # Se excluye el log y no todo `var/log` a propósito: cambio mínimo dirigido. `apk` lo recrea solo en # la siguiente operación. # # COMPARAR DOS MÁQUINAS SIN REEMPLAZARLE EL ROOTFS A NINGUNA — corré esto en las dos y compará: # tar --sort=name --mtime=@1 --owner=0 --group=0 --numeric-owner \ # --exclude alpine/var/log/apk.log -C .dev-fs -cf - alpine | sha256sum # Se compara el TAR y no el `.tar.zst`: el sha comprimido depende de la versión de zstd de cada # máquina, así que dos labs idénticos con zstd distintos darían un falso desacuerdo. empaquetar() { # $1 = destino [ -d "$ROOTFS" ] || { echo "no existe $ROOTFS — corré bootstrap-devfs.sh primero" >&2; exit 1; } tar --sort=name --mtime=@1 --owner=0 --group=0 --numeric-owner \ --exclude alpine/var/log/apk.log \ -C "$DEVFS" -cf - alpine \ | zstd -19 -T0 -q -o "$1" -f } case "${1:---sha}" in --sha) tmp="$(mktemp -p "$DEVFS" lab-image.XXXXXX.tar.zst)" trap 'rm -f "$tmp"' EXIT log "empaquetando (determinista) para calcular el sha…" empaquetar "$tmp" echo "sha256: $(sha256sum "$tmp" | cut -d' ' -f1)" echo "tamaño: $(du -h "$tmp" | cut -f1)" ;; --crear) img="$DEVFS/lab-image.tar.zst" log "empaquetando $ROOTFS (determinista)…" empaquetar "$img" sha="$(sha256sum "$img" | cut -d' ' -f1)" ok "imagen: $(du -h "$img" | cut -f1) · sha256 $sha" log "subiendo a $SB_USER@$SB_HOST:$REMOTO/" $SSH_CMD "$SB_USER@$SB_HOST" "mkdir $REMOTO" >/dev/null 2>&1 || true # El nombre lleva el sha: las imágenes son inmutables y conviven, así que una máquina que # todavía use la anterior no se queda sin su imagen cuando se publica una nueva. rsync -a --partial-dir=.rsync-partial -e "$SSH_CMD" \ "$img" "$SB_USER@$SB_HOST:$REMOTO/lab-$sha.tar.zst" ok "publicada como $REMOTO/lab-$sha.tar.zst" echo echo "AHORA: poné este sha en bootstrap-devfs.sh (LAB_IMAGE_SHA256) y commiteá." echo " LAB_IMAGE_SHA256=\"$sha\"" ;; --traer) [ "$LAB_IMAGE_SHA256" = "PENDIENTE" ] && { echo "no hay imagen pineada todavía: corré --crear en la máquina de referencia" >&2; exit 1; } img="$DEVFS/lab-$LAB_IMAGE_SHA256.tar.zst" if [ -f "$img" ] && [ "$(sha256sum "$img" | cut -d' ' -f1)" = "$LAB_IMAGE_SHA256" ]; then ok "imagen ya presente y verificada" else log "bajando lab-$LAB_IMAGE_SHA256.tar.zst" rsync -a --partial-dir=.rsync-partial -e "$SSH_CMD" \ "$SB_USER@$SB_HOST:$REMOTO/lab-$LAB_IMAGE_SHA256.tar.zst" "$img" real="$(sha256sum "$img" | cut -d' ' -f1)" [ "$real" = "$LAB_IMAGE_SHA256" ] || { echo "!! sha NO coincide: esperaba $LAB_IMAGE_SHA256, llegó $real" >&2 rm -f "$img"; exit 1; } ok "sha verificado" fi # Reemplazo atómico-ish: extraer al lado y rotar. Extraer ENCIMA dejaría una mezcla de los dos # rootfs si algo falla a medias, y una mezcla es justo lo que este fichero existe para evitar. log "extrayendo…" rm -rf "$DEVFS/alpine.nuevo" mkdir -p "$DEVFS/alpine.nuevo" zstd -d -q -c "$img" | tar -x -C "$DEVFS/alpine.nuevo" rm -rf "$DEVFS/alpine.viejo" [ -d "$ROOTFS" ] && mv "$ROOTFS" "$DEVFS/alpine.viejo" mv "$DEVFS/alpine.nuevo/alpine" "$ROOTFS" rmdir "$DEVFS/alpine.nuevo" rm -rf "$DEVFS/alpine.viejo" ok "rootfs reemplazado por la imagen pineada" ;; *) sed -n '2,/^set -euo/p' "$0" | sed 's/^# \?//;$d' exit 0 ;; esac