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 1389Shell 137Markdown 80Rust 802602 Definiciones

⎇ 8b1673d689b25a7854e74067da850d4a8f8a09ea scripts/install-image.sh Shell · 314 líneas · 19.7 KB Historia crudo

paso 4: el ensamblado de imagen toma sus herramientas del STORE — y estaba roto en las 3 máquinas e30ffbbf0 · Sergio ·

1#!/bin/sh
2# install-image.sh — Etapa E1 (SDD 13): produce una imagen de disco AUTO-BOOTEABLE. A diferencia de
3# scripts/disk-image.sh (B3, que se arranca con `qemu -kernel` para verificar el auto-alojamiento),
4# ésta lleva GRUB en el propio disco ⇒ bootea con `qemu-system-x86_64 -drive file=img` SIN `-kernel` ni
5# firmware extra (SeaBIOS de QEMU → MBR → core.img → kernel), como en hardware real.
6#
7# Layout (GPT, BIOS):
8#   /dev/vda1  BIOS boot (ef02, ~2 MiB, sin fs)   core.img de GRUB
9#   /dev/vda2  /                ext4  [takana-root]   + /boot/bzImage + /boot/grub
10#   /dev/vda3  /store           ext4  [takana-store]
11#   /dev/vda4  /var/lib/hammer  ext4  [takana-state]
12#
13# Instalación de GRUB en USERSPACE (sin root ni loop): grub-mkimage arma core.img; grub-bios-setup
14# escribe boot.img→MBR + core.img→partición BIOS sobre el fichero imagen (I/O de bloques). Los módulos
15# de GRUB + el kernel van en /boot de la root, que mke2fs -d puebla desde un directorio.
16#
17# Uso:
18#   ./scripts/install-image.sh                                  # crea work/takana-install.img
19#   BOOT=1 KVM=1 ./scripts/install-image.sh                     # además lo arranca (auto-boot, sin -kernel)
20#
21# Target IN-PLACE (Etapa E2): si IMG es un block device (o PREALLOC=1 sobre un fichero pre-dimensionado),
22# se instala EN SITIO — no se trunca ni se borra el nodo, sólo se valida capacidad. Esa es la base de
23# `takana install /dev/sdX` (ver scripts/takana-install.sh): misma lógica, apuntando a un disco real.
24#
25# Variables:
26#   ROOTFS      dir rootfs a empaquetar       (default work/builder-rootfs)
27#   STAGE       dir de staging                 (default work/.install-stage) — DEBE estar en el
28#               mismo montaje que ROOTFS: el staging hardlinkea y un hardlink no cruza montajes
29#   PREALLOC    1 ⇒ trata IMG como target pre-dimensionado in-place (no trunca; prueba el camino device)
30#   KERNEL      bzImage a embeber en /boot     (default el del store: store/*-linux/boot/bzImage)
31#   IMG         imagen de salida               (default work/takana-install.img)
32#   ROOT_SIZE   MiB de /         (default 4096)
33#   STORE_SIZE  MiB de /store    (default 2048)
34#   STATE_SIZE  MiB de /var/lib/hammer (default 2048)
35#   CMDLINE     línea de comando del kernel    (default "console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init")
36#   BOOT        1 ⇒ arranca la imagen con QEMU tras crearla (auto-boot, sin -kernel)
37#   KVM/MEM     passthrough al arranque de prueba
38set -eu
39ROOT="$(cd "$(dirname "$0")/.." && pwd)"
40cd "$ROOT"
41
42ROOTFS="${ROOTFS:-work/builder-rootfs}"
43IMG="${IMG:-work/takana-install.img}"
44ROOT_SIZE="${ROOT_SIZE:-4096}"
45STORE_SIZE="${STORE_SIZE:-2048}"
46STATE_SIZE="${STATE_SIZE:-2048}"
47CMDLINE="${CMDLINE:-console=ttyS0 root=/dev/vda2 rw rdinit=/sbin/init}"
48KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
49GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
50
51[ -d "$ROOTFS" ] || { echo "no existe ROOTFS: $ROOTFS (corré selfhost-verify para armar el builder)" >&2; exit 1; }
52# `-e` sigue el symlink y lo resuelve contra el HOST, no contra el rootfs. El `/sbin/init` del
53# product-rootfs es un symlink ABSOLUTO a `/usr/bin/arje-zero`, que en el host no existe ⇒ `-e` da
54# falso y esta guarda rechazaba el rootfs CORRECTO. Peor: un rootfs con el init equivocado
55# (`../bin/busybox`, symlink RELATIVO que sí resuelve dentro del árbol) la pasaba sin chistar.
56# O sea que la guarda estaba exactamente invertida. `-L` acepta el symlink por lo que es.
57[ -e "$ROOTFS/sbin/init" ] || [ -L "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&2; exit 1; }
58[ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel a embeber (set KERNEL=<bzImage>)" >&2; exit 1; }
59[ -d "$GRUB_LIB" ] && [ -r "$GRUB_LIB/boot.img" ] || { echo "faltan los módulos GRUB i386-pc en $GRUB_LIB" >&2; exit 1; }
60# ⚠ Las herramientas del ensamblado salen del STORE por HASH VIGENTE (2026-09-22). Medido: en este
61# hub, en su anfitrión y en el worker NO existe `grub-mkimage` ni `grub-bios-setup`, así que ESTA RAMA
62# no se podía correr en ninguna de las tres máquinas — y sólo se veía al intentarlo.
63. "$(dirname "$0")/lib/herramientas-desde-store.sh"
64echo "==> herramientas del ensamblado"
65SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
66MKE2FS=$(herramienta e2fsprogs  sbin/mke2fs mke2fs) || exit 1
67# GRUB es el ÚNICO que no puede salir del store: no hay `recipes/grub.toml`. Y no se puede retirar la
68# rama BIOS «porque ya tenemos EFI-stub»: **Hetzner Cloud arranca por BIOS** —medido en gioser, no
69# existe `/sys/firmware/efi` (SDD 28 §3.1)—, o sea que la imagen del servidor de producción es
70# justamente la que lo necesita. Mientras no haya receta, esta rama depende de un binario ajeno sin
71# hash y el aviso tiene que ser explícito, no un `falta grub-mkimage` que se lee como un olvido.
72for t in grub-mkimage grub-bios-setup; do
73    command -v "$t" >/dev/null 2>&1 || {
74        echo "   ✗ falta $t, y NO hay receta de grub en el corpus." >&2
75        echo "     Esta rama (GPT+BIOS) es la que necesita Hetzner Cloud, que arranca por BIOS." >&2
76        echo "     Alternativas, ninguna gratis: instalar grub en la máquina que ensambla, o escribir" >&2
77        echo "     la receta del gestor de arranque (grub, o limine que cubre BIOS y UEFI de una)." >&2
78        echo "     Para un objetivo UEFI la rama soberana ya existe: scripts/install-image-efi.sh." >&2
79        exit 1
80    }
81    echo "   ⚠ ${t} ← $(command -v "$t") DEL HOST, sin hash: el gestor de arranque no es nuestro" >&2
82done
83unshare -r true 2>/dev/null || { echo "se requiere 'unshare -r' (ficheros root-owned sin sudo)" >&2; exit 1; }
84
85# El staging ENLAZA (hardlink) desde $ROOTFS, y un hardlink no cruza un montaje aunque sea el mismo
86# disco. Con el store en un volumen aparte —el layout normal de este repo: `store` y `work/out` son
87# bind-mounts de /dev/sdb y `work/` está en /dev/sdc— el rootfs vive del otro lado y `cp -al` muere
88# con EXDEV fichero por fichero. Por eso es env: ponelo bajo el MISMO montaje que $ROOTFS.
89STAGE="${STAGE:-work/.install-stage}"
90rm -rf "$STAGE"
91mkdir -p "$STAGE"
92ROOT_TREE="$STAGE/root"
93ROOT_IMG="$STAGE/root.img"
94STORE_IMG="$STAGE/store.img"
95STATE_IMG="$STAGE/state.img"
96GRUB_STAGE="$STAGE/grub"
97
98# --- árbol root: hardlinks del rootfs SIN store/estado + /boot (kernel + GRUB) + wrapper /sbin/init ---
99echo "==> staging root: rootfs (hardlinks) + /boot/bzImage + /boot/grub + wrapper /sbin/init"
100sonda_hardlink "$ROOTFS" "$STAGE" || exit 1
101cp -al "$ROOTFS" "$ROOT_TREE"
102rm -rf "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer"
103mkdir -p "$ROOT_TREE/store" "$ROOT_TREE/var/lib/hammer" "$ROOT_TREE/boot/grub/i386-pc"
104cp "$KERNEL" "$ROOT_TREE/boot/bzImage"; chmod 0644 "$ROOT_TREE/boot/bzImage"
105# Módulos GRUB que core.img cargará en tiempo de boot desde /boot/grub/i386-pc.
106cp "$GRUB_LIB"/*.mod "$GRUB_LIB"/*.lst "$ROOT_TREE/boot/grub/i386-pc/" 2>/dev/null || true
107# grub.cfg con consola serie (para -nographic) y el menuentry del kernel embebido.
108cat > "$ROOT_TREE/boot/grub/grub.cfg" <<CFG
109set timeout=2
110set default=0
111serial --unit=0 --speed=115200
112terminal_input serial console
113terminal_output serial console
114insmod part_gpt
115insmod ext2
116menuentry "hammer" {
117    linux /boot/bzImage $CMDLINE
118}
119CFG
120# Wrapper /sbin/init: monta las particiones dedicadas y hace exec del init real (igual que B3).
121rm -f "$ROOT_TREE/sbin/init"
122cat > "$ROOT_TREE/sbin/init" <<'INIT'
123#!/bin/sh
124# Wrapper PID1 (Etapa E/B3): el kernel montó la raíz; montamos las particiones dedicadas.
125#
126# Se busca por ETIQUETA, que es la que este mismo script pone con `mke2fs -L`. Las dos alternativas
127# que se probaron y NO sirven acá:
128#   · `/dev/vda3` cableado — vale sólo con virtio-blk. En una caja Hetzner Cloud la controladora es
129#     virtio-SCSI y el disco es `sda`, así que los montajes fallaban.
130#   · derivar el disco de `/proc/mounts` — MEDIDO que no funciona: PID1 corre ANTES de que nadie
131#     monte /proc, así que la lectura sale vacía. (Queda de respaldo, montando /proc primero.)
132# `findfs` sí funciona sin /proc: /dev ya está poblado por el kernel (DEVTMPFS_MOUNT=y).
133#
134# Y los avisos van a /dev/kmsg: un `echo` a stdout en PID1 se pierde antes de que haya consola —
135# por eso este wrapper falló en silencio la primera vez.
136_say() { echo "init: $*" > /dev/kmsg 2>/dev/null || echo "init: $*"; }
137_store=$(/bin/busybox findfs LABEL=hammer-store 2>/dev/null)
138_state=$(/bin/busybox findfs LABEL=hammer-state 2>/dev/null)
139if [ -z "$_store" ]; then
140    _say "findfs no resolvió las etiquetas; derivo del disco de la raíz"
141    /bin/busybox mount -t proc proc /proc 2>/dev/null
142    _root=$(/bin/busybox awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null)
143    _disk=${_root%[0-9]}
144    [ -n "$_disk" ] && { _store="${_disk}3"; _state="${_disk}4"; }
145fi
146# ── Crecer el store al disco entero, ANTES de montarlo ──────────────────────────────────────
147# Una imagen se escribe con `dd` sobre un disco casi siempre MÁS GRANDE que ella: la del perfil
148# servidor son 7 G sobre los 76 G de una caja hcloud, o sea 69 G que nadie podía usar. Sin esto la
149# caja arranca perfecta y con el disco a un décimo — de nuevo el fallo que no falla.
150# Es idempotente: si la partición ya llega al final, `sfdisk` y `resize2fs` no hacen nada. Y sólo
151# toca la ÚLTIMA partición (por eso el store va último en el layout): extenderla no mueve datos.
152if [ -n "$_store" ] && [ -x /sbin/sfdisk ]; then
153    _disk=$(echo "$_store" | /bin/busybox sed 's/[0-9]*$//; s/p$//')
154    _num=$(echo "$_store" | /bin/busybox sed 's/^.*[^0-9]//')
155    if [ -n "$_disk" ] && [ -n "$_num" ]; then
156        if echo ', +' | /sbin/sfdisk --no-reread --force -N "$_num" "$_disk" >/dev/null 2>&1; then
157            [ -x /usr/sbin/partx ] && /usr/sbin/partx -u "$_disk" >/dev/null 2>&1
158            [ -x /sbin/e2fsck ] && /sbin/e2fsck -p -f "$_store" >/dev/null 2>&1
159            [ -x /usr/sbin/resize2fs ] && /usr/sbin/resize2fs "$_store" >/dev/null 2>&1 \
160                && _say "store: $_store extendido al final de $_disk"
161        fi
162    fi
163fi
164[ -n "$_store" ] && /bin/busybox mount -t ext4 "$_store" /store          || _say "no pude montar /store ($_store)"
165[ -n "$_state" ] && /bin/busybox mount -t ext4 "$_state" /var/lib/hammer || _say "no pude montar /var/lib/hammer ($_state)"
166# ── OPCIONAL: un filesystem etiquetado `hammer-work` se monta en /work ──────────────────────────
167# Convención, no requisito: la imagen no crea esa partición. Existe para el caso de un HUB, donde el
168# trabajo pesado —`work/sources`, los tarballs, el `CARGO_HOME`— no cabe ni conviene en la raíz. En
169# gioser eso son 85 G. Si la etiqueta no existe, no pasa nada y no se avisa: no es un error.
170_work=$(/bin/busybox findfs LABEL=hammer-work 2>/dev/null)
171if [ -n "$_work" ]; then
172    /bin/busybox mkdir -p /work
173    /bin/busybox mount -t ext4 "$_work" /work || _say "no pude montar /work ($_work)"
174fi
175exec /usr/bin/arje-zero
176INIT
177chmod +x "$ROOT_TREE/sbin/init"
178
179# --- GPT: p1=BIOS boot (ef02), p2=ESP (FAT32), p3=/, p4=/var/lib/hammer, p5=/store ---
180#
181# ── LA IMAGEN ARRANCA EN LOS DOS MODOS, Y NO ES UN LUJO ────────────────────────────────────────
182# «Instalable en Hetzner o en cualquier otro servicio» se rompe en el arranque: Hetzner Cloud arranca
183# por BIOS y otros proveedores sólo ofrecen UEFI. Tener dos imágenes hermanas —una BIOS y otra EFI—
184# obliga a elegir por proveedor, y a que las dos diverjan (ya divergían: la hermana EFI documenta
185# etiquetas `takana-*` que su propio código no escribe).
186#
187# Ésta lleva LAS DOS: el MBR + `core.img` en la partición BIOS-boot, y un `BOOTX64.EFI` en la ESP.
188# **Los dos GRUB leen el MISMO `/boot/grub/grub.cfg`** de la raíz, así que hay una sola línea de
189# comando y un solo sitio donde cambiarla. Un firmware toma uno, el otro toma el otro, y el sistema
190# que arranca es idéntico.
191#
192# No se usa EFI-stub directo acá (sí lo hace `install-image-efi.sh`, ADR 0010): el stub arrancado por
193# la ruta fallback recibe LoadOptions VACÍO, así que necesita la cmdline HORNEADA en el kernel — y la
194# de `linux-generic` sólo trae la consola, sin `root=`. Hornearla ataría la línea de comando al
195# ArtifactHash del kernel. Con GRUB EFI la cmdline vive en el `grub.cfg`, que es donde se edita.
196#
197# EL STORE VA ÚLTIMO, Y ES A PROPÓSITO. Una imagen se escribe con `dd` sobre un disco que casi
198# siempre es MÁS GRANDE que ella (la del perfil servidor son 7 G sobre los 76 G de una caja hcloud),
199# y sólo la ÚLTIMA partición puede crecer sin mover nada. Con el store en el medio —como estaba— el
200# disco quedaba con 69 G inalcanzables y la partición que de verdad crece, la del store, congelada en
201# su tamaño de fábrica. El wrapper de /sbin/init la extiende en el primer arranque.
202SECT_MIB=2048
203BIOS_SECT=4096                                   # 2 MiB para core.img
204ESP_SIZE="${ESP_SIZE:-64}"                       # MiB de la ESP (FAT32 pide ≥ 33 MiB útiles)
205ESP_SECT=$(( ESP_SIZE * SECT_MIB ))
206ROOT_SECT=$(( ROOT_SIZE  * SECT_MIB ))
207STATE_SECT=$(( STATE_SIZE * SECT_MIB ))
208STORE_SECT=$(( STORE_SIZE * SECT_MIB ))
209START1=2048
210START2=$(( START1 + BIOS_SECT ))                 # ESP
211START3=$(( START2 + ESP_SECT ))                  # /
212START4=$(( START3 + ROOT_SECT ))                 # /var/lib/hammer
213START5=$(( START4 + STATE_SECT ))                # /store  (ÚLTIMA: es la que crece)
214TOTAL_SECT=$(( START5 + STORE_SECT + 2048 ))
215BIOS_GUID="21686148-6449-6E6F-744E-656564454649" # GPT "BIOS boot partition"
216
217# Target = block device (instalación in-place, Etapa E2 `takana install /dev/sdX`) o fichero imagen.
218# Un device tiene tamaño fijo: NO se trunca ni se borra el nodo; se valida capacidad. PREALLOC=1 fuerza
219# este modo sobre un fichero pre-dimensionado (para probar el camino de device sin hardware real).
220TOTAL_BYTES=$(( TOTAL_SECT * 512 ))
221if [ -b "$IMG" ] || [ "${PREALLOC:-0}" = 1 ]; then
222    cur=$(blockdev --getsize64 "$IMG" 2>/dev/null || stat -c%s "$IMG" 2>/dev/null || echo 0)
223    [ "$cur" -ge "$TOTAL_BYTES" ] || { echo "target $IMG es ${cur}B < ${TOTAL_BYTES}B requeridos" >&2; exit 1; }
224    echo "==> target IN-PLACE: $IMG ($(( cur/1024/1024 ))M disponibles; root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
225else
226    echo "==> imagen : $IMG (GPT BIOS-boot, root ${ROOT_SIZE}M + store ${STORE_SIZE}M + estado ${STATE_SIZE}M)"
227    rm -f "$IMG"
228    truncate -s "$TOTAL_BYTES" "$IMG"
229fi
230"$SFDISK" --quiet "$IMG" >/dev/null <<EOF
231label: gpt
232start=$START1, size=$BIOS_SECT,  type=$BIOS_GUID, name="bios-boot"
233start=$START2, size=$ESP_SECT,   type=uefi,       name="hammer-esp"
234start=$START3, size=$ROOT_SECT,  type=linux,      name="hammer-root"
235start=$START4, size=$STATE_SECT, type=linux,      name="hammer-state"
236start=$START5, size=$STORE_SECT, type=linux,      name="hammer-store"
237EOF
238
239# --- poblar las 3 particiones ext4 (root-owned vía unshare -r) y empalmar en su offset ---
240echo "==> mke2fs + empalme de las particiones ext4 (unshare -r, sin sudo)"
241unshare -r sh -eu <<EOF
242"$MKE2FS" -q -t ext4 -L hammer-root  -E root_owner=0:0 -d "$ROOT_TREE"               "$ROOT_IMG"  ${ROOT_SIZE}M
243"$MKE2FS" -q -t ext4 -L hammer-store -E root_owner=0:0 -d "$ROOTFS/store"            "$STORE_IMG" ${STORE_SIZE}M
244"$MKE2FS" -q -t ext4 -L hammer-state -E root_owner=0:0 -d "$ROOTFS/var/lib/hammer"   "$STATE_IMG" ${STATE_SIZE}M
245dd if="$ROOT_IMG"  of="$IMG" bs=512 seek=$START3 conv=sparse,notrunc status=none
246dd if="$STATE_IMG" of="$IMG" bs=512 seek=$START4 conv=sparse,notrunc status=none
247dd if="$STORE_IMG" of="$IMG" bs=512 seek=$START5 conv=sparse,notrunc status=none
248EOF
249
250# --- instalar GRUB en userspace SIN root/loop: grub-mkimage arma core.img, y un patch binario
251#     determinista coloca boot.img→MBR + core.img→BIOS boot partition (vda1) ajustando los punteros.
252#     (`grub-bios-setup` se descarta: sondea el disco FÍSICO del host —/dev/nvme…, 660 root:disk— para
253#      "adivinar el root device" del dir -d, y falla sin root; el patch manual evita ese sondeo.)
254echo "==> instalar GRUB (grub-mkimage + patch binario boot/core, sin root/loop)"
255mkdir -p "$GRUB_STAGE"
256# core.img: prefix a (hd0,gpt3)/boot/grub — la raíz es la 3ª partición desde que hay ESP. Se
257# embebe lo justo para leer GPT+ext2, cargar grub.cfg (normal/configfile) y el kernel (linux).
258grub-mkimage -O i386-pc -p '(hd0,gpt3)/boot/grub' -o "$GRUB_STAGE/core.img" \
259    biosdisk part_gpt ext2 normal configfile linux echo ls boot
260# Punteros (ABI estable de GRUB i386-pc):
261#   boot.img  off 0x5c  kernel_sector (u64 LE): LBA del 1er sector de core.img      → START1
262#   core.img  off 0x1F4 blocklist.start (u64 LE): LBA del RESTO de core.img         → START1+1
263# Verificamos los valores por defecto (1 y 2: core.img asumido contiguo tras el MBR) antes de tocar.
264START1="$START1" GRUB_LIB="$GRUB_LIB" GRUB_STAGE="$GRUB_STAGE" IMG="$IMG" python3 - <<'PY'
265import os, struct
266s1   = int(os.environ["START1"])
267img  = os.environ["IMG"]
268boot = open(os.path.join(os.environ["GRUB_LIB"], "boot.img"), "rb").read()
269core = bytearray(open(os.path.join(os.environ["GRUB_STAGE"], "core.img"), "rb").read())
270boot = bytearray(boot)
271assert struct.unpack_from("<Q", boot, 0x5c)[0] == 1,  "boot.img kernel_sector != 1 (ABI GRUB cambió)"
272assert struct.unpack_from("<Q", core, 0x1f4)[0] == 2, "core.img blocklist.start != 2 (ABI GRUB cambió)"
273struct.pack_into("<Q", boot, 0x5c,  s1)       # core.img empieza en START1
274struct.pack_into("<Q", core, 0x1f4, s1 + 1)   # el resto de core.img, justo después
275with open(img, "r+b") as f:
276    f.seek(s1 * 512); f.write(core)           # core.img → BIOS boot partition (vda1)
277    f.seek(0);        f.write(boot[:440])      # boot.img → MBR, sin pisar la GPT protective (446+)
278print(f"   boot.img→MBR (kernel_sector={s1}), core.img→sector {s1} ({len(core)} B), blocklist.start={s1+1}")
279PY
280
281# --- ESP: el MISMO GRUB, compilado para UEFI, leyendo el MISMO grub.cfg -------------------------
282# `BOOTX64.EFI` en \EFI\BOOT es la ruta FALLBACK: la firmware la arranca sin necesitar una entrada
283# NVRAM, que es lo que hace que la imagen sirva en un proveedor cualquiera sin tocarle el firmware.
284# La ESP se puebla con mtools (mformat/mcopy): sin root, sin loop, igual que el resto del script.
285if command -v grub-mkimage >/dev/null 2>&1 && [ -d /usr/lib/grub/x86_64-efi ] \
286   && command -v mformat >/dev/null 2>&1; then
287    echo "==> ESP: BOOTX64.EFI (GRUB x86_64-efi, mismo grub.cfg que el BIOS)"
288    grub-mkimage -O x86_64-efi -p '(hd0,gpt3)/boot/grub' -o "$GRUB_STAGE/bootx64.efi" \
289        part_gpt ext2 normal configfile linux echo ls boot efi_gop efi_uga fat
290    ESP_IMG="$STAGE/esp.img"
291    truncate -s "${ESP_SIZE}M" "$ESP_IMG"
292    mformat -i "$ESP_IMG" -F -v HAMMERESP ::
293    mmd -i "$ESP_IMG" ::/EFI ::/EFI/BOOT
294    mcopy -i "$ESP_IMG" "$GRUB_STAGE/bootx64.efi" ::/EFI/BOOT/BOOTX64.EFI
295    dd if="$ESP_IMG" of="$IMG" bs=512 seek=$START2 conv=sparse,notrunc status=none
296    echo "   ESP de ${ESP_SIZE}M poblada — la imagen arranca por BIOS **y** por UEFI"
297else
298    echo "   ⚠ SIN ESP: falta grub-mkimage con /usr/lib/grub/x86_64-efi, o mtools."
299    echo "     La imagen arranca por BIOS pero NO por UEFI — y eso la ata a los proveedores que"
300    echo "     ofrezcan arranque BIOS. No es un detalle: es la mitad del «cualquier servicio»."
301fi
302
303rm -rf "$STAGE"
304echo "==> imagen instalable creada: $(du -h "$IMG" | cut -f1) en disco (sparse), $(( TOTAL_SECT * 512 / 1024 / 1024 ))M virtuales"
305echo "    auto-boot: qemu-system-x86_64 -drive file=$IMG,format=raw -nographic   (sin -kernel)"
306
307if [ "${BOOT:-0}" = 1 ]; then
308    echo "==> arranque de prueba (auto-boot, sin -kernel)"
309    accel=""
310    [ "${KVM:-0}" = 1 ] && [ -w /dev/kvm ] && accel="-enable-kvm -cpu host"
311    # shellcheck disable=SC2086
312    exec qemu-system-x86_64 -m "${MEM:-4096}" -no-reboot -nographic $accel \
313        -drive file="$IMG",format=raw,if=virtio
314fi