Files
hammer/scripts/selfhost-verify.sh
sergioandClaude Opus 4.8 10961ce47f kernel-from-source: ✓ REPRODUCIBLE con kernel hammer (switch_root wrapper)
CIERRA el frente kernel-from-source: el bzImage hammer-built (Linux 6.16.12,
recipes/linux.toml) bootea la VM del selfhost-verify y el rebuild in-VM reproduce
el of_tree bit a bit (✓ REPRODUCIBLE, DRIVER_RC=0, ~3min).

El muro era bwrap `pivot_root: Invalid argument`: con un kernel hammer el / del
initramfs es el rootfs absoluto del mount-namespace (sin mount padre movible), y
bwrap del sandbox no puede pivotar de ahí (el kernel host lo permitía — quirk
suyo; diagnosticado con un debug-loop de initramfs mínimo, boot ~10s).

Fix portable (HAMMER_KERNEL=1): un /init wrapper PID1 que copia el rootfs a un
tmpfs y hace switch_root, dejando / como mount tmpfs real (pivotable). bwrap
pivota en CUALQUIER kernel. Condicional ⇒ el camino del kernel host (rdinit=
/sbin/init directo) queda intacto. drive-rebuild.py: APPEND env override para
pasar rdinit=/init.

Uso: HAMMER_KERNEL=1 KERNEL=<bzImage> KVM=1 MEM=16384 PRESEED=hammerd \
  ./scripts/selfhost-verify.sh

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:21:35 -04:00

273 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.
# Baseline DETERMINISTA con codegen-units=1 + CARGO_BUILD_JOBS=1 en el sandbox: el segundo serializa
# el backend paralelo de rustc/LLVM (ThinLTO), sin el cual arje-zero divergía ~62 KB a >1 CPU (el
# viejo 0039b2b9… era un build paralelo no-reproducible). Ver §8c y SDD 09 §2.
EXPECT_REF="${EXPECT_REF:-b3:9adefb82b8457c8616cc89e6bb725a787ea52641f65d60d989d819e4872735ac}"
: "${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"
# 0c) Auto-alojamiento del COMPILADOR (variante b, pieza rust). A DIFERENCIA de make/busybox/bwrap/…
# (herramientas que ORQUESTAN o COPIAN ⇒ bytes idénticos), **rustc EMITE los binarios del 4/4**
# (arje-zero, hammerd): un rustc distinto ⇒ of_tree DIVERGE del baseline Alpine 9adefb82. Por eso
# el criterio no es igualdad con Alpine sino AUTO-CONSISTENCIA: el REF se RECOMPUTA con hammer-rust
# (nuevo EXPECT_REF) y la VM debe reproducir ESE. El swap entra en $TOOLCHAIN, que sirve TANTO al
# build host del REF (este script, abajo) COMO al toolchain in-VM del builder (--toolchain); se
# restaura al salir (trap). hammer-rust se construye con scripts/rust-frontier (climb mrustc→1.91.1).
# SWAP_RUST=1 → overlay .scratch/rust-1.91.1-prefix sobre /toolchain/usr/{bin,lib}.
# RUST_PREFIX=DIR → prefix hammer-rust alternativo (default .scratch/rust-1.91.1-prefix).
# RUST_EXPECT_REF=… → REF conocido-bueno de hammer-rust (cross-check; vacío ⇒ sólo lo imprime).
# ⚠️ Usá PRESEED=hammerd con SWAP_RUST: arje-zero (monorepo tawasuyu) vendorea ~1973 crates;
# en un rebuild in-VM completo (PRESEED=all) ese vendor desborda el rootfs en RAM de la VM
# (ENOSPC). PRESEED=hammerd preseedea el arje-zero host-built (hammer-rust, locked) y sólo
# reconstruye hammerd in-VM. ✓ REPRODUCIBLE verificado así (2026-06-18, of_tree 7fa6cb4e).
if [[ "${SWAP_RUST:-0}" == 1 ]]; then
RUST_PREFIX="${RUST_PREFIX:-.scratch/rust-1.91.1-prefix}"
[[ -x "$RUST_PREFIX/bin/rustc" ]] || die "SWAP_RUST=1 pero no veo el prefix hammer-rust en $RUST_PREFIX (corré el climb: scripts/rust-frontier/README.md)"
say "variante b (compilador) — swapear hammer-rust 1.91.1 en $TOOLCHAIN (criterio: auto-consistencia)"
scripts/rust-frontier/swap-rust-into-toolchain.sh --prefix "$RUST_PREFIX" --toolchain "$TOOLCHAIN"
trap 'scripts/rust-frontier/swap-rust-into-toolchain.sh --restore --toolchain "$TOOLCHAIN" >/dev/null 2>&1 || true' EXIT
# OJO: el input-hash del store NO incluye el rustc (recipe.hash_inputs = source+compiler+target+
# link+patches+flags+phases+deps), así que en el ./store compartido arje-zero/hammerd quedarían
# CACHEADOS con los bytes de Alpine y el swap sería un no-op. Usamos un store DEDICADO (store-rust)
# para forzar el rebuild de los 4/4 con hammer-rust; así el of_tree refleja el compilador nuevo.
STORE="${RUST_STORE:-store-rust}"
say "SWAP_RUST: store dedicado $STORE (el ./store baseline no se toca; fuerza rebuild con hammer-rust)"
# Con hammer-rust el REF NO es el 9adefb82 de Alpine; comparamos contra RUST_EXPECT_REF, el
# of_tree(stage1) auto-consistente de hammer-rust 1.91.1 (reproducido 2× en host, 2026-06-17,
# store-rust y store-rust2). Override con RUST_EXPECT_REF= para re-anclar.
EXPECT_REF="${RUST_EXPECT_REF:-b3:7fa6cb4e70a934d72206cf3d95cf46a13f0a6464ec73855b98d547195f4ed047}"
fi
# 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.
# La versión de los módulos DEBE matchear el kernel que bootea la VM ($KERNEL), no el del host
# corriendo el script ($(uname -r)) — en máquinas con varios kernels instalados difieren y el
# insmod falla con "version magic mismatch" → sin overlayfs, bwrap revienta. Sacamos la versión
# del propio bzImage; si no se puede, caemos a uname -r.
KVER="$(file -bL "$KERNEL" 2>/dev/null | grep -oE 'version [0-9][^ ]*' | awk '{print $2}')"
[[ -n "$KVER" && -d "/lib/modules/$KVER" ]] || KVER="$(uname -r)"
say "inyectar overlay.ko + e1000.ko (kernel $KVER) al toolchain"
moddir="/lib/modules/$KVER"
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
# 3b) Auto-alojamiento *puro* (variante b, SDD 11 §7.2b): reemplazar piezas del toolchain Alpine por
# recetas hammer construidas desde fuente, con Stage 2 reverificando que `of_tree(stage1')` NO
# cambia (el make hammer compila los 4/4 igual de bit-a-bit que el de Alpine). Opt-in:
# SWAP_MAKE=1 → construye recipes/make.toml y lo monta sobre /toolchain/usr/bin/make.
# SWAP_BUSYBOX=1 → monta el busybox de hammer sobre /toolchain/bin/busybox (los symlinks de
# applets de Alpine — sh/sed/grep/awk/tar/find — pasan a usarlo; cp/mkdir/
# install siguen siendo GNU coreutils, intactos).
# SWAP_LINUX_HEADERS=1 → construye recipes/linux-headers.toml y swap-directorio de los 13 subdirs
# kernel-owned de /toolchain/usr/include (linux/asm/…); musl bits/sys intactos.
# SWAP_BWRAP=1 → construye recipes/bwrap.toml (+libcap, su dep, vía deps.build) y lo monta
# sobre /toolchain/usr/bin/bwrap (el sandbox del propio lab).
# SWAP_COREUTILS=1 → construye recipes/coreutils.toml (multicall) y monta el binario sobre
# /toolchain/bin/coreutils; los ~100 symlinks (cp/mkdir/install/…) lo siguen.
# SWAPS="name=hash[:rel] …" → swaps explícitos extra (formato del flag --swap).
# Por defecto, off ⇒ corrida pura-Alpine (variante a), idéntica a la baseline conocida-buena.
# Piezas validadas en host: make+busybox reproducen of_tree=9adefb82 in-VM; linux-headers da un
# header-tree byte-idéntico a Alpine (diff -r vacío); bwrap rebuildea musl byte-idéntico de sandbox.
SWAP_ARGS=()
if [[ "${SWAP_MAKE:-0}" == 1 ]]; then
say "variante b — construir make desde fuente (recipes/make.toml) y swapearlo en /toolchain"
MAKE_HASH="$("$HAMMER" --store "$STORE" build recipes/make.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
[[ "$MAKE_HASH" == b3:* ]] || die "no obtuve el hash sellado de make"
say "make hammer: $MAKE_HASH"
SWAP_ARGS+=(--swap "make=$MAKE_HASH")
fi
if [[ "${SWAP_BUSYBOX:-0}" == 1 ]]; then
say "variante b — construir busybox desde fuente y swapearlo en /toolchain/bin/busybox"
BB_HASH="$("$HAMMER" --store "$STORE" build recipes/busybox.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
[[ "$BB_HASH" == b3:* ]] || die "no obtuve el hash sellado de busybox"
say "busybox hammer: $BB_HASH"
SWAP_ARGS+=(--swap "busybox=$BB_HASH:bin/busybox")
fi
if [[ "${SWAP_LINUX_HEADERS:-0}" == 1 ]]; then
# Pieza 3: los headers UAPI del kernel. NO es un binario sino un árbol — el swap-directorio
# (assemble_builder) reemplaza cada subdir entero. Swapeamos exactamente los subdirs que el
# artefacto sellado produce (linux/asm/asm-generic/cxl/fwctl/misc/mtd/rdma/regulator/scsi/sound/
# video/xen), que son justo los que el paquete linux-headers de Alpine posee; los de musl
# (bits/sys/net/…) quedan intactos.
say "variante b — construir linux-headers desde fuente y swapear /toolchain/usr/include/{linux,asm,…}"
LH_HASH="$("$HAMMER" --store "$STORE" build recipes/linux-headers.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
[[ "$LH_HASH" == b3:* ]] || die "no obtuve el hash sellado de linux-headers"
say "linux-headers hammer: $LH_HASH"
LH_INC="$(ls -d "$STORE"/*-linux-headers/usr/include 2>/dev/null | head -1)"
[[ -d "$LH_INC" ]] || die "no encuentro usr/include en el linux-headers sellado"
for d in "$LH_INC"/*/; do
sub="$(basename "$d")"
SWAP_ARGS+=(--swap "linux-headers=${LH_HASH}:usr/include/$sub")
done
fi
if [[ "${SWAP_BWRAP:-0}" == 1 ]]; then
# Pieza 4: bwrap (bubblewrap), EL sandbox del lab. Binario estático ⇒ swap de archivo sobre
# /toolchain/usr/bin/bwrap. Su dep libcap la construye y materializa el lab solo (deps.build).
# bwrap es herramienta, no input del 4/4: no necesita casar byte-a-byte con Alpine, sólo aislar
# igual (validado en host — musl rebuildeó byte-idéntico bajo hammer-bwrap).
say "variante b — construir bwrap (+libcap) desde fuente y swapearlo en /toolchain/usr/bin/bwrap"
BW_HASH="$("$HAMMER" --store "$STORE" build recipes/bwrap.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
[[ "$BW_HASH" == b3:* ]] || die "no obtuve el hash sellado de bwrap"
say "bwrap hammer: $BW_HASH"
SWAP_ARGS+=(--swap "bwrap=${BW_HASH}:usr/bin/bwrap")
fi
if [[ "${SWAP_COREUTILS:-0}" == 1 ]]; then
# Pieza 5: GNU coreutils (cp/mkdir/ln/chmod/mv/install…). Multicall (--enable-single-binary), igual
# layout que Alpine: un binario /bin/coreutils + ~100 symlinks. UN swap del binario rutea todos los
# applets (los symlinks del toolchain ya apuntan a coreutils). Tool, no input: validado en host —
# musl Y busybox rebuildearon byte-idéntico bajo hammer-coreutils.
say "variante b — construir coreutils desde fuente y swapearlo en /toolchain/bin/coreutils"
CU_HASH="$("$HAMMER" --store "$STORE" build recipes/coreutils.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
[[ "$CU_HASH" == b3:* ]] || die "no obtuve el hash sellado de coreutils"
say "coreutils hammer: $CU_HASH"
SWAP_ARGS+=(--swap "coreutils=${CU_HASH}:bin/coreutils")
fi
for s in ${SWAPS:-}; do SWAP_ARGS+=(--swap "$s"); done
# 4) Ensamblar el builder con la referencia embebida.
say "ensamblar builder (toolchain in-rootfs + hammer baseline + ref${SWAP_ARGS:+ + swaps})"
"$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" \
"${SWAP_ARGS[@]}" \
--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
# 4c) HAMMER_KERNEL=1 — wrapper /init para escapar del rootfs (frente kernel-from-source).
# Con un kernel hammer-built (recipes/linux.toml) el `/` del initramfs es el rootfs ABSOLUTO del
# mount-namespace (su propio padre, no movible) y bwrap del sandbox falla en `pivot_root: Invalid
# argument` (el kernel host no lo exigía — quirk suyo). El fix portable (funciona en cualquier
# kernel): un /init PID1 que copia el rootfs a un tmpfs y hace `switch_root`, dejando `/` como un
# mount tmpfs real (pivotable). El kernel debe arrancar con rdinit=/init (ver APPEND abajo). Off por
# defecto ⇒ el camino del kernel host queda intacto (rdinit=/sbin/init directo).
if [[ "${HAMMER_KERNEL:-0}" == 1 ]]; then
say "HAMMER_KERNEL: inyectar /init wrapper (copy-to-tmpfs + switch_root, escapa del rootfs)"
cat > work/builder-rootfs/init <<'INIT'
#!/bin/sh
# PID1 wrapper: el rootfs del initramfs no es pivotable (bwrap pivot_root EINVAL); copiamos a un tmpfs
# y switch_root para que / sea un mount real. Luego exec del init real (arje-zero).
/bin/busybox mkdir -p /newroot
/bin/busybox mount -t tmpfs tmpfs /newroot
cd /
for e in /*; do
[ "$e" = /newroot ] && continue
/bin/busybox cp -a "$e" /newroot/
done
exec /bin/busybox switch_root /newroot /sbin/init
INIT
chmod +x work/builder-rootfs/init
export APPEND="console=ttyS0 rdinit=/init"
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