Cierra el último bloqueo del boot que la corrida en QEMU destapó (runbook §8): arje-zero/hammerd buildeaban DINÁMICOS contra musl (el rust de Alpine es dinámico por defecto) → DT_NEEDED libc.musl-x86_64.so.1, y nuestro libc.so (musl shared con zig cc) no exporta memcpy/memset/… → el loader falla y el init muere. Fix golden-path: para link=static, RUSTFLAGS ahora fuerza -C target-feature=+crt-static -C relocation-model=static ⇒ binarios AUTOCONTENIDOS (musl dentro), ET_EXEC sin interpreter, como busybox: sin libc.so, sin loader, sin el soname de Alpine. musl vuelve a --disable-shared (no se necesita el loader). Boot en QEMU: usar -cpu Broadwell (el qemu64 default no tiene el AVX que zig emite). Validado por la corrida real: los 4 componentes buildan y el rootfs se sella; el kernel arranca el initramfs y ejecuta arje-zero como PID 1. Falta rebuildear hammerd/arje-zero con crt-static para cerrar el boot (cache-bust + rebuild). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Runbook — validar Stage 1 booteando en QEMU
Operativo, no diseño. Valida end-to-end que el rootfs que produce
hammer bootstrap stage1(SDD 11) bootea con arje-zero como PID 1 (SDD 12), levantahammerd+ getty bajo supervisión, y que elCRASHEDreal funciona.Esto es un procedimiento, no un transcript verificado. La lógica de ensamblado/hash/seed está cubierta por unit tests (y la seed se validó contra
card_core::Card), pero el cross-compile y el boot reales no se han corrido todavía — ese es justo el punto de este runbook. La primera corrida puede destapar ajustes (link,/dev/console, getty/tty); §7 los anticipa.
0. Lo que valida / lo que no
- Valida: que el árbol sellado por
stage1arranca como sistema: arje-zero monta los pseudo-FS, carga la seed, incarnahammerdy la getty, y al matarhammerdlo reinicia con backoff (elCRASHEDque la Fase 5 difirió). - No valida (aún): bus único (B.2), overlay del store (I4), atestación (A1/A2), kernel from-source. Aquí el kernel es prestado del host; el overlay no se monta.
1. Prerrequisitos
- Lab de dev montado:
./scripts/bootstrap-devfs.sh(deja.dev-fs/alpine+.dev-fs/tools/zig). - Red: el build de
hammerdyarje-zeroclona repos git (hammer, tawasuyu) y correcargo vendoren el fetch. El build hermético del sandbox es--offline, pero el fetch necesita red. - Herramientas host:
qemu-system-x86_64, un kernel x86_64 (/boot/vmlinuz-*),cpio,gzip. - Espacio/tiempo: construir
arje-zerovendorea las deps del monorepo tawasuyu entero — es pesado y lento la primera vez (se cachea después). - El binario
hammer:cargo build --release -p hammer-cli⇒./target/release/hammer.
El store debe vivir en la raíz del repo (
./store) para queBuildConfigresuelva.dev-fs/alpinecomo hermano (store/..= repo root). El toolchain de Stage 1 no es el de.dev-fs: es la semilla de Stage 0 (paso 2), questage1enchufa comozig_dirdel sandbox.
2. Stage 0 — ingerir la semilla (zig)
export HAMMER=./target/release/hammer
export STORE=./store
"$HAMMER" --store "$STORE" bootstrap stage0 \
--url https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz \
--sha256 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 \
--version 0.16.0
# → imprime el seed_hash (b3:...). Guardalo:
SEED="b3:..." # pegá el hash impreso
Idempotente: re-correr no re-descarga. La semilla queda sellada como seed-zig en el store y anota
su línea en bootstrap.json.
3. Stage 1 — construir y sellar el rootfs
"$HAMMER" --store "$STORE" bootstrap stage1 --seed-hash "$SEED" --recipes recipes
# construye musl + busybox + hammerd + arje-zero, ensambla el rootfs FHS y lo sella.
# → imprime el rootfs_hash (b3:...).
# El árbol sellado (read-only) del rootfs:
ROOTFS_DIR=$(ls -d "$STORE"/*-stage1-rootfs)
echo "$ROOTFS_DIR"
ls "$ROOTFS_DIR"/ente/seed.card.json "$ROOTFS_DIR"/sbin/init "$ROOTFS_DIR"/usr/bin/{hammerd,arje-zero}
Necesita red (git clone + cargo vendor). Si un build Cargo falla en el link (-lgcc_s) o por
static-PIE, ver §7 — el zig cc del lab debería resolver -lgcc_s (bitácora M2 del plan arje↔hammer).
4. Empaquetar el rootfs como initramfs
El árbol del store ya trae /sbin/init→/usr/bin/arje-zero y /ente/seed.card.json. Se empaqueta
como cpio newc + gzip (formato initramfs):
( cd "$ROOTFS_DIR" && find . -print0 | cpio --null -o --format=newc ) | gzip -9 > /tmp/stage1.cpio.gz
ls -lh /tmp/stage1.cpio.gz
cpio newc preserva symlinks y hardlinks. El symlink /sbin/init→/usr/bin/arje-zero es absoluto:
en un initramfs resuelve dentro de su propia raíz. (Si el kernel no auto-monta devtmpfs, ver §7 por
el nodo /dev/console.)
5. Bootear en QEMU
qemu-system-x86_64 -m 512 -nographic \
-kernel /boot/vmlinuz-linux \
-initrd /tmp/stage1.cpio.gz \
-append "console=ttyS0 rdinit=/sbin/init"
rdinit=/sbin/init— el default de un initramfs es/init; lo sobreescribimos a/sbin/init(→arje-zero). Alternativa directa:rdinit=/usr/bin/arje-zero.console=ttyS0+-nographic— consola serie, donde arje escribe y la getty se engancha.- Salir de QEMU:
Ctrl-AluegoX.
6. Criterios de éxito (qué observar)
- PID 1: en kmsg/consola, arje-zero loguea "despierta como PID 1".
- Pseudo-FS: monta
/proc /sys /devsin error fatal (best-effort; unEBUSYse ignora). - Seed: carga
/ente/seed.card.jsonsin "card inválida" (ya validada contracard_core::Card). - hammerd: arranca como Ente — su tracing aparece y/o crea
/run/agent.sock. - getty: aparece un prompt de shell (
/bin/sh) enttyS0. - CRASHED real — la prueba clave. En el shell:
arje registra el
kill "$(pidof hammerd)"on_death(ExitStatus)y reinicia hammerd tras el backoff (200 ms → ×2…). Eso es elCRASHEDque la Fase 5 dejó diferido, ahora manejado por el init propio.
7. Troubleshooting (síntoma → causa → fix)
| Síntoma | Causa probable | Fix |
|---|---|---|
Kernel panic … no init found |
/sbin/init no ejecuta |
probar rdinit=/usr/bin/arje-zero; verificar que el symlink quedó en el cpio (cpio -tv) |
| arje cae a rescue shell "card inválida" | la seed no validó | confirmar que /ente/seed.card.json se empaquetó; comparar con el schema de seeds/arje-qemu.card.json |
hammerd/arje-zero: exec format error |
binario no estático / falta loader | confirmar link = "static"; un static-PIE pide /lib/ld-musl-x86_64.so.1 — para init-clásico desactivar PIE (-C relocation-model=static, bitácora M2) |
Build Cargo falla en -lgcc_s |
hueco del toolchain musl dinámico | el lab usa zig cc (empaqueta su compiler-rt) → no debería aparecer; verificar compiler = "zig-cc" en la receta |
| getty sin prompt | device console no engancha tty |
probar argv con ttyS0 en vez de console; revisar que /dev/console existe |
No working init / sin /dev/console |
kernel sin auto-mount de devtmpfs | crear el nodo en el cpio (mknod -m 600 dev/console c 5 1 antes de empaquetar) o usar un kernel con CONFIG_DEVTMPFS_MOUNT=y |
cargo vendor / git clone falla |
el fetch necesita red | correr Stage 1 con red; el sandbox de build sigue siendo --offline |
8. Bitácora — primera corrida real (2026-06-11)
Corrida en el host de desarrollo (Artix). Resultado: el userland C builda end-to-end; el camino Rust está bloqueado por una decisión de toolchain. Hallazgos, en orden:
| # | Etapa | Resultado |
|---|---|---|
| 1 | Lab (bootstrap-devfs.sh) |
✅ Alpine + zig 0.16.0 + 43 build-tools |
| 2 | Stage 0 (ingerir zig) | ✅ seed_hash sellado, idempotente |
| 3 | musl 1.2.5 | ✅ cross-compila con zig cc y se sella — el cross-compile C hermético funciona |
| 4 | busybox 1.36.1 | ✅ tras tres fixes (abajo); compila y linkea con zig cc, se sella |
| 5 | hammerd (Cargo) | ❌ bloqueado: triple de toolchain (abajo) |
| 6 | arje-zero (Cargo) | ⏸ no alcanzado (mismo bloqueo; además vendorea el monorepo entero) |
Fixes aplicados a recipes/busybox.toml (incompatibilidades GNU-toolchain ↔ zig, esperadas):
gcchardcodeado. El Kbuild de busybox fijaCC = gcc/HOSTCC = gcccon=(ignora el envCCdel sandbox). Fix: pasarCC='zig cc' HOSTCC='zig cc'en línea de comando de make.- Flags GNU-ld que zig cc rechaza. El link pasa
-Wl,--warn-common,-Wl,-Map,…,-Wl,--verbose;zig ccvalida los-Wl,y aborta. Fix: quitarlos descripts/trylinkcon sed. - (
gccpara herramientas host comofixdepygcc-version.shlo cubre el mismoHOSTCC.)
Bloqueo del camino Rust — decisión de toolchain (plan C.2):
- El vendoring (Lote 6) corre OK en el fetch (host, con red). El
cargo buildcorre en el sandbox. - Se añadió
rust cargoal rootfs (apk) — pero el rust de Alpine tiene host triplex86_64-alpine-linux-musl, mientrasBuildSys::Cargopide--target x86_64-unknown-linux-musl. Alpine no trae ese std y no hay rustup →error[E0463]: can't find crate for core. - Opciones (a decidir): (A) construir nativo sin
--target(el sandbox ya es x86_64 musl) — cambio enBuildSys::Cargo; (B) recetas Cargo contarget = "x86_64-alpine-linux-musl"— leakage de Alpine; (C) rustup en el sandbox +rustup target add— pierde hermeticidad/pin. Es la misma tensión de "pin de toolchain entra al hash" que el plan C.2 dejó abierta.
Stage 1 completo + intento de boot
Tras los fixes, los 4 componentes buildan y el rootfs se sella (musl+busybox+hammerd+arje-zero,
29 MB; /sbin/init→arje-zero, /ente/seed.card.json, loader musl). Se empaquetó como initramfs
(cpio newc+gzip, 11 MB) y se booteó en QEMU. La cadena de hallazgos del boot:
- AVX. El CPU default de QEMU (
qemu64) no tiene AVX, pero los binarios (zig/rust nativos) emitenvxorps→SIGILL(invalid_op) en el loader → init muere. Fix:-cpu Broadwell(AVX2 bajo TCG; sin/dev/kvmno hay-cpu host). Para un distro real: compilar a un baseline x86-64 genérico, no al CPU del builder. - musl dinámico. El rust de Alpine produce binarios dinámicos (
DT_NEEDED libc.musl-x86_64.so.1), no crt-static. El loader corre pero no resuelvememcpy/memset/log2/…: nuestrolibc.so(musl 1.2.5 shared con zig cc) tiene 1586 dyn-syms pero no exporta esos símbolos. →Error relocating /sbin/init: memcpy: symbol not found→ init muere.
Conclusión. El bootstrap C (musl+busybox+zig cc) y el build Rust nativo (hammerd y
arje-zero compilan y se sellan) están validados end-to-end; el kernel arranca el initramfs y
ejecuta arje-zero como PID 1. El último bloqueo del boot es el linking dinámico de musl: el fix
correcto y golden-path es crt-static — compilar los componentes Rust con
-C target-feature=+crt-static -C relocation-model=static para binarios autocontenidos (sin
libc.so, sin loader, sin soname, como busybox). Requiere rebuild de hammerd/arje-zero (el de
arje-zero vendorea el monorepo: caro en tiempo y disco). Es la realización plena de la "vía de oro"
estática de hammer y cierra de paso el problema del soname de Alpine.
Actualización (Opción A implementada). BuildSys::Cargo ahora construye nativo sin --target
cuando el target es el del sandbox (SANDBOX_NATIVE_TARGET), con un wrapper .hammer-zig-cc que
(a) resuelve el zig cc de dos palabras para cargo/cc-rs y (b) sanea el triple que cc-rs pasa
(--target=x86_64-alpine-linux-musl → x86_64-linux-musl, que zig sí entiende). Resultado:
hammerd compila nativo y se sella ✓ — el camino Rust funciona. arje-zero queda bloqueado por
otra razón: tawasuyu no commitea Cargo.lock (git ls-files Cargo.lock vacío), así que el
git archive no lo trae y cargo vendor --locked no puede crearlo. Decisión pendiente: commitear
el lock en tawasuyu (reproducible, plan C.2 #5) vs relajar --locked en el vendoring de hammer.
9. Cross-check opcional — arje-packager
arje trae su propio empaquetador (03_ukupacha/arje/init/arje-packager):
arje-packager --seed <CARD.json> --out <initramfs.cpio.gz> [--bin LABEL=PATH]…. Útil para comparar
el initramfs canónico de arje contra el que sale del rootfs de hammer (§4). No reemplaza esta
validación: aquí probamos el rootfs que hammer sella, no el que arje arma.