Files
takana/docs/13-release-engineering.md
T
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
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.
2026-09-09 19:28:48 +00:00

14 KiB
Raw Blame History

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 AD 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 (sin qemu -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 con qemu-system-x86_64 -drive file=img sin firmware extra ni -kernel. Se instala en userspace, sin root ni loop: grub-mkimage arma core.img y grub-bios-setup escribe boot.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 /boot de la partición root, que mke2fs -d puebla 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 exige mtools (ausente). Camino futuro cuando haya OVMF + mtools (o se construyan): ESP con EFI/BOOT/BOOTX64.EFI = el bzImage (EFI-stub), cmdline por loader.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.cfglinux /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 -kernel hasta 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/sdX en vez de un fichero).
  • E3 — mirror del store: primer corte (crate takana-mirror + CLI takana mirror push|pull|status). Replica /store content-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 en of_tree (content-hash del árbol): el receptor recomputa of_tree sobre lo recibido y exige que case el del índice antes de sellar (Store::seal atómico + read-only) — una transferencia corrupta/manipulada se rechaza, no se instala. push/pull son sync(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 el of_tree a una raíz firmada (log de transparencia bootstrap.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 + CLI takana 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 con rollback. 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>/ su manifest.json (tree_dir del store, of_tree, parent, lista de ficheros, cambios) y un backup/ 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), con current (id de la generación viva) como commit-point; un corte a mitad deja current en 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 desde backup/ lo pisado/retirado) y retrocede current al padre — restauración exacta, incluso al estado pre-upgrades. Cada cambio se registra en el journal como HammerHydrate{artifact=tree_dir} (replay-able). Verificación opcional de integridad: si el llamador anuncia el of_tree esperado (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 con scripts/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 N además recorta la cadena a sus N más nuevas (limita la profundidad de rollback, como delete-generations en NixOS). Journal de intención + replay (takana upgrade recover [--rollback] / takana_upgrade::{pending,recover}): el apply escribe el plan completo a pending.json ANTES de proyectar y lo limpia al commitear; si un corte/reinicio lo interrumpe, pending.json sobrevive y el FHS quedó a medias. La proyección es re-entrante (project_plan, backups idempotentesbackup_existing_once nunca pisa el original capturado), así recover completa (roll-forward: re-ejecuta el plan + commitea, re-verificando el of_tree) o deshace (roll-back: restaura backups, borra la generación a medias, current→padre). apply se niega si hay intento pendiente (exige recover explícito); status lo avisa. 5 tests (corte a media proyección → ambos modos) + ejercicio en upgrade-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 CLI takana completo ⇒ hammer-recover es lo único que el sistema instalado necesita: el wrapper /sbin/init (de hammer-live-install.sh) lo corre tras montar /store y /var/lib/hammer y antes de incarnar arje-zero. Sin pending.json es 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 imprime hammer-recover: sin upgrade interrumpido al boot y sirve SSH. Refinamiento #4 (E4 endurecimiento) CERRADO.
  • 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 trae xorriso (y grub-mkrescue lo exige) ⇒ recipes/xorriso.toml lo 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.sh lo 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ódulos iso9660/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 el product-rootfs lean empaquetado como cpio.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 (marcador HAMMER-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 con EFI/BOOT/BOOTX64.EFI = un grubx64.efi (grub-mkimage -O x86_64-efi). GRUB EFI lee el mismo grub.cfg y carga kernel (EFI_STUB, ya en el kernel — sin rebuild) + initrd vía el protocolo EFI. Herramental: poblar la ESP FAT sin root exige mtools (mformat/mcopy), ausente en el host ⇒ recipes/mtools.toml lo construye (GNU mtools 4.0.49, estático musl, dogfoodeado por iso-image.sh). GOTCHA: mformat -F fuerza 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 en takana-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 + GRUB boot.img/core.img MBR-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 (busybox fdisk/mke2fs/mount/dd + uutils cp): 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 dos dd (boot.img→MBR
      • core.img→hueco post-MBR). GOTCHAS: (a) busybox fdisk alinea 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 a grub>; 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 /init desatendido (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.