`product_attested()` + CLI `hammer bootstrap product --attest [--policy] [--rootkey]`:
estructura el spike attest-boot-test.sh dentro de hammer-bootstrap. Hidrata el init
CON gate (arje-zero-attest) sobre /usr/bin/arje-zero, fija attest_policy, deriva los
--bin label=path de la PROPIA seed (PID1 + execs Native, DFS) y FIRMA con arje-packager
(host, static musl) → seed firmada en /ente/seed.card.json; regenera /ente/attest.json
coherente con el init nuevo y sella product-attested-rootfs APARTE (núcleo+product base
intactos, Separación Mecanismo/Política).
- AttestConfig{policy, rootkey:[u8;32]}; DEV_ATTEST_ROOTKEY determinista ⇒ firmas
Ed25519 reproducibles ⇒ árbol sellado reproducible. product_attested_hash v1.
- GOTCHA hardlinks read-only del store: romper /ente/seed.card.json antes de que el
packager escriba (EACCES) y /ente/attest.json antes de reescribir; tmp de firma en
staging/.attest-build se limpia antes de sellar.
- critical_bins_from_seed: getty+sshd comparten /bin/busybox ⇒ 3 concesiones por hash.
- Validado en host contra los sellos reales (3 concesiones, init 13MB, hammer attest ✓).
- Test hermético assemble_attested_swaps_gate_signs_seed_and_regenerates_attest (packager
sintético). 40 tests verde.
- scripts/attest-boot-test.sh: MODO A REAL (PRODUCT_ATTESTED=<hash> bootea el artefacto
real) + MODO B SPIKE fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la integración canónica firmada de I4 (decisión del usuario): la mitad de
arje (gate) + la mitad de hammer (firma) acopladas con la cripto real de
arje-attest/agora — NO el manifiesto plano de /ente/attest.json.
tawasuyu (pusheado, commit c78a0ada en main): `arje-packager --seed-out` emite el
seed FIRMADO standalone (el gate attest_gate.rs ya estaba en main).
hammer:
- recipes/arje-packager.toml: el firmador (build-time tool, static musl, corre en
el host). GOTCHA: lib+bin + monorepo virtual ⇒ flags `-p ... --bin ...`.
- recipes/arje-zero-attest.toml: arje-zero CON gate, variante SÓLO de producto
(commit c78a0ada). El arje-zero del núcleo (9967b02c) NO se toca ⇒ of_tree
baseline (9adefb82/7fa6cb4e) BLINDADO. Separación Mecanismo/Política.
- scripts/attest-boot-test.sh: valida E2E sobre una copia del product-rootfs
(override del PID1 por el gated + seed firmado por arje-packager + boot QEMU).
Validado in-VM (rootkey fija ⇒ firmas Ed25519 deterministas):
- ÍNTEGRO: gate atesta arje-zero+hammerd+busybox ✓ (politica=Halt) → servicios
arriba → SSH OK.
- TAMPER (1 byte en hammerd tras firmar): "atestación ✗ binario no atestado" →
"ARRANQUE FALLIDO ... Halt — abortando antes de incarnar" → shell de rescate,
SIN SSH. La integridad comprometida NO levanta el entorno.
Pendiente (estructurar bien): cablear esto en hammer-bootstrap product() (firmar
+ hidratar el gated automáticamente) y reproducibilidad (Cargo.lock committeado
en tawasuyu para el gated). El mecanismo ya está probado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`hammer install <device>`: vuelca el product-rootfs lean sobre un disco
destino (block device real /dev/sdX, o un fichero pre-dimensionado para
pruebas) con el mismo layout GPT + GRUB BIOS de E1 pero IN-PLACE. Tras
instalar, el disco arranca solo (SeaBIOS → GRUB → arje-zero → hammerd+
getty+sshd). Contraparte "a disco real" de product-image.sh (imagen portátil).
- install-image.sh generalizado: si IMG es block device (o PREALLOC=1 sobre
fichero pre-dimensionado) NO trunca ni borra el nodo — valida capacidad e
instala in-place. sfdisk/mke2fs -d/dd-splice/GRUB son idénticos (dd a un
offset de device = a un offset de fichero). El camino rootless de E1
(unshare -r + dd conv=sparse,notrunc + patch GRUB) sirve igual al device.
- hammer-install.sh: resuelve el product-rootfs, provisiona authorized_keys
(AUTHKEYS=<file> o TESTKEY=1 efímera), guardas de seguridad (FORCE=1 para
un block device, rechazo si está montado), y delega a install-image.sh.
Con BOOT=1+TESTKEY=1 arranca el target y valida por SSH.
Validado: instalación IN-PLACE sobre un fichero pre-dimensionado de 2200M
(ejercita el camino exacto del device, sin hardware real) → el disco
instalado auto-bootea (sin -kernel) y sirve SSH: ls = "uutils coreutils
0.9.0", /dev/vda3→/store y /dev/vda4→/var/lib/hammer montadas. El device
real sólo añade el guard FORCE + autodetección [-b].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el pipeline de release engineering para el product-rootfs: del
artefacto sellado por `hammer bootstrap product` a un disco GRUB-booteable
que arranca solo en QEMU (`-drive file=img`, SIN -kernel) y sirve SSH.
- product-image.sh: hidrata el product-rootfs sellado a un dir escribible,
provisiona authorized_keys de prueba y delega el armado del disco a
install-image.sh (GRUB BIOS, particiones dedicadas vda2=/ vda3=/store
vda4=/var/lib/hammer). A diferencia de install-image por defecto (que
empaqueta el BUILDER con todo el toolchain), la root es el product-rootfs
LEAN ⇒ imagen de PRODUCTO. Con BOOT=1 auto-bootea + handshake SSH (slirp
hostfwd) como validación.
GOTCHA: BOOT=1 (para el boot propio) se hereda por entorno a
install-image.sh, que haría su PROPIO `exec qemu` foreground y bloquearía
⇒ se pasa BOOT=0 explícito al delegar.
- hammer-bootstrap: el producto crea los mountpoints /store y /var/lib/hammer
(disk-ready) para el wrapper /sbin/init de la imagen. Test actualizado.
Validado in-VM: la imagen auto-bootea por GRUB (sin -kernel) → arje PID1 →
hammerd+getty+sshd; SSH OK, ls = "uutils coreutils 0.9.0" (userland Rust del
disco), /dev/vda3→/store y /dev/vda4→/var/lib/hammer montadas. 36 tests verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extiende la capa de producto con el userland Rust-nativo ADOPTADO, fuera del
núcleo blindado (sigue sin tocar STAGE1_COMPONENTS ni el of_tree).
- USERLAND_COMPONENTS = [uutils, findutils, findutils-xargs, diffutils,
ripgrep]: se hidratan sobre el 4/4 DESPUÉS de busybox ⇒ sus symlinks en
/usr/bin ensombrecen los applets busybox.
- ADELGAZAR BUSYBOX (determinista, sin depender del PATH del shell): por cada
nombre que el userland Rust provee en /usr/bin, se RETIRA el symlink
homónimo de busybox en /bin, /sbin, /usr/sbin (busybox suele dejar `ls` en
/bin, fuera del /usr/bin ya ensombrecido). La tool Rust queda ÚNICA en PATH.
El binario busybox y lo no reemplazado (sh/ash, tar, mount) se preservan.
- product_rootfs_hash v2 (incluye userland); build_components() helper;
assemble_product_rootfs hidrata base→userland→servicios.
- 36 tests verde (nuevo: ensombrecido + retiro de applet + sh/busybox quedan).
Validado in-VM (product-boot-test.sh sobre el product-rootfs de la ruta real):
ls = "uutils coreutils 0.9.0" (/usr/bin/ls), find = "find (Rust) 0.9.1",
rg = "ripgrep 14.1.1"; /bin/ls y /bin/cat retirados, /bin/sh + busybox
preservados. SSH sigue verde. "Adelgazar busybox" cerrado en el producto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidación arquitectónica de sshd-como-servicio (Separación Mecanismo/
Política, decisión del usuario). El núcleo NO se toca: STAGE1_COMPONENTS +
STAGE1_SEED_CARD siguen siendo el mecanismo base atómico que el
selfhost-verify reconstruye bit a bit (of_tree 9adefb82/7fa6cb4e blindado).
Nuevo en hammer-bootstrap:
- SERVICE_COMPONENTS = [netup, openssh] (política de producto).
- SSHD_SERVICE_CARD: card genesis Native/Restart (netup + ssh-keygen -A +
exec sshd -D), validado E2E en QEMU.
- product_seed_card(): compone la seed de producto = seed base + cards de
servicio apendados al genesis vía serde_json (hammer sigue autocontenido,
sin dep de card-core). El núcleo (hammerd+getty) se preserva.
- assemble_product_rootfs() + product(): HIDRATACIÓN TARDÍA — hidrata el
stage1-rootfs ya sellado (4/4 verificado) + inyecta openssh/netup encima
+ escribe configs (passwd con sshd, sshd_config con PidFile /run, /var/empty
0711, /root/.ssh) y sella un `product-rootfs` APARTE. of_tree del núcleo
intacto. Idempotente, reproducible.
GOTCHA: los ficheros hidratados son hardlinks read-only al store ⇒ romper
el hardlink (remove+write) en vez de chmod (mutaría el inodo del store).
- CLI: `hammer bootstrap product --rootfs <base> --recipes recipes`.
- 4 tests nuevos (seed compone, hash determinista, assemble inyecta, recetas
de servicio parsean). 36/36 verde.
scripts/product-boot-test.sh: valida que el product-rootfs de la RUTA REAL
bootea en QEMU y sirve SSH (sólo provisiona authorized_keys de prueba, no
ensambla nada). VERDE: arje levanta hammerd+getty+sshd, handshake real
"Accepted publickey for root", guest responde (seed=hammer-product con 3
cards, Linux 6.16.12). Cierra el agujero de verificación con la arquitectura
final, no con el spike sucio.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spike de validación (sucio, temporal) que cierra el agujero que el bwrap
anidado dejó: bootea un work/rootfs-ssh-test desechable (4/4 limpio del
store + openssh + netup PRECOMPILADOS) como initramfs, con sshd como
genesis card supervisado por arje-zero, red slirp con hostfwd :2222->:22,
y hace el handshake SSH real desde el host.
VERDE: arje valida la seed e instancia el card sshd (Native/Restart);
netup toma lease 10.0.2.15 (DHCP slirp); ssh-keygen -A genera host keys;
sshd escucha :22; "Accepted publickey for root" y el guest ejecuta el
comando (HANDSHAKE_OK uid=0, Linux 6.16.12). Quema las 3 incógnitas:
(1) handshake post-chroot/setuid de privsep como root REAL del guest
(lo que el bwrap no pudo), (2) arje gobierna el lifecycle del daemon,
(3) los fds de red no segfaultean contra musl.
Gotchas: cpio -R 0:0 (sin root-owned /var/empty sshd lo rechaza);
chmod u+w sobre el staging (el store está sellado read-only).
NO es plomería de producto: la consolidación (SERVICE_COMPONENTS +
hidratacion tardia sobre el 4/4 verificado, of_tree blindado) es el Paso 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
netup = binario hammer-propio (sync, sólo libc, sin tokio) que configura la red:
levanta el link (netlink RTM_NEWLINK), negocia un lease DHCPv4 a mano sobre UDP
(DISCOVER/OFFER/REQUEST/ACK con flag broadcast), y aplica IP + ruta default +
/etc/resolv.conf (RTM_NEWADDR/RTM_NEWROUTE). Hand-roll de netlink y DHCP porque
no hay cliente DHCP Rust "maduro" para adoptar al estilo ripgrep, y el workspace
es 100% sync (rtnetlink arrastraría tokio). Reemplaza el `ip` estático de busybox.
Validado in-VM contra el DHCP de QEMU slirp: ✓ lease 10.0.2.15 + ruta + DNS +
egress TCP. Autodetecta la NIC (primera no-loopback) y espera carrier por sysfs.
Infra de prueba:
- scripts/drive-netup.py: bootea la VM, corre netup y verifica lease/NAT/DNS.
SLIRP=1 ⇒ red user-mode (cero setup de host); si no, tap del lab.
- scripts/labnet.sh: lab de LAN real opcional (tap directo + dnsmasq + NAT) para
fidelidad de hardware; no requerido para validar netup.
- scripts/netdiag.sh: diagnóstico del camino DHCP del lab (tcpdump+nft+dnsmasq).
Lección: para validar el CÓDIGO conviene slirp primero (cero plomería de host);
la LAN real (firewall INPUT, sutilezas del bridge, perms) es fidelidad posterior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Primer entregable de release engineering (SDD 13 nuevo): scripts/install-image.sh
produce una imagen GPT que se BOOTEA SOLA en QEMU (`-drive file=img`, sin
-kernel ni firmware extra) — SeaBIOS → MBR → GRUB → kernel del propio disco →
arje-zero PID 1. Capaz de arrancar en hardware real.
Layout: vda1=BIOS boot (ef02), vda2=/ (con /boot/bzImage + /boot/grub),
vda3=/store, vda4=/var/lib/hammer. Reusa el particionado sin-root de B3
(sfdisk + mke2fs -d bajo unshare -r + dd conv=sparse) y el wrapper /sbin/init.
GRUB instalado SIN root ni loop: grub-bios-setup sondea el disco físico del
host (/dev/nvme…, 660 root:disk) para adivinar el root device del dir -d y falla
sin privilegios. Lo reemplaza un patch binario determinista sobre la ABI estable
de GRUB i386-pc: grub-mkimage arma core.img; un script Python escribe core.img
en la BIOS boot partition y parchea los punteros (boot.img off 0x5c kernel_sector
→ LBA de core.img; core.img off 0x1F4 blocklist.start → resto de core.img),
con asserts de los valores por defecto (1 y 2) para fallar ruidoso si la ABI
cambia. boot.img va al MBR sin pisar la GPT protective (sólo 440 B).
Verificado in-VM (KVM, sin -kernel): SeaBIOS → GRUB 2.14 → Linux 6.16.12 →
vda1..4 detectadas, vda2 root + vda3/vda4 montadas por el wrapper (0 errores) →
arje-zero PID 1 + hammerd (store=/store journal=/var/lib/hammer/journal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/disk-image.sh ahora arma una imagen GPT de 3 particiones ext4 en vez
de una sola: vda1=/ , vda2=/store (CAS inmutable), vda3=/var/lib/hammer (estado
mutable). Construcción sin root ni loopback: cp -al stagea el rootfs por
hardlinks (vacía store/ y var/lib/hammer/, instala el wrapper /sbin/init),
mke2fs -d puebla cada ext4 bajo unshare -r (root-owned), sfdisk escribe la GPT
y dd conv=sparse,notrunc empalma cada fs en su offset (imagen sparse, ~2G
reales). El wrapper /sbin/init monta vda2/vda3 y hace exec de arje-zero (el
kernel sólo monta vda1). drive-rebuild.py: root=/dev/vda1 en modo DISK.
Consecuencia resuelta: con /store en su propia partición el sellado cruza
filesystems y rename(2) da EXDEV. Store::seal cae a copia recursiva a un
staging dentro del store (preserva symlinks+modos) + rename store-interno
(atómico, mismo FS) + borrado del origen. Test copy_tree añadido.
Verificado in-VM (kernel hammer, KVM): vda{1,2,3} montados dedicados, 0
errores Cross-device, stage1' == stage1 ✓ REPRODUCIBLE. Cierra el ☐ de
SDD 11 §6 (particionado/montaje en la imagen destino).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
B1 — kernel con disco+FS: linux.toml añade VIRTIO_BLK/VIRTIO_PCI/VIRTIO_NET/EXT4_FS
built-in (=y, sin módulos), aditivo al path initramfs. B2 — infra de disco:
- scripts/disk-image.sh: empaqueta un rootfs como imagen ext4 booteable con
`mke2fs -d` bajo `unshare -r` (archivos root-owned sin sudo, como el cpio
--owner=root:root; evita EACCES en el copy-up de overlay del rebuild).
- scripts/drive-rebuild.py: modo DISK= ⇒ QEMU monta la imagen como virtio /dev/vda
y el kernel arranca con `root=/dev/vda rw rdinit=/sbin/init` — sin initramfs.
Verificado en QEMU/KVM: el kernel monta /dev/vda ext4 como root REAL y arje-zero
arranca como PID 1 DIRECTO, sin el hack /init+switch_root (el muro pivot_root del
path initramfs desaparece por construcción: / ya es un mount pivotable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
flex/bison estaban en el devfs base (apk) Y como recipes/{flex,bison}.toml en
deps.build del kernel — pero las capas de deps van BAJO el rootfs, así que el
Alpine flex/bison eclipsaba a los hammer y las recetas eran decorativas. Quito
flex/bison del devfs/bootstrap-devfs.sh; ahora el kernel los toma de deps.build
(hammer). Validado: rebuild en store fresco → kconfig compila con flex/bison
hammer (lexer.lex.o/parser.tab.o), HAMMER_KERNEL=1 verify → ✓ REPRODUCIBLE.
(El bzImage difiere en 1 byte del Alpine-flex/bison build: el timestamp embebido
UTS_VERSION, no flex/bison; el kernel no es input del of_tree del 4/4.)
m4 (que bison invoca) se deja en el base como tool autotools general. Con esto
los build-deps ESPECIALIZADOS del kernel (flex/bison/openssl/elfutils) son todos
hammer-from-source; sólo quedan las libs base/runtime (m4, libz/libzstd de libelf,
gcc) como capa de arranque, igual status que libc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/objtool del kernel enlaza -lelf (libelf+gelf.h); venía de `apk add
elfutils-dev`. Ahora recipes/elfutils.toml construye SÓLO libelf (0.194, lo que
objtool necesita — no libdw/libdwfl/src) desde fuente, wired como deps.build del
kernel. Con esto NINGÚN build-dep del kernel viene de Alpine: el camino del
kernel es 100% hammer-from-source.
elfutils es glibc-céntrico; musl no trae <error.h>/<argp.h>/<libintl.h>/fts/
obstack/rawmemchr. En vez del parche completo de Alpine, shims mínimos en compat/
(vía CPPFLAGS/-include): error.h y argp.h (sólo los usan color.c/printversion.c,
cuyos .o van en libeu.a pero objtool NO referencia ⇒ basta que compilen); libintl
no-op (+ --disable-nls); rawmemchr inline; y libargp/libfts/libobstack.a stubs
para pasar los AC_SEARCH_LIBS del configure (símbolos inertes para libelf). CC=gcc.
bootstrap-devfs.sh: elfutils-dev fuera del NEEDED. Validado: purgué elfutils-dev
del devfs, rebuild → objtool linkea el libelf hammer (sin gelf.h Alpine), y
HAMMER_KERNEL=1 verify → ✓ REPRODUCIBLE bit a bit, DRIVER_RC=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
certs/extract-cert (host-tool del kernel, lo arrastra CONFIG_SYSTEM_DATA_
VERIFICATION=y) enlaza -lcrypto; venía de `apk add openssl-dev`. Ahora
recipes/openssl.toml (OpenSSL 3.5.4, CC=gcc, libcrypto/libssl ESTÁTICAS vía
no-shared) lo construye desde fuente, wired como deps.build del kernel
(materializado como capa overlay en /usr: libcrypto.a + headers + pkgconfig).
bootstrap-devfs.sh: openssl-dev fuera del NEEDED. El runtime libcrypto3/libssl3
(que curl/git necesitan) lo sigue trayendo Alpine aparte — no es openssl-dev.
Validado: purgué openssl-dev del devfs, rebuild del kernel → certs/extract-cert
compila con el libcrypto hammer (sin Alpine openssl), y HAMMER_KERNEL=1 verify →
✓ REPRODUCIBLE bit a bit, DRIVER_RC=0. libelf (objtool) sigue como bootstrap-lib
apk (elfutils-en-musl es el difícil; de-Alpinizable luego).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Generaliza el -append hardcodeado a una env var APPEND (default igual). Útil para
el frente kernel-from-source: experimentar con params del kernel sin tocar el
script. Documenta el hallazgo del muro pivot_root del bwrap (el bzImage hammer
bootea pero bwrap falla porque / es el rootfs absoluto, sin mount padre;
rootfstype=tmpfs no lo arregla).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avance del frente kernel: el build pasó objtool y certs.
- bootstrap-devfs.sh: +flex +bison +elfutils-dev +openssl-dev al NEEDED.
* elfutils-dev (libelf/gelf.h): objtool lo exige — en x86_64 las mitigaciones
seleccionan CONFIG_OBJTOOL aunque se use frame-pointer unwinder.
* openssl-dev: el host-tool certs/extract-cert lo #incluye — el defconfig
fuerza CONFIG_SYSTEM_DATA_VERIFICATION=y vía KEYS←NFS/integrity/dns_resolver
(no apagable sin desarmar media config).
Ambas son build-libs de arranque (status g++/zlib-dev), SEGURAS para el of_tree
del self-host: hammerd no usa openssl (Cargo.lock limpio) y arje-zero va
preseeded/cacheado (no se reconstruye). De-Alpinizables luego con
recipes/{elfutils,openssl}.toml.
- linux.toml: quité los -d de keyring que no pegan (olddefconfig/syncconfig los
revierten por el select-chain); openssl resuelve extract-cert. Comentario
actualizado con el diagnóstico.
Estado: build compila el kernel completo (objtool+certs OK); defconfig es grande
(GPU/wireless), pendiente el bzImage + boot-test + optimización de config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✓ REPRODUCIBLE in-VM (2026-06-18): SWAP_RUST=1 KVM=1 PRESEED=hammerd selfhost-verify.sh
→ el host construye stage1 con hammer-rust (of_tree b3:7fa6cb4e), la VM lo reconstruye con
hammer-rust y reproduce 7fa6cb4e BIT A BIT (stage1' == stage1, DRIVER_RC=0). hammerd
reconstruido in-VM = byte-idéntico al host (d08fd273). Cierra el frente rust del
auto-alojamiento: el compilador (rustc 1.91.1 hammer-built, mrustc→1.90→1.91.0→1.91.1) se
auto-aloja bit a bit, con su propio EXPECT_REF (≠ 9adefb82 de Alpine; rustc emite los bytes).
PRESEED=hammerd es OBLIGATORIO con SWAP_RUST: arje-zero (monorepo tawasuyu) vendorea ~1973
crates; un rebuild in-VM completo (PRESEED=all) desborda el rootfs en RAM (ENOSPC). Con
PRESEED=hammerd se preseedea el arje-zero host-built (hammer-rust, locked desde 9967b02c) y
sólo hammerd se reconstruye in-VM. Documentado en el bloque SWAP_RUST y en rust-frontier/README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-consistencia de hammer-rust VERIFICADA en host (2026-06-17): of_tree(stage1)
construido con hammer-rust 1.91.1 = b3:7fa6cb4e… reproducido 2× (store-rust y store-rust2,
builds frescos independientes). Es el nuevo EXPECT_REF para SWAP_RUST (criterio elegido:
auto-consistencia, ≠ el 9adefb82 de Alpine). Cableado como default de RUST_EXPECT_REF.
swap-rust-into-toolchain.sh --restore: ahora también elimina lo AÑADIDO por el swap
(rustlib x86_64-unknown-linux-musl + librustc_driver de hammer), no sólo restaura
rustc/cargo. Round-trip apply→restore deja el devfs prístino (verificado: sólo
x86_64-alpine-linux-musl + driver de Alpine). Antes dejaba ~350 MB de cruft.
Nota: los leftovers NO afectaban el of_tree (un build Alpine con/ sin ellos diverge igual
del baseline Jun13 — ver hallazgo de drift abajo); la limpieza es higiene/reversibilidad.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recipe.hash_inputs = source+compiler+target+link+patches+flags+phases+deps; NO incluye
el binario rustc del toolchain. En el ./store compartido, arje-zero/hammerd quedarían
cacheados con los bytes de Alpine ⇒ el swap de rust sería un no-op (REF seguiría 9adefb82).
SWAP_RUST=1 ahora usa un store dedicado (store-rust, override RUST_STORE) para forzar el
rebuild de los 4/4 con hammer-rust, sin tocar el ./store baseline. Así of_tree refleja el
compilador nuevo (auto-consistencia).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pieza rust del auto-alojamiento "variante b" (la última del frente). A DIFERENCIA de
make/busybox/bwrap/coreutils/linux-headers (herramientas que orquestan/copian ⇒ bytes
idénticos ⇒ of_tree 9adefb82 por construcción), **rustc EMITE los binarios del 4/4**
(arje-zero, hammerd) ⇒ un rustc distinto diverge of_tree. Criterio elegido: AUTO-
CONSISTENCIA (REF recomputado con hammer-rust = nuevo EXPECT_REF; la VM reproduce ESE),
no igualdad byte-a-byte con el baseline Alpine.
- swap-rust-into-toolchain.sh: overlay aditivo y reversible de .scratch/rust-1.91.1-prefix
sobre /toolchain. El rustlib x86_64-unknown-linux-musl de hammer y sus .so con hash propio
NO chocan con el x86_64-alpine-linux-musl de Alpine ⇒ sólo se reemplazan /usr/bin/{rustc,
cargo} (Alpine guardado en *.alpine; --restore deshace). Los 4/4 compilan NATIVO (target
x86_64-linux-musl == SANDBOX_NATIVE_TARGET, sin --target) ⇒ hammer-rustc emite para su
triple nativo x86_64-unknown-linux-musl.
- selfhost-verify.sh: SWAP_RUST=1 aplica el swap en $TOOLCHAIN antes del build de stage1
(así el REF host se computa con hammer-rust) y restaura al salir (trap). Sirve TANTO al
REF host COMO al toolchain in-VM (--toolchain $TOOLCHAIN). EXPECT_REF pasa a RUST_EXPECT_REF
(no el 9adefb82 de Alpine). Vars: RUST_PREFIX, RUST_EXPECT_REF.
VALIDADO: el swap aplica/restaura limpio; hammer-rust 1.91.1 (host x86_64-unknown-linux-musl)
compila+corre un crate NATIVO (sin --target) en el devfs — el camino exacto de los 4/4.
NEXT: computar el nuevo RUST_EXPECT_REF (bootstrap stage1+stage2 con SWAP_RUST) y verify in-VM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ambos hops del climb construidos con el bootstrap real (x.py) en el sandbox devfs,
reusando LLVM 20.1.8 externo en todo (gate >=19) — sin rebuild de LLVM:
mrustc → rustc 1.90.0 → rustc 1.91.0 → rustc 1.91.1
- hop 1 (1.90.0→1.91.0): bootstrap.toml, stage0 = prefix run_rustc musl-host
1.90.0. stage-2 + install → rust-1.91.0-prefix relocatable (rpath).
- hop 2 (1.91.0→1.91.1): bootstrap-1.91.1.toml, stage0 rustc = el 1.91.0 instalado
(bind ro /stage0 vía STAGE0), stage0 cargo = el 1.90.0 (pasa el check minor-1).
extended+cargo. FIX cargo-native-static=true → feature all-static (openssl/curl/
libgit2/libz vendored desde fuente vía gcc), el devfs hermético no tiene OpenSSL.
- run-xpy.sh: añade bind ro opcional /stage0 (env STAGE0) para el stage0 del hop.
VERIFICADO: rust-1.91.1-prefix/bin/{rustc,cargo} corren standalone (solo prefix
montado, rpath). rustc 1.91.1 (ed61e7d7e) host x86_64-unknown-linux-musl LLVM
20.1.8; cargo 1.91.1. Smoke-test compila+corre. Es la versión EXACTA de Alpine,
auto-alojada desde la cadena mrustc. Cada hop ~45min (LLVM reusado).
NEXT: swap rust 1.91.1 en /toolchain + criterio de auto-consistencia (EXPECT_REF).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frente rust, fase climb: la toolchain mrustc-bootstrapped 1.90.0 (host musl) ya
sirve de stage0 para construir rust 1.91.0 con el bootstrap real (x.py).
- bootstrap.toml: build/host/target=x86_64-unknown-linux-musl, stage0 rustc/cargo
= prefix run_rustc 1.90.0 host-musl, LLVM 20.1.8 reusado vía llvm-config (pasa
el gate >=19, sin rebuild de LLVM), crt-static=false (link dinámico contra el
musl del devfs, como Alpine y la toolchain de run_rustc), rpath=true.
- run-xpy.sh: corre x.py en el sandbox devfs (bind /src, /out, /mrustc) para que
resuelvan los paths de stage0 + llvm-config del bootstrap.toml.
- README: documenta ambos.
dry-run de x.py VALIDADO (stage0 pasa el version-check minor+1, LLVM hallado,
target ok). Build stage-2 de 1.91.0 EN MARCHA en background.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Con el stage0 musl-host, run_rustc all terminó: rustc stage-3 (host=musl, LLVM 20.1.8,
1.90.0-stable-mrustc) + cargo 1.90.0 + sysroot (294 rlibs) construidos y verificados
corriendo. El muro de proc-macros quedó roto (tinystr/displaydoc/icu nativos).
Dos snags finales arreglados (en run_rustc.patch, vs be69c74):
- Makefile:262 (hello_world FINAL): mismos flags CRT que la 172 (--target +
-C target-feature=-crt-static -C link-self-contained=no); solo aflora al completar
stage-2/3.
- Makefile:235 (cargo FINAL): --features vendored-openssl (openssl-sys no halla openssl
del sistema en el devfs hermético; openssl-src lo compila desde fuente). README
documenta ambos como snags #4 y #2(262).
NEXT: climb 1.90 -> 1.91.0 -> 1.91.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Con el muro de proc-macros roto (stage0 musl-host), run_rustc avanzó por primera vez
hasta el smoke-test hello_world del prefix FINAL (Makefile:262) y falló igual que el
de stage-1: usa el default musl -static-pie + CRT self-contained ausente
(rcrt1.o/crti.o/crtbeginS.o/crtendS.o/crtn.o + -lunwind estático) -> ld: cannot find.
Fix idéntico al de la línea 172: añade --target $(RUSTC_TARGET) -C target-feature=
-crt-static -C link-self-contained=no a la regla del hello final. run_rustc.patch
regenerado (vs be69c74) e incluye ahora ambas reglas; README snag #2 documenta las dos.
Verificado parcialmente: stage-2 rustc, stage-3 rustc y cargo se construyeron OK antes
del fallo (proc-macros nativos: tinystr/displaydoc/icu compilan). Rebuild en curso.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Al construir el cargo del stage0 musl-host, minicargo (con RUSTC_TARGET seteado,
aunque host==target==musl) corre los build-scripts bajo cargo-build/host/build_X/
pero el crate target hace include!/include_bytes!(OUT_DIR/..) apuntando a
cargo-build/build_X/ (vacío) -> mrustc aborta con signal 6. Rompió en libsqlite3-sys
(bindgen.rs) y en el binario cargo (man.tgz).
Fix: symlink_outdirs() enlaza cada dir target -> host (salidas bit-idénticas porque
host==target), aplicado en un bucle de reintento alrededor del build incremental de
cargo (los sys-crates tardíos -cargo/curl/openssl/libgit2-sys/libssh2-sys- corren sus
scripts al final, así que un solo pase no basta). Documentado como snag #4 en el README.
Verificado: stage0 musl-host rustc 1.90.0 + cargo 1.90.0 construyen y corren con
host=x86_64-unknown-linux-musl; desbloquea proc-macros nativos en run_rustc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rastrea las correcciones del frente rust que vivían en .scratch/ (gitignored):
- run_rustc.patch (vs mrustc be69c74): minicargo/hello con --target musl;
RUSTFLAGS + sitios de link con link-self-contained=no (CRT vía gcc Alpine,
link dinámico contra musl); proxy host-units con crt-static off (sin inject
de --target, host==target==musl con el stage0 musl-host).
- build-stage0-musl.sh: rebuild del stage0 rustc+cargo con host triple musl
(CFG_COMPILER_HOST_TRIPLE vía RUSTC_TARGET, --target vía MRUSTC_TARGET),
reusa LLVM + bin/mrustc. Gotcha OUTDIR_SUF documentado.
- run-runrustc.sh: corre targets de run_rustc en el sandbox devfs apuntando
al stage0 musl-host.
- README: diagnóstico de los 3 snags acoplados (std-gnu mmap64/open64,
CRT self-contained ausente, host-triple gnu + proc-macros) y el workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El rustc booteado por mrustc agota los pthread keys de musl al correr
("fatal runtime error: out of TLS keys" — gotcha #4 de mrustc issue #388).
Es el musl del TOOLCHAIN (el loader /lib/ld-musl-x86_64.so.1 que toda tool
del rootfs linkea), NO el musl target del 4/4 (recipes/musl.toml, estático,
intacto). Se añade a bootstrap-devfs.sh un paso idempotente que reconstruye
musl 1.2.5 con PTHREAD_KEYS_MAX=256 (sed en limits.h, build gcc lib/libc.so)
y reemplaza el loader (backup .orig128). ABI-compat: gcc/make siguen
corriendo. Seguro para of_tree: el loader runtime de las tools no cambia los
bytes que emiten; el 4/4 es estático contra hammer-musl.
Verificado a mano: run_rustc pasó el punto donde moría (build de libstd
musl) y avanza (27%+).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inicia la última pieza del toolchain hammer-from-source: reemplazar el
rustc/cargo de Alpine por una cadena purista vía mrustc (mrustc → rustc
1.90.0 → 1.91.1, sólo 2 saltos). mrustc es C++14 y DEBE compilarse con g++
(clang/zig rompen sus macros tagged_union, ver mrustc issue #388) y enlaza
-lz; el toolchain traía libstdc++ runtime pero no cc1plus/headers ni
zlib-dev. Se añaden `g++` y `zlib-dev` a la lista NEEDED del devfs como
herramientas de arranque (mismo status que make/zig/gcc — el veto purista
es sobre un *rustc* prebuilt, no sobre un compilador C++; zlib-dev quedará
reemplazado por recipes/zlib.toml al avanzar el frente).
.gitignore: ignora .scratch/ (clone de mrustc + fuentes de rustc, multi-GB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quinta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): GNU coreutils 9.8
(cp/mkdir/ln/chmod/mv/install…), que el `make install` de musl y busybox invocan.
- recipes/coreutils.toml: empaquetada multicall (--enable-single-binary), mismo layout que el
paquete de Alpine (un /bin/coreutils + ~100 symlinks que despachan por argv[0]). Estática musl
con zig cc, vainilla sin patches (es tool del toolchain, no input compilado del 4/4).
- scripts/selfhost-verify.sh: SWAP_COREUTILS=1 construye la receta y monta el único binario sobre
/toolchain/bin/coreutils — los symlinks del toolchain (cp/mkdir/install/…) lo siguen, un solo --swap.
Bisección host fuerte: con hammer-coreutils pisando el de Alpine (trap-restore en .dev-fs), musl Y
busybox rebuildearon BYTE-IDÉNTICO (57b66a2e, 56664d70). Pendiente: corrida in-VM acumulando swaps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.
Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
(libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
(build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].
Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tercera pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): los headers
UAPI del kernel 6.16.12 que musl/busybox #include, hoy tomados del paquete linux-headers
de Alpine.
- recipes/linux-headers.toml: `make headers` (no `headers_install` — su rsync final falta
en el toolchain hermético) + unifdef vía HOSTCC=zig cc; .tar.gz (busybox-tar lo
descomprime sin xz). Sella los 13 subdirs kernel-owned de usr/include.
- assemble_builder: el swap ahora soporta rel_path de DIRECTORIO (reemplaza el árbol
entero: remove + copy), no sólo binarios. Cubierto por test nuevo
(builder_rootfs_swaps_toolchain_header_tree_from_source, incl. borrado de huérfanos).
- recipes/linux-headers-alpine-compat.patch: el paquete de Alpine no es el `make headers`
crudo. Aporta content-pinned 5 archivos que difieren de la salida vainilla; 3 son
REQUERIDOS (scsi/{scsi,scsi_ioctl,sg}.h — legacy userspace, NO UAPI) porque el applet
`eject` de busybox los incluye y sin ellos no compila. install los pisa sobre /out y
limpia el junk (.cmd/Makefile/headers_check.pl/drm) que `cp -a` arrastra.
- scripts/selfhost-verify.sh: SWAP_LINUX_HEADERS=1 construye la receta y emite un --swap
por subdir kernel-owned (auto-derivado del artefacto sellado).
Criterio de éxito fuerte: `diff -r` del header-tree de hammer contra el de Alpine = VACÍO
⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ reproducibilidad por construcción.
Ensamblado end-to-end en host OK (13 swaps, musl bits/sys intactos). 133 tests verdes.
Pendiente: corrida in-VM para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variante (b) pieza 2: el sh+coreutils que el build usa del /toolchain. El /toolchain
de Alpine es mixto — busybox para sh/sed/grep/awk/tar/find (symlinks → /bin/busybox),
GNU coreutils para cp/mkdir/install. El swap monta el busybox estático de hammer
sobre /toolchain/bin/busybox: los applets busybox-backed pasan a usarlo, coreutils
GNU queda intacto. Sin código nuevo — el mecanismo --swap ya soporta rel_path
(--swap busybox=<hash>:bin/busybox).
Validado en host (sin VM): con make+busybox de hammer swapeados, los 4/4 componentes
construyen — incl. busybox compilándose a sí mismo con hammer-busybox de shell — y el
of_tree del rootfs ensamblado da 9adefb82 (el baseline determinista). Las piezas 1 y 2
son reproducibilidad-neutrales: el toolchain es intercambiable sin perturbar el byte-output.
selfhost-verify.sh: SWAP_BUSYBOX=1 (paralelo a SWAP_MAKE). Docs 10/11 actualizadas.
Pendiente: verify in-VM con los swaps para el ✓ REPRODUCIBLE end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verificado: arje-zero con CARGO_BUILD_JOBS=1 y TODAS las CPUs del host sale
byte-idéntico al build serial ⇒ el jobserver serializa el backend paralelo de
rustc/LLVM (ThinLTO) independientemente del nº de CPUs. La pieza que faltaba para
la reproducibilidad host↔VM (codegen-units=1 no bastaba).
- EXPECT_REF re-baseado: 0039b2b9 (build paralelo no-fiable) → 9adefb82 (serial,
determinista, = lo que produce la VM con 1 vCPU).
- Baseline del host refrescado: store/ re-sellado con el arje-zero determinista.
- docs/10-roadmap.md: causa raíz documentada (make inocente; arje-zero/ThinLTO).
Pendiente: re-correr el verify in-VM con el fix para cerrar el ✓ REPRODUCIBLE y
validar de paso la pieza 1 (swap del make).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Segundo paso del auto-alojamiento *puro* (SDD 11 §7.2b): tras sellar GNU make
4.4.1 desde fuente (commit 749ea9e), ahora el builder puede *usarlo*. El swap
reemplaza una a una las piezas que el builder toma de Alpine por recetas hammer,
con Stage 2 reverificando que el byte-output no cambia.
- `BuilderSpec.swaps: Vec<ToolchainSwap{name,artifact,rel_path}>`: monta el binario
sellado sobre el path Alpine en /toolchain (estático musl ⇒ sin shim del loader).
- `swaps_digest` (ordenado por nombre) entra al hash lógico del builder ⇒ la
procedencia deja de ser "todo Alpine" y se vuelve auditable para el log de
transparencia. Vacío ⇒ digest "" (compat hacia atrás: builder pura-Alpine
conserva su hash previo).
- CLI: `hammer bootstrap builder --swap make=<hash>[:rel_path]` (repetible;
rel_path por defecto usr/bin/<name>).
- selfhost-verify.sh: opt-in `SWAP_MAKE=1` (construye recipes/make.toml y lo
swapea) + `SWAPS="name=hash …"` para swaps extra. Default off ⇒ corrida
pura-Alpine idéntica a la baseline conocida-buena.
Validado en el host contra el store real: pura `b3:8a370f5d…` vs swapped
`b3:e7e2282c…`, y /toolchain/usr/bin/make queda hardlinkeado al artefacto
fbad44ac… (ELF estático, no el dinámico de Alpine). 3 tests nuevos
(swap aplica, hash cambia, error si falta el binario). 37 tests verdes.
Pendiente: correr Stage 2 in-VM con el swap y confirmar que of_tree(stage1') == ref
(el make hammer compila los 4/4 idéntico al de Alpine ⇒ toolchain intercambiable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El rebuild in-VM daba ✗ DIVERGENTE (of_tree host 984e002f ≠ VM 94b93585) pese a
que los 4 componentes sellaban bajo el mismo key of_inputs. Bisección por componente
en el host: musl, busybox y hammerd reconstruyen byte-idéntico, y la ensambladura del
rootfs (of_tree) es determinista e independiente del entorno. El culpable era arje-zero:
dos builds del MISMO host daban binarios distintos (Δ ~9.6 KB por .text/.rodata/.eh_frame/
.gcc_except_table) — firma del codegen paralelo de rustc.
Raíz: el workspace de hammer pinea [profile.release] codegen-units=1 (por eso hammerd
reproducía), pero el monorepo tawasuyu no declara perfil → cargo default codegen-units=16,
cuyo reparto del crate en N objetos varía build-a-build.
Fix: el sandbox impone codegen-units=1 para TODAS las crates Cargo (el var de cargo gana
sobre el profile del repo fuente). El lab elimina el no-determinismo en vez de confiar en
upstream (SDD 09 §2). Verificado: dos builds cu=1 de arje-zero → byte-idénticos.
Nueva referencia reproducible 4/4: of_tree(stage1)=b3:0039b2b9… (reemplaza 198f209f… de
cu=16). Actualizados EXPECT_REF (selfhost-verify.sh) y runbook §8b/§8c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos bugs latentes que sólo aparecían en un build real (in-VM); en el host
quedan tapados porque todo sale de caché.
1. scripts/selfhost-verify.sh: los módulos se sacaban de /lib/modules/$(uname -r)
pero la VM bootea $KERNEL (/boot/vmlinuz-linux), que puede ser otra versión.
Mismatch de version-magic ⇒ overlay.ko no carga ⇒ bwrap muere "No such device".
Ahora la versión se deriva del bzImage booteado (file -bL "$KERNEL"; fallback uname -r).
2. crates/hammer-build/src/sandbox.rs: spawn_pump teeaba el stdout del build al
stdout del padre. rebuild-stage1 hace PRIME=$(hammer ... bootstrap stage1); en
un build real son miles de líneas de configure/make capturadas en $PRIME ⇒ el
paso siguiente --rootfs "$PRIME" exec con un arg gigante ⇒ E2BIG (Argument list
too long). Ahora el tee va a stderr; stdout queda para el hash legible-por-máquina.
Con esto el verify corre end-to-end in-VM: los 4 componentes reproducen bit a bit,
pero of_tree(stage1) diverge (host vs VM vs baseline) — no-determinismo del ensamblado
a cazar (SDD 09 §2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
El run en la VM avanzó hasta `configure` y ahí bwrap falló: `Can't make overlay
mount … No such device`. Causa raíz: el kernel trae overlay como MÓDULO
(CONFIG_OVERLAY_FS=m) y el initramfs arranca sin módulos cargados ⇒ ENODEV. El
sandbox de hammer-build usa `bwrap --tmp-overlay` (raíz efímera sobre el
toolchain), así que necesita overlayfs.
Fix: rebuild-stage1 hace `insmod /toolchain/lib/overlay.ko` tras el shim del
loader (overlay no tiene deps; vermagic del kernel destino). El toolchain suma
`kmod` (insmod/modprobe). El `overlay.ko` es específico del kernel (no de Alpine):
se inyecta aparte en el builder (.dev-fs/alpine/lib/overlay.ko), documentado en
bootstrap-devfs.sh.
Progreso del run: shim OK ⇒ bwrap/git/curl corren ⇒ fetch OK ⇒ configure arranca;
overlay era el siguiente muro. 28 tests verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correr el rebuild EN la VM destapó dos faltantes reales del builder (justo lo que
la verificación de Stage 2 debe cazar):
1. El toolchain (variante a, desde Alpine) traía compiladores (cargo/make/zig)
pero NO las herramientas de orquestación del lab — bwrap (anida el sandbox),
git (git archive del mirror) y curl (tarballs). En el host las aporta el
sistema; en la VM el toolchain es el único userland capaz, así que deben vivir
ahí. bootstrap-devfs.sh las añade (paquete `bubblewrap`, no `bwrap`).
2. Esos binarios son Alpine *dinámicos* y no corren desde el userland Stage 1
*estático* (musl --disable-shared ⇒ no hay loader en /lib). rebuild-stage1
ahora instala un shim del loader musl (cp a /lib) + LD_LIBRARY_PATH/PATH al
toolchain antes de invocar hammer; el sandbox de build sigue anidando en
/toolchain. El hammer estático corre por ruta absoluta.
El builder boot end-to-end ya estaba probado; esto lo hace además *capaz de
reconstruirse*. 28 tests verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/boot-builder-vm.sh empaqueta el último eslabón operacional del SDD 11 §7:
bootea work/builder.cpio.gz (el builder rootfs como initramfs cpio+gzip) con
arje-zero como PID 1; en la consola se corre `rebuild-stage1` para reconstruir
stage1' con el toolchain de adentro y comparar con la referencia.
- Defaults: KERNEL=/boot/vmlinuz-linux, MEM=6144, CPU=Broadwell (AVX2 bajo TCG).
- KVM=1 usa -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido).
- Documenta la tensión de RAM: el rootfs vive en tmpfs (~1.2 GB) + el rebuild Rust
necesita varios GB más, todo en RAM; con host ~7 GB y sin KVM va justo/lento.
El initramfs (410 MB gz, 26776 entradas) se verificó: sbin/init→arje-zero, el
hammer estático, rebuild-stage1, rebuild.env y el toolchain/cargo presentes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La verificación de reproducibilidad de Stage 2 (dos builds + diff de bytes) probó
que musl reconstruye bit-idéntico pero busybox NO, y cazó dos no-determinismos:
1. Config interactiva: tras editar .config, silentoldconfig prompea por opciones
NEW leyendo stdin → set de applets variable (build no-determinista y flaky).
Fix en busybox.toml: `yes '' | make oldconfig` (defaults, determinista;
1.36.1 no tiene olddefconfig).
2. Headers UAPI del kernel: el config determinista habilita console-tools, que
exige linux/kd.h (zig no la bundlea). Fix: linux-headers en bootstrap-devfs.sh
(zig cc nativo la halla en /usr/include).
Con ambos, musl y busybox reconstruyen bit-idéntico. Runbook §8b documenta la
verificación y deja pendiente: reproducibilidad de los componentes Rust y el
rebuild dentro de un builder rootfs (auto-alojamiento pleno).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Primera ejecución del runbook en el host. El userland C builda end-to-end; el
camino Rust queda bloqueado por una decisión de toolchain. Hallazgos verificados:
recipes/busybox.toml — tres incompatibilidades GNU-toolchain ↔ zig, resueltas:
- gcc hardcodeado (CC/HOSTCC = gcc con `=` ignora el env): pasar CC='zig cc'
HOSTCC='zig cc' en línea de comando de make
- flags GNU-ld que zig cc valida y rechaza (--warn-common, -Map, --verbose):
quitarlos de scripts/trylink con sed
Con esto musl 1.2.5 y busybox 1.36.1 compilan, linkean y se sellan.
scripts/bootstrap-devfs.sh — añade rust+cargo al rootfs (las recetas Cargo
corren cargo en el sandbox), con CAVEAT: el rust de Alpine es
x86_64-alpine-linux-musl pero BuildSys::Cargo pide x86_64-unknown-linux-musl
(no instalado, sin rustup) → bloquea hammerd/arje-zero. Decisión de toolchain
abierta (plan C.2).
docs/runbooks/stage1-vm-boot.md §8 — bitácora con la tabla de etapas, los fixes
y las opciones para resolver el triple Rust.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el primer entregable del roadmap. Cambios:
- `Source`: ahora admite dos modos mutuamente excluyentes — `git` (repo+commit) o
`tarball` (url+sha256). El hash de entrada del artefacto usa el commit en modo
git y el sha256 en modo tarball; ambos son identificadores inmutables del
contenido fuente. Validación en `Source::kind()` con error claro.
- `fetch`: dispatch por modo. Tarball cacheado en `work/tarballs/<sha>.tar`,
descarga vía `curl -fL`, verificación sha256 con `sha2`, extracción con
`--strip-components` (default 1, GNU-style).
- `recipes/grep.toml`: GNU grep 3.12 desde el tarball release de gnu.org
(sha256 fijado, sin patches, `--disable-perl-regexp --disable-nls`).
- Bootstrap: añade `binutils` al rootfs Alpine — configure de autotools mira
ld/ar/ranlib incluso cuando el compilador real es zig cc.
- `.gitignore`: `/work/` (artefactos transitorios del build).
- `docs/10-roadmap.md`: Fase 0 y Fase 1 marcadas ✅; entregable cerrado.
Nota sobre v3.11 vs v3.12: probé primero v3.11 y el binario producido daba
"memory exhausted" en cualquier regex (bug conocido de grep+musl static al
inicializar DFA). v3.12 corrige y funciona limpio dentro del rootfs Alpine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `BuildConfig.cache_root` (override `HAMMER_CACHE`): bindea `/cache` al sandbox
y exporta `ZIG_GLOBAL_CACHE_DIR=/cache/zig`. Evita ~30s de recompilación de
musl en cada build.
- `scripts/bootstrap-devfs.sh`: idempotente, descarga+verifica alpine-minirootfs
3.23.4 y zig 0.16.0 con sha256 fijo, instala build tools en el rootfs vía apk
bajo bwrap.
- `make_tree_read_only`: solo archivos regulares pierden `w`; los directorios
conservan 0o755 para no estorbar GC ni `rm -rf` administrativo. La
inmutabilidad estricta del store se delega al mount RO de la distro propia.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>