1# SDD 13 — Release engineering (Etapa E): imagen auto-booteable, instalador, mirror, upgrades
2
3> Estado: **EN CURSO**. Etapa E del roadmap (`docs/10-roadmap.md`, memoria distro-completion-roadmap).
4> Es trabajo nuevo: las Etapas A–D dejaron un sistema que (A) se orquesta, (B) bootea de disco
5> particionado real, (C) tiene userland Rust-nativo y (D) atesta confianza. Falta convertir eso en algo
6> que un tercero **instale y arranque sin el host de dev**: una imagen que bootea sola (sin
7> `qemu -kernel`), un instalador a disco físico, un mirror del store y un camino de upgrade.
8
9## 1. El gap concreto que abre la Etapa E
10
11Hasta B3 la imagen de disco (`scripts/disk-image.sh`, GPT con `/`, `/store`, `/var/lib/hammer`
12dedicados) **no se auto-bootea**: el `drive-rebuild.py` la arranca con `qemu -kernel <bzImage>
13-append root=/dev/vdaN` — el kernel lo carga QEMU, no la imagen. Eso sirve para verificar el
14auto-alojamiento, pero un disco así no arranca en hardware real ni en una VM "normal": le falta un
15**bootloader** en el propio disco.
16
17## 2. Decisión de bootloader: GRUB BIOS (i386-pc) ahora, UEFI después
18
19- **GRUB BIOS (i386-pc) — ELEGIDO para el primer corte.** QEMU trae SeaBIOS built-in ⇒ un disco con
20 GRUB en el MBR + una *BIOS boot partition* (GPT type `ef02`) bootea con `qemu-system-x86_64 -drive
21 file=img` **sin firmware extra ni `-kernel`**. Se instala **en userspace, sin root ni loop**:
22 `grub-mkimage` arma `core.img` y `grub-bios-setup` escribe `boot.img`→MBR + `core.img`→partición
23 BIOS sobre el *fichero* imagen (es I/O de bloques, no necesita montar). Los módulos de GRUB y el
24 kernel van en `/boot` de la partición root, que `mke2fs -d` puebla desde un directorio.
25- **UEFI (EFI-stub + ESP FAT) — diferido.** El kernel ya trae `CONFIG_EFI_STUB=y`, pero (a) el host de
26 dev no tiene firmware OVMF usable para verificar el arranque, y (b) poblar un ESP FAT sin root exige
27 `mtools` (ausente). Camino futuro cuando haya OVMF + mtools (o se construyan): ESP con
28 `EFI/BOOT/BOOTX64.EFI` = el bzImage (EFI-stub), cmdline por `loader.conf`/UKI.
29
30## 3. Layout de la imagen instalable (GPT, BIOS)
31
32```
33/dev/vda1 BIOS boot (ef02, ~2 MiB, sin fs) core.img de GRUB
34/dev/vda2 / ext4 [takana-root] + /boot/bzImage + /boot/grub
35/dev/vda3 /store ext4 [takana-store] artefactos CAS (inmutable)
36/dev/vda4 /var/lib/hammer ext4 [takana-state] estado mutable (journal, overlays)
37```
38
39Cadena de arranque: SeaBIOS → MBR (boot.img) → core.img (BIOS boot part) → lee `(hd0,gpt2)/boot/grub/
40grub.cfg` → `linux /boot/bzImage root=/dev/vda2 rdinit=/sbin/init` → wrapper `/sbin/init` monta
41vda3/vda4 → `exec /usr/bin/arje-zero` (PID 1). El wrapper es el mismo puente de B3 (arje todavía no lee
42fstab/cards de montaje por sí mismo).
43
44`scripts/install-image.sh` produce esta imagen. Reusa la técnica de particionado sin-root de B3
45(sfdisk + `mke2fs -d` bajo `unshare -r` + `dd conv=sparse,notrunc`) y añade la BIOS boot partition, el
46kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-mkimage`/`grub-bios-setup`.
47
48## 4. Piezas pendientes de la Etapa E (orden tentativo)
49
50- **E1 — imagen auto-booteable (GRUB BIOS): ✅ primer corte** (`scripts/install-image.sh`). Bootea en
51 QEMU sin `-kernel` hasta la shell de arje-zero. Hoy empaqueta el *builder rootfs* (con toolchain);
52 un *product rootfs* lean es refinamiento.
53- **E2 — instalador a disco físico:** un `takana install <device>` que particiona un disco real y
54 vuelca las tres particiones + GRUB (la misma lógica, apuntando a `/dev/sdX` en vez de un fichero).
55- **E3 — mirror del store: ✅ primer corte** (crate `takana-mirror` + CLI `takana mirror push|pull|status`).
56 Replica `/store` content-addressed (BLAKE3) entre máquinas; el hash ES la dirección, así que un mirror es
57 un CAS replicado + resolución por hash. La integridad se ancla en `of_tree` (content-hash del árbol): el
58 receptor **recomputa** `of_tree` sobre lo recibido y exige que case el del índice **antes** de sellar
59 (`Store::seal` atómico + read-only) — una transferencia corrupta/manipulada se **rechaza**, no se instala.
60 `push`/`pull` son `sync(src,dst)` (idempotente, salta presentes, reporta conflictos = mismo hash con otro
61 contenido). Transporte de esta entrega = **sistema de ficheros** (el remoto es una ruta a otro store, p.ej.
62 un montaje sshfs); el transporte sobre SSH/red que el producto ya expone es un envoltorio posterior.
63 Endurecimiento pendiente: anclar el `of_tree` a una raíz firmada (log de transparencia `bootstrap.json` /
64 atestación de Etapa D) para defenderse de un origen plenamente malicioso (hoy el invariante forzado es
65 "el contenido recibido hashea a lo que el índice anuncia", que ataja corrupción de transporte).
66- **E4 — upgrades: ✅ primer corte** (crate `takana-upgrade` + CLI `takana upgrade apply|rollback|status`).
67 Aplica un árbol Stage1/producto **nuevo** (artefacto sellado del store, CAS BLAKE3) sobre el FHS vivo
68 sin reinstalar, dejando una **generación**, y revierte exactamente al árbol anterior con `rollback`.
69 **Modelo de generaciones** (inspirado en NixOS, in-place al FHS porque el boot lee el root directo, no
70 un symlink indirecto): cada generación graba bajo `<state>/generations/<N>/` su `manifest.json`
71 (tree_dir del store, `of_tree`, parent, lista de ficheros, cambios) y un `backup/` con los bytes
72 **previos** de todo path que pisó o retiró. **Atomicidad:** proyección fichero-a-fichero (escritura a
73 temporal + `rename` ⇒ cada fichero conmuta atómicamente), con `current` (id de la generación viva) como
74 commit-point; un corte a mitad deja `current` en la vieja y los backups intactos ⇒ re-apply/rollback
75 recuperan. **Diff vs el árbol previo:** el apply añade/pisa los ficheros del árbol nuevo y **retira** los
76 que la generación viva aportaba y el nuevo no tiene. **Rollback:** deshace en orden inverso (borra lo
77 añadido, restaura desde `backup/` lo pisado/retirado) y retrocede `current` al padre — restauración
78 *exacta*, incluso al estado pre-upgrades. Cada cambio se registra en el [journal](05-journal.md) como
79 `HammerHydrate{artifact=tree_dir}` (replay-able). Verificación opcional de integridad: si el llamador
80 anuncia el `of_tree` esperado (de un índice de **mirror E3** o release firmada), el apply lo exige antes
81 de tocar el root. Maneja ficheros regulares + symlinks; valida E2E en host con `scripts/upgrade-e2e-test.sh`
82 (apply v1→v2→rollback→rollback contra el binario real). **GC de generaciones ✅** (`takana upgrade
83 prune [--keep N]` / `hammer_upgrade::prune`): borra las generaciones que el rollback ya no alcanza
84 (huérfanas tras un rollback — las de la [cadena viva](#) siempre se conservan); `--keep N` además
85 recorta la cadena a sus N más nuevas (limita la profundidad de rollback, como `delete-generations` en
86 NixOS). **Journal de intención + replay ✅** (`takana upgrade recover [--rollback]` /
87 `hammer_upgrade::{pending,recover}`): el apply escribe el **plan completo** a `pending.json` ANTES de
88 proyectar y lo limpia al commitear; si un corte/reinicio lo interrumpe, `pending.json` sobrevive y el
89 FHS quedó a medias. La proyección es **re-entrante** (`project_plan`, backups *idempotentes* —
90 `backup_existing_once` nunca pisa el original capturado), así `recover` **completa** (roll-forward:
91 re-ejecuta el plan + commitea, re-verificando el `of_tree`) o **deshace** (roll-back: restaura backups,
92 borra la generación a medias, `current`→padre). `apply` se **niega** si hay intento pendiente (exige
93 recover explícito); `status` lo avisa. 5 tests (corte a media proyección → ambos modos) + ejercicio en
94 `upgrade-e2e-test.sh`.
95 - **Auto-recover al arranque ✅** (`crates/hammer-recover` — mini-binario static-musl). El modelo de
96 generaciones es **in-place** (el FHS es la generación viva), así que NO hay menú de generaciones tipo
97 NixOS que ofrecer en GRUB; lo que encaja es **auto-sanar** un upgrade interrumpido al boot. El producto
98 no lleva el CLI `takana` completo ⇒ `hammer-recover` es lo único que el sistema instalado necesita: el
99 wrapper `/sbin/init` (de `hammer-live-install.sh`) lo corre **tras montar `/store` y `/var/lib/hammer`**
100 y **antes** de incarnar arje-zero. Sin `pending.json` es no-op; con uno, **completa** (roll-forward) o,
101 si no puede, **deshace** (roll-back) ⇒ el FHS siempre arranca consistente. Nunca aborta el boot. El
102 instalador lo bundlea desde el payload a `/usr/sbin/hammer-recover`. Validado in-VM
103 (`iso-install-test.sh`): el disco imprime `hammer-recover: sin upgrade interrumpido` al boot y sirve SSH.
104 **Refinamiento #4 (E4 endurecimiento) CERRADO.**
105- **E5 — ISO/medio de arranque: ✅ primer corte** (`recipes/xorriso.toml` + `scripts/iso-image.sh` +
106 `scripts/iso-boot-test.sh`). Medio **live** que arranca ENTERO desde RAM: GRUB (El Torito) carga
107 kernel + initramfs desde el ISO 9660, el kernel desempaqueta el rootfs del producto a un tmpfs y corre
108 `/init` — sin tocar disco. Es el medio para correr el instalador (`takana install /dev/sdX`) en
109 hardware nuevo o probar el sistema sin instalarlo. **Herramental construido por takana:** el host no
110 trae `xorriso` (y `grub-mkrescue` lo exige) ⇒ `recipes/xorriso.toml` lo construye desde fuente (GNU
111 xorriso 1.5.8 = libburn+libisofs+libisoburn, zig 0.13.0, estático musl, libs opcionales off ⇒ binario
112 autocontenido). `iso-image.sh` lo **dogfooda**: arma el ISO con el xorriso del store. **Cadena de
113 boot BIOS:** reusa los módulos GRUB i386-pc de E1 pero con el core El Torito (`grub-mkimage -O
114 i386-pc-eltorito` + módulos `iso9660`/`biosdisk`) en vez del core de disco (ext2); `xorriso -as
115 mkisofs -b …/eltorito.img -no-emul-boot -boot-info-table --grub2-boot-info --grub2-mbr boot_hybrid.img`
116 ⇒ ISO **isohybrid** (booteable como CD *y* USB/disco). El initramfs es el `product-rootfs` lean
117 empaquetado como `cpio.gz` (live de RAM; la variante con squashfs+overlay para rootfs grandes es
118 refinamiento). Validado E2E in-VM (`iso-boot-test.sh`, ISO 113M): SeaBIOS → GRUB 2.14 (El Torito) →
119 Linux 6.16.12 takana-built → `/init` (marcador `HAMMER-ISO-LIVE-OK`) → arje-zero PID1 → hammerd +
120 netup (lease DHCP) → **sshd escuchando**: el producto entero corriendo desde el medio live. **Pendiente
121 (refinamiento):** squashfs+overlay para no cargar todo el rootfs en RAM (diferido: el kernel no trae
122 SQUASHFS ni CDROM/SR ⇒ exige rebuild de kernel, valor marginal en el producto lean).
123 - **Arranque UEFI ✅** (`iso-image.sh EFI=1` + `recipes/mtools.toml` + `scripts/efi-boot-test.sh`). El
124 mismo ISO se vuelve **híbrido BIOS+UEFI**: además del El Torito i386-pc se añade un 2º El Torito EFI
125 (`-eltorito-alt-boot -e efi.img -no-emul-boot -isohybrid-gpt-basdat`) que arranca una ESP FAT con
126 `EFI/BOOT/BOOTX64.EFI` = un `grubx64.efi` (`grub-mkimage -O x86_64-efi`). GRUB EFI lee el mismo
127 `grub.cfg` y carga kernel (EFI_STUB, ya en el kernel — sin rebuild) + initrd vía el protocolo EFI.
128 **Herramental:** poblar la ESP FAT sin root exige `mtools` (`mformat`/`mcopy`), ausente en el host ⇒
129 `recipes/mtools.toml` lo construye (GNU mtools 4.0.49, estático musl, dogfoodeado por iso-image.sh).
130 **GOTCHA:** `mformat -F` fuerza **FAT32** (mínimo ~33 MiB); sobre una ESP chica deja un FAT inválido
131 que la firmware no lee ("failed to load … Not Found") ⇒ sin `-F`, mformat auto-elige FAT12/16.
132 Validado E2E: el MISMO ISO bootea por UEFI (QEMU+OVMF → grubx64.efi → "Loaded initrd from
133 LINUX_EFI_INITRD_MEDIA_GUID" → arje-zero → sshd) y por BIOS (SeaBIOS, El Torito). **Refinamiento #3
134 CERRADO.** (Falta como pulido: rama EFI del INSTALADOR a disco — GPT+ESP en `takana-install`, hoy el
135 instalado es MBR-BIOS.)
136 - **Instalar DESDE el live ✅** (`scripts/iso-image.sh INSTALLER=1` + `scripts/hammer-live-install.sh`
137 → `/usr/bin/hammer-install` + `scripts/iso-install-test.sh`). Cierra el lazo **ISO live → disco
138 instalado → bootea solo**. El ISO instalador bundlea un *payload de disco* en el initramfs bajo
139 `/usr/lib/hammer/install/` (kernel + GRUB `boot.img`/`core.img` MBR-prefix `(hd0,msdos1)/boot/grub`
140 + módulos, armado en build-time con `grub-mkimage`). `takana-install <dev>` corre **dentro del live
141 como root real** (sin los rodeos rootless de E1) usando sólo lo que el producto ya trae (busybox
142 `fdisk`/`mke2fs`/`mount`/`dd` + uutils `cp`): particiona **MBR** (p1=/, p2=/store, p3=/var/lib/hammer),
143 formatea ext2 (montable ext4), copia la **propia raíz del live** (autoinstalador) excluyendo
144 virtuales/payload, escribe `/boot` + wrapper `/sbin/init`, e instala GRUB con dos `dd` (boot.img→MBR
145 + core.img→hueco post-MBR). **GOTCHAS:** (a) busybox `fdisk` alinea a CHS (sector 63 ⇒ hueco de 62
146 sectores) y el core.img de GRUB (~281 sectores) lo DESBORDA pisando el FS de p1 ⇒ GRUB cae a `grub>`;
147 fix = **sectores de inicio/fin EXPLÍCITOS** (p1@2048, hueco de 2047) porque el default de busybox
148 ofrece el sector libre más bajo (mete p2 en el hueco pre-p1). (b) Con install contiguo MBR los
149 punteros por defecto de GRUB (kernel_sector=1, blocklist=2) **ya valen** ⇒ NO hace falta el parcheo
150 binario del caso GPT de E1. (c) copiar el top-level de `/` ENUMERÁNDOLO (no una lista fija) o se
151 escapa `/ente/seed.card.json` ⇒ arje-zero aborta "seed.card no encontrada". `AUTO_INSTALL=<dev>`
152 hace el `/init` desatendido (install + poweroff) = instalación unattended + scaffolding del test.
153 Validado E2E in-VM: boot ISO+disco en blanco → HAMMER-INSTALL-OK → boot del disco solo → GRUB→kernel
154 →arje-zero→sshd, /store y /var/lib/hammer montadas. **Refinamiento #1 CERRADO.**
13-release-engineering.md
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 (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.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 -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] / hammer_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] /
hammer_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 idempotentes —
backup_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.