sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ b89695739b0547d8d01b3438a9d800affd2fdf61
scripts/product-base-system-boot-test.sh
Ñawpa willay
ch'uya
1#!/usr/bin/env bash
2# product-base-system-boot-test.sh — E2E de la INTEGRACIÓN del userland foundational (base-system) al
3# producto: toma el product-rootfs sellado (arje-zero PID1 + sshd + userland Rust), le HIDRATA encima
4# los paquetes C del base-system (bash/git/sudo/util-linux/shadow/red/disco/TLS/gpg+pinentry/proceso/editor)
5# desde sus artefactos sellados del store, lo bootea en QEMU y verifica —DENTRO del guest booteado, por
6# SSH— que esos binarios están presentes y CORREN. Es la prueba de que "el sistema instalado los trae".
7#
8# La hidratación usa `cp -an` (no-clobber): añade binarios/ficheros nuevos pero PRESERVA los configs del
9# producto (/etc/passwd, /etc/shadow, /etc/ssh/*, la seed card) ⇒ el login SSH y arje-zero intactos.
10#
11# Uso: ./scripts/product-base-system-boot-test.sh
12# Vars: PRODUCT (hash del product-rootfs; def el más reciente) KERNEL MEM (def 2048) KVM (def 1)
13# PORT (def 2224) DEADLINE (def 150)
14set -euo pipefail
15ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
16KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
17MEM="${MEM:-2048}"; KVM="${KVM:-1}"; PORT="${PORT:-2224}"; DEADLINE="${DEADLINE:-150}"
18WORK="$ROOT/work/product-base-system"; RFS="$WORK/rootfs"; LOG="$WORK/console.log"
19
20if [ -n "${PRODUCT:-}" ]; then PDIR=$(ls -d store/"${PRODUCT#b3:}"*-product-rootfs 2>/dev/null | head -1)
21else PDIR=$(ls -dt store/*-product-rootfs 2>/dev/null | head -1); fi
22[ -n "$PDIR" ] && [ -d "$PDIR" ] || { echo "no encuentro product-rootfs"; exit 1; }
23[ -n "$KERNEL" ] && [ -e "$KERNEL" ] || { echo "no encuentro kernel"; exit 1; }
24echo "==> product-rootfs base: $PDIR"
25
26rm -rf "$WORK"; mkdir -p "$WORK"
27cp -a "$PDIR"/. "$RFS"/; chmod -R u+w "$RFS"; rm -rf "$RFS/.hammer"
28
29# --- hidratar el base-system encima (no-clobber: preserva configs del producto) ---
30BASE_SYSTEM="bash git sudo doas util-linux shadow kbd procps-ng iproute2 iputils dhcpcd e2fsprogs dosfstools parted rsync sed tzdata vim dbus ca-certificates gnupg pinentry lsof strace pciutils usbutils mandoc"
31# Hidratación NO-CLOBBER: añade los binarios nuevos del base-system (bash/git/gpg/useradd/vim/…) pero
32# NO pisa nada del producto — ni configs (/etc/passwd, /etc/shadow, /etc/ssh) ni libs/applets. Preserva
33# el ABI musl del sshd del producto y su login (un clobber ciego de /usr/lib lo ROMPE: arje-zero+hammerd
34# arrancan pero sshd/login queda mudo — comprobado 2026-07-10).
35#
36# NOTA de empaque (fuera de alcance de este test): los nombres que colisionan con applets de busybox
37# (sed/ps/ip/mount/vi) resuelven a BUSYBOX por precedencia de PATH; los binarios full ESTÁN a sus rutas.
38# Hacer que ganen es política del ensamblado del producto (el mismo mecanismo "adelgazar busybox" que ya
39# apunta /usr/bin/ls→coreutils), que debe re-linkear el applet name a la versión full SIN tocar /usr/lib.
40echo "==> hidratando base-system encima del producto (cp -an, preserva el producto entero)"
41hyd=0
42for n in $BASE_SYSTEM; do
43 d=$(ls -dt store/*-"$n" 2>/dev/null | head -1)
44 [ -n "$d" ] && [ -d "$d" ] || { echo " !! $n sin artefacto"; continue; }
45 for top in bin sbin lib usr etc; do
46 [ -e "$d/$top" ] && cp -an "$d/$top" "$RFS/" 2>/dev/null || true
47 done
48 hyd=$((hyd+1))
49done
50# El total sale de la LISTA, no de un 26 escrito a mano: el número envejecía cada vez que la lista
51# cambiaba y entonces "25/26" se leía como fallo cuando era el contador el que estaba viejo.
52echo " $hyd/$(set -- $BASE_SYSTEM; echo $#) paquetes hidratados"
53
54# provisión de admin (authorized_keys de prueba), igual que product-boot-test
55"$(ls store/*-openssh/usr/bin/ssh-keygen | head -1)" -t ed25519 -N '' -f "$WORK/clientkey" -q
56install -d -m 0700 "$RFS/root/.ssh"; cat "$WORK/clientkey.pub" > "$RFS/root/.ssh/authorized_keys"
57chmod 0600 "$RFS/root/.ssh/authorized_keys"
58
59# sanity local: los binarios del base-system llegaron al árbol
60echo "==> chequeo local del árbol aumentado:"
61for f in bin/bash usr/bin/git usr/bin/sudo usr/sbin/useradd bin/ps sbin/ip usr/bin/gpg usr/bin/pinentry usr/bin/vim etc/ssl/certs/ca-certificates.crt; do
62 [ -e "$RFS/$f" ] && echo " ok $f" || { echo " FALTA $f"; exit 1; }
63done
64# no clobbereamos el login del producto
65grep -q '^root:' "$RFS/etc/passwd" && echo " ok /etc/passwd del producto intacto"
66
67( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
68accel=(-cpu Broadwell); { [ "$KVM" = 1 ] && [ -w /dev/kvm ]; } && accel=(-enable-kvm -cpu host)
69echo "==> boot QEMU (mem ${MEM}), hostfwd :$PORT->:22"
70qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \
71 -kernel "$KERNEL" -initrd "$WORK/initramfs.cpio.gz" -append "console=ttyS0 rdinit=/sbin/init" \
72 -netdev "user,id=n0,hostfwd=tcp::$PORT-:22" -device e1000,netdev=n0 > "$LOG" 2>&1 &
73QPID=$!; trap 'kill $QPID 2>/dev/null || true' EXIT
74
75SSHOPTS=(-p "$PORT" -i "$WORK/clientkey" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
76 -o GlobalKnownHostsFile=/dev/null -o PasswordAuthentication=no -o ConnectTimeout=4 -o LogLevel=ERROR)
77echo "==> esperando handshake + verificación del base-system (deadline ${DEADLINE}s)…"
78start=$(date +%s); ok=0
79while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
80 kill -0 $QPID 2>/dev/null || { echo "!! QEMU murió"; break; }
81 if out=$(ssh "${SSHOPTS[@]}" root@localhost '
82 echo BASE_SYSTEM_OK uid=$(id -u); uname -sr
83 /bin/bash -c "echo bash=\$BASH_VERSION"
84 echo "git=$(git --version)"
85 echo "gpg=$(gpg --version | head -1)"
86 # SONDA DE PINENTRY (2026-09-22, paso 9). `gpg --version` contestaba bien con el gpg a medias
87 # inerte: sin pinentry, NINGUNA clave con contrasena se puede usar y `git commit -S` falla. La
88 # sonda firma con una clave protegida y mira el error: sin tty (esto corre por ssh) el veredicto
89 # es "Not a tty" si pinentry esta y se lanzo, y "No pinentry" si falta. Medido en los dos
90 # sentidos contra los artefactos sellados antes de escribirla.
91 export GNUPGHOME=/tmp/gpgprueba; rm -rf $GNUPGHOME; mkdir -p $GNUPGHOME; chmod 700 $GNUPGHOME
92 gpg --batch --pinentry-mode loopback --passphrase x --quick-gen-key "Prueba <p@takana.invalid>" ed25519 sign 0 >/dev/null 2>&1
93 sonda=$(gpg --batch --yes -u p@takana.invalid -o /dev/null --detach-sign /etc/passwd 2>&1 | head -1)
94 case "$sonda" in
95 *"No pinentry"*) echo "pinentry=AUSENTE ($sonda)" ;;
96 *) echo "pinentry=ok $(pinentry --version </dev/null 2>&1 | head -1) [$sonda]" ;;
97 esac
98 echo "ps=$(ps --version 2>&1 | head -1)"
99 echo "ip=$(ip -V 2>&1)"
100 echo "useradd=$(useradd --help 2>&1 | head -1)"
101 echo "sed=$(sed --version | head -1)"
102 echo "vim=$(vim --version | head -1)"
103 echo "certs=$(grep -c "BEGIN CERT" /etc/ssl/certs/ca-certificates.crt) CA"
104 ' 2>/dev/null); then
105 echo "============== GUEST BASE-SYSTEM =============="; echo "$out"; echo "=============================================="
106 echo "$out" | grep -q BASE_SYSTEM_OK && ok=1
107 # Un guardian que sólo mira el handshake deja pasar un binario presente y a medias inerte:
108 # la sonda de pinentry es condición de aprobado, no informativa.
109 echo "$out" | grep -q "pinentry=ok" || { echo "!! la sonda de pinentry NO aprobó"; ok=0; }
110 break
111 fi
112 sleep 3
113done
114kill $QPID 2>/dev/null || true
115if [ "$ok" = 1 ]; then echo "✓✓ base-system integrado + booteado: el producto trae y corre el userland foundational"; exit 0
116else echo "✗ no hubo handshake/verificación; console:"; tail -25 "$LOG"; exit 1; fi