From 294959c1da94747e7294b909c3bdccce7911ad93 Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 11 Sep 2026 20:40:34 +0000 Subject: [PATCH] =?UTF-8?q?arranque=20y=20GC:=20las=20dos=20rutas=20EFI,?= =?UTF-8?q?=20guardi=C3=A1n=20de=20ESP=20y=20el=20kernel=20vivo=20como=20r?= =?UTF-8?q?a=C3=ADz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj --- docs/adr/0017-ciclo-de-vida-del-kernel.md | 17 ++++++ docs/adr/0018-soberania-del-arranque.md | 28 ++++++++++ scripts/install-image-efi.sh | 62 ++++++++++++++++++--- scripts/store-gc.sh | 68 +++++++++++++++++++++++ scripts/takana-live-install.sh | 44 +++++++++++++-- 5 files changed, 208 insertions(+), 11 deletions(-) diff --git a/docs/adr/0017-ciclo-de-vida-del-kernel.md b/docs/adr/0017-ciclo-de-vida-del-kernel.md index b1a5c56b..3cc470a2 100644 --- a/docs/adr/0017-ciclo-de-vida-del-kernel.md +++ b/docs/adr/0017-ciclo-de-vida-del-kernel.md @@ -109,6 +109,23 @@ en el CAS: lo que no es alcanzable desde una raíz desaparece sin avisar. ⇒ `scripts/store-gc.sh` y cualquier GC nuevo leen el hash del kernel vivo y lo tratan como raíz. Guardián con **rotura a propósito y control que tiene que pasar**, no sólo con el caso feliz. +> **ENMIENDA 2026-09-11 — el §4 está implementado en `scripts/store-gc.sh` y probado en los tres +> sentidos.** El GC suma ahora una raíz: el artefacto de kernel cuyo `boot/config-*` coincide byte a +> byte con el `.config` del kernel vivo (`/proc/config.gz`, o `/boot/config-$(uname -r)`). +> +> | caso | esperado | resultado | +> |---|---|---| +> | store de juguete con el kernel **vivo** marcado «superado» | se rescata | ✅ movido a vigentes, y lo **dice**: «1 estaban marcados para BORRAR y se rescataron» | +> | **control:** un kernel con `.config` distinto, también marcado | **sigue condenado** | ✅ no protege de más | +> | store real, en el hub (kernel ajeno de Artix) | no protege nada, y lo explica | ✅ «NINGUN artefacto del store lo tiene» | +> +> El control del medio es el que importa: sin él, una función que devolviera «protegido» siempre +> pasaría la primera prueba y volvería inútil al GC sin que nadie lo notara. +> +> Identifica por **contenido del `.config`**, no por hash de artefacto: dos artefactos con el mismo +> `.config` y distinta toolchain se protegen los dos. Es el lado correcto para un GC, y se vuelve +> exacto el día que el §3 ponga el hash en el kernel vivo. + ### 5. Si alguna vez hace falta livepatch, es una **variante**, no un flag La salida correcta **no** es encender `MODULES` en el kernel general —eso revierte una decisión de diff --git a/docs/adr/0018-soberania-del-arranque.md b/docs/adr/0018-soberania-del-arranque.md index 3134557b..b882e31a 100644 --- a/docs/adr/0018-soberania-del-arranque.md +++ b/docs/adr/0018-soberania-del-arranque.md @@ -38,6 +38,7 @@ Conviene además separar los dos modos de fallo, porque se arreglan distinto y s | receta `efibootmgr`, `efivar` en el catálogo | **no existen** | | scripts que escriben `\EFI\BOOT\BOOTX64.EFI` | **5** (`install-image-efi.sh:190`, `install-image.sh`, `iso-image.sh`, `metal-usb-sdboot.sh:64`, `takana-live-install.sh`) | | entradas NVRAM que takana crea | **ninguna, nunca** | +| el instalador live en UEFI, ¿respeta particiones existentes? | **no**: particiona el disco ENTERO en MBR (`takana-live-install.sh:286-292`) ⇒ **hoy no existe el caso «instalar al lado de Windows»**, no es que esté frágil | **El hallazgo que ordena todo el ADR:** takana **jamás crea una entrada de arranque**. Depende íntegramente de `\EFI\BOOT\BOOTX64.EFI`, que **por especificación UEFI es la ruta de medios @@ -80,6 +81,33 @@ Hace falta algo que no dependa de haber arrancado: tonto. Es lo único que funciona cuando todo lo demás falló, y **ninguna distro te lo dice antes de que lo necesites** — te lo dice un foro, a las dos de la mañana, desde otro dispositivo. +> **ENMIENDA 2026-09-11 — implementado el §3 en las dos imágenes, y la medición corrigió el alcance.** +> +> `install-image-efi.sh` y `takana-live-install.sh` escriben ya el kernel en las **dos** rutas, con +> guardián de capacidad previo. Verificado construyendo una imagen real y arrancándola en OVMF: +> +> | prueba | resultado | +> |---|---| +> | las dos copias en la ESP vs el bzImage del store | **sha256 idéntico** (`162cd18e4f8f1f5c`), 15 299 584 B cada una | +> | arranque de la imagen completa | ✅ `HAMMER-EFI-DISK-PIVOT-OK` → `HAMMER-EFI-INIT-OK: arje-zero PID1` | +> | **control negativo:** pisar `\EFI\BOOT\BOOTX64.EFI` con 4 KiB de basura, sin otra vía | ✅ `BdsDxe: No bootable option or device was found` — **cero** apariciones de `HAMMER-EFI` | +> +> El control negativo es el que da valor al resto: confirma que **pisar la ruta fallback mata el +> arranque por completo** cuando es el único camino. Es el escenario de Windows, reproducido. +> +> ⚠ **Y lo que NO se pudo verificar cambia el alcance del §3, así que va acá y no en una nota al +> pie:** no se logró arrancar POR el vendor path. Sin entrada NVRAM el firmware no lo busca —OVMF +> cayó en `No bootable option` con el kernel intacto en `\EFI\takana\takanax64.efi`— y no hay +> forma de crear esa entrada desde este hub (`efibootmgr` existe en el host pero escribe en la NVRAM +> **del host**, no en un fichero `OVMF_VARS`; no hay `virt-firmware`; este OVMF no cae al UEFI Shell, +> así que el truco del `startup.nsh` tampoco corre). +> +> ⇒ **La copia en el vendor path es hoy un seguro que todavía no se puede cobrar solo.** Protege +> contra que *pisen el fichero*, pero sólo si además existe la entrada NVRAM del §1; sin ella el +> rescate es manual, desde el menú del firmware, en los firmwares que dejan navegar a un `.efi` +> arbitrario. **El §3 es condición necesaria y NO suficiente: sin el §1 la mitad del seguro falta.** +> Eso mueve la receta `efibootmgr` de «dependencia que este ADR crea» a **bloqueante del §3**. + ### 4. El instalador mira el terreno y lo dice en voz alta Antes de tocar nada: qué hay en la ESP, cuánto espacio libre queda, cómo está `BootOrder`, si hay un diff --git a/scripts/install-image-efi.sh b/scripts/install-image-efi.sh index 11d74ad6..81ed5a04 100755 --- a/scripts/install-image-efi.sh +++ b/scripts/install-image-efi.sh @@ -20,12 +20,14 @@ # 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\BOOT\BOOTX64.EFI (=bzImage) + \initramfs.cpio.gz +# 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\BOOT\BOOTX64.EFI (EFI-stub) → initramfs pivote +# 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` @@ -61,7 +63,20 @@ 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=)" >&2; exit 1; } -[ -e "$ROOTFS/sbin/init" ] || { echo "ROOTFS sin /sbin/init (arje-zero)" >&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; } @@ -182,14 +197,47 @@ exec /usr/bin/arje-zero RINIT chmod +x "$ROOT_TREE/sbin/init" -# --- 3) ESP FAT32: BOOTX64.EFI (=bzImage EFI-stub) + initramfs.cpio.gz, poblada con mtools de takana --- -echo "==> ESP FAT32 (${ESP_MB}M): \\EFI\\BOOT\\BOOTX64.EFI (=bzImage) + \\initramfs.cpio.gz" +# --- 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 +"$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 -"$MTOOLS/mdir" -i "$ESP_IMG" ::/EFI/BOOT 2>/dev/null | grep -iE 'BOOTX64|initram' || true +# 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 diff --git a/scripts/store-gc.sh b/scripts/store-gc.sh index 9071403a..b8048267 100755 --- a/scripts/store-gc.sh +++ b/scripts/store-gc.sh @@ -87,6 +87,74 @@ for f, xs in (("vigentes", viv), ("superados", sup), ("huerfanos", hue)): print(f" artefactos: {len(dirs)} · vigentes {len(viv)} · superados {len(sup)} · huérfanos {len(hue)}") PY +# ── 2 bis. RAÍZ EXTRA: el kernel EN EJECUCIÓN (ADR 0017 §4) ───────────────────────────────────── +# El conjunto vivo de arriba sale de `takana hash` sobre las recetas: es «el hash VIGENTE de hoy». +# El kernel que la máquina está CORRIENDO no tiene por qué ser ese. Si la receta se movió después +# del último sellado, el artefacto del kernel vivo queda clasificado SUPERADO y el default lo borra. +# +# Y se pierde en silencio: el sistema sigue andando perfecto —el kernel ya está en RAM— hasta el día +# que hace falta volver atrás, y ese día el artefacto al que volver no existe. Es el mismo patrón +# que ya costó caro en el CAS: lo que no es alcanzable desde una raíz desaparece sin avisar. +# +# Cómo se identifica hoy, que es lo que se puede: por el `.config`. El kernel vivo expone el suyo en +# /proc/config.gz (o /boot/config-$(uname -r)), y cada artefacto de kernel guarda el suyo en +# boot/config-*. Si coinciden BYTE A BYTE, ese artefacto es el kernel que corre — o uno idéntico, y +# proteger de más es el lado correcto para un GC. +# ⚠ Es una identificación por CONTENIDO DEL CONFIG, no por hash del artefacto: dos artefactos con el +# mismo .config y distinta toolchain se protegen los dos. Cuando el hash del artefacto viaje en el +# kernel vivo (ADR 0017 §3) esto se vuelve exacto y esta función se simplifica. +python3 - "$TMP" "$STORE" <<'PYK' +import sys, os, gzip, hashlib +tmp, store = sys.argv[1], sys.argv[2] + +def config_vivo(): + try: + with gzip.open("/proc/config.gz", "rb") as f: return f.read() + except Exception: pass + try: + with open("/boot/config-" + os.uname().release, "rb") as f: return f.read() + except Exception: return None + +cfg = config_vivo() +if cfg is None: + print(" kernel vivo: sin .config legible (ni /proc/config.gz ni /boot/config-)") + print(" ⇒ NO se protege ningún kernel por esta vía. Si vas a borrar en una máquina") + print(" que arranca desde este store, comprobalo a mano antes de --aplicar.") + raise SystemExit(0) + +vivo = hashlib.sha256(cfg).hexdigest() +protegidos = [] +for d in sorted(os.listdir(store)): + b = os.path.join(store, d, "boot") + if not os.path.isdir(b): continue + for f in os.listdir(b): + if not f.startswith("config-"): continue + try: + with open(os.path.join(b, f), "rb") as fh: h = hashlib.sha256(fh.read()).hexdigest() + except OSError: continue + if h == vivo: protegidos.append(d); break + +if not protegidos: + print(f" kernel vivo: .config sha256 {vivo[:12]} — NINGUN artefacto del store lo tiene") + print(" (normal en el hub: acá corre un kernel ajeno, no uno de takana)") + raise SystemExit(0) + +# Sacar los protegidos de superados/huérfanos y contarlos como vigentes. +movidos = 0 +for nombre in ("superados", "huerfanos"): + ruta = f"{tmp}/{nombre}.txt" + xs = [l.strip() for l in open(ruta) if l.strip()] + quedan = [x for x in xs if x not in protegidos] + movidos += len(xs) - len(quedan) + open(ruta, "w").write("".join(x + "\n" for x in quedan)) +with open(f"{tmp}/vigentes.txt", "a") as f: + for d in protegidos: f.write(d + "\n") + +print(f" kernel vivo: .config sha256 {vivo[:12]} ⇒ {len(protegidos)} artefacto(s) RAIZ: {', '.join(protegidos)}") +if movidos: + print(f" !! {movidos} de ellos estaban marcados para BORRAR y se rescataron (ADR 0017 §4)") +PYK + espacio() { # du sobre una lista de dirs, tolerante a lista vacía [ -s "$1" ] || { echo "0"; return; } sed "s|^|$STORE/|" "$1" | tr '\n' '\0' | du -sch --files0-from=- 2>/dev/null | tail -1 | cut -f1 diff --git a/scripts/takana-live-install.sh b/scripts/takana-live-install.sh index bc545d2d..62a24676 100755 --- a/scripts/takana-live-install.sh +++ b/scripts/takana-live-install.sh @@ -366,15 +366,51 @@ INIT "$BB" chmod +x "$IRD_TREE/init" ( cd "$IRD_TREE" && "$BB" find . | "$BB" cpio -o -H newc 2>/dev/null | "$BB" gzip -1 ) > /run/hammer-efi-ird.cpio.gz - # Poblar la ESP (FAT32, montable por el live): BOOTX64.EFI = bzImage metal + el initramfs de pivote. - echo "==> poblando la ESP (\\EFI\\BOOT\\BOOTX64.EFI = kernel metal + \\initramfs.cpio.gz)" + # Poblar la ESP con el kernel en DOS rutas (ADR 0018 §3), porque una sola es un punto único de + # fallo que NO controlamos: + # \EFI\takana\takanax64.efi el vendor path — nuestro, no lo reclama nadie más. + # \EFI\BOOT\BOOTX64.EFI la ruta FALLBACK de medios removibles (spec UEFI): tierra de nadie + # y lo único que arranca SIN entrada NVRAM, que es lo que hay hoy + # (takana todavía no crea entradas: hace falta efibootmgr/efivar). + # Es el mismo fichero dos veces: cuesta bytes de ESP, no complejidad. + echo "==> poblando la ESP (\\EFI\\takana\\takanax64.efi + \\EFI\\BOOT\\BOOTX64.EFI = kernel metal + \\initramfs.cpio.gz)" ESPMNT=/run/hammer-esp "$BB" mkdir -p "$ESPMNT" "$BB" mount -t vfat "$P1" "$ESPMNT" - "$BB" mkdir -p "$ESPMNT/EFI/BOOT" + "$BB" mkdir -p "$ESPMNT/EFI/BOOT" "$ESPMNT/EFI/takana" + + # Guardián de capacidad (ADR 0018 §4): comprobar ANTES, con los números a la vista. Un `cp` que + # se queda sin sitio en FAT32 deja un fichero TRUNCADO y devuelve error tarde — y un BOOTX64.EFI + # a medias no falla: arranca y muere en el firmware, que es la peor forma de enterarse. + _k_kb=$( "$BB" du -k "$PAYLOAD/kernel/bzImage-efi" | "$BB" awk '{print $1}' ) + _i_kb=$( "$BB" du -k /run/hammer-efi-ird.cpio.gz | "$BB" awk '{print $1}' ) + _free_kb=$( "$BB" df -k "$ESPMNT" | "$BB" awk 'NR==2{print $4}' ) + _need_kb=$(( _k_kb * 2 + _i_kb + 256 )) + echo " kernel $(( _k_kb / 1024 ))M x2 + initramfs $(( _i_kb / 1024 ))M = $(( _need_kb / 1024 ))M sobre $(( _free_kb / 1024 ))M libres en la ESP" + if [ "$_need_kb" -gt "$_free_kb" ]; then + "$BB" umount "$ESPMNT" + echo "TAKANA-INSTALL-FAIL: no entra en la ESP — hacen falta $(( _need_kb / 1024 ))M y hay $(( _free_kb / 1024 ))M libres." >&2 + echo " subí ESP_MB (actual ${ESP_MB}M) o liberá la ESP." >&2 + exit 1 + fi + + cp "$PAYLOAD/kernel/bzImage-efi" "$ESPMNT/EFI/takana/takanax64.efi" cp "$PAYLOAD/kernel/bzImage-efi" "$ESPMNT/EFI/BOOT/BOOTX64.EFI" cp /run/hammer-efi-ird.cpio.gz "$ESPMNT/initramfs.cpio.gz" - "$BB" sync; "$BB" umount "$ESPMNT" + "$BB" sync + # Verificar las DOS rutas y el initramfs por TAMAÑO, no por presencia: un `cp` truncado deja el + # fichero ahí y `test -e` diría que todo salió bien (regla 3 de CLAUDE.md). + for _pair in "EFI/takana/takanax64.efi:$_k_kb" "EFI/BOOT/BOOTX64.EFI:$_k_kb" "initramfs.cpio.gz:$_i_kb"; do + _f="${_pair%:*}"; _want="${_pair##*:}" + _got=$( "$BB" du -k "$ESPMNT/$_f" 2>/dev/null | "$BB" awk '{print $1}' ) + [ "${_got:-0}" = "$_want" ] || { + "$BB" umount "$ESPMNT" + echo "TAKANA-INSTALL-FAIL: $_f quedó en ${_got:-0}K, se esperaban ${_want}K (ESP llena o cp truncado)" >&2 + exit 1 + } + done + echo " ESP verificada: las dos rutas del kernel y el initramfs con su tamaño completo" + "$BB" umount "$ESPMNT" # Poblar la root (copia del live) — sin wrapper que monte particiones (el pivote ya montó store/estado). echo "==> copiando la raíz del live → $P2 (excluye virtuales/efímeros, /store, /var/lib/hammer, payload)"