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…
el saltito sin USB: kexec primero, espacio libre si la máquina no puede, y nunca desde adentro
1#!/bin/sh
2# takana-install-partition.sh — instalar takana EN UNA PARTICIÓN, sin tocar la tabla ni las vecinas.
3#
4# ── QUÉ LO SEPARA DE LOS OTROS DOS INSTALADORES ────────────────────────────────────────────────
5# `install-image-efi.sh` fabrica una IMAGEN de disco entera, y `takana-live-install.sh` reparticiona
6# el DISCO entero (`o` en fdisk = tabla nueva). Los dos presuponen que el disco es nuestro. Éste
7# presupone lo contrario: **el disco es de otro y takana llega de visita**.
8#
9# - NO escribe la tabla de particiones. Ni una vez. No invoca fdisk/sfdisk/parted.
10# (La única excepción es `--use-free-space`, que AÑADE una entrada en un hueco sin asignar y no
11# modifica ninguna de las existentes. Se dice aparte porque es la única vez que la tabla cambia.)
12# - Formatea EXACTAMENTE una partición: la que se le nombra.
13# - Reusa la ESP que ya existe, y escribe sólo dentro de `\EFI\takana\` — la ruta de VENDOR, que
14# es nuestra. `\EFI\BOOT\BOOTX64.EFI` es tierra de nadie (Windows la repuebla) y **no se pisa**
15# si ya existe, salvo `--claim-fallback` explícito.
16# - Registra la entrada NVRAM propia (`takana boot entry add`, ADR 0018) tras hacer un `backup`
17# del estado de arranque. Convivir, no desplazar.
18#
19# ⚠ **Lo que este instalador NO hace, a propósito: redimensionar.** Encoger pide desmontar el
20# filesystem (ext4 sólo crece online), y desmontar la partición donde estás corriendo no se puede.
21# Eso es física, no una carencia del script: el redimensionado vive en el instalador que corre desde
22# OTRO medio. Acá la partición destino tiene que existir y estar libre.
23#
24# Uso:
25# sudo scripts/takana-install-partition.sh --root-part /dev/nvme0n1p5 --esp /dev/nvme0n1p1
26# # prueba sin hardware (dos ficheros pre-dimensionados, sin root):
27# scripts/takana-install-partition.sh --root-part work/p-root.img --esp work/p-esp.img --no-nvram
28#
29# Opciones:
30# --root-part <dev|fichero> DESTINO. Se formatea ext4 con LABEL=hammer-root. Lo ÚNICO destructivo.
31# --use-free-space no formatear nada existente: crear la partición en el hueco sin
32# asignar más grande de `--disk` e instalar ahí. Al lado, no encima.
33# --on-active-root <modo> qué hacer si `--root-part` ES la raíz viva (auto|kexec|free-space|abort).
34# `auto` (def) = intentar kexec y, si esta máquina no puede, espacio libre.
35# --esp <dev|fichero> ESP existente a poblar (FAT). Sin ella no hay arranque.
36# --disk <dev> --esp-part N para la entrada NVRAM; si no se dan, se deducen del `--esp`.
37# --claim-fallback además, escribir \EFI\BOOT\BOOTX64.EFI (pisa lo que haya ahí).
38# --no-nvram no tocar la NVRAM (pruebas, o firmware que se registra a mano).
39# --yes no preguntar (para guiones; en interactivo pide confirmación).
40# Variables: ROOTFS KERNEL BUSYBOX (como install-image-efi.sh) · STAGE (def work/.install-part-stage)
41set -eu
42ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
43
44DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
45USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"
46while [ $# -gt 0 ]; do
47 case "$1" in
48 --root-part) DEST="${2:?}"; shift 2 ;;
49 --esp) ESP="${2:?}"; shift 2 ;;
50 --disk) DISK="${2:?}"; shift 2 ;;
51 --esp-part) ESP_PART="${2:?}"; shift 2 ;;
52 --claim-fallback) CLAIM_FALLBACK=1; shift ;;
53 --no-nvram) NO_NVRAM=1; shift ;;
54 --use-free-space) USE_FREE=1; shift ;;
55 --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
56 --yes|-y) ASSUME_YES=1; shift ;;
57 -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
58 *) echo "opción desconocida: $1 (--help)" >&2; exit 2 ;;
59 esac
60done
61[ -n "$DEST" ] || [ -n "$USE_FREE" ] || { echo "falta --root-part (o --use-free-space) — ver --help" >&2; exit 2; }
62case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root: auto|kexec|free-space|abort" >&2; exit 2 ;; esac
63
64# ── Las herramientas, ANTES que nada ───────────────────────────────────────────────────────────
65# Se resuelven acá arriba y no junto a su uso porque `--use-free-space` necesita `sfdisk` durante
66# las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
67# inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
68. "$(dirname "$0")/lib/herramientas-desde-store.sh"
69MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
70SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
71# Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
72# device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
73# si alguna partición del disco está montada — por eso se intentan los dos, en ese orden. Los dos
74# salen del `util-linux` sellado: en el anfitrión musl no existe ninguno (ni `partprobe`).
75PARTX=$(herramienta util-linux usr/sbin/partx partx 2>/dev/null || echo "")
76BLOCKDEV=$(herramienta util-linux sbin/blockdev blockdev 2>/dev/null || echo "")
77MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1
78MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1
79MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1
80KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
81[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
82HAMMER="${HAMMER:-$ROOT/target/release/takana}"
83
84# ── LAS GUARDAS VAN PRIMERO, Y SON EL PUNTO DE ESTE SCRIPT ─────────────────────────────────────
85# Un instalador que borra de más falla del lado silencioso: el usuario se entera cuando reinicia y
86# le falta su /home. Cada guarda de abajo existe porque su ausencia es irreversible.
87es_bloque() { [ -b "$1" ]; }
88nombre_kernel() { basename "$(readlink -f "$1")"; }
89disco_de() { echo "/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$1")/..")")"; }
90
91# ── releer_tabla <disco> — que el KERNEL se entere de la entrada nueva ────────────────────────
92# Medido el 2026-09-23 en el banco: `sfdisk --append` escribió la entrada perfectamente y
93# `/dev/loop0p4` NO APARECIÓ. El script lo detectó y abortó —bien— pero el motivo estaba fuera de
94# él: en el anfitrión musl no existen `partprobe`, `partx` ni `blockdev`, así que los tres intentos
95# de releer la tabla eran no-ops silenciosos. La tabla en disco y la que el kernel tiene en memoria
96# son dos cosas distintas, y nada avisa cuando divergen.
97# ⚠ Y el comando es `partx -u`, NO `partx -a`: medido en el banco el mismo día. `-a` («añadir»)
98# intenta registrar TODAS las particiones y falla entero —«error adding partitions 1-4»— cuando las
99# viejas ya están, que es siempre en este flujo. `-u` («actualizar») añade lo que falte y deja en paz
100# lo que ya hay. El nombre de la opción sugiere justo lo contrario de lo que sirve acá.
101releer_tabla() {
102 [ -n "${PARTX:-}" ] && "$PARTX" -u "$1" >/dev/null 2>&1 && return 0
103 [ -n "${PARTX:-}" ] && "$PARTX" -a "$1" >/dev/null 2>&1 && return 0
104 [ -n "${BLOCKDEV:-}" ] && "$BLOCKDEV" --rereadpt "$1" >/dev/null 2>&1 && return 0
105 partprobe "$1" 2>/dev/null || true
106}
107
108# ── mib_necesarios — cuánto pedirle al hueco ──────────────────────────────────────────────────
109# El rootfs medido + 40 %, con piso de 2 GiB. El margen no es superstición: `mke2fs` se queda con
110# ~1,5 % en metadatos y el sistema instalado tiene que poder escribir algo antes de que alguien lo
111# agrande. Un destino que entra JUSTO produce un sistema que arranca y se llena a la semana.
112mib_necesarios() {
113 _mn_r="${ROOTFS:-}"
114 [ -z "$_mn_r" ] && _mn_r=$(ls -dt store/*-product-rootfs /store/*-product-rootfs 2>/dev/null | head -1)
115 _mn_m=$(du -sm "$_mn_r" 2>/dev/null | cut -f1)
116 [ -z "${_mn_m:-}" ] && _mn_m=1024
117 _mn_m=$(( _mn_m * 14 / 10 ))
118 [ "$_mn_m" -lt 2048 ] && _mn_m=2048
119 echo "$_mn_m"
120}
121
122# ── crear_en_hueco <disco> <mib> — la ÚNICA vez que este script escribe la tabla ───────────────
123# `sfdisk --append` añade una entrada y **no reescribe las existentes**: es la diferencia entre
124# «instalar al lado» y el `o` de `takana-live-install.sh`, que empieza por tirar la tabla entera.
125# Aun así es la operación más delicada de acá, así que: se elige el hueco MÁS GRANDE, se dice cuál
126# es con sus sectores a la vista, y se pide confirmación aparte de la del formateo.
127crear_en_hueco() {
128 _ch_disco="$1"; _ch_mib="$2"
129 _ch_libre=$("$SFDISK" -F "$_ch_disco" 2>/dev/null | awk 'NF==4 && $1 ~ /^[0-9]+$/ {if ($3+0 > m) {m=$3+0; s=$1; e=$2}} END {if (m>0) print s, e, m}')
130 [ -n "$_ch_libre" ] || { echo "takana-install-partition: $_ch_disco no tiene espacio SIN ASIGNAR" >&2; return 1; }
131 _ch_start=$(echo "$_ch_libre" | cut -d' ' -f1)
132 _ch_sect=$(echo "$_ch_libre" | cut -d' ' -f3)
133 _ch_libre_mib=$(( _ch_sect / 2048 ))
134 echo " hueco más grande: ${_ch_libre_mib} MiB desde el sector ${_ch_start}" >&2
135 [ "$_ch_libre_mib" -ge "$_ch_mib" ] || {
136 echo "takana-install-partition: el hueco da ${_ch_libre_mib} MiB y hacen falta ${_ch_mib} MiB." >&2
137 echo " Nada que hacer sin encoger algo, y encoger pide otro medio." >&2; return 1; }
138 _ch_size=$(( _ch_mib * 2048 ))
139 if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
140 printf " voy a AÑADIR una partición de %s MiB a la tabla de %s (las existentes no se tocan) [escribí si] " "$_ch_mib" "$_ch_disco" >&2
141 read -r _ch_r; [ "$_ch_r" = "si" ] || { echo " cancelado." >&2; return 1; }
142 fi
143 # La lista de ANTES, para saber después cuál es la nueva por DIFERENCIA y no por su nombre.
144 # ⚠ El primer intento buscaba la línea por `start=<sector>` y nunca la encontraba: `sfdisk -d`
145 # **alinea los campos con espacios** (`start= 3614720`), así que el patrón sin espacios no
146 # casa jamás. El síntoma era idéntico al de un device que no aparece — y mandó a buscar el fallo
147 # en `partx` y en udev, que estaban bien. Comparar conjuntos no depende del formato.
148 _ch_lista_antes=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
149 _ch_antes=$(printf '%s\n' "$_ch_lista_antes" | grep -c . || true)
150 echo "start=$_ch_start, size=$_ch_size, type=linux, name=\"takana-root\"" \
151 | "$SFDISK" --append --quiet "$_ch_disco" >/dev/null 2>&1 \
152 || { echo "takana-install-partition: sfdisk --append falló sobre $_ch_disco" >&2; return 1; }
153 # El device nuevo tarda en aparecer (udev, o `partx` en un loop): esperarlo es obligatorio, y el
154 # nombre se LEE de la tabla en vez de construirlo a mano — `nvme0n1p4` y `sda4` no se escriben igual.
155 releer_tabla "$_ch_disco"
156 _ch_nuevo=""
157 _ch_i=0
158 while [ "$_ch_i" -lt 15 ]; do
159 _ch_lista_post=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
160 for _ch_d in $_ch_lista_post; do
161 case " $(printf '%s ' $_ch_lista_antes)" in
162 *" $_ch_d "*) ;;
163 *) _ch_nuevo="$_ch_d" ;;
164 esac
165 done
166 [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] && break
167 _ch_nuevo=""
168 _ch_i=$(( _ch_i + 1 )); sleep 1
169 releer_tabla "$_ch_disco"
170 done
171 [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] || {
172 echo "takana-install-partition: la partición se añadió pero su device no apareció (¿hace falta un partprobe?)" >&2; return 1; }
173 _ch_despues=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}' | grep -c . || true)
174 [ "$_ch_despues" = "$(( _ch_antes + 1 ))" ] || {
175 echo "takana-install-partition: la tabla pasó de $_ch_antes a $_ch_despues entradas — esperaba UNA más" >&2; return 1; }
176 echo "$_ch_nuevo"
177}
178
179# `--use-free-space` sin `--root-part`: el destino todavía no existe, se crea en un hueco. Esto es
180# «instalar al lado», y es el camino que la política de la raíz viva usa cuando no hay kexec.
181if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
182 _ufs_disco="${DISK:-}"
183 if [ -z "$_ufs_disco" ] && [ -n "$ESP" ] && [ -b "$ESP" ]; then _ufs_disco=$(disco_de "$ESP"); fi
184 [ -n "$_ufs_disco" ] || { echo "takana-install-partition: --use-free-space necesita --disk (o un --esp real)" >&2; exit 2; }
185 echo "==> buscando espacio SIN ASIGNAR en $_ufs_disco"
186 DEST=$(crear_en_hueco "$_ufs_disco" "$(mib_necesarios)") || exit 1
187 echo " partición creada: $DEST"
188fi
189
190if es_bloque "$DEST"; then
191 _n=$(nombre_kernel "$DEST")
192 # 1) DESTINO TIENE QUE SER UNA PARTICIÓN, NO UN DISCO. `/sys/class/block/<x>/partition` sólo
193 # existe en particiones: es la diferencia entre formatear 200 G y formatear 1,8 T.
194 [ -r "/sys/class/block/$_n/partition" ] || {
195 echo "takana-install-partition: $DEST NO es una partición (¿es el disco entero?)." >&2
196 echo " Este instalador nunca escribe un disco completo." >&2; exit 1; }
197 # 2) NO PUEDE ESTAR MONTADA. mke2fs sobre un fs montado corrompe el que está en uso.
198 if grep -q "^$(readlink -f "$DEST") " /proc/mounts 2>/dev/null; then
199 echo "takana-install-partition: $DEST está MONTADA. Desmontala primero (o elegí otra)." >&2
200 grep "^$(readlink -f "$DEST") " /proc/mounts >&2; exit 1
201 fi
202 # 3) NO PUEDE SER LA RAÍZ VIVA. Es el caso que el usuario pregunta siempre: «¿puedo instalar
203 # encima del Linux que estoy usando?». No desde él: hay que saltar antes a otro kernel/medio.
204 _rootdev=$(awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null || true)
205 if [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
206 politica_raiz_viva # kexec → espacio libre → abortar, según --on-active-root
207 fi
208 # 4) NO PUEDE SER LA ESP que vamos a reusar.
209 [ -n "$ESP" ] && [ "$(readlink -f "$DEST")" = "$(readlink -f "$ESP")" ] && {
210 echo "takana-install-partition: --root-part y --esp son la MISMA partición" >&2; exit 1; }
211else
212 [ -f "$DEST" ] || { echo "takana-install-partition: $DEST no existe (ni bloque ni fichero)" >&2; exit 1; }
213fi
214
215# ── politica_raiz_viva — el «saltito»: qué hacer si el destino ES el sistema que corre ────────
216# La pregunta que todo el mundo hace es «¿puedo poner takana encima del Linux que estoy usando, sin
217# USB?». La respuesta honesta tiene tres partes y este bloque las implementa en orden:
218#
219# 1. Desde adentro, NO. Un filesystem montado y en uso no se formatea; eso es el kernel, no una
220# política nuestra.
221# 2. Pero se puede SALIR de adentro sin reiniciar por el firmware: `kexec` salta a otro kernel con
222# un instalador en RAM, y en ese salto el sistema viejo deja de correr ⇒ su disco queda libre.
223# Es el ADR 0017 §1, y el syscall ya estaba implementado (`kexec_file_load`, sin kexec-tools).
224# 3. Y si esta máquina no puede saltar —kernel sin `CONFIG_KEXEC_FILE`, o lockdown por Secure
225# Boot— no se fuerza nada: se instala AL LADO, en espacio sin asignar, y el usuario elige en el
226# arranque. Peor camino, cero riesgo.
227#
228# `--on-active-root` permite fijar uno de los tres a mano; `auto` es la política de arriba.
229politica_raiz_viva() {
230 echo "⚠ $DEST es la RAÍZ VIVA: el sistema que está corriendo ahora mismo."
231 echo " Un filesystem montado y en uso no se puede formatear desde adentro."
232 case "$ON_ACTIVE" in
233 abort)
234 echo " --on-active-root=abort ⇒ no hago nada más." >&2; exit 1 ;;
235 esac
236 _prv_kexec=no
237 if [ "$ON_ACTIVE" = auto ] || [ "$ON_ACTIVE" = kexec ]; then
238 echo "==> ¿puede esta máquina saltar a otro kernel sin pasar por el firmware?"
239 if _prv_out=$("$HAMMER" kernel kexec --probe 2>&1); then _prv_kexec=si; fi
240 printf '%s\n' "$_prv_out" | sed 's/^/ /'
241 fi
242 if [ "$_prv_kexec" = si ]; then
243 camino_kexec # no vuelve: salta
244 exit 0
245 fi
246 if [ "$ON_ACTIVE" = kexec ]; then
247 echo " --on-active-root=kexec y esta máquina no puede saltar (motivos arriba)." >&2; exit 1
248 fi
249 echo "==> sin kexec ⇒ INSTALO AL LADO, en espacio sin asignar (la raíz viva no se toca)"
250 _prv_disco=$(disco_de "$DEST")
251 DEST=$(crear_en_hueco "$_prv_disco" "$(mib_necesarios)") || {
252 echo "takana-install-partition: no hay hueco donde instalar al lado, y sin kexec no hay otra" >&2
253 echo " desde este sistema. Queda el camino del medio externo (USB)." >&2
254 exit 1; }
255 echo " destino nuevo: $DEST (la raíz viva sigue intacta)"
256}
257
258# ── camino_kexec — armar el instalador en RAM y saltar. NO VUELVE ─────────────────────────────
259camino_kexec() {
260 echo "==> camino KEXEC: el instalador viaja en RAM y el sistema viejo muere en el salto"
261 _ck_args="--root-part $DEST"
262 [ -n "$ESP" ] && _ck_args="$_ck_args --esp $ESP"
263 [ -n "$CLAIM_FALLBACK" ] && _ck_args="$_ck_args --claim-fallback"
264 # ⚠ `--on-active-root abort` ADENTRO, y no es decorativo: si por lo que sea el destino siguiera
265 # siendo la raíz viva del sistema nuevo, `auto` volvería a intentar un kexec y tendríamos una
266 # máquina saltando a sí misma en bucle. Abortar ahí es un fallo legible.
267 _ck_args="$_ck_args --on-active-root abort"
268 _ck_ird="${IRD_INSTALADOR:-work/takana-instalador.cpio.gz}"
269 # shellcheck disable=SC2086
270 OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh $_ck_args || {
271 echo "takana-install-partition: no pude armar el instalador en RAM" >&2; exit 1; }
272 _ck_art=$(dirname "$(dirname "$KERNEL")") # …/<hash>-linux-metal/boot/bzImage → el artefacto
273 echo
274 echo " kernel: $_ck_art"
275 echo " initrd: $_ck_ird ($(du -h "$_ck_ird" | cut -f1))"
276 echo " destino: $DEST"
277 if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
278 printf " ⚡ ESTE COMANDO MATA EL SISTEMA QUE ESTÁS USANDO y arranca el instalador. [escribí saltar] "
279 read -r _ck_r; [ "$_ck_r" = "saltar" ] || { echo " cancelado — no se saltó y no se tocó nada."; exit 1; }
280 fi
281 "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" \
282 --cmdline "console=tty0 console=ttyS0,115200" --jump
283 echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
284 exit 1
285}
286
287# ── El terreno, DICHO EN VOZ ALTA antes de tocar nada (ADR 0018 §4) ───────────────────────────
288echo "==> destino: $DEST ($( [ -b "$DEST" ] && echo 'partición real' || echo 'fichero de prueba'))"
289if [ -b "$DEST" ] && command -v blkid >/dev/null 2>&1; then
290 echo " lo que hay HOY ahí (se va a PERDER):"
291 blkid "$DEST" 2>/dev/null | sed 's/^/ /' || echo " (sin filesystem reconocible)"
292fi
293if [ -n "$ESP" ] && [ -b "$ESP" ] && [ -x "$HAMMER" ]; then
294 _d="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$ESP")/..")")}"
295 echo " terreno de $_d (nada de esto se toca):"
296 "$HAMMER" boot entry survey --disk "$_d" 2>&1 | sed 's/^/ /' || true
297fi
298if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
299 printf " ¿formateo %s y dejo el resto del disco INTACTO? [escribí si] " "$DEST"
300 read -r _r; [ "$_r" = "si" ] || { echo " cancelado."; exit 1; }
301fi
302
303# ── 1) Los árboles (initramfs de pivote + rootfs con su /sbin/init) los arma el script VALIDADO ──
304# No se copian acá: se PIDEN. Ver el comentario de `STAGE_ONLY` en install-image-efi.sh.
305STAGE="${STAGE:-work/.install-part-stage}"
306# `STAGE_LISTO=1` — el árbol ya está armado y no hay que (ni se puede) rearmarlo. Es lo que usa el
307# instalador cuando corre DENTRO de un initramfs: allí no hay repo, ni store, ni `install-image-efi.sh`;
308# el stage viajó horneado en el propio initramfs. Se comprueba que esté completo, no se confía.
309if [ -n "${STAGE_LISTO:-}" ]; then
310 echo "==> stage ya armado: $STAGE (STAGE_LISTO=1)"
311else
312 echo "==> armando árboles (STAGE_ONLY sobre install-image-efi.sh)"
313 STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
314fi
315ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
316[ -d "$ROOT_TREE" ] && [ -r "$IRD" ] || { echo "takana-install-partition: el stage no quedó completo" >&2; exit 1; }
317
318# ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
319# `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
320# montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
321# este script existe para no tener. Si no somos root, `unshare -r` da los dueños root sin sudo.
322echo "==> ext4 LABEL=hammer-root en $DEST (única escritura destructiva de todo el script)"
323if [ "$(id -u)" = "0" ]; then
324 "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
325else
326 unshare -r "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
327fi
328echo " ✓ $DEST formateada y poblada"
329
330# ── 3) La ESP: sólo nuestra carpeta de vendor, con mtools y SIN montar ─────────────────────────
331if [ -n "$ESP" ]; then
332 "$MDIR" -i "$ESP" :: >/dev/null 2>&1 || { echo "takana-install-partition: $ESP no parece FAT (¿es la ESP?)" >&2; exit 1; }
333 # Guardián de capacidad: una ESP de fábrica son 100 MiB y Windows ya vive ahí. El margen se
334 # comprueba ANTES, con los números a la vista, no como un «disk full» a mitad de copiar.
335 # ⚠ `mdir` imprime el espacio libre con SEPARADOR DE MILES en espacios («60 056 064 bytes
336 # free»), así que tomar `$1` devuelve **60** y el guardián aborta una instalación perfectamente
337 # posible. Medido en la primera corrida de este script. Hay que juntar los dígitos de la línea.
338 _free=$("$MDIR" -i "$ESP" :: 2>/dev/null | awk '/bytes free/ {l=$0; sub(/ bytes free.*/,"",l); gsub(/[^0-9]/,"",l); print l}' | tail -1)
339 _need=$(( $(wc -c < "$KERNEL") + $(wc -c < "$IRD") ))
340 [ -n "${_free:-}" ] && [ "$_free" -lt "$_need" ] && {
341 echo "takana-install-partition: la ESP tiene ${_free}B libres y hacen falta ${_need}B." >&2
342 echo " No se escribió nada en ella." >&2; exit 1; }
343 # ⚠ **Un FAT puede declarar MÁS espacio del que su partición tiene, y nadie se queja hasta que
344 # una escritura grande cae más allá del final.** Medido acá el 2026-09-23 fabricando el disco de
345 # prueba: `mformat -i disco.img@@<offset>` formatea usando el tamaño del FICHERO ENTERO, no el de
346 # la partición ⇒ una ESP de 64 MiB con un FAT que anuncia 2,5 G. Y no es un artefacto del banco de
347 # pruebas: cualquier ESP que alguien encogió con una herramienta de particionado sin reformatear
348 # queda igual. El `df` del kernel repite la mentira —la leyó del mismo sector de arranque— así que
349 # NO alcanza con preguntarle al filesystem: hay que contrastar contra el TAMAÑO REAL del
350 # contenedor, que es lo único que no puede mentir.
351 if [ -b "$ESP" ]; then
352 _cap=$(blockdev --getsize64 "$ESP" 2>/dev/null || echo "")
353 [ -z "$_cap" ] && _cap=$(( $(cat "/sys/class/block/$(nombre_kernel "$ESP")/size" 2>/dev/null || echo 0) * 512 ))
354 else
355 _cap=$(wc -c < "$ESP")
356 fi
357 if [ -n "${_free:-}" ] && [ "${_cap:-0}" -gt 0 ] && [ "$_free" -gt "$_cap" ]; then
358 echo " ! el FAT de $ESP dice ${_free}B libres pero la partición mide ${_cap}B: el filesystem" >&2
359 echo " declara más espacio del que existe (ESP encogida sin reformatear, o formateada" >&2
360 echo " sobre un contenedor mayor). Uso el tamaño REAL como cota." >&2
361 _free="$_cap"
362 fi
363 echo "==> ESP $ESP: \\EFI\\takana\\ (${_need}B sobre ${_free:-?}B libres)"
364 "$MMD" -i "$ESP" ::/EFI 2>/dev/null || true
365 "$MMD" -i "$ESP" ::/EFI/takana 2>/dev/null || true
366 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/takana/takanax64.efi
367 "$MCOPY" -o -i "$ESP" "$IRD" ::/initramfs.cpio.gz
368 # La ruta fallback es tierra de nadie: si YA hay algo, es de otro sistema y no se pisa sin que
369 # el usuario lo pida. Si no hay nada, ocuparla es gratis y salva el caso «el firmware ignora la
370 # entrada NVRAM», que es el modo de fallo más común y el más difícil de diagnosticar.
371 if "$MDIR" -i "$ESP" ::/EFI/BOOT/BOOTX64.EFI >/dev/null 2>&1; then
372 if [ -n "$CLAIM_FALLBACK" ]; then
373 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
374 echo " ! \\EFI\\BOOT\\BOOTX64.EFI PISADO (--claim-fallback)"
375 else
376 echo " · \\EFI\\BOOT\\BOOTX64.EFI ya existe y es de otro: NO se toca (--claim-fallback para pisarlo)"
377 fi
378 else
379 "$MMD" -i "$ESP" ::/EFI/BOOT 2>/dev/null || true
380 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
381 echo " + \\EFI\\BOOT\\BOOTX64.EFI estaba libre ⇒ ocupada (arranca sin depender de la NVRAM)"
382 fi
383 for _p in ::/EFI/takana/takanax64.efi ::/initramfs.cpio.gz; do
384 "$MDIR" -i "$ESP" "$_p" >/dev/null 2>&1 || { echo "takana-install-partition: falta $_p en la ESP" >&2; exit 1; }
385 done
386 echo " ✓ ESP poblada y verificada (sin montarla)"
387else
388 echo " ! sin --esp: el sistema queda instalado pero NO arrancable todavía"
389fi
390
391# ── 4) La entrada de arranque: convivir con el vecino, con respaldo previo ─────────────────────
392if [ -z "$NO_NVRAM" ] && [ -n "$ESP" ] && [ -b "$ESP" ] && [ -d /sys/firmware/efi/efivars ] && [ -x "$HAMMER" ]; then
393 if [ -z "$DISK" ] || [ -z "$ESP_PART" ]; then
394 _en=$(nombre_kernel "$ESP")
395 DISK="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$_en/..")")}"
396 ESP_PART="${ESP_PART:-$(cat "/sys/class/block/$_en/partition" 2>/dev/null || echo 1)}"
397 fi
398 echo "==> respaldo del arranque ANTES de tocar la NVRAM (ADR 0018 §5)"
399 "$HAMMER" boot entry backup || echo " ! el respaldo falló — seguí sólo si sabés por qué" >&2
400 echo "==> entrada NVRAM: $DISK p$ESP_PART → \\EFI\\takana\\takanax64.efi"
401 "$HAMMER" boot entry add --disk "$DISK" --partition "$ESP_PART" --loader '/EFI/takana/takanax64.efi'
402else
403 echo " · NVRAM no tocada (--no-nvram, sin efivarfs, o destino de prueba)"
404fi
405
406if [ -n "$USE_FREE" ]; then
407 echo "✓ takana instalada en $DEST, una partición NUEVA creada en espacio sin asignar."
408 echo " La tabla creció en una entrada; ninguna de las que ya estaban se modificó."
409else
410 echo "✓ takana instalada en $DEST. La tabla de particiones NO se tocó y las demás particiones siguen como estaban."
411fi
412echo " Comprobalo antes de reiniciar: lsblk -o NAME,SIZE,FSTYPE,LABEL · $HAMMER boot entry list"