250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
14 KiB
SDD 13 — Release engineering (Etapa E): imagen auto-booteable, instalador, mirror, upgrades
Estado: EN CURSO. Etapa E del roadmap (
docs/10-roadmap.md, memoria distro-completion-roadmap). Es trabajo nuevo: las Etapas A–D dejaron un sistema que (A) se orquesta, (B) bootea de disco particionado real, (C) tiene userland Rust-nativo y (D) atesta confianza. Falta convertir eso en algo que un tercero instale y arranque sin el host de dev: una imagen que bootea sola (sinqemu -kernel), un instalador a disco físico, un mirror del store y un camino de upgrade.
1. El gap concreto que abre la Etapa E
Hasta B3 la imagen de disco (scripts/disk-image.sh, GPT con /, /store, /var/lib/hammer
dedicados) no se auto-bootea: el drive-rebuild.py la arranca con qemu -kernel <bzImage> -append root=/dev/vdaN — el kernel lo carga QEMU, no la imagen. Eso sirve para verificar el
auto-alojamiento, pero un disco así no arranca en hardware real ni en una VM "normal": le falta un
bootloader en el propio disco.
2. Decisión de bootloader: GRUB BIOS (i386-pc) ahora, UEFI después
- GRUB BIOS (i386-pc) — ELEGIDO para el primer corte. QEMU trae SeaBIOS built-in ⇒ un disco con
GRUB en el MBR + una BIOS boot partition (GPT type
ef02) bootea conqemu-system-x86_64 -drive file=imgsin firmware extra ni-kernel. Se instala en userspace, sin root ni loop:grub-mkimagearmacore.imgygrub-bios-setupescribeboot.img→MBR +core.img→partición BIOS sobre el fichero imagen (es I/O de bloques, no necesita montar). Los módulos de GRUB y el kernel van en/bootde la partición root, quemke2fs -dpuebla desde un directorio. - UEFI (EFI-stub + ESP FAT) — diferido. El kernel ya trae
CONFIG_EFI_STUB=y, pero (a) el host de dev no tiene firmware OVMF usable para verificar el arranque, y (b) poblar un ESP FAT sin root exigemtools(ausente). Camino futuro cuando haya OVMF + mtools (o se construyan): ESP conEFI/BOOT/BOOTX64.EFI= el bzImage (EFI-stub), cmdline porloader.conf/UKI.
3. Layout de la imagen instalable (GPT, BIOS)
/dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB
/dev/vda2 / ext4 [takana-root] + /boot/bzImage + /boot/grub
/dev/vda3 /store ext4 [takana-store] artefactos CAS (inmutable)
/dev/vda4 /var/lib/hammer ext4 [takana-state] estado mutable (journal, overlays)
Cadena de arranque: SeaBIOS → MBR (boot.img) → core.img (BIOS boot part) → lee (hd0,gpt2)/boot/grub/ grub.cfg → linux /boot/bzImage root=/dev/vda2 rdinit=/sbin/init → wrapper /sbin/init monta
vda3/vda4 → exec /usr/bin/arje-zero (PID 1). El wrapper es el mismo puente de B3 (arje todavía no lee
fstab/cards de montaje por sí mismo).
scripts/install-image.sh produce esta imagen. Reusa la técnica de particionado sin-root de B3
(sfdisk + mke2fs -d bajo unshare -r + dd conv=sparse,notrunc) y añade la BIOS boot partition, el
kernel en /boot, los módulos GRUB en /boot/grub/i386-pc y los pasos grub-mkimage/grub-bios-setup.
4. Piezas pendientes de la Etapa E (orden tentativo)
- E1 — imagen auto-booteable (GRUB BIOS): ✅ primer corte (
scripts/install-image.sh). Bootea en QEMU sin-kernelhasta la shell de arje-zero. Hoy empaqueta el builder rootfs (con toolchain); un product rootfs lean es refinamiento. - E2 — instalador a disco físico: un
takana install <device>que particiona un disco real y vuelca las tres particiones + GRUB (la misma lógica, apuntando a/dev/sdXen vez de un fichero). - E3 — mirror del store: ✅ primer corte (crate
takana-mirror+ CLItakana mirror push|pull|status). Replica/storecontent-addressed (BLAKE3) entre máquinas; el hash ES la dirección, así que un mirror es un CAS replicado + resolución por hash. La integridad se ancla enof_tree(content-hash del árbol): el receptor recomputaof_treesobre lo recibido y exige que case el del índice antes de sellar (Store::sealatómico + read-only) — una transferencia corrupta/manipulada se rechaza, no se instala.push/pullsonsync(src,dst)(idempotente, salta presentes, reporta conflictos = mismo hash con otro contenido). Transporte de esta entrega = sistema de ficheros (el remoto es una ruta a otro store, p.ej. un montaje sshfs); el transporte sobre SSH/red que el producto ya expone es un envoltorio posterior. Endurecimiento pendiente: anclar elof_treea una raíz firmada (log de transparenciabootstrap.json/ atestación de Etapa D) para defenderse de un origen plenamente malicioso (hoy el invariante forzado es "el contenido recibido hashea a lo que el índice anuncia", que ataja corrupción de transporte). - E4 — upgrades: ✅ primer corte (crate
takana-upgrade+ CLItakana upgrade apply|rollback|status). Aplica un árbol Stage1/producto nuevo (artefacto sellado del store, CAS BLAKE3) sobre el FHS vivo sin reinstalar, dejando una generación, y revierte exactamente al árbol anterior conrollback. Modelo de generaciones (inspirado en NixOS, in-place al FHS porque el boot lee el root directo, no un symlink indirecto): cada generación graba bajo<state>/generations/<N>/sumanifest.json(tree_dir del store,of_tree, parent, lista de ficheros, cambios) y unbackup/con los bytes previos de todo path que pisó o retiró. Atomicidad: proyección fichero-a-fichero (escritura a temporal +rename⇒ cada fichero conmuta atómicamente), concurrent(id de la generación viva) como commit-point; un corte a mitad dejacurrenten la vieja y los backups intactos ⇒ re-apply/rollback recuperan. Diff vs el árbol previo: el apply añade/pisa los ficheros del árbol nuevo y retira los que la generación viva aportaba y el nuevo no tiene. Rollback: deshace en orden inverso (borra lo añadido, restaura desdebackup/lo pisado/retirado) y retrocedecurrental padre — restauración exacta, incluso al estado pre-upgrades. Cada cambio se registra en el journal comoHammerHydrate{artifact=tree_dir}(replay-able). Verificación opcional de integridad: si el llamador anuncia elof_treeesperado (de un índice de mirror E3 o release firmada), el apply lo exige antes de tocar el root. Maneja ficheros regulares + symlinks; valida E2E en host conscripts/upgrade-e2e-test.sh(apply v1→v2→rollback→rollback contra el binario real). GC de generaciones ✅ (takana upgrade prune [--keep N]/takana_upgrade::prune): borra las generaciones que el rollback ya no alcanza (huérfanas tras un rollback — las de la cadena viva siempre se conservan);--keep Nademás recorta la cadena a sus N más nuevas (limita la profundidad de rollback, comodelete-generationsen NixOS). Journal de intención + replay ✅ (takana upgrade recover [--rollback]/takana_upgrade::{pending,recover}): el apply escribe el plan completo apending.jsonANTES de proyectar y lo limpia al commitear; si un corte/reinicio lo interrumpe,pending.jsonsobrevive y el FHS quedó a medias. La proyección es re-entrante (project_plan, backups idempotentes —backup_existing_oncenunca pisa el original capturado), asírecovercompleta (roll-forward: re-ejecuta el plan + commitea, re-verificando elof_tree) o deshace (roll-back: restaura backups, borra la generación a medias,current→padre).applyse niega si hay intento pendiente (exige recover explícito);statuslo avisa. 5 tests (corte a media proyección → ambos modos) + ejercicio enupgrade-e2e-test.sh.- Auto-recover al arranque ✅ (
crates/hammer-recover— mini-binario static-musl). El modelo de generaciones es in-place (el FHS es la generación viva), así que NO hay menú de generaciones tipo NixOS que ofrecer en GRUB; lo que encaja es auto-sanar un upgrade interrumpido al boot. El producto no lleva el CLItakanacompleto ⇒hammer-recoveres lo único que el sistema instalado necesita: el wrapper/sbin/init(dehammer-live-install.sh) lo corre tras montar/storey/var/lib/hammery antes de incarnar arje-zero. Sinpending.jsones no-op; con uno, completa (roll-forward) o, si no puede, deshace (roll-back) ⇒ el FHS siempre arranca consistente. Nunca aborta el boot. El instalador lo bundlea desde el payload a/usr/sbin/hammer-recover. Validado in-VM (iso-install-test.sh): el disco imprimehammer-recover: sin upgrade interrumpidoal boot y sirve SSH. Refinamiento #4 (E4 endurecimiento) CERRADO.
- Auto-recover al arranque ✅ (
- E5 — ISO/medio de arranque: ✅ primer corte (
recipes/xorriso.toml+scripts/iso-image.sh+scripts/iso-boot-test.sh). Medio live que arranca ENTERO desde RAM: GRUB (El Torito) carga kernel + initramfs desde el ISO 9660, el kernel desempaqueta el rootfs del producto a un tmpfs y corre/init— sin tocar disco. Es el medio para correr el instalador (takana install /dev/sdX) en hardware nuevo o probar el sistema sin instalarlo. Herramental construido por takana: el host no traexorriso(ygrub-mkrescuelo exige) ⇒recipes/xorriso.tomllo construye desde fuente (GNU xorriso 1.5.8 = libburn+libisofs+libisoburn, zig 0.13.0, estático musl, libs opcionales off ⇒ binario autocontenido).iso-image.shlo dogfooda: arma el ISO con el xorriso del store. Cadena de boot BIOS: reusa los módulos GRUB i386-pc de E1 pero con el core El Torito (grub-mkimage -O i386-pc-eltorito+ módulosiso9660/biosdisk) en vez del core de disco (ext2);xorriso -as mkisofs -b …/eltorito.img -no-emul-boot -boot-info-table --grub2-boot-info --grub2-mbr boot_hybrid.img⇒ ISO isohybrid (booteable como CD y USB/disco). El initramfs es elproduct-rootfslean empaquetado comocpio.gz(live de RAM; la variante con squashfs+overlay para rootfs grandes es refinamiento). Validado E2E in-VM (iso-boot-test.sh, ISO 113M): SeaBIOS → GRUB 2.14 (El Torito) → Linux 6.16.12 takana-built →/init(marcadorHAMMER-ISO-LIVE-OK) → arje-zero PID1 → hammerd + netup (lease DHCP) → sshd escuchando: el producto entero corriendo desde el medio live. Pendiente (refinamiento): squashfs+overlay para no cargar todo el rootfs en RAM (diferido: el kernel no trae SQUASHFS ni CDROM/SR ⇒ exige rebuild de kernel, valor marginal en el producto lean).- Arranque UEFI ✅ (
iso-image.sh EFI=1+recipes/mtools.toml+scripts/efi-boot-test.sh). El mismo ISO se vuelve híbrido BIOS+UEFI: además del El Torito i386-pc se añade un 2º El Torito EFI (-eltorito-alt-boot -e efi.img -no-emul-boot -isohybrid-gpt-basdat) que arranca una ESP FAT conEFI/BOOT/BOOTX64.EFI= ungrubx64.efi(grub-mkimage -O x86_64-efi). GRUB EFI lee el mismogrub.cfgy carga kernel (EFI_STUB, ya en el kernel — sin rebuild) + initrd vía el protocolo EFI. Herramental: poblar la ESP FAT sin root exigemtools(mformat/mcopy), ausente en el host ⇒recipes/mtools.tomllo construye (GNU mtools 4.0.49, estático musl, dogfoodeado por iso-image.sh). GOTCHA:mformat -Ffuerza FAT32 (mínimo ~33 MiB); sobre una ESP chica deja un FAT inválido que la firmware no lee ("failed to load … Not Found") ⇒ sin-F, mformat auto-elige FAT12/16. Validado E2E: el MISMO ISO bootea por UEFI (QEMU+OVMF → grubx64.efi → "Loaded initrd from LINUX_EFI_INITRD_MEDIA_GUID" → arje-zero → sshd) y por BIOS (SeaBIOS, El Torito). Refinamiento #3 CERRADO. (Falta como pulido: rama EFI del INSTALADOR a disco — GPT+ESP entakana-install, hoy el instalado es MBR-BIOS.) - Instalar DESDE el live ✅ (
scripts/iso-image.sh INSTALLER=1+scripts/hammer-live-install.sh→/usr/bin/hammer-install+scripts/iso-install-test.sh). Cierra el lazo ISO live → disco instalado → bootea solo. El ISO instalador bundlea un payload de disco en el initramfs bajo/usr/lib/hammer/install/(kernel + GRUBboot.img/core.imgMBR-prefix(hd0,msdos1)/boot/grub- módulos, armado en build-time con
grub-mkimage).takana-install <dev>corre dentro del live como root real (sin los rodeos rootless de E1) usando sólo lo que el producto ya trae (busyboxfdisk/mke2fs/mount/dd+ uutilscp): particiona MBR (p1=/, p2=/store, p3=/var/lib/hammer), formatea ext2 (montable ext4), copia la propia raíz del live (autoinstalador) excluyendo virtuales/payload, escribe/boot+ wrapper/sbin/init, e instala GRUB con dosdd(boot.img→MBR - core.img→hueco post-MBR). GOTCHAS: (a) busybox
fdiskalinea a CHS (sector 63 ⇒ hueco de 62 sectores) y el core.img de GRUB (~281 sectores) lo DESBORDA pisando el FS de p1 ⇒ GRUB cae agrub>; fix = sectores de inicio/fin EXPLÍCITOS (p1@2048, hueco de 2047) porque el default de busybox ofrece el sector libre más bajo (mete p2 en el hueco pre-p1). (b) Con install contiguo MBR los punteros por defecto de GRUB (kernel_sector=1, blocklist=2) ya valen ⇒ NO hace falta el parcheo binario del caso GPT de E1. (c) copiar el top-level de/ENUMERÁNDOLO (no una lista fija) o se escapa/ente/seed.card.json⇒ arje-zero aborta "seed.card no encontrada".AUTO_INSTALL=<dev>hace el/initdesatendido (install + poweroff) = instalación unattended + scaffolding del test. Validado E2E in-VM: boot ISO+disco en blanco → HAMMER-INSTALL-OK → boot del disco solo → GRUB→kernel →arje-zero→sshd, /store y /var/lib/hammer montadas. Refinamiento #1 CERRADO.
- módulos, armado en build-time con
- Arranque UEFI ✅ (