scripts: tarea selfhost-verify end-to-end (correr el 4/4 in-VM en KVM)
El veredicto pleno 4/4 in-VM pide KVM + RAM holgada; el host de dev (7.6 GB, sin KVM) colgó por presión de RAM bajo TCG a los ~17 min del make de musl. Esta tarea traslada la verificación a una máquina capaz (laptop) en un solo comando. - scripts/selfhost-verify.sh: pipeline completo — stage0 → stage1 baseline → stage2 (ancla la ref of_tree) → inyecta overlay.ko/e1000.ko del kernel local al toolchain → ensambla el builder con la ref embebida → empaqueta (cpio --owner=root:root) → bootea + driver no-interactivo → veredicto. KVM auto-detect; PRESEED=hammerd para el camino barato (preseed C+arje, sólo hammerd in-VM). Cross-check entre máquinas: compara su of_tree(stage1) contra EXPECT_REF (198f209f… conocida-buena del dev). - scripts/drive-rebuild.py: driver no-interactivo parametrizado (env: BUILDER_CPIO, KERNEL, MEM, CPU, KVM, NET, DEADLINE, LOG). Bootea, espera la shell de arje-zero, manda rebuild-stage1 y sale 0 si REPRODUCIBLE / 1 si DIVERGENTE. (Versión de repo del driver ad-hoc que vivía en work/.) - scripts/boot-builder-vm.sh: añade la NIC e1000 (NET=1 por defecto) — el vendoring Rust necesita red. - runbook §8c: documenta la tarea y el muro de RAM del host de dev. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9e3d6862ca
commit
7025f93bcd
@@ -418,11 +418,28 @@ limpio, y se promovió a `./store` (el nativo quedó en `store-native-bak`). `ar
|
||||
el cambio de bytes de arje no movió su key: la misma escotilla C.2). El builder se re-ensambló con
|
||||
ese toolchain + el `hammer` baseline + `--ref-content 198f209f…` (embebida en
|
||||
`/etc/hammer/rebuild.env`) y se repackó **`work/builder.cpio.gz`** (415 MB, 27 027 entradas,
|
||||
`--owner=root:root`). **Listo para bootear:** `scripts/boot-builder-vm.sh` (o `work/drive-rebuild.py`
|
||||
`--owner=root:root`). **Listo para bootear:** `scripts/boot-builder-vm.sh` (o `scripts/drive-rebuild.py`
|
||||
no-interactivo) → adentro `rebuild-stage1` debe reproducir **`198f209f…`** ⇒ **✓ REPRODUCIBLE**
|
||||
cerraría el auto-alojamiento **4/4** bit a bit. Bajo TCG es lento y la RAM va justa; el veredicto
|
||||
pleno gana con **KVM + más RAM** (y boot desde disco).
|
||||
|
||||
**Tarea reproducible end-to-end: `scripts/selfhost-verify.sh`.** Un solo comando hace TODO el pipeline
|
||||
(stage0 → stage1 baseline → stage2 ref → inyectar `overlay.ko`/`e1000.ko` del kernel local → ensamblar
|
||||
builder → empaquetar → bootear + driver no-interactivo → veredicto), pensado para correr en una
|
||||
máquina con **KVM + RAM holgada** (laptop):
|
||||
|
||||
```sh
|
||||
./scripts/bootstrap-devfs.sh # una vez: el lab (.dev-fs/alpine + zig)
|
||||
KVM=1 MEM=10240 ./scripts/selfhost-verify.sh
|
||||
# PRESEED=hammerd → preseed C+arje y reconstruye sólo hammerd in-VM (barato si la RAM va justa)
|
||||
```
|
||||
|
||||
Cross-check de reproducibilidad **entre máquinas**: el script compara su `of_tree(stage1)` contra
|
||||
`EXPECT_REF` (la referencia conocida-buena `198f209f…` del host de dev). Si difieren, o cambió un pin
|
||||
(toolchain/recipes/seed) o hay un no-determinismo nuevo. En el host de dev (Artix, 7.6 GB, **sin KVM**)
|
||||
el rebuild **4/4** in-VM colgó por presión de RAM bajo TCG a los ~17 min del `make` de musl — el muro
|
||||
de hardware que esta tarea traslada a un host capaz.
|
||||
|
||||
## 9. Cross-check opcional — `arje-packager`
|
||||
|
||||
arje trae su propio empaquetador (`03_ukupacha/arje/init/arje-packager`):
|
||||
|
||||
@@ -53,10 +53,16 @@ echo "==> mem : ${MEM} MiB"
|
||||
echo "==> en la consola, tras el boot: corre rebuild-stage1"
|
||||
echo
|
||||
|
||||
# NIC e1000 (qemu user-mode 10.0.2.0/24): el rebuild Rust hace `cargo vendor` desde crates.io ⇒
|
||||
# necesita red. El builder la levanta (insmod e1000.ko + eth0 estática). NET=0 la desactiva.
|
||||
net=()
|
||||
[[ "${NET:-1}" == "1" ]] && net=(-netdev user,id=n0 -device e1000,netdev=n0)
|
||||
|
||||
exec qemu-system-x86_64 \
|
||||
-m "$MEM" \
|
||||
-no-reboot -nographic \
|
||||
"${accel[@]}" \
|
||||
"${net[@]}" \
|
||||
-kernel "$KERNEL" \
|
||||
-initrd "$INITRAMFS" \
|
||||
-append "console=ttyS0 rdinit=/sbin/init"
|
||||
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# drive-rebuild.py — Driver NO-interactivo del rebuild in-rootfs (auto-alojamiento, SDD 11 §7;
|
||||
# runbook docs/runbooks/stage1-vm-boot.md §8c).
|
||||
#
|
||||
# Bootea el builder en QEMU, espera la shell de arje-zero (PID 1), manda `rebuild-stage1` y captura
|
||||
# todo hasta el veredicto (✓ REPRODUCIBLE / ✗ DIVERGENTE), el RC o el timeout. Imprime un resumen y
|
||||
# sale con 0 si REPRODUCIBLE, 1 si DIVERGENTE/otro.
|
||||
#
|
||||
# Variables de entorno (todas opcionales):
|
||||
# BUILDER_CPIO initramfs del builder (default work/builder.cpio.gz)
|
||||
# KERNEL bzImage/vmlinuz a bootear (default /boot/vmlinuz-linux)
|
||||
# MEM RAM de la VM en MiB (default 6144)
|
||||
# CPU modelo de CPU TCG (default Broadwell; AVX2 sin KVM)
|
||||
# KVM 1 ⇒ -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido; recomendado)
|
||||
# DEADLINE techo en segundos (default 14400 = 4h)
|
||||
# LOG fichero de captura (default work/rebuild-run.log)
|
||||
# NET 1 ⇒ NIC e1000 user-mode para el vendoring Rust (default 1)
|
||||
#
|
||||
# El rebuild Rust (hammerd/arje-zero) hace `cargo vendor` desde crates.io ⇒ necesita red: por eso la
|
||||
# NIC e1000 (qemu user-mode 10.0.2.0/24). Sin red, sólo sellan los componentes C.
|
||||
import os, pty, select, subprocess, sys, time, re
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
def envpath(name, default):
|
||||
v = os.environ.get(name, default)
|
||||
return v if os.path.isabs(v) else os.path.join(ROOT, v)
|
||||
|
||||
|
||||
CPIO = envpath("BUILDER_CPIO", "work/builder.cpio.gz")
|
||||
KERNEL = os.environ.get("KERNEL", "/boot/vmlinuz-linux")
|
||||
LOG = envpath("LOG", "work/rebuild-run.log")
|
||||
MEM = os.environ.get("MEM", "6144")
|
||||
CPU = os.environ.get("CPU", "Broadwell")
|
||||
DEADLINE = int(os.environ.get("DEADLINE", "14400"))
|
||||
WANT_KVM = os.environ.get("KVM", "0") == "1"
|
||||
WANT_NET = os.environ.get("NET", "1") == "1"
|
||||
|
||||
for p, what in [(CPIO, "initramfs del builder"), (KERNEL, "kernel")]:
|
||||
if not os.path.exists(p):
|
||||
sys.exit(f"no existe {what}: {p}")
|
||||
|
||||
accel = ["-cpu", CPU]
|
||||
if WANT_KVM and os.access("/dev/kvm", os.W_OK):
|
||||
accel = ["-enable-kvm", "-cpu", "host"]
|
||||
print("==> KVM activo (-cpu host)")
|
||||
else:
|
||||
print(f"==> TCG (sin KVM); -cpu {CPU}. Lento — el rebuild Rust puede tardar.")
|
||||
|
||||
net = ["-netdev", "user,id=n0", "-device", "e1000,netdev=n0"] if WANT_NET else []
|
||||
|
||||
QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel + net +
|
||||
["-kernel", KERNEL, "-initrd", CPIO, "-append", "console=ttyS0 rdinit=/sbin/init"])
|
||||
|
||||
print(f"==> kernel : {KERNEL}")
|
||||
print(f"==> initramfs : {CPIO}")
|
||||
print(f"==> mem : {MEM} MiB deadline: {DEADLINE}s net: {'sí' if WANT_NET else 'no'}")
|
||||
print(f"==> log : {LOG}")
|
||||
|
||||
ansi = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]')
|
||||
strip = lambda b: ansi.sub(b'', b)
|
||||
|
||||
master, slave = pty.openpty()
|
||||
p = subprocess.Popen(QEMU, stdin=slave, stdout=slave, stderr=slave, close_fds=True)
|
||||
os.close(slave)
|
||||
log = open(LOG, "wb")
|
||||
buf = b""
|
||||
sent = False
|
||||
start = time.time()
|
||||
send = lambda s: os.write(master, s.encode())
|
||||
|
||||
while True:
|
||||
if time.time() - start > DEADLINE:
|
||||
log.write(b"\n[DRIVER] DEADLINE\n")
|
||||
break
|
||||
r, _, _ = select.select([master], [], [], 10)
|
||||
if r:
|
||||
try:
|
||||
data = os.read(master, 8192)
|
||||
except OSError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
log.write(data)
|
||||
log.flush()
|
||||
buf = strip(buf + data)[-20000:]
|
||||
# Boot listo: arje-zero levantó hammerd (fanotify) o apareció la shell.
|
||||
if not sent and (b"watcher fanotify activo" in buf or b"\n~ #" in buf or b"\r~ #" in buf):
|
||||
time.sleep(3)
|
||||
send("echo DRIVER_BEGIN; rebuild-stage1; echo DRIVER_RC=$?\n")
|
||||
sent = True
|
||||
log.write(b"\n[DRIVER] enviado rebuild-stage1\n")
|
||||
log.flush()
|
||||
# OJO: el comando tecleado ECHOA "DRIVER_RC=$?"; el marcador real es la SALIDA
|
||||
# "DRIVER_RC=<digito>". Matchear sólo eso (o el veredicto) evita salir antes de tiempo.
|
||||
if sent and (re.search(rb'DRIVER_RC=[0-9]', buf) or b"REPRODUCIBLE" in buf or b"DIVERGENTE" in buf):
|
||||
time.sleep(2)
|
||||
try:
|
||||
log.write(os.read(master, 8192))
|
||||
except Exception:
|
||||
pass
|
||||
log.write(b"\n[DRIVER] FIN (marcador)\n")
|
||||
break
|
||||
else:
|
||||
if p.poll() is not None:
|
||||
log.write(b"\n[DRIVER] qemu salio\n")
|
||||
break
|
||||
|
||||
try:
|
||||
p.terminate()
|
||||
time.sleep(2)
|
||||
p.kill()
|
||||
except Exception:
|
||||
pass
|
||||
log.close()
|
||||
|
||||
# Veredicto a partir de lo capturado.
|
||||
blob = strip(open(LOG, "rb").read())
|
||||
ok = (b"REPRODUCIBLE" in blob) and (b"DIVERGENTE" not in blob)
|
||||
print(f"\nDRIVER done sent={sent} elapsed={int(time.time() - start)}s")
|
||||
if ok:
|
||||
print("RESULTADO: ✓ REPRODUCIBLE — auto-alojamiento bit a bit verificado")
|
||||
elif b"DIVERGENTE" in blob:
|
||||
print("RESULTADO: ✗ DIVERGENTE — hay no-determinismo que cazar (SDD 09 §2)")
|
||||
else:
|
||||
print("RESULTADO: ? incompleto (timeout/boot/OOM) — revisá el log")
|
||||
sys.exit(0 if ok else 1)
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# selfhost-verify.sh — Verifica el auto-alojamiento bit a bit de Stage 1 de punta a punta
|
||||
# (SDD 11 §7; runbook docs/runbooks/stage1-vm-boot.md §8c).
|
||||
#
|
||||
# Hace TODO el pipeline en una corrida, pensado para un host con KVM + RAM holgada (p. ej. una
|
||||
# laptop): construye el store baseline, ensambla el builder, lo empaqueta como initramfs y lo bootea
|
||||
# en QEMU, donde `rebuild-stage1` reconstruye stage1' con el toolchain de adentro y compara su
|
||||
# content-hash (`of_tree`) con la referencia anclada afuera → ✓ REPRODUCIBLE / ✗ DIVERGENTE.
|
||||
#
|
||||
# Prerequisitos:
|
||||
# - Lab de dev: ./scripts/bootstrap-devfs.sh (deja .dev-fs/alpine + .dev-fs/tools/zig)
|
||||
# - qemu-system-x86_64, cpio, gzip; un kernel x86_64 en $KERNEL.
|
||||
# - Red (el fetch de Rust hace `cargo vendor` desde crates.io).
|
||||
#
|
||||
# Variables (override por entorno):
|
||||
# STORE store content-addressed (default ./store)
|
||||
# KERNEL kernel a bootear en la VM (default /boot/vmlinuz-linux)
|
||||
# MEM RAM de la VM en MiB (default 6144; con KVM subí a 8192+)
|
||||
# KVM 1 ⇒ -enable-kvm -cpu host (default auto: 1 si hay /dev/kvm)
|
||||
# PRESEED "all" rebuild 4/4 in-VM | "hammerd" preseed C+arje, sólo hammerd in-VM
|
||||
# (default all; "hammerd" es el camino barato/rápido si la RAM va justa)
|
||||
# EXPECT_REF content-hash esperado (cross-check de reproducibilidad entre máquinas; opcional)
|
||||
# STAGE0_URL / STAGE0_SHA256 / SEED_VERSION semilla zig (defaults: zig 0.16.0)
|
||||
#
|
||||
# Uso típico en la laptop (con KVM):
|
||||
# KVM=1 MEM=10240 ./scripts/selfhost-verify.sh
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
STORE="${STORE:-store}"
|
||||
KERNEL="${KERNEL:-/boot/vmlinuz-linux}"
|
||||
MEM="${MEM:-6144}"
|
||||
PRESEED="${PRESEED:-all}"
|
||||
SEED_VERSION="${SEED_VERSION:-0.16.0}"
|
||||
STAGE0_URL="${STAGE0_URL:-https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz}"
|
||||
STAGE0_SHA256="${STAGE0_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}"
|
||||
TOOLCHAIN="${TOOLCHAIN:-.dev-fs/alpine}"
|
||||
TOOLCHAIN_TAG="${TOOLCHAIN_TAG:-alpine-3.23.4-builder}"
|
||||
# Referencia conocida-buena (store baseline en el host de dev, runbook §8c). Si tu corrida produce
|
||||
# otra, o bien cambió algo del pin (toolchain/recipes/seed) o hay un no-determinismo nuevo.
|
||||
EXPECT_REF="${EXPECT_REF:-b3:198f209f5e2c9d3a37411823b2ea42a09074d4e482f044278958b1b86f232dbb}"
|
||||
: "${KVM:=$([[ -w /dev/kvm ]] && echo 1 || echo 0)}"
|
||||
|
||||
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||
die() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
[[ -x "$TOOLCHAIN/usr/bin/rustc" ]] || die "falta el lab: corré ./scripts/bootstrap-devfs.sh (no veo $TOOLCHAIN)"
|
||||
[[ -r "$KERNEL" ]] || die "no puedo leer el kernel $KERNEL (set KERNEL=…)"
|
||||
command -v qemu-system-x86_64 >/dev/null || die "falta qemu-system-x86_64"
|
||||
|
||||
# 0) Binario hammer estático (musl, crt-static) — el builder lo bootea adentro.
|
||||
HAMMER_BIN="target/x86_64-unknown-linux-musl/release/hammer"
|
||||
say "build hammer estático ($HAMMER_BIN)"
|
||||
cargo build --release --target x86_64-unknown-linux-musl -p hammer-cli
|
||||
HAMMER="./$HAMMER_BIN"
|
||||
|
||||
say "stage0 — ingerir la semilla zig (idempotente)"
|
||||
"$HAMMER" --store "$STORE" bootstrap stage0 \
|
||||
--url "$STAGE0_URL" --sha256 "$STAGE0_SHA256" --version "$SEED_VERSION"
|
||||
# El seed_hash sale del nombre del artefacto sellado (robusto: no parsea stdout, donde el sha256 del
|
||||
# tarball también es 64-hex y podría confundirse).
|
||||
SEED_HASH="$(ls -d "$STORE"/*-seed-zig 2>/dev/null | head -1 | sed -E 's#.*/([0-9a-f]{64})-seed-zig#b3:\1#')"
|
||||
[[ "$SEED_HASH" == b3:* && ${#SEED_HASH} -eq 67 ]] || die "no encuentro la semilla sellada en $STORE"
|
||||
say "semilla: $SEED_HASH"
|
||||
|
||||
# 1) Store baseline: stage1 (idempotente; rebuildea lo que falte con -mcpu=baseline ya en fuente).
|
||||
say "stage1 — construir y sellar el rootfs baseline"
|
||||
"$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED_HASH" --recipes recipes
|
||||
ROOTFS_HASH="$(ls -d "$STORE"/*-stage1-rootfs | head -1 | sed -E 's#.*/([0-9a-f]{64})-stage1-rootfs#b3:\1#')"
|
||||
[[ "$ROOTFS_HASH" == b3:* ]] || die "no encuentro el stage1-rootfs en $STORE"
|
||||
say "stage1 rootfs: $ROOTFS_HASH"
|
||||
|
||||
# 2) Referencia of_tree(stage1) que la VM debe reproducir.
|
||||
REF="$("$HAMMER" --store "$STORE" bootstrap stage2 --rootfs "$ROOTFS_HASH" 2>&1 \
|
||||
| grep -oE 'content-hash: b3:[0-9a-f]{64}|content=b3:[0-9a-f]{64}' | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
|
||||
[[ "$REF" == b3:* ]] || die "no obtuve la referencia de stage2"
|
||||
say "referencia of_tree(stage1) = $REF"
|
||||
if [[ -n "$EXPECT_REF" && "$REF" != "$EXPECT_REF" ]]; then
|
||||
printf '\033[1;33mAVISO: la referencia difiere de la conocida-buena del host de dev:\n esta %s\n espera %s\nReproducibilidad entre máquinas rota O cambió un pin (toolchain/recipes/seed). Seguimos igual.\033[0m\n' "$REF" "$EXPECT_REF"
|
||||
fi
|
||||
|
||||
# 3) Inyectar los módulos del kernel DE ESTA máquina al toolchain (el builder los carga: overlay para
|
||||
# el sandbox bwrap, e1000 para la red del vendoring). Deben matchear el kernel que bootea la VM.
|
||||
say "inyectar overlay.ko + e1000.ko (kernel $(uname -r)) al toolchain"
|
||||
moddir="/lib/modules/$(uname -r)"
|
||||
for m in overlay e1000; do
|
||||
src="$(find "$moddir" -name "$m.ko*" 2>/dev/null | head -1)"
|
||||
if [[ -n "$src" ]]; then
|
||||
# Descomprimir si viene .ko.zst/.ko.gz/.ko.xz; el builder hace insmod del .ko crudo.
|
||||
case "$src" in
|
||||
*.zst) zstd -dqf "$src" -o "$TOOLCHAIN/lib/$m.ko" 2>/dev/null || cp "$src" "$TOOLCHAIN/lib/$(basename "$src")" ;;
|
||||
*.gz) gzip -dc "$src" > "$TOOLCHAIN/lib/$m.ko" ;;
|
||||
*.xz) xz -dc "$src" > "$TOOLCHAIN/lib/$m.ko" ;;
|
||||
*) cp "$src" "$TOOLCHAIN/lib/$m.ko" ;;
|
||||
esac
|
||||
echo " $m: $src"
|
||||
else
|
||||
echo " $m: NO encontrado en $moddir (si la VM lo necesita, fallará: bwrap overlay / red)"
|
||||
fi
|
||||
done
|
||||
|
||||
# 4) Ensamblar el builder con la referencia embebida.
|
||||
say "ensamblar builder (toolchain in-rootfs + hammer baseline + ref)"
|
||||
"$HAMMER" --store "$STORE" bootstrap builder \
|
||||
--stage1 "$ROOTFS_HASH" --seed-hash "$SEED_HASH" \
|
||||
--hammer-bin "$HAMMER_BIN" \
|
||||
--toolchain "$TOOLCHAIN" --toolchain-tag "$TOOLCHAIN_TAG" \
|
||||
--ref-content "$REF" \
|
||||
--work-cache work --out work/builder-rootfs
|
||||
|
||||
# 4b) Preseed opcional para abaratar el rebuild in-VM (sólo hammerd se reconstruye).
|
||||
if [[ "$PRESEED" == "hammerd" ]]; then
|
||||
say "preseed musl+busybox+arje-zero (sólo hammerd se reconstruye in-VM)"
|
||||
for n in musl busybox arje-zero; do
|
||||
d="$(ls -d "$STORE"/*-"$n" 2>/dev/null | head -1)"
|
||||
[[ -n "$d" ]] && cp -a "$d" work/builder-rootfs/store/ && echo " preseed: $(basename "$d")"
|
||||
done
|
||||
fi
|
||||
|
||||
# 5) Empaquetar como initramfs. --owner=root:root OBLIGATORIO: si los ficheros viajan con el uid del
|
||||
# host, el userns de bwrap (mapea 0→0) no lo mapea y el copy-up de overlay falla con EACCES.
|
||||
say "empaquetar initramfs (cpio newc, --owner=root:root)"
|
||||
( cd work/builder-rootfs && find . -print0 | cpio --null -o -H newc --owner=root:root 2>/dev/null | gzip -1 ) > work/builder.cpio.gz
|
||||
echo " work/builder.cpio.gz: $(du -h work/builder.cpio.gz | cut -f1)"
|
||||
|
||||
# 6) Bootear + driver no-interactivo → veredicto.
|
||||
say "bootear la VM y correr rebuild-stage1 (KVM=$KVM MEM=$MEM)"
|
||||
KVM="$KVM" MEM="$MEM" KERNEL="$KERNEL" BUILDER_CPIO="work/builder.cpio.gz" \
|
||||
python3 scripts/drive-rebuild.py
|
||||
Reference in New Issue
Block a user