gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1379Shell 129Rust 79Markdown 732473 Sutichaykuna

⎇ 98edf488bbffd2dae507a6317a78bb61d5a9c07a scripts/install-image-efi.sh Shell · 413 siq'ikuna · 25.9 KB Ñawpa willay ch'uya

feat(metal): --keep conserva el /home viejo, y los usuarios se crean con SU uid dcb83e1fd · Sergio ·

1#!/bin/sh
2# install-image-efi.sh — imagen de disco EFI-STUB SOBERANA (ADR 0010 paso 5), validable en OVMF.
3#
4# Hermana EFI de scripts/install-image.sh (que es GPT+BIOS+GRUB). Ésta NO lleva GRUB ni systemd-boot:
5# el propio bzImage es el binario EFI (EFI-STUB) que la firmware lanza como \EFI\BOOT\BOOTX64.EFI (la
6# ruta de arranque FALLBACK de medios removibles, sin necesidad de una entrada NVRAM). Es el loader
7# soberano del ADR 0010: sin bootloader ajeno, sin el GRUB 2.14 que cuelga la rama UEFI en el metal.
8#
9# Cómo pasa el cmdline sin LoadOptions: el kernel `linux-metal` hornea CONFIG_CMDLINE (CMDLINE_BOOL, sin
10# FORCE) = "... initrd=/initramfs.cpio.gz rdinit=/init". Arrancado por la ruta fallback la firmware pasa
11# LoadOptions VACÍO ⇒ el kernel usa ese cmdline horneado ⇒ el EFI-stub carga \initramfs.cpio.gz desde la
12# MISMA ESP y corre /init. (En un boot con bootloader que sí pasa LoadOptions, el horneado se ignora.)
13#
14# El initramfs es MÍNIMO — sólo pivota: findfs LABEL=hammer-root → switch_root a la ext4 real. Ese
15# "initrd chico" es lo que destraba EFI-stub directo (ADR 0010 paso 2: el cuelgue del metal era por un
16# initrd de 100MB). El sistema real vive en las particiones ext4, no en RAM.
17#
18# Layout (GPT, UEFI):
19# ⚠ Las etiquetas son `hammer-*` y NO `takana-*`: están en el fstab y el arranque de
20# sistemas YA INSTALADOS, así que el renombre del ADR 0016 las dejó congeladas a propósito.
21# Este comentario decía `takana-*` y el código escribía `hammer-*` — mandaba a buscar una
22# etiqueta que no existe.
23#   p1  ESP  FAT32  [EFI System Partition]   \EFI\takana\takanax64.efi + \EFI\BOOT\BOOTX64.EFI
24#                                            (las DOS = el mismo bzImage, ADR 0018 §3) + \initramfs.cpio.gz
25#   p2  /                ext4  [hammer-root]  el rootfs real del producto (/sbin/init → arje-zero)
26#   p3  /store           ext4  [hammer-store]
27#   p4  /var/lib/hammer  ext4  [hammer-state]
28#
29# Cadena de arranque del disco: firmware UEFI → \EFI\takana\takanax64.efi (con entrada NVRAM) o
30# \EFI\BOOT\BOOTX64.EFI (sin ella, y es lo que usa OVMF en las pruebas) → EFI-stub → initramfs pivote
31# (findfs LABEL=hammer-root → switch_root) → /sbin/init real → arje-zero PID1.
32#
33# La ESP se puebla con el mtools de takana (mformat/mcopy, sin root ni loop). Las ext4 con `mke2fs -d`
34# bajo `unshare -r` (ficheros root-owned sin sudo), igual que install-image.sh.
35#
36# Uso:
37#   ./scripts/install-image-efi.sh                       # crea work/hammer-efi-disk.img
38#   BOOT=1 KVM=1 ./scripts/install-image-efi.sh          # además lo arranca en OVMF (sin -kernel)
39#
40# Variables:
41#   ROOTFS      dir rootfs a instalar          (default el product-rootfs más reciente del store)
42#   KERNEL      bzImage EFI-stub a embeber      (default store/*-linux-metal/boot/bzImage más reciente)
43#   BUSYBOX     busybox ESTÁTICO para el initramfs (default el del ROOTFS)
44#   MTOOLS      dir usr/bin de mtools de takana  (default store/*-mtools/usr/bin)
45#   IMG         imagen de salida                (default work/hammer-efi-disk.img)
46#   ESP_MB      MiB de la ESP FAT32             (default 64)
47#   ROOT_SIZE   MiB de /         (default 2048)
48#   STORE_SIZE  MiB de /store    (default 512)
49#   STATE_SIZE  MiB de /var/lib/hammer (default 512)
50#   BOOT        1 ⇒ arranca en OVMF tras crearla   KVM/MEM passthrough al arranque
51#   OVMF_CODE/OVMF_VARS  firmware OVMF (default Arch/CachyOS)
52set -eu
53ROOT="$(cd "$(dirname "$0")/.." && pwd)"
54cd "$ROOT"
55
56ROOTFS="${ROOTFS:-$(ls -dt store/*-product-rootfs 2>/dev/null | head -1)}"
57KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
58IMG="${IMG:-work/hammer-efi-disk.img}"
59ESP_MB="${ESP_MB:-64}"
60ROOT_SIZE="${ROOT_SIZE:-2048}"
61STORE_SIZE="${STORE_SIZE:-512}"
62STATE_SIZE="${STATE_SIZE:-512}"
63# ⚠ Las herramientas del ensamblado salen del STORE, por HASH VIGENTE, no del PATH del host ni del
64# artefacto más reciente por fecha (2026-09-22). En este hub, en su anfitrión y en el worker NO existe
65# ninguna de `cpio mformat xorriso grub-mkimage`: el pipeline estaba roto en las tres máquinas y sólo
66# se veía al correrlo. Ver `scripts/lib/herramientas-desde-store.sh`.
67. "$(dirname "$0")/lib/herramientas-desde-store.sh"
68
69[ -n "${ROOTFS:-}" ] && [ -d "$ROOTFS" ] || { echo "no existe ROOTFS: '${ROOTFS:-}' (set ROOTFS=<dir product-rootfs>)" >&2; exit 1; }
70# ⚠ Esto decía `[ -e "$ROOTFS/sbin/init" ]` y era un chequeo ROTO para todo rootfs sano: `/sbin/init`
71# es un symlink ABSOLUTO (→ `/usr/bin/arje-zero`) y `-e` LO SIGUE contra la raíz del HOST, donde no
72# existe. ⇒ el script abortaba con «ROOTFS sin /sbin/init» teniendo el init perfectamente presente
73# adentro. Un chequeo que valida algo distinto de lo que cree validar, y que además falla del lado
74# ruidoso: nunca dejó pasar una imagen mala, pero bloqueaba todas las buenas.
75_init_l="$ROOTFS/sbin/init"
76if [ -L "$_init_l" ]; then
77    _init_d="$(readlink "$_init_l")"
78    case "$_init_d" in
79        /*) _init_l="$ROOTFS$_init_d" ;;          # absoluto: es absoluto DENTRO del rootfs
80        *)  _init_l="$ROOTFS/sbin/$_init_d" ;;    # relativo: contra el directorio que lo contiene
81    esac
82fi
83[ -e "$_init_l" ] || { echo "ROOTFS sin /sbin/init utilizable (arje-zero): $_init_l" >&2; exit 1; }
84[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (build recipes/linux-metal.toml o set KERNEL=)" >&2; exit 1; }
85BUSYBOX="${BUSYBOX:-$ROOTFS/bin/busybox}"
86[ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX (set BUSYBOX=)" >&2; exit 1; }
87echo "==> herramientas del ensamblado"
88MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
89MMD=$(herramienta     mtools usr/bin/mmd     mmd)     || exit 1
90MCOPY=$(herramienta   mtools usr/bin/mcopy   mcopy)   || exit 1
91MDIR=$(herramienta    mtools usr/bin/mdir    mdir)    || exit 1
92SFDISK=$(herramienta  util-linux sbin/sfdisk sfdisk)  || exit 1
93MKE2FS=$(herramienta  e2fsprogs  sbin/mke2fs mke2fs)  || exit 1
94CPIO=$(herramienta    libarchive usr/bin/bsdcpio cpio) || exit 1
95GZIP=$(herramienta    gzip bin/gzip gzip)             || exit 1
96# `MTOOLS=` sigue existiendo como override manual (un directorio con los binarios), que es como lo
97# documenta la cabecera; si se pasa, gana sobre el store.
98if [ -n "${MTOOLS:-}" ]; then
99    MFORMAT="${MTOOLS}/mformat"; MMD="${MTOOLS}/mmd"; MCOPY="${MTOOLS}/mcopy"; MDIR="${MTOOLS}/mdir"
100    [ -x "$MFORMAT" ] || { echo "MTOOLS=$MTOOLS no tiene mformat" >&2; exit 1; }
101fi
102unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
103
104STAGE="${STAGE:-work/.install-efi-stage}"
105rm -rf "$STAGE"; mkdir -p "$STAGE"
106ROOT_TREE="$STAGE/root"
107IRD_TREE="$STAGE/initramfs"
108ROOT_IMG="$STAGE/root.img"; STORE_IMG="$STAGE/store.img"; STATE_IMG="$STAGE/state.img"
109ESP_IMG="$STAGE/esp.img"; IRD="$STAGE/initramfs.cpio.gz"
110
111# --- 1) initramfs de pivote: busybox estático + /init (findfs LABEL=hammer-root → switch_root) --------
112echo "==> initramfs de pivote (busybox estático + /init switch_root a hammer-root)"
113mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" "$IRD_TREE/newroot"
114cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
115# ⚠ `sed` está en la lista porque el bloque del /home conservado lo usa para leer `home.conf`. Un
116# applet que falta en el initramfs no da «command not found» visible: el `$(…)` sale vacío, el `if`
117# elige la otra rama y el arranque sigue como si no hubiera nada que montar.
118for a in sh mount umount findfs switch_root mkdir echo sleep ls cat setsid dmesg blkid sed; do ln -sf busybox "$IRD_TREE/bin/$a"; done
119cat > "$IRD_TREE/init" <<'INIT'
120#!/bin/busybox sh
121# initramfs de pivote (EFI-stub → aquí). Monta las pseudo-fs, resuelve la ext4 real por LABEL, monta
122# /store y /var/lib/hammer bajo ella y hace switch_root al init real (arje-zero). Sin root= en cmdline:
123# el descubrimiento por LABEL vive acá, así el disco es reubicable (cualquier /dev/…, virtio o nvme).
124export PATH=/bin
125mount -t proc     proc /proc 2>/dev/null || true
126mount -t sysfs    sys  /sys  2>/dev/null || true
127mount -t devtmpfs dev  /dev  2>/dev/null || true
128# ⚠ EL CMDLINE HORNEADO ES "console=tty0 console=ttyS0,115200 …" Y EL KERNEL HACE /dev/console = LA
129# ÚLTIMA `console=` ⇒ **ttyS0**. O sea que el stdout de este script va al SERIAL, no a la pantalla: en
130# un metal sin puerto serie TODO lo que pase acá es INVISIBLE y el arranque parece congelado en el
131# último printk del kernel. Es el mismo bug que costó el 1er viaje físico de KDE, una capa más
132# temprano: allá se arregló para DESPUÉS del switch_root (getty en tty1) y acá seguía sin arreglar.
133# Por eso el log va explícitamente a **/dev/tty0** (la VT en primer plano = la pantalla) además del
134# serial, y nunca se confía en el stdout heredado.
135log() {
136    echo "$*"
137    for _d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$_d" 2>/dev/null || true; done
138}
139# Shell de rescate VISIBLE: si algo falla, un `exec sh` a secas nace en el serial y el usuario ve una
140# pantalla muerta sin manera de averiguar nada. Se abre uno en tty1 (pantalla) y se conserva el serial.
141rescate() {
142    log "HAMMER-EFI-FALLO: $*"
143    log "   bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
144    log "   abriendo shell de rescate en tty1 — probá: blkid ; dmesg | tail -30"
145    setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
146    exec sh
147}
148log "HAMMER-EFI: initramfs de pivote — resolviendo hammer-root por LABEL"
149# ⏱ LA ESPERA TIENE QUE SER LARGA, Y ÉSTE ES EL MOTIVO: el rdinit arranca en cuanto se desempaqueta el
150# initramfs, pero el bus USB enumera de forma ASÍNCRONA y tarda SEGUNDOS (reset del hub, settling de 1s
151# por dispositivo, scan SCSI). En QEMU el disco es virtio y está desde el instante cero, así que la
152# carrera NUNCA se ve en validación: sólo aparece al bootear de un USB en metal, y con 5 reintentos de
153# 1s se pierde. Es el equivalente de `rootwait`, que acá no aplica porque no hay root= en el cmdline.
154ROOTDEV=""
155_try=0
156while [ "$_try" -lt 60 ]; do
157    ROOTDEV="$(findfs LABEL=hammer-root 2>/dev/null || true)"
158    [ -n "$ROOTDEV" ] && break
159    _try=$(( _try + 1 ))
160    [ $(( _try % 5 )) -eq 0 ] && log "HAMMER-EFI: esperando hammer-root… ${_try}s (el USB tarda en enumerar)"
161    sleep 1
162done
163[ -n "$ROOTDEV" ] || rescate "no encuentro LABEL=hammer-root tras ${_try}s"
164log "HAMMER-EFI: hammer-root = $ROOTDEV → montando en /newroot"
165mount -t ext4 "$ROOTDEV" /newroot || rescate "no pude montar $ROOTDEV"
166# /store y /var/lib/hammer por LABEL (no bloqueante: el wrapper del root también reintenta).
167STOREDEV="$(findfs LABEL=hammer-store 2>/dev/null || true)"
168STATEDEV="$(findfs LABEL=hammer-state 2>/dev/null || true)"
169[ -n "$STOREDEV" ] && mount -t ext4 "$STOREDEV" /newroot/store          2>/dev/null || true
170[ -n "$STATEDEV" ] && mount -t ext4 "$STATEDEV" /newroot/var/lib/hammer  2>/dev/null || true
171# ── El /home CONSERVADO (`takana metal install --keep`) ────────────────────────────────────────
172# El instalador no formatea esa partición y deja dicho cuál es en `/etc/takana/home.conf`, DENTRO de
173# la raíz nueva — que a esta altura ya está montada, así que se puede leer. Sin ese fichero no hay
174# nada que hacer y el arranque sigue igual: un sistema sin /home conservado es el caso normal.
175#
176# ⚠ **Se monta por `UUID` del filesystem y NO por `PARTUUID`, y eso está MEDIDO**: el `findfs` de
177# busybox 1.36.1 sólo entiende `LABEL=` y `UUID=` (`Usage: findfs LABEL=label or UUID=uuid`). Un
178# `findfs PARTUUID=…` acá devuelve vacío **sin error**, y el resultado sería un arranque sin /home
179# con todo «funcionando». El PARTUUID se anota igual en el fichero, para quien tenga que repararlo a
180# mano; el que monta es el UUID, que tampoco cambia porque esa partición no se formatea nunca.
181#
182# ⚠ Y `sed`, no `.`: el fichero NO se sourcea. Lo escribe nuestro instalador, pero un fichero de
183# configuración que se ejecuta es una clase de bug que este repo ya pagó (un comentario dentro de un
184# heredoc sin comillas), y leer dos valores con sed no cuesta nada.
185if [ -r /newroot/etc/takana/home.conf ]; then
186    HOMEUUID="$(sed -n 's/^HOME_UUID=//p' /newroot/etc/takana/home.conf 2>/dev/null | head -1)"
187    if [ -n "$HOMEUUID" ]; then
188        HOMEDEV=""; _ht=0
189        while [ "$_ht" -lt 30 ]; do
190            HOMEDEV="$(findfs "UUID=$HOMEUUID" 2>/dev/null || true)"
191            [ -n "$HOMEDEV" ] && break
192            _ht=$(( _ht + 1 )); sleep 1
193        done
194        if [ -n "$HOMEDEV" ]; then
195            mkdir -p /newroot/home
196            # Sin `-t`: el /home viejo puede ser ext4, xfs o btrfs y no nos toca elegir por él.
197            if mount "$HOMEDEV" /newroot/home 2>/dev/null; then
198                log "HAMMER-EFI: /home conservado = $HOMEDEV (UUID=$HOMEUUID) montado"
199            else
200                log "HAMMER-EFI: ⚠ $HOMEDEV existe pero NO pude montarlo en /home — arranco sin él"
201            fi
202        else
203            # Ruidoso a propósito: el usuario pidió conservar su /home y hay que decirle que no
204            # está, en vez de arrancar con un /home vacío que se lee como «se me borró todo».
205            log "HAMMER-EFI: ⚠ el /home conservado (UUID=$HOMEUUID) NO APARECE tras ${_ht}s."
206            log "HAMMER-EFI:   arranco SIN él. Tus datos NO se borraron: falta el disco que los tiene."
207        fi
208    fi
209fi
210log "HAMMER-EFI-DISK-PIVOT-OK: switch_root → /sbin/init"
211exec switch_root /newroot /sbin/init
212INIT
213chmod +x "$IRD_TREE/init"
214# `bsdcpio` de libarchive acepta el mismo `--null -o -H newc --owner=root:root` que el GNU cpio
215# (comprobado 2026-09-22 contra el artefacto sellado: mismo `newc`, dueños root).
216( cd "$IRD_TREE" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$IRD"
217echo "    initramfs: $(du -h "$IRD" | cut -f1)"
218
219# --- 2) árbol root: hardlinks del rootfs SIN store/estado (van en sus particiones dedicadas) -----------
220if [ -n "${KEEP_STORE:-}" ]; then
221    echo "==> staging root (hardlinks del rootfs, CON /store y /var/lib/hammer adentro: KEEP_STORE=1)"
222else
223    echo "==> staging root (hardlinks del rootfs, sin /store ni /var/lib/hammer)"
224fi
225sonda_hardlink "$ROOTFS" "$STAGE" || exit 1
226cp -al "$ROOTFS" "$ROOT_TREE"
227# ⚠ `KEEP_STORE=1` — el vaciado de abajo es correcto para la IMAGEN (store y estado son particiones
228# dedicadas y el pivote las monta por LABEL) y es DESTRUCTIVO para una instalación en UNA sola
229# partición, donde los dos viven dentro de la raíz. `takana-install-partition.sh` lo pone a 1.
230if [ -z "${KEEP_STORE:-}" ]; then
231    rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
232fi
233mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
234# CLI `takana` (static musl) para el menú de arranque por grafo (ADR 0010): el hook de init lo invoca
235# como `takana boot menu`. El producto trae arje-zero/hammerd pero NO el CLI ⇒ lo inyectamos acá si
236# está compilado (igual patrón que hammer-recover). Opcional: sin él, el wrapper saltea el menú.
237HAMMER_MUSL="${TAKANA_MUSL:-${HAMMER_MUSL:-$ROOT/target/x86_64-unknown-linux-musl/release/hammer}}"
238if [ -x "$HAMMER_MUSL" ]; then
239    cp "$HAMMER_MUSL" "$ROOT_TREE/usr/bin/hammer"; chmod 0755 "$ROOT_TREE/usr/bin/hammer"
240    echo "    + CLI hammer (static musl) para el menú de arranque por grafo"
241else
242    echo "    ! sin hammer musl ($HAMMER_MUSL) ⇒ el disco no traerá el menú de arranque (cargo build --target x86_64-unknown-linux-musl -p takana-cli)" >&2
243fi
244# /sbin/init: wrapper que emite un marcador a /dev/ttyS0 (sobrevive `quiet`, que suprime el printk
245# kernel-side) y hace exec de arje-zero. Prueba positiva de que el switch_root llegó al init real.
246rm -f "$ROOT_TREE/sbin/init"
247cat > "$ROOT_TREE/sbin/init" <<'RINIT'
248#!/bin/sh
249# PID1 real tras el switch_root del pivote (store/estado ya montados por LABEL en el initramfs).
250# busybox switch_root NO mueve /dev al nuevo root ⇒ montamos devtmpfs antes del marcador (si no,
251# /dev/ttyS0 no existe todavía y el echo se pierde). arje-zero re-monta lo que necesite, es idempotente.
252/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
253# A la PANTALLA (tty0) además del serial: en metal sin puerto serie éste es el único indicio de que el
254# pivote llegó al init real. Ver el comentario largo del initramfs.
255for _d in /dev/tty0 /dev/ttyS0; do
256    echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > "$_d" 2>/dev/null || true
257done
258# Menú de arranque por grafo (ADR 0010): emite /run/hammer/boot-graph.json y, si hay un compositor
259# (mirada), lo pinta sobre KMS para elegir un nodo. Graceful: sin compositor, emite el grafo y sigue.
260# Salida al serial (no pinta tty0 ⇒ respeta el cero-parpadeo). No bloquea el arranque si falla.
261# El `timeout` no es paranoia decorativa: esto corre ANTES del exec de arje-zero, o sea que si se
262# cuelga no hay PID1, no hay getty y no hay pantalla — un arranque muerto e indistinguible de un kernel
263# colgado. El `|| true` protege del fallo, no del bloqueo.
264if [ -x /usr/bin/hammer ]; then
265    /bin/busybox timeout 15 /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
266fi
267
268# efivarfs: la NVRAM del firmware NO es visible hasta que se monta, y el kernel `linux-metal` ya trae
269# CONFIG_EFIVAR_FS=y (verificado en el .config sellado, no supuesto) — sólo faltaba montarlo. El
270# directorio sólo existe si la máquina arrancó por UEFI, así que el `[ -d ]` es la prueba de firmware
271# y no una precaución: en BIOS no está y no hay nada que montar.
272# ⚠ `busybox switch_root` NO arrastra /sys, igual que no arrastra /dev (por eso existe la línea del
273# devtmpfs de arriba). Sin sysfs, `/sys/firmware/efi/efivars` NO EXISTE y el reconciliador concluye
274# «esta máquina no arrancó por UEFI» — en una máquina que arrancó por UEFI. Medido: el primer intento
275# de este hook dio exactamente ese diagnóstico falso.
276/bin/busybox mount -t sysfs sys /sys 2>/dev/null || true
277if [ -d /sys/firmware/efi/efivars ]; then
278    /bin/busybox mount -t efivarfs efivarfs /sys/firmware/efi/efivars 2>/dev/null || true
279fi
280
281# Reconciliador del arranque (ADR 0018 §2). La NVRAM del firmware es estado compartido: otro sistema
282# operativo instalado al lado la reescribe sin coordinarse —Windows Update poniéndose primero en
283# BootOrder es el caso típico—. Contra eso no sirve confiar, sirve CONVERGER en cada arranque.
284#
285# Va al SERIAL **y a /dev/tty0**, a diferencia del menú de arriba: si takana tuvo que reponer su
286# entrada, el usuario tiene que poder enterarse mirando la pantalla. Un reconciliador que repara en
287# silencio deja al usuario conviviendo con una rareza intermitente que no entiende (§2).
288#
289# Mismo `timeout` y mismo `|| true` que arriba, por la misma razón: esto corre ANTES del exec de
290# arje-zero y colgarse acá es un arranque muerto.
291if [ -x /usr/bin/hammer ]; then
292    /bin/busybox timeout 15 /usr/bin/hammer boot entry reconcile 2>&1 \
293        | /bin/busybox tee /dev/tty0 > /dev/ttyS0 2>/dev/null || true
294fi
295
296# Respaldo del arranque (ADR 0018 §5). Se hace DESPUÉS de reconciliar, así lo que queda guardado es
297# el estado bueno y no el que dejó el vecino. El nombre del fichero sale del CONTENIDO: un arranque
298# que no cambió produce el mismo fichero, así que esto corre en cada arranque sin llenar la
299# partición de estado con copias idénticas.
300#
301# Vive en /var/lib/hammer, que en las imágenes es su propia partición y sobrevive a una reinstalación
302# del sistema. **No va al store**: el store lo barre `store-gc.sh`, que clasifica por nombre de
303# receta — un respaldo ahí sería «huérfano» y se borraría en el primer `--huerfanos`.
304[ -x /usr/bin/hammer ] && /bin/busybox timeout 15 /usr/bin/hammer boot entry backup > /dev/ttyS0 2>&1 || true
305exec /usr/bin/arje-zero
306RINIT
307chmod +x "$ROOT_TREE/sbin/init"
308
309# ⚠ `STAGE_ONLY=1` — cortar acá y no ensamblar ninguna imagen. Los pasos 1 y 2 son el ÚNICO lugar
310# del repo donde se arma el initramfs de pivote y el `/sbin/init` que reconcilia el arranque;
311# `takana-install-partition.sh` los necesita idénticos y copiarlos allá los haría divergir en
312# silencio — que es peor que un hook feo. Con esto el otro instalador los PIDE en vez de repetirlos.
313if [ -n "${STAGE_ONLY:-}" ]; then
314    echo "==> STAGE_ONLY: árboles listos en $STAGE (root=$ROOT_TREE initramfs=$IRD) — no se ensambla imagen"
315    exit 0
316fi
317
318# --- 3) ESP FAT32: DOS rutas al MISMO kernel + initramfs.cpio.gz, poblada con mtools de takana ------
319# ADR 0018 §1 y §3. El kernel se escribe en DOS sitios de la ESP, y no es redundancia decorativa:
320#
321#   \EFI\takana\takanax64.efi   VENDOR PATH — el nuestro. Nadie más lo reclama. Es el que apunta la
322#                                 entrada NVRAM cuando la hay, y el que sobrevive a que otro sistema
323#                                 operativo repueble la ruta de abajo.
324#   \EFI\BOOT\BOOTX64.EFI       RUTA FALLBACK de medios REMOVIBLES (spec UEFI). Es tierra de nadie:
325#                                 la usa cualquier USB y varias rutas de recuperación de Windows la
326#                                 reescriben. Se mantiene porque es lo que arranca SIN entrada NVRAM
327#                                 —incluido OVMF en las pruebas— pero ya no es el único camino.
328#
329# Content-addressed ⇒ son el MISMO artefacto dos veces: el coste es bytes en la ESP, no complejidad.
330echo "==> ESP FAT32 (${ESP_MB}M): \\EFI\\takana\\takanax64.efi + \\EFI\\BOOT\\BOOTX64.EFI (=bzImage) + \\initramfs.cpio.gz"
331
332# Guardián de capacidad (ADR 0018 §4): que el contenido ENTRE se comprueba ANTES, con los números a
333# la vista. Sin esto el fallo llega como un «disk full» de mcopy a mitad de poblado — o peor, en una
334# ESP de fábrica compartida (100 MiB, medio llena de Windows) donde el margen real es el que decide
335# si takana se puede instalar. Duplicar el kernel CUESTA, y el costo se dice en voz alta.
336_k_b=$(wc -c < "$KERNEL"); _i_b=$(wc -c < "$IRD")
337_need_b=$(( _k_b * 2 + _i_b ))
338_need_mb=$(( (_need_b + 1048575) / 1048576 ))
339_esp_usable_mb=$(( ESP_MB - 2 ))          # ~2 MiB de FAT32: sector de arranque, dos FAT y el raíz
340echo "    kernel $(( _k_b / 1048576 ))M x2 (vendor + fallback) + initramfs $(( (_i_b + 1048575) / 1048576 ))M = ${_need_mb}M sobre ${_esp_usable_mb}M utiles"
341if [ "$_need_mb" -gt "$_esp_usable_mb" ]; then
342    echo "install-image-efi: NO ENTRA en la ESP — hacen falta ${_need_mb}M utiles y la ESP de ${ESP_MB}M da ${_esp_usable_mb}M." >&2
343    echo "                   subí ESP_MB (p.ej. ESP_MB=$(( _need_mb + 8 ))) o achicá el initramfs." >&2
344    exit 1
345fi
346
347rm -f "$ESP_IMG"; truncate -s "$(( ESP_MB * 1024 * 1024 ))" "$ESP_IMG"
348"$MFORMAT" -F -v HAMMER -i "$ESP_IMG" ::
349"$MMD" -i "$ESP_IMG" ::/EFI ::/EFI/BOOT ::/EFI/takana
350"$MCOPY" -i "$ESP_IMG" "$KERNEL" ::/EFI/takana/takanax64.efi
351"$MCOPY" -i "$ESP_IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
352"$MCOPY" -i "$ESP_IMG" "$IRD"    ::/initramfs.cpio.gz
353# Verificar las DOS rutas: que una exista no dice nada de la otra, y el modo de fallo que esto
354# previene es precisamente quedarse con una sola sin enterarse.
355for _p in ::/EFI/takana/takanax64.efi ::/EFI/BOOT/BOOTX64.EFI ::/initramfs.cpio.gz; do
356    "$MDIR" -i "$ESP_IMG" "$_p" >/dev/null 2>&1 || { echo "install-image-efi: falta $_p en la ESP" >&2; exit 1; }
357done
358echo "    ESP poblada y verificada: las dos rutas del kernel + el initramfs"
359
360# --- 4) GPT: p1=ESP, p2=hammer-root, p3=hammer-store, p4=hammer-state --------------------------------
361SECT_MIB=2048
362ESP_SECT=$(( ESP_MB    * SECT_MIB ))
363ROOT_SECT=$(( ROOT_SIZE  * SECT_MIB ))
364STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
365STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
366START1=2048
367START2=$(( START1 + ESP_SECT ))
368START3=$(( START2 + ROOT_SECT ))
369START4=$(( START3 + STORE_SECT ))
370TOTAL_SECT=$(( START4 + STATE_SECT + 2048 ))
371ESP_GUID="C12A7328-F81F-11D2-BA4B-00A0C93EC93B"   # GPT "EFI System Partition"
372
373echo "==> imagen: $IMG (GPT UEFI; ESP ${ESP_MB}M + root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
374rm -f "$IMG"; truncate -s "$(( TOTAL_SECT * 512 ))" "$IMG"
375"$SFDISK" --quiet "$IMG" >/dev/null <<EOF
376label: gpt
377start=$START1, size=$ESP_SECT,   type=$ESP_GUID, name="EFI System Partition"
378start=$START2, size=$ROOT_SECT,  type=linux,     name="hammer-root"
379start=$START3, size=$STORE_SECT, type=linux,     name="hammer-store"
380start=$START4, size=$STATE_SECT, type=linux,     name="hammer-state"
381EOF
382
383# --- 5) poblar las 3 ext4 (root-owned vía unshare -r) y empalmar ESP + ext4 en sus offsets ----------
384echo "==> mke2fs -d + empalme (ESP con mtools ya lista; ext4 bajo unshare -r)"
385unshare -r sh -eu <<EOF
386"$MKE2FS" -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"             "$ROOT_IMG"  ${ROOT_SIZE}M
387"$MKE2FS" -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"          "$STORE_IMG" ${STORE_SIZE}M
388"$MKE2FS" -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
389EOF
390dd if="$ESP_IMG"   of="$IMG" bs=512 seek=$START1 conv=sparse,notrunc status=none
391dd if="$ROOT_IMG"  of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
392dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
393dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START4 conv=sparse,notrunc status=none
394
395rm -rf "$STAGE"
396echo "==> imagen EFI creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
397echo "    auto-boot UEFI: qemu-system-x86_64 -drive if=pflash,...OVMF... -drive file=$IMG   (sin -kernel)"
398
399# --- 6) arranque de prueba en OVMF (sin -kernel: la firmware lanza \EFI\BOOT\BOOTX64.EFI) -----------
400if [ "${BOOT:-0}" = 1 ]; then
401    OVMF_CODE="${OVMF_CODE:-/usr/share/edk2/x64/OVMF_CODE.4m.fd}"
402    OVMF_VARS="${OVMF_VARS:-/usr/share/edk2/x64/OVMF_VARS.4m.fd}"
403    [ -r "$OVMF_CODE" ] && [ -r "$OVMF_VARS" ] || { echo "no encuentro OVMF (set OVMF_CODE/OVMF_VARS): $OVMF_CODE" >&2; exit 1; }
404    cp "$OVMF_VARS" work/.efi-vars.fd
405    accel=""
406    [ "${KVM:-0}" = 1 ] && [ -w /dev/kvm ] && accel="-enable-kvm -cpu host"
407    echo "==> arranque de prueba en OVMF (sin -kernel) — deadline ${DEADLINE:-180}s"
408    # shellcheck disable=SC2086
409    exec timeout "${DEADLINE:-180}" qemu-system-x86_64 -m "${MEM:-4096}" -no-reboot -nographic $accel \
410        -drive if=pflash,unit=0,format=raw,readonly=on,file="$OVMF_CODE" \
411        -drive if=pflash,unit=1,format=raw,file="work/.efi-vars.fd" \
412        -drive file="$IMG",format=raw,if=virtio
413fi