gioser

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…

TOML 604Rust 56Shell 36Markdown 281227 Sutichaykuna

⎇ c98dea090a7f433d914dbea113d05cb1c4237329 scripts/selfhost-verify.sh Shell · 311 siq'ikuna · 20.4 KB Ñawpa willay ch'uya

selfhost-verify: el work-cache se puede acotar — con el corpus entero no arranca a11740e1c · Sergio ·

1#!/usr/bin/env bash
2# selfhost-verify.sh — Verifica el auto-alojamiento bit a bit de Stage 1 de punta a punta
3# (SDD 11 §7; runbook docs/runbooks/stage1-vm-boot.md §8c).
4#
5# Hace TODO el pipeline en una corrida, pensado para un host con KVM + RAM holgada (p. ej. una
6# laptop): construye el store baseline, ensambla el builder, lo empaqueta como initramfs y lo bootea
7# en QEMU, donde `rebuild-stage1` reconstruye stage1' con el toolchain de adentro y compara su
8# content-hash (`of_tree`) con la referencia anclada afuera → ✓ REPRODUCIBLE / ✗ DIVERGENTE.
9#
10# Prerequisitos:
11#   - Lab de dev:  ./scripts/bootstrap-devfs.sh   (deja .dev-fs/alpine + .dev-fs/tools/zig)
12#   - qemu-system-x86_64, cpio, gzip; un kernel x86_64 en $KERNEL.
13#   - Red (el fetch de Rust hace `cargo vendor` desde crates.io).
14#
15# Variables (override por entorno):
16#   STORE      store content-addressed         (default ./store)
17#   KERNEL     kernel a bootear en la VM        (default /boot/vmlinuz-linux)
18#   MEM        RAM de la VM en MiB              (default 6144; con KVM subí a 8192+)
19#   KVM        1 ⇒ -enable-kvm -cpu host        (default auto: 1 si hay /dev/kvm)
20#   PRESEED    "all" rebuild 4/4 in-VM | "hammerd" preseed C+arje, sólo hammerd in-VM
21#              (default all; "hammerd" es el camino barato/rápido si la RAM va justa)
22#   EXPECT_REF content-hash esperado (cross-check de reproducibilidad entre máquinas; opcional)
23#   STAGE0_URL / STAGE0_SHA256 / SEED_VERSION  semilla zig (defaults: zig 0.16.0)
24#
25# Uso típico en la laptop (con KVM):
26#   KVM=1 MEM=10240 ./scripts/selfhost-verify.sh
27set -euo pipefail
28
29REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
30cd "$REPO_ROOT"
31
32STORE="${STORE:-store}"
33KERNEL="${KERNEL:-/boot/vmlinuz-linux}"
34MEM="${MEM:-6144}"
35PRESEED="${PRESEED:-all}"
36# Caché de fetch que viaja DENTRO del builder (sólo `repos/` y `tarballs/`; ver el paso 7 de
37# `builder_rootfs`). Por defecto `work`, que es lo correcto en una laptop de dev.
38#
39# ⚠ **En una máquina que ya construyó el corpus entero, `work` son DECENAS DE GIGAS** y el builder
40# se vuelve inarrancable: el initramfs se carga ENTERO en RAM, así que 15 G de mirrors y tarballs
41# contra `MEM=8192` matan la VM a los dos segundos y el script reporta «? incompleto
42# (timeout/boot/OOM)», que suena a cualquier cosa menos a lo que es. Medido en el worker el
43# 2026-09-24: `work/repos` 8,7 G + `work/tarballs` 6,3 G ⇒ `builder.cpio.gz` de 15 G.
44#
45# El rebuild in-VM sólo necesita las fuentes del NÚCLEO. Un caché mínimo son ~176 M:
46#     mkdir -p /tmp/wc/{repos,tarballs}
47#     cp -al work/repos/{takana,arje-zero}.git /tmp/wc/repos/     # hammerd · arje-zero
48#     cp -al work/tarballs/<sha de musl,busybox,util-linux>.tar /tmp/wc/tarballs/
49#     WORK_CACHE=/tmp/wc ./scripts/selfhost-verify.sh
50WORK_CACHE="${WORK_CACHE:-work}"
51SEED_VERSION="${SEED_VERSION:-0.16.0}"
52STAGE0_URL="${STAGE0_URL:-https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz}"
53STAGE0_SHA256="${STAGE0_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}"
54TOOLCHAIN="${TOOLCHAIN:-.dev-fs/alpine}"
55TOOLCHAIN_TAG="${TOOLCHAIN_TAG:-alpine-3.23.4-builder}"
56# Referencia conocida-buena (store baseline en el host de dev, runbook §8c). Si tu corrida produce
57# otra, o bien cambió algo del pin (toolchain/recipes/seed) o hay un no-determinismo nuevo.
58# Baseline DETERMINISTA con codegen-units=1 + CARGO_BUILD_JOBS=1 en el sandbox: el segundo serializa
59# el backend paralelo de rustc/LLVM (ThinLTO), sin el cual arje-zero divergía ~62 KB a >1 CPU (el
60# viejo 0039b2b9… era un build paralelo no-reproducible). Ver §8c y SDD 09 §2.
61#
62# ⚠ **Re-anclada el 2026-09-22: `util-linux` entró a `STAGE1_COMPONENTS`** y el núcleo pasó de 4 a 5
63# componentes, así que el árbol del stage1 cambió y con él su `of_tree`. La anterior era
64# `b3:9adefb82b8457c8616cc89e6bb725a787ea52641f65d60d989d819e4872735ac`, válida mientras el núcleo
65# fue `musl busybox hammerd arje-zero`; se deja escrita acá porque una corrida vieja que la produzca
66# no está rota, está desactualizada, y distinguir las dos cosas es justo para lo que sirve este
67# valor. Por qué entró util-linux: la card `console-getty` vive en la seed del NÚCLEO y su `exec`
68# tiene que existir en ESE rootfs — ver `takana-bootstrap/src/lib.rs`, `STAGE1_COMPONENTS`.
69#
70# ⚠ Y esto es lo que el HOST calcula, no el veredicto de auto-alojamiento. Que la VM reproduzca este
71# mismo árbol con su propio toolchain es lo que dice `✓ REPRODUCIBLE` más abajo; son dos
72# comprobaciones distintas y esta variable sólo cruza máquinas, no capas.
73EXPECT_REF="${EXPECT_REF:-b3:d152f060f570dbf6ace4b0cb61732caee3671f1f7a270eaa3a7755ed2fb94fb8}"
74: "${KVM:=$([[ -w /dev/kvm ]] && echo 1 || echo 0)}"
75
76say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
77die() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
78
79[[ -x "$TOOLCHAIN/usr/bin/rustc" ]] || die "falta el lab: corré ./scripts/bootstrap-devfs.sh (no veo $TOOLCHAIN)"
80[[ -r "$KERNEL" ]] || die "no puedo leer el kernel $KERNEL (set KERNEL=…)"
81command -v qemu-system-x86_64 >/dev/null || die "falta qemu-system-x86_64"
82
83# 0) Binario takana estático (musl, crt-static) — el builder lo bootea adentro.
84HAMMER_BIN="target/x86_64-unknown-linux-musl/release/hammer"
85say "build hammer estático ($HAMMER_BIN)"
86cargo build --release --target x86_64-unknown-linux-musl -p takana-cli
87HAMMER="./$HAMMER_BIN"
88
89say "stage0 — ingerir la semilla zig (idempotente)"
90"$HAMMER" --store "$STORE" bootstrap stage0 \
91    --url "$STAGE0_URL" --sha256 "$STAGE0_SHA256" --version "$SEED_VERSION"
92# El seed_hash sale del nombre del artefacto sellado (robusto: no parsea stdout, donde el sha256 del
93# tarball también es 64-hex y podría confundirse).
94SEED_HASH="$(ls -d "$STORE"/*-seed-zig 2>/dev/null | head -1 | sed -E 's#.*/([0-9a-f]{64})-seed-zig#b3:\1#')"
95[[ "$SEED_HASH" == b3:* && ${#SEED_HASH} -eq 67 ]] || die "no encuentro la semilla sellada en $STORE"
96say "semilla: $SEED_HASH"
97
98# 0c) Auto-alojamiento del COMPILADOR (variante b, pieza rust). A DIFERENCIA de make/busybox/bwrap/…
99#     (herramientas que ORQUESTAN o COPIAN ⇒ bytes idénticos), **rustc EMITE los binarios del 4/4**
100#     (arje-zero, hammerd): un rustc distinto ⇒ of_tree DIVERGE del baseline Alpine 9adefb82. Por eso
101#     el criterio no es igualdad con Alpine sino AUTO-CONSISTENCIA: el REF se RECOMPUTA con takana-rust
102#     (nuevo EXPECT_REF) y la VM debe reproducir ESE. El swap entra en $TOOLCHAIN, que sirve TANTO al
103#     build host del REF (este script, abajo) COMO al toolchain in-VM del builder (--toolchain); se
104#     restaura al salir (trap). takana-rust se construye con scripts/rust-frontier (climb mrustc→1.91.1).
105#       SWAP_RUST=1        → overlay .scratch/rust-1.91.1-prefix sobre /toolchain/usr/{bin,lib}.
106#       RUST_PREFIX=DIR    → prefix takana-rust alternativo (default .scratch/rust-1.91.1-prefix).
107#       RUST_EXPECT_REF=…  → REF conocido-bueno de takana-rust (cross-check; vacío ⇒ sólo lo imprime).
108#     ⚠️ Usá PRESEED=hammerd con SWAP_RUST: arje-zero (monorepo tawasuyu) vendorea ~1973 crates;
109#     en un rebuild in-VM completo (PRESEED=all) ese vendor desborda el rootfs en RAM de la VM
110#     (ENOSPC). PRESEED=hammerd preseedea el arje-zero host-built (takana-rust, locked) y sólo
111#     reconstruye hammerd in-VM. ✓ REPRODUCIBLE verificado así (2026-06-18, of_tree 7fa6cb4e).
112if [[ "${SWAP_RUST:-0}" == 1 ]]; then
113    RUST_PREFIX="${RUST_PREFIX:-.scratch/rust-1.91.1-prefix}"
114    [[ -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)"
115    say "variante b (compilador) — swapear hammer-rust 1.91.1 en $TOOLCHAIN (criterio: auto-consistencia)"
116    scripts/rust-frontier/swap-rust-into-toolchain.sh --prefix "$RUST_PREFIX" --toolchain "$TOOLCHAIN"
117    trap 'scripts/rust-frontier/swap-rust-into-toolchain.sh --restore --toolchain "$TOOLCHAIN" >/dev/null 2>&1 || true' EXIT
118    # OJO: el input-hash del store NO incluye el rustc (recipe.hash_inputs = source+compiler+target+
119    # link+patches+flags+phases+deps), así que en el ./store compartido arje-zero/hammerd quedarían
120    # CACHEADOS con los bytes de Alpine y el swap sería un no-op. Usamos un store DEDICADO (store-rust)
121    # para forzar el rebuild de los 4/4 con takana-rust; así el of_tree refleja el compilador nuevo.
122    STORE="${RUST_STORE:-store-rust}"
123    say "SWAP_RUST: store dedicado $STORE (el ./store baseline no se toca; fuerza rebuild con hammer-rust)"
124    # Con takana-rust el REF NO es el 9adefb82 de Alpine; comparamos contra RUST_EXPECT_REF, el
125    # of_tree(stage1) auto-consistente de takana-rust 1.91.1 (reproducido 2× en host, 2026-06-17,
126    # store-rust y store-rust2). Override con RUST_EXPECT_REF= para re-anclar.
127    EXPECT_REF="${RUST_EXPECT_REF:-b3:7fa6cb4e70a934d72206cf3d95cf46a13f0a6464ec73855b98d547195f4ed047}"
128fi
129
130# 1) Store baseline: stage1 (idempotente; rebuildea lo que falte con -mcpu=baseline ya en fuente).
131say "stage1 — construir y sellar el rootfs baseline"
132"$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED_HASH" --recipes recipes
133ROOTFS_HASH="$(ls -d "$STORE"/*-stage1-rootfs | head -1 | sed -E 's#.*/([0-9a-f]{64})-stage1-rootfs#b3:\1#')"
134[[ "$ROOTFS_HASH" == b3:* ]] || die "no encuentro el stage1-rootfs en $STORE"
135say "stage1 rootfs: $ROOTFS_HASH"
136
137# 2) Referencia of_tree(stage1) que la VM debe reproducir.
138REF="$("$HAMMER" --store "$STORE" bootstrap stage2 --rootfs "$ROOTFS_HASH" 2>&1 \
139    | grep -oE 'content-hash: b3:[0-9a-f]{64}|content=b3:[0-9a-f]{64}' | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
140[[ "$REF" == b3:* ]] || die "no obtuve la referencia de stage2"
141say "referencia of_tree(stage1) = $REF"
142if [[ -n "$EXPECT_REF" && "$REF" != "$EXPECT_REF" ]]; then
143    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"
144fi
145
146# 3) Inyectar los módulos del kernel DE ESTA máquina al toolchain (el builder los carga: overlay para
147#    el sandbox bwrap, e1000 para la red del vendoring). Deben matchear el kernel que bootea la VM.
148# La versión de los módulos DEBE matchear el kernel que bootea la VM ($KERNEL), no el del host
149# corriendo el script ($(uname -r)) — en máquinas con varios kernels instalados difieren y el
150# insmod falla con "version magic mismatch" → sin overlayfs, bwrap revienta. Sacamos la versión
151# del propio bzImage; si no se puede, caemos a uname -r.
152KVER="$(file -bL "$KERNEL" 2>/dev/null | grep -oE 'version [0-9][^ ]*' | awk '{print $2}')"
153[[ -n "$KVER" && -d "/lib/modules/$KVER" ]] || KVER="$(uname -r)"
154say "inyectar overlay.ko + e1000.ko (kernel $KVER) al toolchain"
155moddir="/lib/modules/$KVER"
156# ⚠ `|| true` y el chequeo del directorio no son paranoia: sin ellos este bloque MATA el script EN
157# SILENCIO en cualquier máquina sin `/lib/modules` — un contenedor, por ejemplo. Con `set -euo
158# pipefail`, un `find` sobre un directorio inexistente falla, el pipe falla, la asignación falla y
159# `set -e` aborta ANTES del `echo "NO encontrado"` que el `else` de abajo tenía preparado. Medido el
160# 2026-09-22 en el worker (LXC de Proxmox: comparte el kernel del host y no trae árbol de módulos):
161# el log terminaba exactamente en la línea de arriba, sin una palabra, y el veredicto no llegaba
162# nunca. Un fallo que se lee como «sigue corriendo».
163if [[ ! -d "$moddir" ]]; then
164    echo "    (sin $moddir — esta máquina no trae árbol de módulos; sigo sin inyectar)"
165    echo "    ⚠ sólo es un problema si el kernel de la VM los necesita: los nuestros traen"
166    echo "      OVERLAY_FS y E1000 built-in y ni siquiera tienen CONFIG_MODULES."
167fi
168for m in overlay e1000; do
169    src="$(find "$moddir" -name "$m.ko*" 2>/dev/null | head -1 || true)"
170    if [[ -n "$src" ]]; then
171        # Descomprimir si viene .ko.zst/.ko.gz/.ko.xz; el builder hace insmod del .ko crudo.
172        case "$src" in
173            *.zst) zstd -dqf "$src" -o "$TOOLCHAIN/lib/$m.ko" 2>/dev/null || cp "$src" "$TOOLCHAIN/lib/$(basename "$src")" ;;
174            *.gz)  gzip -dc "$src" > "$TOOLCHAIN/lib/$m.ko" ;;
175            *.xz)  xz -dc "$src" > "$TOOLCHAIN/lib/$m.ko" ;;
176            *)     cp "$src" "$TOOLCHAIN/lib/$m.ko" ;;
177        esac
178        echo "    $m: $src"
179    else
180        echo "    $m: NO encontrado en $moddir (si la VM lo necesita, fallará: bwrap overlay / red)"
181    fi
182done
183
184# 3b) Auto-alojamiento *puro* (variante b, SDD 11 §7.2b): reemplazar piezas del toolchain Alpine por
185#     recetas takana construidas desde fuente, con Stage 2 reverificando que `of_tree(stage1')` NO
186#     cambia (el make takana compila los 4/4 igual de bit-a-bit que el de Alpine). Opt-in:
187#       SWAP_MAKE=1     → construye recipes/make.toml y lo monta sobre /toolchain/usr/bin/make.
188#       SWAP_BUSYBOX=1  → monta el busybox de takana sobre /toolchain/bin/busybox (los symlinks de
189#                         applets de Alpine — sh/sed/grep/awk/tar/find — pasan a usarlo; cp/mkdir/
190#                         install siguen siendo GNU coreutils, intactos).
191#       SWAP_LINUX_HEADERS=1 → construye recipes/linux-headers.toml y swap-directorio de los 13 subdirs
192#                         kernel-owned de /toolchain/usr/include (linux/asm/…); musl bits/sys intactos.
193#       SWAP_BWRAP=1    → construye recipes/bwrap.toml (+libcap, su dep, vía deps.build) y lo monta
194#                         sobre /toolchain/usr/bin/bwrap (el sandbox del propio lab).
195#       SWAP_COREUTILS=1 → construye recipes/coreutils.toml (multicall) y monta el binario sobre
196#                         /toolchain/bin/coreutils; los ~100 symlinks (cp/mkdir/install/…) lo siguen.
197#       SWAPS="name=hash[:rel] …" → swaps explícitos extra (formato del flag --swap).
198#     Por defecto, off ⇒ corrida pura-Alpine (variante a), idéntica a la baseline conocida-buena.
199#     Piezas validadas en host: make+busybox reproducen of_tree=9adefb82 in-VM; linux-headers da un
200#     header-tree byte-idéntico a Alpine (diff -r vacío); bwrap rebuildea musl byte-idéntico de sandbox.
201SWAP_ARGS=()
202if [[ "${SWAP_MAKE:-0}" == 1 ]]; then
203    say "variante b — construir make desde fuente (recipes/make.toml) y swapearlo en /toolchain"
204    MAKE_HASH="$("$HAMMER" --store "$STORE" build recipes/make.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
205    [[ "$MAKE_HASH" == b3:* ]] || die "no obtuve el hash sellado de make"
206    say "make hammer: $MAKE_HASH"
207    SWAP_ARGS+=(--swap "make=$MAKE_HASH")
208fi
209if [[ "${SWAP_BUSYBOX:-0}" == 1 ]]; then
210    say "variante b — construir busybox desde fuente y swapearlo en /toolchain/bin/busybox"
211    BB_HASH="$("$HAMMER" --store "$STORE" build recipes/busybox.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
212    [[ "$BB_HASH" == b3:* ]] || die "no obtuve el hash sellado de busybox"
213    say "busybox hammer: $BB_HASH"
214    SWAP_ARGS+=(--swap "busybox=$BB_HASH:bin/busybox")
215fi
216if [[ "${SWAP_LINUX_HEADERS:-0}" == 1 ]]; then
217    # Pieza 3: los headers UAPI del kernel. NO es un binario sino un árbol — el swap-directorio
218    # (assemble_builder) reemplaza cada subdir entero. Swapeamos exactamente los subdirs que el
219    # artefacto sellado produce (linux/asm/asm-generic/cxl/fwctl/misc/mtd/rdma/regulator/scsi/sound/
220    # video/xen), que son justo los que el paquete linux-headers de Alpine posee; los de musl
221    # (bits/sys/net/…) quedan intactos.
222    say "variante b — construir linux-headers desde fuente y swapear /toolchain/usr/include/{linux,asm,…}"
223    LH_HASH="$("$HAMMER" --store "$STORE" build recipes/linux-headers.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
224    [[ "$LH_HASH" == b3:* ]] || die "no obtuve el hash sellado de linux-headers"
225    say "linux-headers hammer: $LH_HASH"
226    LH_INC="$(ls -d "$STORE"/*-linux-headers/usr/include 2>/dev/null | head -1)"
227    [[ -d "$LH_INC" ]] || die "no encuentro usr/include en el linux-headers sellado"
228    for d in "$LH_INC"/*/; do
229        sub="$(basename "$d")"
230        SWAP_ARGS+=(--swap "linux-headers=${LH_HASH}:usr/include/$sub")
231    done
232fi
233if [[ "${SWAP_BWRAP:-0}" == 1 ]]; then
234    # Pieza 4: bwrap (bubblewrap), EL sandbox del lab. Binario estático ⇒ swap de archivo sobre
235    # /toolchain/usr/bin/bwrap. Su dep libcap la construye y materializa el lab solo (deps.build).
236    # bwrap es herramienta, no input del 4/4: no necesita casar byte-a-byte con Alpine, sólo aislar
237    # igual (validado en host — musl rebuildeó byte-idéntico bajo takana-bwrap).
238    say "variante b — construir bwrap (+libcap) desde fuente y swapearlo en /toolchain/usr/bin/bwrap"
239    BW_HASH="$("$HAMMER" --store "$STORE" build recipes/bwrap.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
240    [[ "$BW_HASH" == b3:* ]] || die "no obtuve el hash sellado de bwrap"
241    say "bwrap hammer: $BW_HASH"
242    SWAP_ARGS+=(--swap "bwrap=${BW_HASH}:usr/bin/bwrap")
243fi
244if [[ "${SWAP_COREUTILS:-0}" == 1 ]]; then
245    # Pieza 5: GNU coreutils (cp/mkdir/ln/chmod/mv/install…). Multicall (--enable-single-binary), igual
246    # layout que Alpine: un binario /bin/coreutils + ~100 symlinks. UN swap del binario rutea todos los
247    # applets (los symlinks del toolchain ya apuntan a coreutils). Tool, no input: validado en host —
248    # musl Y busybox rebuildearon byte-idéntico bajo takana-coreutils.
249    say "variante b — construir coreutils desde fuente y swapearlo en /toolchain/bin/coreutils"
250    CU_HASH="$("$HAMMER" --store "$STORE" build recipes/coreutils.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
251    [[ "$CU_HASH" == b3:* ]] || die "no obtuve el hash sellado de coreutils"
252    say "coreutils hammer: $CU_HASH"
253    SWAP_ARGS+=(--swap "coreutils=${CU_HASH}:bin/coreutils")
254fi
255for s in ${SWAPS:-}; do SWAP_ARGS+=(--swap "$s"); done
256
257# 4) Ensamblar el builder con la referencia embebida.
258say "ensamblar builder (toolchain in-rootfs + hammer baseline + ref${SWAP_ARGS:+ + swaps})"
259"$HAMMER" --store "$STORE" bootstrap builder \
260    --stage1 "$ROOTFS_HASH" --seed-hash "$SEED_HASH" \
261    --takana-bin "$HAMMER_BIN" \
262    --toolchain "$TOOLCHAIN" --toolchain-tag "$TOOLCHAIN_TAG" \
263    --ref-content "$REF" \
264    "${SWAP_ARGS[@]}" \
265    --work-cache "$WORK_CACHE" --out work/builder-rootfs
266
267# 4b) Preseed opcional para abaratar el rebuild in-VM (sólo hammerd se reconstruye).
268if [[ "$PRESEED" == "hammerd" ]]; then
269    say "preseed musl+busybox+arje-zero (sólo hammerd se reconstruye in-VM)"
270    for n in musl busybox arje-zero; do
271        d="$(ls -d "$STORE"/*-"$n" 2>/dev/null | head -1)"
272        [[ -n "$d" ]] && cp -a "$d" work/builder-rootfs/store/ && echo "    preseed: $(basename "$d")"
273    done
274fi
275
276# 4c) HAMMER_KERNEL=1 — wrapper /init para escapar del rootfs (frente kernel-from-source).
277#     Con un kernel takana-built (recipes/linux.toml) el `/` del initramfs es el rootfs ABSOLUTO del
278#     mount-namespace (su propio padre, no movible) y bwrap del sandbox falla en `pivot_root: Invalid
279#     argument` (el kernel host no lo exigía — quirk suyo). El fix portable (funciona en cualquier
280#     kernel): un /init PID1 que copia el rootfs a un tmpfs y hace `switch_root`, dejando `/` como un
281#     mount tmpfs real (pivotable). El kernel debe arrancar con rdinit=/init (ver APPEND abajo). Off por
282#     defecto ⇒ el camino del kernel host queda intacto (rdinit=/sbin/init directo).
283if [[ "${TAKANA_KERNEL:-${HAMMER_KERNEL:-0}}" == 1 ]]; then
284    say "HAMMER_KERNEL: inyectar /init wrapper (copy-to-tmpfs + switch_root, escapa del rootfs)"
285    cat > work/builder-rootfs/init <<'INIT'
286#!/bin/sh
287# PID1 wrapper: el rootfs del initramfs no es pivotable (bwrap pivot_root EINVAL); copiamos a un tmpfs
288# y switch_root para que / sea un mount real. Luego exec del init real (arje-zero).
289/bin/busybox mkdir -p /newroot
290/bin/busybox mount -t tmpfs tmpfs /newroot
291cd /
292for e in /*; do
293    [ "$e" = /newroot ] && continue
294    /bin/busybox cp -a "$e" /newroot/
295done
296exec /bin/busybox switch_root /newroot /sbin/init
297INIT
298    chmod +x work/builder-rootfs/init
299    export APPEND="console=ttyS0 rdinit=/init"
300fi
301
302# 5) Empaquetar como initramfs. --owner=root:root OBLIGATORIO: si los ficheros viajan con el uid del
303#    host, el userns de bwrap (mapea 0→0) no lo mapea y el copy-up de overlay falla con EACCES.
304say "empaquetar initramfs (cpio newc, --owner=root:root)"
305( cd work/builder-rootfs && find . -print0 | cpio --null -o -H newc --owner=root:root 2>/dev/null | gzip -1 ) > work/builder.cpio.gz
306echo "    work/builder.cpio.gz: $(du -h work/builder.cpio.gz | cut -f1)"
307
308# 6) Bootear + driver no-interactivo → veredicto.
309say "bootear la VM y correr rebuild-stage1 (KVM=$KVM MEM=$MEM)"
310KVM="$KVM" MEM="$MEM" KERNEL="$KERNEL" BUILDER_CPIO="work/builder.cpio.gz" \
311    python3 scripts/drive-rebuild.py