Files
takana/scripts/install-image-efi.sh
T
SergioandClaude Opus 5 294959c1da arranque y GC: las dos rutas EFI, guardián de ESP y el kernel vivo como raíz
Primera tanda de los ADR 0017/0018, con las mediciones que la corrigieron.

ADR 0018 §3 — install-image-efi.sh y takana-live-install.sh escriben el
kernel en \EFI\takana\takanax64.efi ADEMAS de la ruta fallback, con un
guardián de capacidad que comprueba ANTES y con los números a la vista
(duplicar el kernel cuesta, y el costo se dice). En el live-install la
verificación es por TAMAÑO, no por presencia: un cp truncado en FAT32
deja el fichero ahí y test -e diría que todo salió bien.

Verificado con imagen real en OVMF: las dos copias dan el mismo sha256
que el bzImage del store, y la imagen arranca hasta arje-zero PID1.
CONTROL NEGATIVO: pisando la fallback con 4K de basura el firmware dice
"No bootable option or device was found" y no aparece HAMMER-EFI ni una
vez ⇒ el escenario de Windows, reproducido.

Y lo que NO se pudo verificar cambia el alcance, así que va en el ADR:
sin entrada NVRAM el firmware NO busca el vendor path. La copia vendor
es hoy un seguro que no se cobra solo — el §3 es necesario y NO
suficiente sin el §1. Eso asciende la receta efibootmgr a bloqueante.

ADR 0017 §4 — store-gc.sh suma como raíz el kernel EN EJECUCIÓN,
identificado por .config byte a byte. Sin esto el artefacto del kernel
vivo cae en "superados" cuando la receta se movió, y se borra: el
sistema sigue andando perfecto hasta el día que hace falta volver atrás.
Probado en los tres sentidos, incluido el control que TIENE que seguir
condenado.

Además, dos bugs preexistentes que aparecieron al ir a medir:

- install-image-efi.sh abortaba con "ROOTFS sin /sbin/init" en TODO
  rootfs sano: /sbin/init es un symlink ABSOLUTO (→/usr/bin/arje-zero) y
  [ -e ] lo sigue contra la raíz del HOST. Un chequeo que validaba algo
  distinto de lo que creía validar. Arreglado resolviendo el destino
  dentro del rootfs.
- (no arreglado, es del entorno) el cp -al del staging da EXDEV si
  ROOTFS y STAGE no están en el MISMO MOUNT — y el bind-mount del store
  cuenta como otro mount aunque sea el mismo /dev/sdb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 20:40:34 +00:00

296 lines
18 KiB
Bash
Executable File

#!/bin/sh
# install-image-efi.sh — imagen de disco EFI-STUB SOBERANA (ADR 0010 paso 5), validable en OVMF.
#
# Hermana EFI de scripts/install-image.sh (que es GPT+BIOS+GRUB). Ésta NO lleva GRUB ni systemd-boot:
# el propio bzImage es el binario EFI (EFI-STUB) que la firmware lanza como \EFI\BOOT\BOOTX64.EFI (la
# ruta de arranque FALLBACK de medios removibles, sin necesidad de una entrada NVRAM). Es el loader
# soberano del ADR 0010: sin bootloader ajeno, sin el GRUB 2.14 que cuelga la rama UEFI en el metal.
#
# Cómo pasa el cmdline sin LoadOptions: el kernel `linux-metal` hornea CONFIG_CMDLINE (CMDLINE_BOOL, sin
# FORCE) = "... initrd=/initramfs.cpio.gz rdinit=/init". Arrancado por la ruta fallback la firmware pasa
# LoadOptions VACÍO ⇒ el kernel usa ese cmdline horneado ⇒ el EFI-stub carga \initramfs.cpio.gz desde la
# MISMA ESP y corre /init. (En un boot con bootloader que sí pasa LoadOptions, el horneado se ignora.)
#
# El initramfs es MÍNIMO — sólo pivota: findfs LABEL=hammer-root → switch_root a la ext4 real. Ese
# "initrd chico" es lo que destraba EFI-stub directo (ADR 0010 paso 2: el cuelgue del metal era por un
# initrd de 100MB). El sistema real vive en las particiones ext4, no en RAM.
#
# Layout (GPT, UEFI):
# ⚠ Las etiquetas son `hammer-*` y NO `takana-*`: están en el fstab y el arranque de
# sistemas YA INSTALADOS, así que el renombre del ADR 0016 las dejó congeladas a propósito.
# Este comentario decía `takana-*` y el código escribía `hammer-*` — mandaba a buscar una
# etiqueta que no existe.
# p1 ESP FAT32 [EFI System Partition] \EFI\takana\takanax64.efi + \EFI\BOOT\BOOTX64.EFI
# (las DOS = el mismo bzImage, ADR 0018 §3) + \initramfs.cpio.gz
# p2 / ext4 [hammer-root] el rootfs real del producto (/sbin/init → arje-zero)
# p3 /store ext4 [hammer-store]
# p4 /var/lib/hammer ext4 [hammer-state]
#
# Cadena de arranque del disco: firmware UEFI → \EFI\takana\takanax64.efi (con entrada NVRAM) o
# \EFI\BOOT\BOOTX64.EFI (sin ella, y es lo que usa OVMF en las pruebas) → EFI-stub → initramfs pivote
# (findfs LABEL=hammer-root → switch_root) → /sbin/init real → arje-zero PID1.
#
# La ESP se puebla con el mtools de takana (mformat/mcopy, sin root ni loop). Las ext4 con `mke2fs -d`
# bajo `unshare -r` (ficheros root-owned sin sudo), igual que install-image.sh.
#
# Uso:
# ./scripts/install-image-efi.sh # crea work/hammer-efi-disk.img
# BOOT=1 KVM=1 ./scripts/install-image-efi.sh # además lo arranca en OVMF (sin -kernel)
#
# Variables:
# ROOTFS dir rootfs a instalar (default el product-rootfs más reciente del store)
# KERNEL bzImage EFI-stub a embeber (default store/*-linux-metal/boot/bzImage más reciente)
# BUSYBOX busybox ESTÁTICO para el initramfs (default el del ROOTFS)
# MTOOLS dir usr/bin de mtools de takana (default store/*-mtools/usr/bin)
# IMG imagen de salida (default work/hammer-efi-disk.img)
# ESP_MB MiB de la ESP FAT32 (default 64)
# ROOT_SIZE MiB de / (default 2048)
# STORE_SIZE MiB de /store (default 512)
# STATE_SIZE MiB de /var/lib/hammer (default 512)
# BOOT 1 ⇒ arranca en OVMF tras crearla KVM/MEM passthrough al arranque
# OVMF_CODE/OVMF_VARS firmware OVMF (default Arch/CachyOS)
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ROOTFS="${ROOTFS:-$(ls -dt store/*-product-rootfs 2>/dev/null | head -1)}"
KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
IMG="${IMG:-work/hammer-efi-disk.img}"
ESP_MB="${ESP_MB:-64}"
ROOT_SIZE="${ROOT_SIZE:-2048}"
STORE_SIZE="${STORE_SIZE:-512}"
STATE_SIZE="${STATE_SIZE:-512}"
MTOOLS="${MTOOLS:-$(ls -dt store/*-mtools/usr/bin 2>/dev/null | head -1)}"
[ -n "${ROOTFS:-}" ] && [ -d "$ROOTFS" ] || { echo "no existe ROOTFS: '${ROOTFS:-}' (set ROOTFS=<dir product-rootfs>)" >&2; exit 1; }
# ⚠ Esto decía `[ -e "$ROOTFS/sbin/init" ]` y era un chequeo ROTO para todo rootfs sano: `/sbin/init`
# es un symlink ABSOLUTO (→ `/usr/bin/arje-zero`) y `-e` LO SIGUE contra la raíz del HOST, donde no
# existe. ⇒ el script abortaba con «ROOTFS sin /sbin/init» teniendo el init perfectamente presente
# adentro. Un chequeo que valida algo distinto de lo que cree validar, y que además falla del lado
# ruidoso: nunca dejó pasar una imagen mala, pero bloqueaba todas las buenas.
_init_l="$ROOTFS/sbin/init"
if [ -L "$_init_l" ]; then
_init_d="$(readlink "$_init_l")"
case "$_init_d" in
/*) _init_l="$ROOTFS$_init_d" ;; # absoluto: es absoluto DENTRO del rootfs
*) _init_l="$ROOTFS/sbin/$_init_d" ;; # relativo: contra el directorio que lo contiene
esac
fi
[ -e "$_init_l" ] || { echo "ROOTFS sin /sbin/init utilizable (arje-zero): $_init_l" >&2; exit 1; }
[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (build recipes/linux-metal.toml o set KERNEL=)" >&2; exit 1; }
BUSYBOX="${BUSYBOX:-$ROOTFS/bin/busybox}"
[ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX (set BUSYBOX=)" >&2; exit 1; }
[ -n "${MTOOLS:-}" ] && [ -x "$MTOOLS/mformat" ] || { echo "no encuentro mtools de hammer (build recipes/mtools.toml o set MTOOLS=)" >&2; exit 1; }
for t in sfdisk mke2fs cpio gzip; do command -v "$t" >/dev/null 2>&1 || { echo "falta $t" >&2; exit 1; }; done
unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
STAGE="${STAGE:-work/.install-efi-stage}"
rm -rf "$STAGE"; mkdir -p "$STAGE"
ROOT_TREE="$STAGE/root"
IRD_TREE="$STAGE/initramfs"
ROOT_IMG="$STAGE/root.img"; STORE_IMG="$STAGE/store.img"; STATE_IMG="$STAGE/state.img"
ESP_IMG="$STAGE/esp.img"; IRD="$STAGE/initramfs.cpio.gz"
# --- 1) initramfs de pivote: busybox estático + /init (findfs LABEL=hammer-root → switch_root) --------
echo "==> initramfs de pivote (busybox estático + /init switch_root a hammer-root)"
mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" "$IRD_TREE/newroot"
cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
for a in sh mount umount findfs switch_root mkdir echo sleep ls cat setsid dmesg blkid; do ln -sf busybox "$IRD_TREE/bin/$a"; done
cat > "$IRD_TREE/init" <<'INIT'
#!/bin/busybox sh
# initramfs de pivote (EFI-stub → aquí). Monta las pseudo-fs, resuelve la ext4 real por LABEL, monta
# /store y /var/lib/hammer bajo ella y hace switch_root al init real (arje-zero). Sin root= en cmdline:
# el descubrimiento por LABEL vive acá, así el disco es reubicable (cualquier /dev/…, virtio o nvme).
export PATH=/bin
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sys /sys 2>/dev/null || true
mount -t devtmpfs dev /dev 2>/dev/null || true
# ⚠ EL CMDLINE HORNEADO ES "console=tty0 console=ttyS0,115200 …" Y EL KERNEL HACE /dev/console = LA
# ÚLTIMA `console=` ⇒ **ttyS0**. O sea que el stdout de este script va al SERIAL, no a la pantalla: en
# un metal sin puerto serie TODO lo que pase acá es INVISIBLE y el arranque parece congelado en el
# último printk del kernel. Es el mismo bug que costó el 1er viaje físico de KDE, una capa más
# temprano: allá se arregló para DESPUÉS del switch_root (getty en tty1) y acá seguía sin arreglar.
# Por eso el log va explícitamente a **/dev/tty0** (la VT en primer plano = la pantalla) además del
# serial, y nunca se confía en el stdout heredado.
log() {
echo "$*"
for _d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$_d" 2>/dev/null || true; done
}
# Shell de rescate VISIBLE: si algo falla, un `exec sh` a secas nace en el serial y el usuario ve una
# pantalla muerta sin manera de averiguar nada. Se abre uno en tty1 (pantalla) y se conserva el serial.
rescate() {
log "HAMMER-EFI-FALLO: $*"
log " bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
log " abriendo shell de rescate en tty1 — probá: blkid ; dmesg | tail -30"
setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
exec sh
}
log "HAMMER-EFI: initramfs de pivote — resolviendo hammer-root por LABEL"
# ⏱ LA ESPERA TIENE QUE SER LARGA, Y ÉSTE ES EL MOTIVO: el rdinit arranca en cuanto se desempaqueta el
# initramfs, pero el bus USB enumera de forma ASÍNCRONA y tarda SEGUNDOS (reset del hub, settling de 1s
# por dispositivo, scan SCSI). En QEMU el disco es virtio y está desde el instante cero, así que la
# carrera NUNCA se ve en validación: sólo aparece al bootear de un USB en metal, y con 5 reintentos de
# 1s se pierde. Es el equivalente de `rootwait`, que acá no aplica porque no hay root= en el cmdline.
ROOTDEV=""
_try=0
while [ "$_try" -lt 60 ]; do
ROOTDEV="$(findfs LABEL=hammer-root 2>/dev/null || true)"
[ -n "$ROOTDEV" ] && break
_try=$(( _try + 1 ))
[ $(( _try % 5 )) -eq 0 ] && log "HAMMER-EFI: esperando hammer-root… ${_try}s (el USB tarda en enumerar)"
sleep 1
done
[ -n "$ROOTDEV" ] || rescate "no encuentro LABEL=hammer-root tras ${_try}s"
log "HAMMER-EFI: hammer-root = $ROOTDEV → montando en /newroot"
mount -t ext4 "$ROOTDEV" /newroot || rescate "no pude montar $ROOTDEV"
# /store y /var/lib/hammer por LABEL (no bloqueante: el wrapper del root también reintenta).
STOREDEV="$(findfs LABEL=hammer-store 2>/dev/null || true)"
STATEDEV="$(findfs LABEL=hammer-state 2>/dev/null || true)"
[ -n "$STOREDEV" ] && mount -t ext4 "$STOREDEV" /newroot/store 2>/dev/null || true
[ -n "$STATEDEV" ] && mount -t ext4 "$STATEDEV" /newroot/var/lib/hammer 2>/dev/null || true
log "HAMMER-EFI-DISK-PIVOT-OK: switch_root → /sbin/init"
exec switch_root /newroot /sbin/init
INIT
chmod +x "$IRD_TREE/init"
( cd "$IRD_TREE" && find . -print0 | cpio --null -o -H newc --owner=root:root 2>/dev/null | gzip -1 ) > "$IRD"
echo " initramfs: $(du -h "$IRD" | cut -f1)"
# --- 2) árbol root: hardlinks del rootfs SIN store/estado (van en sus particiones dedicadas) -----------
echo "==> staging root (hardlinks del rootfs, sin /store ni /var/lib/hammer)"
cp -al "$ROOTFS" "$ROOT_TREE"
rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
# CLI `takana` (static musl) para el menú de arranque por grafo (ADR 0010): el hook de init lo invoca
# como `takana boot menu`. El producto trae arje-zero/hammerd pero NO el CLI ⇒ lo inyectamos acá si
# está compilado (igual patrón que hammer-recover). Opcional: sin él, el wrapper saltea el menú.
HAMMER_MUSL="${TAKANA_MUSL:-${HAMMER_MUSL:-$ROOT/target/x86_64-unknown-linux-musl/release/hammer}}"
if [ -x "$HAMMER_MUSL" ]; then
cp "$HAMMER_MUSL" "$ROOT_TREE/usr/bin/hammer"; chmod 0755 "$ROOT_TREE/usr/bin/hammer"
echo " + CLI hammer (static musl) para el menú de arranque por grafo"
else
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
fi
# /sbin/init: wrapper que emite un marcador a /dev/ttyS0 (sobrevive `quiet`, que suprime el printk
# kernel-side) y hace exec de arje-zero. Prueba positiva de que el switch_root llegó al init real.
rm -f "$ROOT_TREE/sbin/init"
cat > "$ROOT_TREE/sbin/init" <<'RINIT'
#!/bin/sh
# PID1 real tras el switch_root del pivote (store/estado ya montados por LABEL en el initramfs).
# busybox switch_root NO mueve /dev al nuevo root ⇒ montamos devtmpfs antes del marcador (si no,
# /dev/ttyS0 no existe todavía y el echo se pierde). arje-zero re-monta lo que necesite, es idempotente.
/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
# A la PANTALLA (tty0) además del serial: en metal sin puerto serie éste es el único indicio de que el
# pivote llegó al init real. Ver el comentario largo del initramfs.
for _d in /dev/tty0 /dev/ttyS0; do
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > "$_d" 2>/dev/null || true
done
# Menú de arranque por grafo (ADR 0010): emite /run/hammer/boot-graph.json y, si hay un compositor
# (mirada), lo pinta sobre KMS para elegir un nodo. Graceful: sin compositor, emite el grafo y sigue.
# Salida al serial (no pinta tty0 ⇒ respeta el cero-parpadeo). No bloquea el arranque si falla.
# El `timeout` no es paranoia decorativa: esto corre ANTES del exec de arje-zero, o sea que si se
# cuelga no hay PID1, no hay getty y no hay pantalla — un arranque muerto e indistinguible de un kernel
# colgado. El `|| true` protege del fallo, no del bloqueo.
if [ -x /usr/bin/hammer ]; then
/bin/busybox timeout 15 /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
fi
exec /usr/bin/arje-zero
RINIT
chmod +x "$ROOT_TREE/sbin/init"
# --- 3) ESP FAT32: DOS rutas al MISMO kernel + initramfs.cpio.gz, poblada con mtools de takana ------
# ADR 0018 §1 y §3. El kernel se escribe en DOS sitios de la ESP, y no es redundancia decorativa:
#
# \EFI\takana\takanax64.efi VENDOR PATH — el nuestro. Nadie más lo reclama. Es el que apunta la
# entrada NVRAM cuando la hay, y el que sobrevive a que otro sistema
# operativo repueble la ruta de abajo.
# \EFI\BOOT\BOOTX64.EFI RUTA FALLBACK de medios REMOVIBLES (spec UEFI). Es tierra de nadie:
# la usa cualquier USB y varias rutas de recuperación de Windows la
# reescriben. Se mantiene porque es lo que arranca SIN entrada NVRAM
# —incluido OVMF en las pruebas— pero ya no es el único camino.
#
# Content-addressed ⇒ son el MISMO artefacto dos veces: el coste es bytes en la ESP, no complejidad.
echo "==> ESP FAT32 (${ESP_MB}M): \\EFI\\takana\\takanax64.efi + \\EFI\\BOOT\\BOOTX64.EFI (=bzImage) + \\initramfs.cpio.gz"
# Guardián de capacidad (ADR 0018 §4): que el contenido ENTRE se comprueba ANTES, con los números a
# la vista. Sin esto el fallo llega como un «disk full» de mcopy a mitad de poblado — o peor, en una
# ESP de fábrica compartida (100 MiB, medio llena de Windows) donde el margen real es el que decide
# si takana se puede instalar. Duplicar el kernel CUESTA, y el costo se dice en voz alta.
_k_b=$(wc -c < "$KERNEL"); _i_b=$(wc -c < "$IRD")
_need_b=$(( _k_b * 2 + _i_b ))
_need_mb=$(( (_need_b + 1048575) / 1048576 ))
_esp_usable_mb=$(( ESP_MB - 2 )) # ~2 MiB de FAT32: sector de arranque, dos FAT y el raíz
echo " kernel $(( _k_b / 1048576 ))M x2 (vendor + fallback) + initramfs $(( (_i_b + 1048575) / 1048576 ))M = ${_need_mb}M sobre ${_esp_usable_mb}M utiles"
if [ "$_need_mb" -gt "$_esp_usable_mb" ]; then
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
echo " subí ESP_MB (p.ej. ESP_MB=$(( _need_mb + 8 ))) o achicá el initramfs." >&2
exit 1
fi
rm -f "$ESP_IMG"; truncate -s "$(( ESP_MB * 1024 * 1024 ))" "$ESP_IMG"
"$MTOOLS/mformat" -F -v HAMMER -i "$ESP_IMG" ::
"$MTOOLS/mmd" -i "$ESP_IMG" ::/EFI ::/EFI/BOOT ::/EFI/takana
"$MTOOLS/mcopy" -i "$ESP_IMG" "$KERNEL" ::/EFI/takana/takanax64.efi
"$MTOOLS/mcopy" -i "$ESP_IMG" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
"$MTOOLS/mcopy" -i "$ESP_IMG" "$IRD" ::/initramfs.cpio.gz
# Verificar las DOS rutas: que una exista no dice nada de la otra, y el modo de fallo que esto
# previene es precisamente quedarse con una sola sin enterarse.
for _p in ::/EFI/takana/takanax64.efi ::/EFI/BOOT/BOOTX64.EFI ::/initramfs.cpio.gz; do
"$MTOOLS/mdir" -i "$ESP_IMG" "$_p" >/dev/null 2>&1 || { echo "install-image-efi: falta $_p en la ESP" >&2; exit 1; }
done
echo " ESP poblada y verificada: las dos rutas del kernel + el initramfs"
# --- 4) GPT: p1=ESP, p2=hammer-root, p3=hammer-store, p4=hammer-state --------------------------------
SECT_MIB=2048
ESP_SECT=$(( ESP_MB * SECT_MIB ))
ROOT_SECT=$(( ROOT_SIZE * SECT_MIB ))
STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
START1=2048
START2=$(( START1 + ESP_SECT ))
START3=$(( START2 + ROOT_SECT ))
START4=$(( START3 + STORE_SECT ))
TOTAL_SECT=$(( START4 + STATE_SECT + 2048 ))
ESP_GUID="C12A7328-F81F-11D2-BA4B-00A0C93EC93B" # GPT "EFI System Partition"
echo "==> imagen: $IMG (GPT UEFI; ESP ${ESP_MB}M + root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
rm -f "$IMG"; truncate -s "$(( TOTAL_SECT * 512 ))" "$IMG"
sfdisk --quiet "$IMG" >/dev/null <<EOF
label: gpt
start=$START1, size=$ESP_SECT, type=$ESP_GUID, name="EFI System Partition"
start=$START2, size=$ROOT_SECT, type=linux, name="hammer-root"
start=$START3, size=$STORE_SECT, type=linux, name="hammer-store"
start=$START4, size=$STATE_SECT, type=linux, name="hammer-state"
EOF
# --- 5) poblar las 3 ext4 (root-owned vía unshare -r) y empalmar ESP + ext4 en sus offsets ----------
echo "==> mke2fs -d + empalme (ESP con mtools ya lista; ext4 bajo unshare -r)"
unshare -r sh -eu <<EOF
mke2fs -q -t ext4 -L hammer-root -E root_owner=0:0 -d "$ROOT_TREE" "$ROOT_IMG" ${ROOT_SIZE}M
mke2fs -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store" "$STORE_IMG" ${STORE_SIZE}M
mke2fs -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer" "$STATE_IMG" ${STATE_SIZE}M
EOF
dd if="$ESP_IMG" of="$IMG" bs=512 seek=$START1 conv=sparse,notrunc status=none
dd if="$ROOT_IMG" of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START4 conv=sparse,notrunc status=none
rm -rf "$STAGE"
echo "==> imagen EFI creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
echo " auto-boot UEFI: qemu-system-x86_64 -drive if=pflash,...OVMF... -drive file=$IMG (sin -kernel)"
# --- 6) arranque de prueba en OVMF (sin -kernel: la firmware lanza \EFI\BOOT\BOOTX64.EFI) -----------
if [ "${BOOT:-0}" = 1 ]; then
OVMF_CODE="${OVMF_CODE:-/usr/share/edk2/x64/OVMF_CODE.4m.fd}"
OVMF_VARS="${OVMF_VARS:-/usr/share/edk2/x64/OVMF_VARS.4m.fd}"
[ -r "$OVMF_CODE" ] && [ -r "$OVMF_VARS" ] || { echo "no encuentro OVMF (set OVMF_CODE/OVMF_VARS): $OVMF_CODE" >&2; exit 1; }
cp "$OVMF_VARS" work/.efi-vars.fd
accel=""
[ "${KVM:-0}" = 1 ] && [ -w /dev/kvm ] && accel="-enable-kvm -cpu host"
echo "==> arranque de prueba en OVMF (sin -kernel) — deadline ${DEADLINE:-180}s"
# shellcheck disable=SC2086
exec timeout "${DEADLINE:-180}" qemu-system-x86_64 -m "${MEM:-4096}" -no-reboot -nographic $accel \
-drive if=pflash,unit=0,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,unit=1,format=raw,file="work/.efi-vars.fd" \
-drive file="$IMG",format=raw,if=virtio
fi