tawasuyu

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 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 93c035f15f8f8aab331a99650e173b325ebf91b0 scripts/takana-install-partition.sh Shell · 1310 siq'ikuna · 86.1 KB Ñawpa willay ch'uya

feat(instalador): el firmware que el kernel pide ANTES de montar la raiz lo decide la maquina 19df6c148 · sergio ·

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|PARTUUID=…|UUID=…|LABEL=…>  DESTINO. Se formatea ext4 con LABEL=hammer-root.
31#                              Lo ÚNICO destructivo. **Usá PARTUUID=**: los nombres nvmeXn1 NO son
32#                              estables entre arranques (medido) y el kexec cambia quién enumera.
33#   --survey                   mirar y NO tocar: el censo del terreno de esta máquina, con el
34#                              PARTUUID de cada partición y qué se puede usar. Empezá por acá.
35#   --json                     (con --survey) el mismo censo en JSON. Es el CONTRATO que consume
36#                              una cara gráfica: `takana metal survey --json` lo envuelve.
37#                              **Versión 2** desde el 2026-09-25: cada fila trae además `clase`,
38#                              `disco`, `inicio_bytes`, `tamano_bytes` y `tipo_guid`, y hay un array
39#                              `discos` con el aparato. Es lo que hace falta para DIBUJAR el terreno
40#                              en vez de listarlo; nada de eso decide — `veredicto` y `usable`
41#                              siguen siendo los únicos que deciden.
42#                              ⚠ El JSON sale por stdout y SOLO el JSON: la línea de qué herramienta
43#                              se resolvió del store va por stderr. Si algún día se cuela en stdout,
44#                              la cara deja de parsear y el censo se lee como «no hay discos».
45#   --pick <fichero>           mostrar el censo, DEJAR ELEGIR y NO TOCAR NADA: deja la elección en
46#                              <fichero> (`DEST=…`, `FREE=1`, `DISK=…`) y sale. Es lo que permite
47#                              decidir ANTES del salto — así lo de después no necesita pantalla.
48#                              La respuesta va a un FICHERO y no a stdout porque stdout es donde
49#                              sale el menú: mezclarlos obliga a quien lo llama a adivinar dónde
50#                              termina la conversación y empieza el dato.
51#   (sin destino)              modo INTERACTIVO: muestra el censo y te deja elegir. Es lo que hace
52#                              que un mismo medio sirva en cualquier máquina.
53#   --use-free-space           no formatear nada existente: crear la partición en el hueco sin
54#                              asignar más grande de `--disk` e instalar ahí. Al lado, no encima.
55#   --on-active-root <modo>    qué hacer si `--root-part` ES la raíz viva (auto|kexec|free-space|abort).
56#                              `auto` (def) = intentar kexec y, si esta máquina no puede, espacio libre.
57#   --esp <dev|fichero>        ESP existente a poblar (FAT). Sin ella no hay arranque.
58#   --disk <dev> --esp-part N  para la entrada NVRAM; si no se dan, se deducen del `--esp`.
59#   --claim-fallback           además, escribir \EFI\BOOT\BOOTX64.EFI (pisa lo que haya ahí).
60#   --keep <dev|PARTUUID=…|UUID=…|LABEL=…>:<punto>
61#                              CONSERVAR esa partición y montarla en <punto> en el sistema nuevo,
62#                              SIN formatearla. Hoy el único <punto> admitido es `/home`. Los
63#                              usuarios que encuentre ahí se crean en el sistema nuevo CON SU MISMO
64#                              uid, y con el nombre del directorio como login. Se comprueba que
65#                              monte ANTES de tocar la raíz: descubrir que el /home que se iba a
66#                              conservar no monta, con la raíz ya formateada, es el peor momento.
67#   --adopt                    NO FORMATEAR el destino: mover el sistema que ya hay ahí a
68#                              `/sistema-anterior` y dejar su `/home` donde está. Es el caso más
69#                              común de todos —una sola partición con un Linux y el /home adentro—
70#                              y el que peor resuelve formatear: lo único irreponible de una
71#                              máquina es el /home de quien la usa. Mover dentro del mismo
72#                              filesystem es un `rename(2)`: no copia un byte. El sistema anterior
73#                              queda ENTERO y accesible, que es mejor que un respaldo.
74#                              Incompatible con `--use-free-space` (ahí no hay nada que adoptar).
75#   --no-nvram                 no tocar la NVRAM (pruebas, o firmware que se registra a mano).
76#   --yes                      no preguntar (para guiones; en interactivo pide confirmación).
77# Variables: ROOTFS KERNEL BUSYBOX (como install-image-efi.sh) · STAGE (def work/.install-part-stage)
78set -eu
79ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
80
81DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
82USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""; CENSO_JSON=""; SOLO_ELEGIR=""
83KEEP_SPEC=""; KEEP_PUNTO=""; KEEP_DEV=""; KEEP_UUID=""; KEEP_PARTUUID=""; KEEP_DIR=""; KEEP_MONTE_YO=""
84ADOPT=""; ADOPT_DIR=""; ADOPT_MONTE_YO=""; ADOPT_SUB=""; ADOPT_MUEVE=""
85while [ $# -gt 0 ]; do
86    case "$1" in
87        --root-part) DEST="${2:?}"; shift 2 ;;
88        --esp)       ESP="${2:?}";  shift 2 ;;
89        --disk)      DISK="${2:?}"; shift 2 ;;
90        --esp-part)  ESP_PART="${2:?}"; shift 2 ;;
91        --claim-fallback) CLAIM_FALLBACK=1; shift ;;
92        --no-nvram)  NO_NVRAM=1; shift ;;
93        --survey)    SOLO_CENSO=1; shift ;;
94        --json)      CENSO_JSON=1; shift ;;
95        --pick)      SOLO_ELEGIR="${2:?--pick necesita el fichero donde dejar la respuesta}"; shift 2 ;;
96        --use-free-space) USE_FREE=1; shift ;;
97        --keep)      KEEP_SPEC="${2:?--keep necesita <dev|PARTUUID=…|UUID=…|LABEL=…>:<punto>}"; shift 2 ;;
98        --adopt)     ADOPT=1; shift ;;
99        --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
100        --yes|-y)    ASSUME_YES=1; shift ;;
101        -h|--help)   sed -n '2,40p' "$0"; exit 0 ;;
102        *) echo "opción desconocida: $1 (--help)" >&2; exit 2 ;;
103    esac
104done
105# Sin destino no se aborta: se PREGUNTA. Eso es lo que separa un instalador de un guion que hay que
106# preparar por máquina. Sólo si tampoco hay con quién hablar (sin terminal) se pide el parámetro.
107NECESITA_ELEGIR=""
108if [ -z "$DEST" ] && [ -z "$USE_FREE" ] && [ -z "$SOLO_CENSO" ]; then NECESITA_ELEGIR=1; fi
109case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root: auto|kexec|free-space|abort" >&2; exit 2 ;; esac
110
111# ── Las herramientas, ANTES que nada ───────────────────────────────────────────────────────────
112# Se resuelven acá arriba y no junto a su uso porque `--use-free-space` necesita `sfdisk` durante
113# las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
114# inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
115. "$(dirname "$0")/lib/herramientas-desde-store.sh"
116# Los dos caminos que conservan datos ajenos viven en sus propias librerías, y eso es lo que permite
117# certificarlos SIN root (`scripts/adoptar-test.sh`): el instalador entero necesita losetup, mount y
118# un disco; estas funciones, sobre directorios, se prueban enteras en un segundo.
119. "$(dirname "$0")/lib/usuarios-del-home.sh"
120. "$(dirname "$0")/lib/adoptar.sh"
121# La cuenta de quien va a usar la máquina. Este camino —`--keep`/`--adopt`, el más común de todos—
122# no preguntaba NADA: los usuarios salían del `/home` conservado con `!` en shadow y root quedaba
123# cerrado, o sea una máquina instalada e inadministrable. Medido en metal el 2026-10-14.
124. "$(dirname "$0")/lib/cuenta.sh"
125# El firmware que el kernel pide ANTES de montar la raíz. El driver del wifi va compilado en el
126# kernel y pide su blob a los 0,49 s, cuando el único filesystem es el initramfs: `/lib/firmware` de
127# la raíz no alcanza, y «que ni ethernet funciona» era eso. Medido el 2026-10-15.
128. "$(dirname "$0")/lib/firmware-initramfs.sh"
129SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
130# `e2label` sólo hace falta para `--adopt`, que no formatea: ahí la etiqueta por la que el arranque
131# encuentra la raíz hay que ponerla a mano. Se resuelve acá arriba —con el resto— y sólo cuando se
132# pidió adoptar, para no exigir una herramienta que el camino normal no usa.
133# ⚠ **Sin `|| exit 1`, y eso es el punto.** `e2label` es OPCIONAL: cuando no está, la etiqueta la
134# escribe `takana fs-label`, que viaja en el mismo medio. Con el `|| exit 1` que tenía, el script se
135# moría acá arriba —antes de tocar nada, sí, pero también antes de llegar al plan B— y la
136# instalación entera fallaba con «falta e2label» sobre una máquina que podía perfectamente adoptar.
137# Medido en QEMU el 2026-10-02. Una herramienta con alternativa no se exige: se busca.
138E2LABEL=""
139if [ -n "$ADOPT" ]; then
140    E2LABEL=$(herramienta e2fsprogs sbin/e2label e2label 2>/dev/null || true)
141fi
142# Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
143# device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
144# si alguna partición del disco está montada — por eso se intentan los dos, en ese orden. Los dos
145# salen del `util-linux` sellado: en el anfitrión musl no existe ninguno (ni `partprobe`).
146PARTX=$(herramienta util-linux usr/sbin/partx partx 2>/dev/null || echo "")
147BLOCKDEV=$(herramienta util-linux sbin/blockdev blockdev 2>/dev/null || echo "")
148# ⚠ **MIRAR no puede costar lo mismo que ESCRIBIR.** Todo lo de abajo —los tres binarios de mtools,
149# mke2fs y el bzImage— sólo hace falta para INSTALAR, y hasta el 2026-09-23 se exigía también para
150# `--survey`: en una máquina ajena, sin store y sin kernel del corpus, el censo moría con «no
151# encuentro el kernel EFI-stub» ANTES de imprimir una sola línea del terreno. Y el censo es
152# justamente lo primero que se corre en una máquina desconocida, donde nada de eso existe todavía.
153# El censo necesita `sfdisk` y `blkid`, nada más.
154if [ -z "$SOLO_CENSO" ] && [ -z "$SOLO_ELEGIR" ]; then
155    MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
156    MMD=$(herramienta    mtools usr/bin/mmd   mmd)     || exit 1
157    MCOPY=$(herramienta  mtools usr/bin/mcopy mcopy)   || exit 1
158    MDIR=$(herramienta   mtools usr/bin/mdir  mdir)    || exit 1
159    KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
160    [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
161fi
162HAMMER="${HAMMER:-$ROOT/target/release/takana}"
163
164# ── LAS GUARDAS VAN PRIMERO, Y SON EL PUNTO DE ESTE SCRIPT ─────────────────────────────────────
165# Un instalador que borra de más falla del lado silencioso: el usuario se entera cuando reinicia y
166# le falta su /home. Cada guarda de abajo existe porque su ausencia es irreversible.
167es_bloque() { [ -b "$1" ]; }
168nombre_kernel() { basename "$(readlink -f "$1")"; }
169disco_de() { echo "/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$1")/..")")"; }
170
171# ── resolver_dev <spec> — NUNCA confíes en `/dev/nvme1n1p6` ───────────────────────────────────
172# ⚠ **Los nombres `nvmeXn1` NO son estables entre arranques, y esto está MEDIDO, no temido**
173# (2026-09-23, QEMU con dos NVMe): el disco declarado primero en la línea de comandos apareció
174# como `nvme1n1` y el segundo como `nvme0n1` — **cruzados**. Salen del orden de sondeo del bus, no
175# del orden físico ni del que vos ves hoy con `lsblk`.
176#
177# Para un instalador eso no es una molestia: es la diferencia entre formatear la raíz vieja y
178# formatear el disco de Windows del usuario. Y el salto por kexec lo agrava, porque el kernel que
179# enumera después del salto NO es el que te dio los nombres antes.
180#
181# Por eso se acepta —y se recomienda— direccionar por identidad y no por posición:
182#   PARTUUID=<uuid>   el de la TABLA GPT. **No cambia al formatear la partición**, así que es el
183#                     correcto para nombrar un destino que se va a formatear.
184#   UUID=<uuid>       el del FILESYSTEM. Sirve para identificar lo que hay HOY (y muere al formatear).
185#   LABEL=<etiqueta>  cómodo, pero puede estar repetido en dos discos.
186resolver_dev() {
187    case "$1" in
188        /dev/*) echo "$1"; return 0 ;;
189        PARTUUID=*|partuuid=*) _rd_v="${1#*=}"; _rd_por=partuuid ;;
190        UUID=*|uuid=*)         _rd_v="${1#*=}"; _rd_por=uuid ;;
191        LABEL=*|label=*)       _rd_v="${1#*=}"; _rd_por=label ;;
192        *) echo "$1"; return 0 ;;          # un fichero (pruebas)
193    esac
194    # `findfs` resuelve UUID y LABEL en busybox y en util-linux; para PARTUUID no se puede contar
195    # con él, así que ese caso se resuelve leyendo la TABLA con sfdisk — que es donde vive el dato.
196    if [ "$_rd_por" != partuuid ] && command -v findfs >/dev/null 2>&1; then
197        _rd_d=$(findfs "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=$_rd_v" 2>/dev/null || true)
198        [ -n "${_rd_d:-}" ] && { echo "$_rd_d"; return 0; }
199    fi
200    # `/dev/loop?` está en la lista porque el banco de pruebas trabaja sobre loop devices: sin él,
201    # la resolución por identidad quedaba SIN COBERTURA en el único sitio donde se puede probar sin
202    # arriesgar un disco de verdad. Lo detectó el propio banco al pasar a PARTUUID.
203    for _rd_disco in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
204        [ -b "$_rd_disco" ] || continue
205        case "$_rd_por" in
206            partuuid)
207                _rd_hit=$("$SFDISK" -d "$_rd_disco" 2>/dev/null \
208                    | awk -v u="$(printf '%s' "$_rd_v" | tr 'A-Z' 'a-z')" \
209                        'tolower($0) ~ ("uuid=" u) {print $1; exit}')
210                ;;
211            *)
212                _rd_hit=$(for _rd_p in "$_rd_disco"*; do
213                    [ -b "$_rd_p" ] || continue
214                    blkid "$_rd_p" 2>/dev/null | grep -qi "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=\"$_rd_v\"" && { echo "$_rd_p"; break; }
215                  done)
216                ;;
217        esac
218        [ -n "${_rd_hit:-}" ] && { echo "$_rd_hit"; return 0; }
219    done
220    echo "takana-install-partition: no encuentro ninguna partición con $1" >&2
221    echo "                          (mirá 'lsblk -o NAME,SIZE,PARTUUID,LABEL' en el sistema vivo)" >&2
222    return 1
223}
224
225# ── releer_tabla <disco> — que el KERNEL se entere de la entrada nueva ────────────────────────
226# Medido el 2026-09-23 en el banco: `sfdisk --append` escribió la entrada perfectamente y
227# `/dev/loop0p4` NO APARECIÓ. El script lo detectó y abortó —bien— pero el motivo estaba fuera de
228# él: en el anfitrión musl no existen `partprobe`, `partx` ni `blockdev`, así que los tres intentos
229# de releer la tabla eran no-ops silenciosos. La tabla en disco y la que el kernel tiene en memoria
230# son dos cosas distintas, y nada avisa cuando divergen.
231# ⚠ Y el comando es `partx -u`, NO `partx -a`: medido en el banco el mismo día. `-a` («añadir»)
232# intenta registrar TODAS las particiones y falla entero —«error adding partitions 1-4»— cuando las
233# viejas ya están, que es siempre en este flujo. `-u` («actualizar») añade lo que falte y deja en paz
234# lo que ya hay. El nombre de la opción sugiere justo lo contrario de lo que sirve acá.
235releer_tabla() {
236    [ -n "${PARTX:-}" ] && "$PARTX" -u "$1" >/dev/null 2>&1 && return 0
237    [ -n "${PARTX:-}" ] && "$PARTX" -a "$1" >/dev/null 2>&1 && return 0
238    [ -n "${BLOCKDEV:-}" ] && "$BLOCKDEV" --rereadpt "$1" >/dev/null 2>&1 && return 0
239    partprobe "$1" 2>/dev/null || true
240}
241
242# ── EL CENSO DEL TERRENO ───────────────────────────────────────────────────────────────────────
243# Un instalador que hay que preparar a mano para cada máquina no es un instalador. Esto enumera lo
244# que hay —en CUALQUIER máquina— y dice de cada partición si se puede usar, si no se debe tocar y
245# por qué. Es la base del modo interactivo y de la recomendación automática.
246#
247# Dos fuentes, porque ninguna sola alcanza:
248#   · `sfdisk -d`  da el TIPO de la tabla (GUID) y el **PARTUUID**, que es la identidad estable;
249#   · `blkid`      da lo que hay ADENTRO (ntfs, crypto_LUKS, ext4, swap…), que el tipo GPT miente
250#                  a menudo: media instalación de Windows vive en «datos básicos», y un LUKS puede
251#                  estar en una partición marcada como Linux.
252# Y `/proc/mounts` dice qué está en uso AHORA, que es lo único que distingue «vacía» de «tu raíz».
253
254# ── raiz_viva_majmin — quién es «/» de verdad ─────────────────────────────────────────────────
255# ⚠ **`/proc/mounts` NO alcanza para saber cuál es la raíz viva, y esto está MEDIDO** (2026-09-23,
256# sobre el anfitrión de este hub): la línea de «/» dice `/dev/root`, un nombre que no existe en
257# `/dev` ni casa con ninguna partición. Comparar por nombre daba «ninguna partición es la raíz» y el
258# censo **ofrecía formatear la raíz del sistema que estaba corriendo**. Pasa también con `UUID=…`
259# en la cmdline, con initramfs que montan por label, y con `/dev/mapper/*`.
260#
261# Lo que sí es inequívoco es el NÚMERO de dispositivo: `stat` lo da para «/» y cada partición lo
262# publica en `/sys/class/block/<n>/dev`. La descomposición es la de Linux (12 bits de major, minor
263# partido en dos), no el `/256` ingenuo, que se rompe justo en NVMe (major 259).
264raiz_viva_majmin() {
265    _rv_hex=$(stat -c %D / 2>/dev/null) || return 1
266    [ -n "${_rv_hex:-}" ] || return 1
267    _rv_d=$(( 0x$_rv_hex )) 2>/dev/null || return 1
268    _rv_maj=$(( (_rv_d >> 8) & 0xfff ))
269    _rv_min=$(( (_rv_d & 0xff) | ((_rv_d >> 12) & 0xfff00) ))
270    echo "${_rv_maj}:${_rv_min}"
271}
272RAIZ_MAJMIN="$(raiz_viva_majmin || true)"
273
274es_la_raiz_viva() {   # <dev>
275    [ -n "${RAIZ_MAJMIN:-}" ] || return 1
276    _er_n=$(nombre_kernel "$1")
277    [ "$(cat "/sys/class/block/$_er_n/dev" 2>/dev/null)" = "$RAIZ_MAJMIN" ]
278}
279
280GUID_ESP="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
281GUID_MSR="e3c9e316-0b5c-4db8-817d-f92df00215ae"
282GUID_WINRE="de94bba4-06d1-4d40-a16a-bfd50179d6ac"
283GUID_MSBASIC="ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"
284GUID_LINUX="0fc63daf-8483-4772-8e79-3d69d8477de4"
285GUID_LUKS="ca7d7ccb-63ed-4c53-861c-1742536059cc"
286GUID_SWAP="0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"
287
288discos_del_sistema() {
289    for _dd in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
290        [ -b "$_dd" ] || continue
291        # Tamaño 0 = un `loop` libre sin fichero detrás. Existen por docenas y no son discos: si se
292        # listan, el censo se llena de ruido y lo que importa deja de verse. Un guardián ruidoso se
293        # ignora igual que uno mudo.
294        [ "$(cat "/sys/class/block/$(basename "$(readlink -f "$_dd")")/size" 2>/dev/null || echo 0)" -gt 0 ] || continue
295        echo "$_dd"
296    done
297}
298
299mib_de_sectores() { echo $(( ${1:-0} / 2048 )); }
300mib_de_bytes()    { echo $(( ${1:-0} / 1048576 )); }
301
302# ── sector_bytes_de <disco> — el sector LÓGICO, que no siempre es 512 ──────────────────────────
303# `sfdisk -d` cuenta en sectores LÓGICOS y lo declara en su cabecera (`sector-size: 512`). En un
304# disco 4Kn eso son 4096, y dividir por 2048 a ciegas da un tamaño **ocho veces menor**: una
305# partición de 40 G se muestra como 5 G y el piso de `mib_necesarios` la rechaza por chica — o sea
306# que el error no se queda en la pantalla, **decide**.
307#
308# ⚠ Y no se puede unificar con `/sys/class/block/<n>/size`, que es lo que usa la rama del disco sin
309# tabla: **ése siempre cuenta en unidades de 512**, por convención del kernel, incluso en 4Kn.
310# Mezclar las dos conversiones es el error que esta separación evita.
311sector_bytes_de() {
312    _sb_v=$("$SFDISK" -d "$1" 2>/dev/null | sed -n 's/^sector-size:[ ]*\([0-9]*\).*/\1/p' | head -1)
313    case "${_sb_v:-}" in ''|*[!0-9]*) echo 512 ;; *) echo "$_sb_v" ;; esac
314}
315
316# ── bytes_del_disco <disco> — el tamaño del APARATO, en bytes ──────────────────────────────────
317bytes_del_disco() { echo $(( $(cat "/sys/class/block/$(nombre_kernel "$1")/size" 2>/dev/null || echo 0) * 512 )); }
318
319# ── mib_minimos — el piso, calculado UNA vez ──────────────────────────────────────────────────
320# `mib_necesarios` mide el árbol y no es gratis (`du -sm` sobre el rootfs); el censo la llamaría una
321# vez por partición. Se cachea en una global, que es la única forma de memoizar en sh.
322mib_minimos() {
323    [ -n "${MIB_MINIMOS:-}" ] || MIB_MINIMOS=$(mib_necesarios)
324    echo "$MIB_MINIMOS"
325}
326
327# clasificar <guid-tipo> <fstype> <montaje> [mib] → VEREDICTO|ETIQUETA
328# El veredicto es lo que decide el menú: SOLO_ESP, NO_TOCAR, CANDIDATA, RAIZ_VIVA, EN_USO.
329clasificar() {
330    _cl_guid=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
331    _cl_fs="${2:-}"; _cl_mnt="${3:-}"; _cl_mib="${4:-0}"
332    case "$_cl_guid" in
333        "$GUID_ESP") echo "SOLO_ESP|ESP (partición de arranque EFI)"; return ;;
334        "$GUID_MSR") echo "NO_TOCAR|reservada de Microsoft"; return ;;
335        "$GUID_WINRE") echo "NO_TOCAR|recuperación de Windows"; return ;;
336        "$GUID_SWAP") echo "NO_TOCAR|swap"; return ;;
337        # «Datos básicos de Microsoft» es el tipo que Windows le pone a sus volúmenes. Aunque el
338        # filesystem no se reconozca —puede estar cerrado por BitLocker, o simplemente vacío— no se
339        # ofrece: el coste de equivocarse acá es el disco de otro sistema operativo.
340        "$GUID_MSBASIC") echo "NO_TOCAR|volumen de datos de Windows (tipo GPT «basic data»)"; return ;;
341        # ⚠ El tipo LUKS de la TABLA tiene que bastar por sí solo. Estaba definido y **no se
342        # consultaba**: en la prueba con el layout del usuario, una partición marcada como LUKS en
343        # GPT salió ofrecida como «se puede usar» porque `blkid` no veía cabecera dentro. Un
344        # contenedor cifrado es justo el caso donde no se puede mirar adentro para decidir — el
345        # único dato disponible es el tipo, así que el tipo manda.
346        "$GUID_LUKS") echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS, por tipo de la tabla)"; return ;;
347    esac
348    case "$_cl_fs" in
349        crypto_LUKS) echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS) — lo que hay dentro no se puede ver desde acá"; return ;;
350        ntfs)        echo "NO_TOCAR|NTFS (Windows u otro sistema)"; return ;;
351        vfat|msdos)  echo "NO_TOCAR|FAT — puede ser una ESP o datos compartidos"; return ;;
352        swap)        echo "NO_TOCAR|swap"; return ;;
353        BitLocker|bitlocker) echo "NO_TOCAR|BitLocker"; return ;;
354    esac
355    if [ "$_cl_mnt" = "/" ]; then
356        # ⚠ **La raíz viva no siempre es del usuario.** Arrancado desde un USB o una imagen, la raíz
357        # viva es EL MEDIO — y ofrecerlo como destino es ofrecer formatear el instalador mientras
358        # corre. El medio se sabe en el arranque (`TAKANA_MEDIO`, lo pone el `/init` mirando si el
359        # cmdline trae `rdinit=`), no se adivina acá.
360        if [ -n "${TAKANA_MEDIO:-}" ]; then
361            echo "NO_TOCAR|es el MEDIO desde el que arrancaste (el instalador corre desde acá)"; return
362        fi
363        echo "RAIZ_VIVA|la raíz del sistema que está corriendo AHORA"; return
364    fi
365    if [ -n "$_cl_mnt" ]; then
366        echo "EN_USO|montada en $_cl_mnt"; return
367    fi
368    # ⚠ **«No reconozco nada» NO es «está vacío», y confundirlos DESTRUYE.** Un almacén de
369    # negación plausible (`qullqa`, tawasuyu) es por diseño un dispositivo lleno de aleatorio y sin
370    # ninguna firma — o sea **exactamente** lo que un instalador ingenuo llama «partición vacía, se
371    # puede usar». Formatearla no filtra información: la borra, y el dueño se entera cuando ya no
372    # está. La contención que el editor de discos de tawasuyu ya implementó (su T16) es general y
373    # ésta la copia: **lo que no se pudo identificar nace CRUDO, no libre**, y sólo se usa con una
374    # confirmación fuerte.
375    #
376    # Y el mensaje es EL MISMO para un pozo, un disco recién borrado y un contenedor cifrado ajeno.
377    # Eso no es pereza: un aviso que dijera «esto parece un volumen oculto» delataría exactamente lo
378    # que el diseño existe para no delatar. Acá no se nombra nada de eso, a propósito.
379    case "$_cl_fs" in
380        ext2|ext3|ext4|xfs|btrfs|f2fs) _cl_q="Linux ($_cl_fs) sin montar" ;;
381        "")  _cl_min=$(mib_minimos)
382             if [ "${_cl_mib:-0}" -lt "$_cl_min" ] 2>/dev/null; then
383                 echo "NO_TOCAR|sin nada que se pueda identificar, y son ${_cl_mib} M (hacen falta ${_cl_min} M)"; return
384             fi
385             echo "CRUDA|no reconozco NADA acá: puede estar vacía o en uso por algo que este instalador no sabe leer"; return ;;
386        *)   echo "NO_TOCAR|$_cl_fs (desconocido para este instalador)"; return ;;
387    esac
388    # ⚠ **Una candidata que no entra no es una candidata.** Sin esta puerta el censo de este mismo
389    # hub ofrecía `/dev/sda1` —2 MiB, sin filesystem, el hueco de arranque— como «✓ se puede usar»:
390    # el veredicto sólo miraba QUÉ hay dentro y nunca CUÁNTO cabe. Elegirla formatea algo real y
391    # muere a mitad de la copia, que es el peor momento para enterarse. El piso es el mismo número
392    # con el que se talla un hueco (`mib_necesarios`), así que las dos puertas no pueden divergir.
393    _cl_min=$(mib_minimos)
394    if [ "${_cl_mib:-0}" -lt "$_cl_min" ] 2>/dev/null; then
395        echo "NO_TOCAR|$_cl_q, pero son ${_cl_mib} M y hacen falta al menos ${_cl_min} M"; return
396    fi
397    echo "CANDIDATA|$_cl_q"
398}
399
400# censo [--solo-candidatas] → una línea por partición, hueco o disco sin tabla:
401#   <dev>|<partuuid>|<mib>|<fstype>|<label>|<montaje>|<VEREDICTO>|
402#   <clase>|<disco>|<inicio_bytes>|<tamano_bytes>|<tipo_guid>|<explicación>
403#
404# ⚠ **La explicación va ÚLTIMA a propósito**: es el único campo de texto libre, y con `IFS='|'` el
405# último `read` se come lo que sobre. Un campo nuevo va ANTES de ella, y hay que tocar los tres
406# lectores (`mostrar_censo`, `censo_json`, `elegir_destino`) — no hay cuarto.
407#
408# `clase` distingue las tres cosas que no son lo mismo y que hasta hoy se veían iguales:
409#   `particion`  una entrada de la tabla
410#   `hueco`      espacio sin asignar — **no tiene `dev` propio**: `dev` es el DISCO, y dos huecos
411#                del mismo disco traen el mismo `dev`. La identidad de un hueco es
412#                `disco` + `inicio_bytes`, y nada más.
413#   `disco`      un disco SIN tabla de particiones (formateado directo, o crudo)
414censo() {
415    _ce_solo="${1:-}"
416    for _ce_d in $(discos_del_sistema); do
417        _ce_sb=$(sector_bytes_de "$_ce_d")
418        # ⚠ Un disco SIN TABLA no es un disco vacío: puede estar formateado directo y montado —así
419        # vive el store de este hub, `/dev/sdb` con un ext4 encima y nada más—. Sin esta rama el
420        # censo lo OMITE por completo, que es la peor forma de presentar algo que existe y está en
421        # uso: invisible. Se lista, se dice que no tiene tabla, y no se ofrece.
422        if [ -z "$("$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}")" ]; then
423            _ce_bl=$(blkid "$_ce_d" 2>/dev/null || true)
424            _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"'  | head -1 | sed 's/^ TYPE="//; s/"$//')
425            _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
426            _ce_mn=$(awk -v d="$_ce_d" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
427            es_la_raiz_viva "$_ce_d" && _ce_mn="/"
428            _ce_sz=$(( $(cat "/sys/class/block/$(nombre_kernel "$_ce_d")/size" 2>/dev/null || echo 0) ))
429            if [ -n "$_ce_mn" ]; then _ce_v="EN_USO"; _ce_e="disco SIN TABLA de particiones, montado en $_ce_mn"
430            else _ce_v="NO_TOCAR"; _ce_e="disco SIN TABLA de particiones (${_ce_fs:-sin filesystem})"; fi
431            echo "$_ce_d|-|$(mib_de_sectores "$_ce_sz")|${_ce_fs}|${_ce_lb}|${_ce_mn}|$_ce_v|disco|$_ce_d|0|$(( _ce_sz * 512 ))|-|$_ce_e"
432            continue
433        fi
434        "$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}" | while IFS= read -r _ce_l; do
435            _ce_dev=$(printf '%s' "$_ce_l" | awk '{print $1}')
436            _ce_sz=$(printf '%s'  "$_ce_l" | sed -n 's/.*size=[ ]*\([0-9]*\).*/\1/p')
437            _ce_st=$(printf '%s'  "$_ce_l" | sed -n 's/.*start=[ ]*\([0-9]*\).*/\1/p')
438            _ce_ty=$(printf '%s'  "$_ce_l" | sed -n 's/.*type=\([0-9A-Fa-f-]*\).*/\1/p')
439            _ce_uu=$(printf '%s'  "$_ce_l" | sed -n 's/.*uuid=\([0-9A-Fa-f-]*\).*/\1/p')
440            _ce_bl=$(blkid "$_ce_dev" 2>/dev/null || true)
441            _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"'  | head -1 | sed 's/^ TYPE="//; s/"$//')
442            _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
443            _ce_mn=$(awk -v d="$_ce_dev" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
444            # La raíz viva se decide por número de dispositivo, no por el nombre de /proc/mounts.
445            if es_la_raiz_viva "$_ce_dev"; then _ce_mn="/"; fi
446            # En bytes y con el sector del disco, no con un 2048 horneado: en 4Kn el tamaño sale
447            # ocho veces menor y `clasificar` rechaza por chica una partición que sobra.
448            _ce_by=$(( _ce_sz * _ce_sb ))
449            _ce_ib=$(( ${_ce_st:-0} * _ce_sb ))
450            _ce_cl=$(clasificar "$_ce_ty" "$_ce_fs" "$_ce_mn" "$(mib_de_bytes "$_ce_by")")
451            _ce_ver="${_ce_cl%%|*}"; _ce_exp="${_ce_cl#*|}"
452            [ "$_ce_solo" = "--solo-candidatas" ] && [ "$_ce_ver" != "CANDIDATA" ] && continue
453            echo "$_ce_dev|$_ce_uu|$(mib_de_bytes "$_ce_by")|$_ce_fs|$_ce_lb|$_ce_mn|$_ce_ver|particion|$_ce_d|$_ce_ib|$_ce_by|${_ce_ty:--}|$_ce_exp"
454        done
455        # El espacio SIN ASIGNAR es un candidato más, y el único que no destruye nada.
456        # ⚠ Las columnas de `sfdisk -F` son <inicio> <fin> <sectores> <tamaño-legible>, y el inicio
457        # es lo que convierte un hueco en algo DIBUJABLE: sin él la cara no sabe entre qué dos
458        # particiones cae, y «acá no hay nada» es justo el sitio donde más conviene instalar.
459        "$SFDISK" -F "$_ce_d" 2>/dev/null | awk -v d="$_ce_d" -v min="$(mib_minimos)" -v sb="$_ce_sb" \
460            'NF==4 && $1 ~ /^[0-9]+$/ { by=$3*sb; ib=$1*sb; m=int(by/1048576);
461               if (m >= min) print d"|-|"m"|-|-|-|LIBRE|hueco|"d"|"ib"|"by"|-|espacio sin asignar en "d;
462               else print d"|-|"m"|-|-|-|NO_TOCAR|hueco|"d"|"ib"|"by"|-|hueco sin asignar en "d", pero son "m" M y hacen falta al menos "min" M" }'
463    done
464}
465
466# mostrar_censo — el censo en forma de tabla, con los NO TOCAR dichos en voz alta.
467mostrar_censo() {
468    _mc_f="${1:-}"
469    echo "── el terreno de esta máquina ─────────────────────────────────────────────────"
470    [ -n "${RAIZ_MAJMIN:-}" ] || echo "  ⚠ NO pude determinar cuál es la raíz viva de esta máquina: tratá TODO con sospecha."
471    printf "  %-3s %-16s %-9s %-11s %s\n" "nº" "partición" "tamaño" "qué es" "veredicto"
472    _mc_n=0
473    { if [ -n "$_mc_f" ]; then cat "$_mc_f"; else censo; fi; } | while IFS='|' read -r _d _u _m _fs _lb _mn _ver _cla _dis _ib _tb _tg _exp; do
474        _mc_n=$(( _mc_n + 1 ))
475        case "$_ver" in
476            CANDIDATA) _mc_marca="✓ se puede usar (SE BORRA lo que tenga)" ;;
477            CRUDA)     _mc_marca="? sin identificar — se puede usar, con confirmación" ;;
478            LIBRE)     _mc_marca="✓ instalar AL LADO sin borrar nada" ;;
479            RAIZ_VIVA) _mc_marca="⚡ sólo con el salto (kexec)" ;;
480            SOLO_ESP)  _mc_marca="· se reusa como ESP, no se formatea" ;;
481            EN_USO)    _mc_marca="· en uso" ;;
482            *)         _mc_marca="✗ NO TOCAR" ;;
483        esac
484        printf "  %-3s %-16s %6s M  %-11s %s\n" "$_mc_n" "$_d" "$_m" "${_fs:-—}" "$_mc_marca"
485        printf "      %s%s\n" "$_exp" "$( [ -n "$_lb" ] && [ "$_lb" != "-" ] && printf ' · etiqueta «%s»' "$_lb")"
486        # ⚠ **Un `a && b && c` como ÚLTIMO comando de un bucle decide el estado de salida del
487        # script entero.** Con `set -e`, si la última fila del censo no tiene PARTUUID —un disco sin
488        # tabla, un hueco libre— esta línea daba 1, el `while` daba 1, la tubería daba 1 y
489        # `mostrar_censo` MORÍA ahí: el separador de cierre no se imprimía y `--survey` salía con 1
490        # habiendo funcionado. Medido en este hub el 2026-09-23, donde la última fila es `/dev/sdc`.
491        # Un `if` no propaga nada; el `&&` sí.
492        if [ -n "$_u" ] && [ "$_u" != "-" ]; then printf "      PARTUUID=%s\n" "$_u"; fi
493    done
494    echo "───────────────────────────────────────────────────────────────────────────────"
495    # Un censo VACÍO no es un error, pero callarlo se lee como «no hay nada que hacer acá». Pasa de
496    # verdad: dentro de esta jaula `/dev` no tiene un solo dispositivo de bloque.
497    if [ -z "$({ if [ -n "$_mc_f" ]; then cat "$_mc_f"; else censo; fi; } | grep -c . 2>/dev/null | grep -v '^0$')" ]; then
498        echo "  (no veo NINGÚN disco desde acá — ¿estás dentro de un contenedor, o sin privilegios?)"
499    fi
500}
501
502# ── censo_json — el MISMO censo, en JSON. Es el CONTRATO con cualquier cara gráfica ───────────
503#
504# ⚠ **Existe para que no haya un segundo censo.** La tentación al poner una interfaz encima es que
505# la interfaz lea `sfdisk`/`blkid` por su cuenta y clasifique ella; ahí es donde los dos divergen y
506# uno de los dos empieza a ofrecer lo que el otro protege — y el que se equivoca formatea. El SDD 28
507# §8 ya nombra ese riesgo («un motor, tres frentes»). Entonces: UN censo, UNA clasificación, y las
508# caras consumen esto. Si `veredicto` cambia acá, cambia para todas a la vez.
509#
510# El campo `usable` es la lectura de una sola pregunta —¿se puede instalar acá?— para que una cara
511# no tenga que reimplementar el `case` de los veredictos y equivocarse en el default: lo que no
512# reconozca NO es usable.
513jstr() {
514    printf '"%s"' "$(printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/ /g' | tr -d '\000-\010\013\014\016-\037')"
515}
516# ── discos_json — el APARATO, una vez por disco y no repetido en cada fila ─────────────────────
517#
518# `/dev/sdc` no contesta «¿cuál es el pendrive que acabo de enchufar?». El modelo y si se puede
519# desenchufar son las dos señas que lo distinguen del disco de la máquina.
520#
521# ⚠ Va en un array PROPIO y no como campos de cada partición —que es como se pidió— porque un dato
522# del disco repetido en cada fila se puede contradecir entre filas, y entonces hay que decidir a
523# cuál creerle. Cada fila trae `disco`, que es la clave acá. Y no hace falta ninguna herramienta:
524# todo sale de `/sys` y de la cabecera de `sfdisk -d`.
525#
526# ⚠ `removible` es el flag del kernel, que es lo que hay: dice «este aparato admite que le saques el
527# medio». Un USB sale 1 y un NVMe interno sale 0, que es el caso que importa — pero un lector de
528# tarjetas vacío también sale 1, así que **es una seña para pintar, no una garantía**.
529discos_json() {
530    printf '  "discos": [\n'
531    _dj_pri=1
532    for _dj_d in $(discos_del_sistema); do
533        _dj_n=$(nombre_kernel "$_dj_d")
534        _dj_mo=$(sed 's/[[:space:]]*$//' "/sys/class/block/$_dj_n/device/model" 2>/dev/null || true)
535        _dj_rm=false
536        [ "$(cat "/sys/class/block/$_dj_n/removable" 2>/dev/null || echo 0)" = 1 ] && _dj_rm=true
537        _dj_tb=$("$SFDISK" -d "$_dj_d" 2>/dev/null | sed -n 's/^label:[[:space:]]*\(.*\)$/\1/p' | head -1)
538        [ "$_dj_pri" = 1 ] || printf ',\n'
539        _dj_pri=0
540        printf '    {"dev": %s, "modelo": %s, "removible": %s, "tamano_bytes": %s, "sector_bytes": %s, "tabla": %s}' \
541            "$(jstr "$_dj_d")" \
542            "$( [ -n "${_dj_mo:-}" ] && jstr "$_dj_mo" || printf 'null' )" \
543            "$_dj_rm" \
544            "$(bytes_del_disco "$_dj_d")" \
545            "$(sector_bytes_de "$_dj_d")" \
546            "$( [ -n "${_dj_tb:-}" ] && jstr "$_dj_tb" || printf 'null' )"
547    done
548    printf '\n  ],\n'
549}
550
551censo_json() {
552    _cj_f="${1:-}"
553    printf '{\n'
554    printf '  "version": 2,\n'
555    printf '  "raiz_viva": %s,\n' "$( [ -n "${RAIZ_MAJMIN:-}" ] && jstr "$RAIZ_MAJMIN" || printf 'null' )"
556    discos_json
557    printf '  "particiones": [\n'
558    _cj_pri=1
559    { if [ -n "$_cj_f" ]; then cat "$_cj_f"; else censo; fi; } | while IFS='|' read -r _d _u _m _fs _lb _mn _ver _cla _dis _ib _tb _tg _exp; do
560        [ -n "$_d" ] || continue
561        case "$_ver" in CANDIDATA|LIBRE|RAIZ_VIVA|CRUDA) _cj_us=true ;; *) _cj_us=false ;; esac
562        [ "$_cj_pri" = 1 ] || printf ',\n'
563        _cj_pri=0
564        printf '    {"dev": %s, "partuuid": %s, "mib": %s, "fs": %s, "etiqueta": %s, "montaje": %s, "veredicto": %s, "usable": %s, "clase": %s, "disco": %s, "inicio_bytes": %s, "tamano_bytes": %s, "tipo_guid": %s, "explicacion": %s}' \
565            "$(jstr "$_d")" \
566            "$( [ "${_u:--}" = "-" ] && printf 'null' || jstr "$_u" )" \
567            "${_m:-0}" \
568            "$( [ "${_fs:--}" = "-" ] || [ -z "${_fs:-}" ] && printf 'null' || jstr "$_fs" )" \
569            "$( [ "${_lb:--}" = "-" ] || [ -z "${_lb:-}" ] && printf 'null' || jstr "$_lb" )" \
570            "$( [ "${_mn:--}" = "-" ] || [ -z "${_mn:-}" ] && printf 'null' || jstr "$_mn" )" \
571            "$(jstr "$_ver")" "$_cj_us" \
572            "$(jstr "${_cla:-particion}")" \
573            "$( [ -n "${_dis:-}" ] && [ "${_dis}" != "-" ] && jstr "$_dis" || printf 'null' )" \
574            "${_ib:-0}" "${_tb:-0}" \
575            "$( [ "${_tg:--}" = "-" ] || [ -z "${_tg:-}" ] && printf 'null' || jstr "$(printf '%s' "$_tg" | tr 'A-Z' 'a-z')" )" \
576            "$(jstr "$_exp")"
577    done
578    printf '\n  ]\n}\n'
579}
580
581# ── partuuid_de <dev> — la identidad estable de una partición, leída de la TABLA ──────────────
582partuuid_de() {
583    "$SFDISK" -d "$(disco_de "$1")" 2>/dev/null \
584        | awk -v d="$1" '$1==d {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i; exit}}'
585}
586
587# ── elegir_destino — el modo interactivo, que es lo que hace de esto un instalador ────────────
588# Sin esto hay que saber de antemano el PARTUUID de la máquina de destino, o sea preparar un medio
589# distinto por máquina. Con esto el MISMO medio sirve en cualquiera: arranca, mira y ofrece.
590# Deja la elección en DEST / USE_FREE / DISK del llamador.
591# ⚠ `/tmp` puede NO EXISTIR. Medido dentro del initramfs: el censo no pudo escribir su fichero y el
592# mensaje que salió fue «no veo ningún disco en esta máquina» — con once discos a la vista dos
593# líneas más abajo. Un diagnóstico que culpa al hardware cuando falta un directorio manda a
594# desarmar la máquina equivocada. Ahora se busca un sitio escribible y, si no hay ninguno, se dice
595# ESO.
596dir_temporal() {
597    for _dt in "${TMPDIR:-}" /tmp /run /dev/shm .; do
598        [ -n "$_dt" ] || continue
599        [ -d "$_dt" ] || continue
600        if ( : > "$_dt/.takana-escritura.$$" ) 2>/dev/null; then
601            rm -f "$_dt/.takana-escritura.$$"; echo "$_dt"; return 0
602        fi
603    done
604    return 1
605}
606
607elegir_destino() {
608    _ed_dir=$(dir_temporal) || {
609        echo "takana-install-partition: no encuentro ningún directorio donde escribir (/tmp, /run…)" >&2
610        return 1; }
611    _ed_f="$_ed_dir/.takana-censo.$$"
612    censo > "$_ed_f" 2>/dev/null
613    [ -s "$_ed_f" ] || { echo "takana-install-partition: no veo ningún disco en esta máquina" >&2; rm -f "$_ed_f"; return 1; }
614    mostrar_censo "$_ed_f"
615    echo "  Lo marcado ✗ NO TOCAR no se ofrece: son sistemas de otros, volúmenes cifrados o swap."
616    echo "  Elegir una ✓ BORRA lo que tenga. «Al lado» no borra nada."
617    _ed_n=0
618    printf "  opciones: "
619    while IFS='|' read -r _d _u _m _fs _lb _mn _ver _cla _dis _ib _tb _tg _exp; do
620        _ed_n=$(( _ed_n + 1 ))
621        # ⚠ `CRUDA` tiene que estar acá. Sin ella la línea salía VACÍA en un disco cuyo único
622        # destino posible era una partición sin identificar: el menú ofrecía la opción 4 y a la vez
623        # decía «opciones: (ninguna)». Medido en el arnés del USB el 2026-09-23. Una lista de
624        # opciones que no coincide con lo que la tabla marca es peor que no tenerla.
625        case "$_ver" in CANDIDATA|LIBRE|RAIZ_VIVA|CRUDA) printf "%s " "$_ed_n" ;; esac
626    done < "$_ed_f"
627    echo
628    printf "  nº (o 'no' para salir sin tocar nada): "
629    read -r _ed_sel || _ed_sel=no
630    case "$_ed_sel" in
631        ''|no|NO|n) echo "  cancelado: no se tocó nada."; rm -f "$_ed_f"; return 1 ;;
632    esac
633    _ed_linea=$(sed -n "${_ed_sel}p" "$_ed_f" 2>/dev/null)
634    rm -f "$_ed_f"
635    [ -n "$_ed_linea" ] || { echo "takana-install-partition: «$_ed_sel» no es una de las opciones" >&2; return 1; }
636    _ed_dev=$(printf '%s' "$_ed_linea" | cut -d'|' -f1)
637    _ed_uu=$(printf  '%s' "$_ed_linea" | cut -d'|' -f2)
638    _ed_ver=$(printf '%s' "$_ed_linea" | cut -d'|' -f7)
639    case "$_ed_ver" in
640        LIBRE)     USE_FREE=1; DISK="$_ed_dev"; DEST=""
641                   echo "  ⇒ instalar AL LADO, en el espacio libre de $_ed_dev" ;;
642        CRUDA)
643                   # Confirmación FUERTE, y distinta de un «sí»: se teclea el nombre del device.
644                   # Un `sí` se contesta por reflejo; escribir `/dev/nvme0n1p5` obliga a mirar.
645                   echo "  ⚠ En $_ed_dev no se reconoce NADA. Puede estar vacía — o puede tener algo"
646                   echo "    que este instalador no sabe leer, y formatearla lo borra sin aviso."
647                   printf "    Escribí el nombre completo (%s) para confirmar, o cualquier otra cosa para salir: " "$_ed_dev"
648                   read -r _ed_conf
649                   [ "$_ed_conf" = "$_ed_dev" ] || { echo "  cancelado: no se tocó nada."; return 1; }
650                   if [ -n "$_ed_uu" ] && [ "$_ed_uu" != "-" ]; then DEST="PARTUUID=$_ed_uu"; else DEST="$_ed_dev"; fi
651                   echo "  ⇒ destino: $_ed_dev ($DEST)" ;;
652        CANDIDATA|RAIZ_VIVA)
653                   # Por PARTUUID SIEMPRE que se pueda: el nombre de device puede cambiar entre
654                   # este momento y el arranque siguiente (ver `resolver_dev`).
655                   if [ -n "$_ed_uu" ] && [ "$_ed_uu" != "-" ]; then DEST="PARTUUID=$_ed_uu"; else DEST="$_ed_dev"; fi
656                   echo "  ⇒ destino: $_ed_dev ($DEST)" ;;
657        *) echo "takana-install-partition: esa partición está marcada como NO TOCAR y no se ofrece" >&2; return 1 ;;
658    esac
659    return 0
660}
661
662# ── esp_automatica [disco-del-destino] — la ESP no se pregunta si se puede deducir ────────────
663# Regla: la ESP del MISMO disco que el destino; si ahí no hay, la única del sistema. Si hay varias y
664# ninguna en ese disco, se pregunta: elegir entre las ESP de dos sistemas operativos distintos es
665# justo la clase de decisión que un instalador no debe tomar solo.
666esp_automatica() {
667    _ea_disco="${1:-}"
668    _ea_todas=$(censo | awk -F'|' '$7=="SOLO_ESP" {print $1}')
669    [ -n "$_ea_todas" ] || return 1
670    if [ -n "$_ea_disco" ]; then
671        for _ea_e in $_ea_todas; do
672            case "$_ea_e" in "$_ea_disco"*) echo "$_ea_e"; return 0 ;; esac
673        done
674    fi
675    _ea_n=$(printf '%s\n' "$_ea_todas" | grep -c .)
676    if [ "$_ea_n" = "1" ]; then printf '%s\n' "$_ea_todas"; return 0; fi
677    echo "takana-install-partition: hay $_ea_n ESP y ninguna en el disco del destino. Pasá --esp:" >&2
678    printf '%s\n' "$_ea_todas" | sed 's/^/      /' >&2
679    return 1
680}
681
682# ── mib_necesarios — cuánto pedirle al hueco ──────────────────────────────────────────────────
683# El rootfs medido + 40 %, con piso de 2 GiB. El margen no es superstición: `mke2fs` se queda con
684# ~1,5 % en metadatos y el sistema instalado tiene que poder escribir algo antes de que alguien lo
685# agrande. Un destino que entra JUSTO produce un sistema que arranca y se llena a la semana.
686mib_necesarios() {
687    _mn_r="${ROOTFS:-}"
688    [ -z "$_mn_r" ] && _mn_r=$(ls -dt store/*-product-rootfs /store/*-product-rootfs 2>/dev/null | head -1)
689    _mn_m=$(du -sm "$_mn_r" 2>/dev/null | cut -f1)
690    [ -z "${_mn_m:-}" ] && _mn_m=1024
691    _mn_m=$(( _mn_m * 14 / 10 ))
692    [ "$_mn_m" -lt 2048 ] && _mn_m=2048
693    echo "$_mn_m"
694}
695
696# ── crear_en_hueco <disco> <mib> — la ÚNICA vez que este script escribe la tabla ───────────────
697# `sfdisk --append` añade una entrada y **no reescribe las existentes**: es la diferencia entre
698# «instalar al lado» y el `o` de `takana-live-install.sh`, que empieza por tirar la tabla entera.
699# Aun así es la operación más delicada de acá, así que: se elige el hueco MÁS GRANDE, se dice cuál
700# es con sus sectores a la vista, y se pide confirmación aparte de la del formateo.
701crear_en_hueco() {
702    _ch_disco="$1"; _ch_mib="$2"
703    _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}')
704    [ -n "$_ch_libre" ] || { echo "takana-install-partition: $_ch_disco no tiene espacio SIN ASIGNAR" >&2; return 1; }
705    _ch_start=$(echo "$_ch_libre" | cut -d' ' -f1)
706    _ch_sect=$(echo  "$_ch_libre" | cut -d' ' -f3)
707    _ch_libre_mib=$(( _ch_sect / 2048 ))
708    echo "    hueco más grande: ${_ch_libre_mib} MiB desde el sector ${_ch_start}" >&2
709    [ "$_ch_libre_mib" -ge "$_ch_mib" ] || {
710        echo "takana-install-partition: el hueco da ${_ch_libre_mib} MiB y hacen falta ${_ch_mib} MiB." >&2
711        echo "                          Nada que hacer sin encoger algo, y encoger pide otro medio." >&2; return 1; }
712    # **El hueco ENTERO, no el mínimo.** `$_ch_mib` es lo que hace falta para que entre el sistema;
713    # usar eso deja una raíz del tamaño justo y el resto del hueco sin asignar, que no le sirve a
714    # nadie. Quien dice «instalá en mi espacio libre» quiere su espacio libre: medido el 2026-10-02
715    # sobre un disco con 12 GiB sin asignar, la partición salía de 2 GiB —el mínimo— y la pantalla
716    # ya había prometido los 12. Un instalador que promete una cosa y hace otra es peor que uno que
717    # promete poco.
718    #
719    # Se recorta a múltiplo de MiB para no dejar una cola de sectores sueltos al final.
720    _ch_size=$(( (_ch_sect / 2048) * 2048 ))
721    _ch_usa_mib=$(( _ch_size / 2048 ))
722    if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
723        printf "    voy a AÑADIR una partición de %s MiB a la tabla de %s (las existentes no se tocan) [escribí si] " "$_ch_usa_mib" "$_ch_disco" >&2
724        read -r _ch_r; [ "$_ch_r" = "si" ] || { echo "    cancelado." >&2; return 1; }
725    fi
726    echo "    la partición nueva se lleva el hueco entero: ${_ch_usa_mib} MiB" >&2
727    # La lista de ANTES, para saber después cuál es la nueva por DIFERENCIA y no por su nombre.
728    # ⚠ El primer intento buscaba la línea por `start=<sector>` y nunca la encontraba: `sfdisk -d`
729    # **alinea los campos con espacios** (`start=     3614720`), así que el patrón sin espacios no
730    # casa jamás. El síntoma era idéntico al de un device que no aparece — y mandó a buscar el fallo
731    # en `partx` y en udev, que estaban bien. Comparar conjuntos no depende del formato.
732    _ch_lista_antes=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
733    _ch_antes=$(printf '%s\n' "$_ch_lista_antes" | grep -c . || true)
734    echo "start=$_ch_start, size=$_ch_size, type=linux, name=\"takana-root\"" \
735        | "$SFDISK" --append --quiet "$_ch_disco" >/dev/null 2>&1 \
736        || { echo "takana-install-partition: sfdisk --append falló sobre $_ch_disco" >&2; return 1; }
737    # El device nuevo tarda en aparecer (udev, o `partx` en un loop): esperarlo es obligatorio, y el
738    # nombre se LEE de la tabla en vez de construirlo a mano — `nvme0n1p4` y `sda4` no se escriben igual.
739    releer_tabla "$_ch_disco"
740    _ch_nuevo=""
741    _ch_i=0
742    while [ "$_ch_i" -lt 15 ]; do
743        _ch_lista_post=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
744        for _ch_d in $_ch_lista_post; do
745            case " $(printf '%s ' $_ch_lista_antes)" in
746                *" $_ch_d "*) ;;
747                *) _ch_nuevo="$_ch_d" ;;
748            esac
749        done
750        [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] && break
751        _ch_nuevo=""
752        _ch_i=$(( _ch_i + 1 )); sleep 1
753        releer_tabla "$_ch_disco"
754    done
755    [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] || {
756        echo "takana-install-partition: la partición se añadió pero su device no apareció (¿hace falta un partprobe?)" >&2; return 1; }
757    _ch_despues=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}' | grep -c . || true)
758    [ "$_ch_despues" = "$(( _ch_antes + 1 ))" ] || {
759        echo "takana-install-partition: la tabla pasó de $_ch_antes a $_ch_despues entradas — esperaba UNA más" >&2; return 1; }
760    echo "$_ch_nuevo"
761}
762
763# `--survey`: mirar y no tocar. Es el comando con el que se empieza en una máquina desconocida.
764if [ -n "$SOLO_CENSO" ]; then
765    if [ -n "$CENSO_JSON" ]; then censo_json; else mostrar_censo; fi
766    exit 0
767fi
768
769# ── `--pick`: ELEGIR sin tocar ────────────────────────────────────────────────────────────────
770# Existe por una razón que costó un susto: hasta hoy el orden era «saltá primero, elegí después»,
771# o sea que TODA la conversación con la persona pasaba DESPUÉS del kexec — en una máquina donde el
772# panel puede no volver a encenderse. El usuario dijo que sí al salto, la pantalla se fue a negro y
773# se quedó así: el instalador estaba vivo del otro lado esperando una tecla que nadie podía ver que
774# le estaba pidiendo.
775#
776# Con `--pick` la elección se hace ANTES, en el sistema que todavía funciona, y lo que pasa después
777# del salto ya no necesita pantalla: va con la respuesta puesta en la cmdline. Un panel muerto pasa
778# de ser un cuelgue a ser un detalle estético.
779#
780# No toca nada: no resuelve el device, no formatea, no escribe la tabla. Sólo imprime la elección.
781if [ -n "$SOLO_ELEGIR" ]; then
782    [ -t 0 ] || { echo "takana-install-partition: --pick necesita una terminal para preguntar" >&2; exit 2; }
783    elegir_destino || exit 1
784    # Se imprime en stdout y en el formato más tonto posible, para que lo lea un programa sin
785    # adivinar: una línea por respuesta, `CLAVE=valor`, nada más.
786    : > "$SOLO_ELEGIR"
787    [ -n "$DEST" ]     && echo "DEST=$DEST" >> "$SOLO_ELEGIR"
788    [ -n "$USE_FREE" ] && echo "FREE=1"     >> "$SOLO_ELEGIR"
789    [ -n "$DISK" ]     && echo "DISK=$DISK" >> "$SOLO_ELEGIR"
790    exit 0
791fi
792
793if [ -n "$NECESITA_ELEGIR" ]; then
794    if [ -t 0 ]; then
795        elegir_destino || exit 1
796    else
797        echo "takana-install-partition: no me diste destino y no hay terminal para preguntarte." >&2
798        echo "                          Pasá --root-part PARTUUID=… o --use-free-space --disk …" >&2
799        echo "                          (y mirá el terreno con --survey)" >&2
800        exit 2
801    fi
802fi
803
804# La identidad se resuelve ANTES de cualquier guarda: a partir de acá se trabaja con el device real,
805# y si el `PARTUUID` no existe en esta máquina se aborta sin haber tocado nada.
806if [ -n "$DEST" ]; then DEST=$(resolver_dev "$DEST") || exit 1; fi
807if [ -n "$ESP" ];  then ESP=$(resolver_dev "$ESP")   || exit 1; fi
808
809# `--use-free-space` sin `--root-part`: el destino todavía no existe, se crea en un hueco. Esto es
810# «instalar al lado», y es el camino que la política de la raíz viva usa cuando no hay kexec.
811if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
812    _ufs_disco="${DISK:-}"
813    if [ -z "$_ufs_disco" ] && [ -n "$ESP" ] && [ -b "$ESP" ]; then _ufs_disco=$(disco_de "$ESP"); fi
814    [ -n "$_ufs_disco" ] || { echo "takana-install-partition: --use-free-space necesita --disk (o un --esp real)" >&2; exit 2; }
815    echo "==> buscando espacio SIN ASIGNAR en $_ufs_disco"
816    DEST=$(crear_en_hueco "$_ufs_disco" "$(mib_necesarios)") || exit 1
817    echo "    partición creada: $DEST"
818fi
819
820# La ESP: si no se dijo cuál, se deduce del disco del destino. Una pregunta menos que hacerle al
821# usuario, y la respuesta correcta en el caso normal (un disco, una ESP).
822if [ -z "$ESP" ] && [ -n "$DEST" ] && [ -b "$DEST" ]; then
823    _esp_auto=$(esp_automatica "$(disco_de "$DEST")" 2>/dev/null || true)
824    [ -n "${_esp_auto:-}" ] && { ESP="$_esp_auto"; echo "==> ESP deducida: $ESP (del mismo disco que el destino)"; }
825fi
826
827if es_bloque "$DEST"; then
828    _n=$(nombre_kernel "$DEST")
829    # 1) DESTINO TIENE QUE SER UNA PARTICIÓN, NO UN DISCO. `/sys/class/block/<x>/partition` sólo
830    #    existe en particiones: es la diferencia entre formatear 200 G y formatear 1,8 T.
831    [ -r "/sys/class/block/$_n/partition" ] || {
832        echo "takana-install-partition: $DEST NO es una partición (¿es el disco entero?)." >&2
833        echo "                          Este instalador nunca escribe un disco completo." >&2; exit 1; }
834    # 2) NO PUEDE ESTAR MONTADA. mke2fs sobre un fs montado corrompe el que está en uso.
835    if grep -q "^$(readlink -f "$DEST") " /proc/mounts 2>/dev/null; then
836        echo "takana-install-partition: $DEST está MONTADA. Desmontala primero (o elegí otra)." >&2
837        grep "^$(readlink -f "$DEST") " /proc/mounts >&2; exit 1
838    fi
839    # 3) NO PUEDE SER LA RAÍZ VIVA. Es el caso que el usuario pregunta siempre: «¿puedo instalar
840    #    encima del Linux que estoy usando?». No desde él: hay que saltar antes a otro kernel/medio.
841    # Por número de dispositivo, no por nombre: ver `raiz_viva_majmin`. El fallback por nombre se
842    # conserva porque no estorba y cubre el caso en que `stat` no exista.
843    _rootdev=$(awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null || true)
844    if es_la_raiz_viva "$DEST" || \
845       [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
846        politica_raiz_viva      # kexec → espacio libre → abortar, según --on-active-root
847    fi
848    # 4) NO PUEDE SER LA ESP que vamos a reusar.
849    [ -n "$ESP" ] && [ "$(readlink -f "$DEST")" = "$(readlink -f "$ESP")" ] && {
850        echo "takana-install-partition: --root-part y --esp son la MISMA partición" >&2; exit 1; }
851    # 5) ¿ENTRA? Esto AVISA y no aborta, a propósito. El censo esconde del menú lo que no llega al
852    #    piso de `mib_necesarios` —que es el mismo con el que se talla un hueco, generoso a
853    #    propósito—, pero quien nombra una partición con `--root-part` la está nombrando a mano y
854    #    puede saber que su destino justo entra: el banco de pruebas instala en 1200 MiB y funciona.
855    #    Abortar acá rechazaría configuraciones que andan; callarse dejaría el fallo para la mitad
856    #    de la copia, con el destino ya formateado. Avisar es lo único que no miente.
857    _mib_dest=$(mib_de_sectores "$(cat "/sys/class/block/$_n/size" 2>/dev/null || echo 0)")
858    _mib_pide=$(mib_minimos)
859    if [ "${_mib_dest:-0}" -gt 0 ] && [ "$_mib_dest" -lt "$_mib_pide" ] 2>/dev/null; then
860        echo "    ⚠ $DEST son ${_mib_dest} MiB y el sistema pide ~${_mib_pide} MiB con holgura." >&2
861        echo "      Sigo porque lo nombraste vos; si no entra, la copia falla con el destino ya formateado." >&2
862    fi
863else
864    [ -f "$DEST" ] || { echo "takana-install-partition: $DEST no existe (ni bloque ni fichero)" >&2; exit 1; }
865fi
866
867# ── politica_raiz_viva — el «saltito»: qué hacer si el destino ES el sistema que corre ────────
868# La pregunta que todo el mundo hace es «¿puedo poner takana encima del Linux que estoy usando, sin
869# USB?». La respuesta honesta tiene tres partes y este bloque las implementa en orden:
870#
871#   1. Desde adentro, NO. Un filesystem montado y en uso no se formatea; eso es el kernel, no una
872#      política nuestra.
873#   2. Pero se puede SALIR de adentro sin reiniciar por el firmware: `kexec` salta a otro kernel con
874#      un instalador en RAM, y en ese salto el sistema viejo deja de correr ⇒ su disco queda libre.
875#      Es el ADR 0017 §1, y el syscall ya estaba implementado (`kexec_file_load`, sin kexec-tools).
876#   3. Y si esta máquina no puede saltar —kernel sin `CONFIG_KEXEC_FILE`, o lockdown por Secure
877#      Boot— no se fuerza nada: se instala AL LADO, en espacio sin asignar, y el usuario elige en el
878#      arranque. Peor camino, cero riesgo.
879#
880# `--on-active-root` permite fijar uno de los tres a mano; `auto` es la política de arriba.
881politica_raiz_viva() {
882    echo "⚠ $DEST es la RAÍZ VIVA: el sistema que está corriendo ahora mismo."
883    echo "  Un filesystem montado y en uso no se puede formatear desde adentro."
884    case "$ON_ACTIVE" in
885        abort)
886            echo "  --on-active-root=abort ⇒ no hago nada más." >&2; exit 1 ;;
887    esac
888    _prv_kexec=no
889    if [ "$ON_ACTIVE" = auto ] || [ "$ON_ACTIVE" = kexec ]; then
890        echo "==> ¿puede esta máquina saltar a otro kernel sin pasar por el firmware?"
891        if _prv_out=$("$HAMMER" kernel kexec --probe 2>&1); then _prv_kexec=si; fi
892        printf '%s\n' "$_prv_out" | sed 's/^/    /'
893    fi
894    if [ "$_prv_kexec" = si ]; then
895        camino_kexec    # no vuelve: salta
896        exit 0
897    fi
898    if [ "$ON_ACTIVE" = kexec ]; then
899        echo "  --on-active-root=kexec y esta máquina no puede saltar (motivos arriba)." >&2; exit 1
900    fi
901    echo "==> sin kexec ⇒ INSTALO AL LADO, en espacio sin asignar (la raíz viva no se toca)"
902    _prv_disco=$(disco_de "$DEST")
903    DEST=$(crear_en_hueco "$_prv_disco" "$(mib_necesarios)") || {
904        echo "takana-install-partition: no hay hueco donde instalar al lado, y sin kexec no hay otra" >&2
905        echo "                          desde este sistema. Queda el camino del medio externo (USB)." >&2
906        exit 1; }
907    echo "    destino nuevo: $DEST (la raíz viva sigue intacta)"
908}
909
910# ── camino_kexec — armar el instalador en RAM y saltar. NO VUELVE ─────────────────────────────
911camino_kexec() {
912    echo "==> camino KEXEC: el instalador viaja en RAM y el sistema viejo muere en el salto"
913    # ⚠ **La elección viaja por la CMDLINE, no horneada en el initramfs.** Un initramfs con el
914    # destino adentro sirve para una máquina y hay que rehacerlo para la siguiente; el mismo fichero
915    # con la elección en la cmdline sirve para todas. Y va por **PARTUUID**: el kernel de después
916    # del salto puede enumerar los discos con otros nombres (medido), así que `/dev/nvme1n1p6` es
917    # justamente lo que NO hay que pasar.
918    _ck_ru=$(partuuid_de "$DEST" 2>/dev/null || true)
919    _ck_eu=""; [ -n "$ESP" ] && _ck_eu=$(partuuid_de "$ESP" 2>/dev/null || true)
920    _ck_cl="${CMDLINE_SALTO:-console=ttyS0,115200 console=tty0}"
921    if [ -n "$_ck_ru" ]; then _ck_cl="$_ck_cl takana.root=PARTUUID=$_ck_ru"
922    else                      _ck_cl="$_ck_cl takana.root=$DEST"; fi
923    if [ -n "$_ck_eu" ]; then _ck_cl="$_ck_cl takana.esp=PARTUUID=$_ck_eu"
924    elif [ -n "$ESP" ];  then _ck_cl="$_ck_cl takana.esp=$ESP"; fi
925    [ -n "$ASSUME_YES" ] && _ck_cl="$_ck_cl takana.yes"
926    _ck_ird="${IRD_INSTALADOR:-work/takana-instalador.cpio.gz}"
927    OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh || {
928        echo "takana-install-partition: no pude armar el instalador en RAM" >&2; exit 1; }
929    _ck_art=$(dirname "$(dirname "$KERNEL")")     # …/<hash>-linux-metal/boot/bzImage → el artefacto
930    echo
931    echo "  kernel:  $_ck_art"
932    echo "  initrd:  $_ck_ird ($(du -h "$_ck_ird" | cut -f1)) — genérico, sin esta máquina adentro"
933    echo "  destino: $DEST"
934    echo "  cmdline: $_ck_cl"
935    if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
936        printf "  ⚡ ESTE COMANDO MATA EL SISTEMA QUE ESTÁS USANDO y arranca el instalador. [escribí saltar] "
937        read -r _ck_r; [ "$_ck_r" = "saltar" ] || { echo "  cancelado — no se saltó y no se tocó nada."; exit 1; }
938    fi
939    # ⚠ **El orden de las `console=` decide cuál es `/dev/console`: gana la ÚLTIMA.** Con
940    # `console=tty0 console=ttyS0,115200` la consola es el SERIAL, y un portátil no tiene puerto
941    # serie: el instalador arrancaría y la pantalla se quedaría en negro con todo pasando en un
942    # cable que no existe. Es el bug que este repo ya pagó dos veces (ver `arranque-visible-cada-capa`).
943    # Por eso acá tty0 va ÚLTIMO: pantalla primero, serial de regalo para quien lo tenga.
944    "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" --cmdline "$_ck_cl" --jump
945    echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
946    exit 1
947}
948
949# ── CONSERVAR UNA PARTICIÓN (`--keep`) — el /home que se queda donde está ──────────────────────
950#
951# El pedido, textual: «/home, al menos, quiero que quede donde está». Conservarlo es fácil —basta
952# con NO formatearlo—; lo difícil es que el sistema nuevo pueda USARLO, y eso es una decisión que no
953# se puede dejar a una cara gráfica:
954#
955#   **Si el usuario que crea takana no tiene el mismo uid que el del sistema viejo, los archivos
956#   quedan de otro dueño y el escritorio arranca sin poder escribir en su propia carpeta.**
957#
958# Decidido (2026-09-25): takana **crea los usuarios con el uid que encuentra**, todos los que haya,
959# y el login por defecto es el NOMBRE DEL DIRECTORIO en /home. No se hace `chown` de nada: recorrer
960# cientos de miles de ficheros es lento, es destructivo sobre los metadatos y pierde el reparto si
961# había más de un dueño. El uid de los datos manda; el del sistema nuevo se adapta.
962#
963# Las tres condiciones son de este lado y se cumplen acá:
964#   1. NO se formatea. Es el punto entero.
965#   2. Se comprueba que MONTA y que tiene espacio **antes** de tocar la raíz. Descubrir que el /home
966#      que se iba a conservar no monta, con la raíz ya formateada, es el peor momento posible.
967#   3. Se DECLARA en el plan, junto a lo que sí se borra.
968
969# keep_parsear — <spec>:<punto>. El punto se corta por el ÚLTIMO «:» porque el spec puede traerlo
970# (`UUID=…` no, pero un `/dev/…` raro sí, y un LABEL con dos puntos es legal).
971keep_parsear() {
972    KEEP_PUNTO="${KEEP_SPEC##*:}"
973    _kp_spec="${KEEP_SPEC%:*}"
974    [ -n "$_kp_spec" ] && [ "$_kp_spec" != "$KEEP_SPEC" ] || {
975        echo "takana-install-partition: --keep se escribe <partición>:<punto>, p.ej. PARTUUID=…:/home" >&2; exit 2; }
976    # Un solo punto admitido hoy, y se dice en voz alta en vez de aceptar cualquiera y montar mal.
977    # /usr o /etc conservados de otro sistema no es «conservar datos»: es arrancar un híbrido.
978    [ "$KEEP_PUNTO" = "/home" ] || {
979        echo "takana-install-partition: --keep hoy sólo admite /home (pediste '$KEEP_PUNTO')." >&2
980        echo "                          Conservar /usr o /etc de otro sistema no es conservar datos:" >&2
981        echo "                          es arrancar un híbrido de dos distros. No se ofrece." >&2; exit 2; }
982    KEEP_DEV=$(resolver_dev "$_kp_spec") || exit 1
983}
984
985# keep_validar — todas las guardas + la cosecha de usuarios. Corre ANTES de la confirmación, para
986# que el plan pueda decir a quién va a crear. Deja la lista en $KEEP_USUARIOS (login:uid:gid).
987KEEP_USUARIOS="${TMPDIR:-/tmp}/takana-keep-usuarios.$$"
988keep_validar() {
989    es_bloque "$KEEP_DEV" || { echo "takana-install-partition: --keep $KEEP_DEV no es un dispositivo de bloque" >&2; exit 1; }
990    # 1) No puede ser el destino ni la ESP: conservar lo que se va a formatear no significa nada, y
991    #    que el error sea «no significa nada» no impide que borre.
992    [ "$(readlink -f "$KEEP_DEV")" = "$(readlink -f "$DEST")" ] && {
993        echo "takana-install-partition: --keep y --root-part son la MISMA partición ($KEEP_DEV)." >&2
994        echo "                          El destino SE FORMATEA: no se puede conservar." >&2; exit 1; }
995    [ -n "$ESP" ] && [ "$(readlink -f "$KEEP_DEV")" = "$(readlink -f "$ESP")" ] && {
996        echo "takana-install-partition: --keep es la ESP ($KEEP_DEV). La ESP no es un /home." >&2; exit 1; }
997    # 2) Ni la raíz viva: es la que el kexec va a soltar, y montarla como /home del sistema nuevo
998    #    mete un árbol de sistema entero dentro de /home.
999    es_la_raiz_viva "$KEEP_DEV" && {
1000        echo "takana-install-partition: --keep $KEEP_DEV es la RAÍZ VIVA, no un /home suelto." >&2; exit 1; }
1001    # 3) El UUID del FILESYSTEM, que es con lo que se va a montar en el arranque.
1002    #    ⚠ Por UUID y no por PARTUUID **porque el `findfs` de busybox no entiende PARTUUID**: sólo
1003    #    LABEL y UUID (medido, 1.36.1). Un findfs PARTUUID=… sale VACÍO sin error, y el resultado
1004    #    sería un arranque sin /home con todo aparentemente bien. El PARTUUID se anota igual.
1005    KEEP_UUID=$(blkid "$KEEP_DEV" 2>/dev/null | grep -o ' UUID="[^"]*"' | head -1 | sed 's/^ UUID="//; s/"$//')
1006    [ -n "$KEEP_UUID" ] || KEEP_UUID=$(blkid -s UUID -o value "$KEEP_DEV" 2>/dev/null || true)
1007    [ -n "$KEEP_UUID" ] || {
1008        echo "takana-install-partition: $KEEP_DEV no tiene UUID de filesystem — no hay nada formateado ahí." >&2
1009        echo "                          Conservar una partición cruda no conserva ningún dato." >&2; exit 1; }
1010    KEEP_PARTUUID=$(partuuid_de "$KEEP_DEV" 2>/dev/null || true)
1011    # 4) ¿MONTA? Ésta es la guarda que el pedido pone primero, y con razón.
1012    #    ⚠ Si YA está montada, se lee de donde está y **no se desmonta**: un `umount` de un montaje
1013    #    que no hicimos nosotros puede propagarse al original (medido en este repo con /sys/fs/cgroup).
1014    KEEP_DIR=$(awk -v d="$(readlink -f "$KEEP_DEV")" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
1015    if [ -n "$KEEP_DIR" ]; then
1016        echo "    (el /home a conservar ya está montado en $KEEP_DIR: lo leo de ahí y NO lo desmonto)"
1017    else
1018        KEEP_DIR="/run/takana-keep"
1019        mkdir -p "$KEEP_DIR"
1020        mount -o ro,nodev,nosuid,noexec "$KEEP_DEV" "$KEEP_DIR" 2>/dev/null || {
1021            echo "takana-install-partition: $KEEP_DEV NO SE PUEDE MONTAR (probé sólo-lectura)." >&2
1022            echo "                          No sigo: conservar un /home que no monta dejaría la raíz" >&2
1023            echo "                          formateada y los datos inalcanzables." >&2; exit 1; }
1024        KEEP_MONTE_YO=1
1025    fi
1026    # 5) La cosecha: un usuario por DIRECTORIO de primer nivel, con el dueño de ese directorio.
1027    cosechar_usuarios "$KEEP_DIR" "$KEEP_USUARIOS"
1028    # 6) ¿Tiene espacio? Avisa y no aborta: un /home lleno es un problema del usuario, no un fallo
1029    #    de la instalación. Pero callarlo lo convierte en «el escritorio no arranca» la semana que viene.
1030    _kv_libre=$(df -m "$KEEP_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || echo "")
1031    case "${_kv_libre:-}" in
1032        ''|*[!0-9]*) ;;
1033        *) [ "$_kv_libre" -lt 512 ] && echo "    ⚠ al /home conservado le quedan ${_kv_libre} MiB libres: el escritorio nuevo va a pelear por espacio." ;;
1034    esac
1035    keep_desmontar
1036}
1037
1038keep_desmontar() {
1039    [ -n "$KEEP_MONTE_YO" ] || return 0
1040    umount "$KEEP_DIR" 2>/dev/null || echo "    ⚠ no pude desmontar $KEEP_DIR (sigo: sólo lo leí)." >&2
1041    KEEP_MONTE_YO=""
1042}
1043
1044# keep_aplicar <árbol> — mete los usuarios y la declaración del montaje en la raíz NUEVA.
1045# Corre DESPUÉS de armar el árbol y ANTES del `mke2fs`: si algo falla acá, todavía no se destruyó nada.
1046keep_aplicar() {
1047    _ka_t="$1"
1048    usuarios_aplicar "$_ka_t" "$KEEP_USUARIOS"
1049    # 2) La declaración que lee el initramfs de pivote justo antes del switch_root.
1050    mkdir -p "$_ka_t/etc/takana"
1051    {
1052        echo "# Lo escribe 'takana metal install --keep' y lo lee el initramfs de pivote."
1053        echo "# Monta por HOME_UUID: el findfs de busybox NO entiende PARTUUID (medido)."
1054        echo "HOME_UUID=$KEEP_UUID"
1055        echo "HOME_PARTUUID=${KEEP_PARTUUID:-}"
1056        echo "HOME_DEV_AL_INSTALAR=$KEEP_DEV"
1057    } > "$_ka_t/etc/takana/home.conf"
1058    echo "    ✓ /home conservado declarado (UUID=$KEEP_UUID) — esa partición NO se formatea"
1059}
1060
1061# ── El terreno, DICHO EN VOZ ALTA antes de tocar nada (ADR 0018 §4) ───────────────────────────
1062# `--keep` se valida ACÁ, antes de decir el plan: así el plan puede nombrar lo que se conserva y a
1063# quién se va a crear, y un /home que no monta aborta con el disco todavía intacto.
1064if [ -n "$KEEP_SPEC" ]; then
1065    keep_parsear
1066    echo "==> conservar: $KEEP_DEV → $KEEP_PUNTO   (NO se formatea)"
1067    keep_validar
1068fi
1069# `--adopt` se valida ACÁ por lo mismo que `--keep`: para que el plan pueda decir qué se mueve y a
1070# quién se va a crear, y para que un destino sin sistema adentro aborte con el disco todavía intacto.
1071if [ -n "$ADOPT" ]; then
1072    echo "==> adoptar: $DEST   (NO se formatea — el sistema de ahí se muda a un subdirectorio)"
1073    adopt_validar
1074fi
1075echo "==> destino: $DEST   ($( [ -b "$DEST" ] && echo 'partición real' || echo 'fichero de prueba'))"
1076if [ -b "$DEST" ] && command -v blkid >/dev/null 2>&1; then
1077    if [ -n "$ADOPT" ]; then
1078        echo "    lo que hay HOY ahí (NO se pierde: se mueve a /$ADOPT_SUB):"
1079    else
1080        echo "    lo que hay HOY ahí (se va a PERDER):"
1081    fi
1082    blkid "$DEST" 2>/dev/null | sed 's/^/      /' || echo "      (sin filesystem reconocible)"
1083fi
1084if [ -n "$ESP" ] && [ -b "$ESP" ] && [ -x "$HAMMER" ]; then
1085    _d="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$ESP")/..")")}"
1086    echo "    terreno de $_d (nada de esto se toca):"
1087    "$HAMMER" boot entry survey --disk "$_d" 2>&1 | sed 's/^/      /' || true
1088fi
1089if [ -n "$KEEP_SPEC" ]; then
1090    echo "    lo que se CONSERVA (no se formatea): $KEEP_DEV en $KEEP_PUNTO · UUID=$KEEP_UUID"
1091    if [ -s "$KEEP_USUARIOS" ]; then
1092        echo "    usuarios que voy a crear en el sistema nuevo, con el uid que ya tienen sus archivos:"
1093        while IFS=':' read -r _pl_n _pl_u _pl_g; do
1094            [ -n "$_pl_n" ] && printf "      · %s (uid %s, gid %s) → /home/%s\n" "$_pl_n" "$_pl_u" "$_pl_g" "$_pl_n"
1095        done < "$KEEP_USUARIOS"
1096        # Lo que NO se resuelve solo, dicho antes y no después: es la trampa 2 del pedido de churay,
1097        # y la instantánea de dotfiles que la desarma vive del lado de la suite, no acá.
1098        echo "    ⚠ los dotfiles del sistema viejo se conservan tal cual. Un .config de una app vieja"
1099        echo "      puede hacer que la app nueva arranque rota, y el síntoma no menciona el archivo."
1100    else
1101        echo "    (no encontré ningún usuario ahí: se conserva igual, montado en $KEEP_PUNTO)"
1102    fi
1103fi
1104if [ -n "$ADOPT" ]; then
1105    echo "    se MUEVE a /$ADOPT_SUB (sigue entero, no se copia ni se borra):"
1106    echo "      $ADOPT_MUEVE"
1107    echo "    se QUEDA donde está, con todos sus archivos: /home"
1108    if [ -s "$ADOPT_USUARIOS" ]; then
1109        echo "    usuarios que voy a crear en el sistema nuevo, con el uid que ya tienen sus archivos:"
1110        while IFS=':' read -r _pa_n _pa_u _pa_g; do
1111            [ -n "$_pa_n" ] && printf "      · %s (uid %s, gid %s) → /home/%s\n" "$_pa_n" "$_pa_u" "$_pa_g" "$_pa_n"
1112        done < "$ADOPT_USUARIOS"
1113        echo "    ⚠ los dotfiles del sistema viejo se conservan tal cual. Un .config de una app vieja"
1114        echo "      puede hacer que la app nueva arranque rota, y el síntoma no menciona el archivo."
1115    else
1116        echo "    (no hay ningún /home con usuarios adentro: se adopta igual)"
1117    fi
1118fi
1119if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
1120    if [ -n "$ADOPT" ]; then
1121        printf "    ¿muevo el sistema de %s a /%s e instalo encima, SIN formatear? [escribí si] " "$DEST" "$ADOPT_SUB"
1122    else
1123        printf "    ¿formateo %s y dejo el resto del disco INTACTO? [escribí si] " "$DEST"
1124    fi
1125    read -r _r; [ "$_r" = "si" ] || { echo "    cancelado."; exit 1; }
1126fi
1127
1128# ── 1) Los árboles (initramfs de pivote + rootfs con su /sbin/init) los arma el script VALIDADO ──
1129# No se copian acá: se PIDEN. Ver el comentario de `STAGE_ONLY` en install-image-efi.sh.
1130STAGE="${STAGE:-work/.install-part-stage}"
1131# `STAGE_LISTO=1` — el árbol ya está armado y no hay que (ni se puede) rearmarlo. Es lo que usa el
1132# instalador cuando corre DENTRO de un initramfs: allí no hay repo, ni store, ni `install-image-efi.sh`;
1133# el stage viajó horneado en el propio initramfs. Se comprueba que esté completo, no se confía.
1134if [ -n "${STAGE_LISTO:-}" ]; then
1135    echo "==> stage ya armado: $STAGE (STAGE_LISTO=1)"
1136else
1137    echo "==> armando árboles (STAGE_ONLY sobre install-image-efi.sh)"
1138    STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
1139fi
1140ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
1141[ -d "$ROOT_TREE" ] && [ -r "$IRD" ] || { echo "takana-install-partition: el stage no quedó completo" >&2; exit 1; }
1142
1143# ── EL FIRMWARE DEL ARRANQUE, DECIDIDO POR LA MÁQUINA QUE SE ESTÁ INSTALANDO ─────────────────────
1144# Va acá, con el IRD ya validado y antes de que se copie a la ESP (y antes del cálculo de espacio,
1145# que es lo que se mide contra los 64 MiB de la partición de arranque).
1146#
1147# El instalador corre en el metal de destino y **arrancó con el mismo problema** —su initramfs
1148# tampoco trae firmware—, así que su propio `dmesg` ya tiene la lista EXACTA de blobs que este
1149# hardware pidió, con el nombre que el driver usó. No se adivina nada: se leen de ahí y se copian
1150# del árbol que se va a instalar. En una máquina donde todo cargó bien, la lista sale vacía y el
1151# initramfs queda bit a bit igual.
1152echo "══ el firmware que este hardware pidió al arrancar ══════════════════════════"
1153firmware_al_initramfs "$IRD" "$ROOT_TREE"
1154
1155# El /home conservado se declara en el ÁRBOL, antes de que el árbol entre al filesystem nuevo. Si
1156# hay un choque de uid, `keep_aplicar` aborta acá — con el destino todavía sin tocar.
1157if [ -n "$KEEP_SPEC" ]; then keep_aplicar "$ROOT_TREE"; fi
1158# El `/home` adoptado NO se declara en `home.conf`: no es otra partición que haya que montar, está
1159# dentro de la raíz. Lo único que hace falta es que sus dueños existan en el sistema nuevo.
1160if [ -n "$ADOPT" ] && [ -s "$ADOPT_USUARIOS" ]; then usuarios_aplicar "$ROOT_TREE" "$ADOPT_USUARIOS"; fi
1161
1162# ── LA CUENTA: se pregunta acá y se escribe acá, con el destino TODAVÍA SIN TOCAR ─────────────────
1163# El orden importa en las dos direcciones. Después de `usuarios_aplicar`, para que un usuario ya
1164# cosechado del `/home` conservado sólo reciba su contraseña en vez de crearse dos veces (ése es el
1165# caso del operador: `sergio` existía con el uid de sus archivos y `!` por contraseña). Y antes del
1166# `mke2fs`, por el mismo motivo que `keep_aplicar`: si esto falla, nada se destruyó.
1167echo "══ la cuenta de quien va a usar la máquina ═══════════════════════════════════"
1168cuenta_preguntar "$ROOT_TREE"
1169cuenta_aplicar "$ROOT_TREE" \
1170    || { echo "takana-install-partition: la cuenta NO quedó escrita; no sigo y NADA se formateó." >&2; exit 1; }
1171
1172# ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
1173# `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
1174# montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
1175# este script existe para no tener. Si no somos root, `unshare -r` da los dueños root sin sudo.
1176if [ -n "$ADOPT" ]; then
1177echo "==> adoptando $DEST: el sistema anterior se muda y el nuevo se instala encima (NO se formatea)"
1178adopt_mudar "$ROOT_TREE"
1179else
1180echo "==> ext4 LABEL=hammer-root en $DEST (única escritura destructiva de todo el script)"
1181# ⚠ La detección de root NO puede fallar en silencio. Medido dentro del initramfs el 2026-09-23: sin
1182# el applet `id` el `$(id -u)` quedaba VACÍO, la comparación daba falso, el script se iba a la rama
1183# de `unshare` —que tampoco estaba— y **siguió adelante sin formatear nada**. Un `if` cuyo dato de
1184# entrada no existe elige una rama igual, y elige mal.
1185_uid=$(id -u 2>/dev/null || echo "")
1186[ -n "$_uid" ] || { echo "takana-install-partition: no hay 'id' en este entorno y no puedo saber si soy root" >&2; exit 1; }
1187if [ "$_uid" = "0" ]; then
1188    "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
1189else
1190    command -v unshare >/dev/null 2>&1 || {
1191        echo "takana-install-partition: no soy root (uid $_uid) y no hay 'unshare' para suplirlo" >&2; exit 1; }
1192    unshare -r "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
1193fi
1194fi
1195# Y no se da por hecho: se mira. Regla 3 del CLAUDE.md — un ausente falla ruidosamente, un vacío
1196# llega hasta el final diciendo que todo fue bien. Acá el vacío sería un destino sin filesystem.
1197# ⚠ La verificación tiene que funcionar con LOS DOS `blkid`, y la primera versión no lo hacía:
1198# `-s LABEL -o value` es de util-linux y **busybox lo ignora**, devolviendo la línea entera
1199# (`/dev/vda2: LABEL="hammer-root" UUID=…`). Resultado medido en QEMU: el formateo había salido
1200# PERFECTO y el guardián abortó igual, con un mensaje que mostraba la label correcta dentro del
1201# texto de «no quedó con LABEL». Tercer caso hoy del mismo patrón: un chequeo que valida algo
1202# distinto de lo que cree validar acusa al inocente, y un guardián así se desactiva a la semana.
1203# ⚠ Y el patrón tiene que distinguir `LABEL` de **`PARTLABEL`**, que es otra cosa: el primero es la
1204# etiqueta del FILESYSTEM y el segundo el nombre de la ENTRADA GPT. Con `sed 's/.*LABEL="…"/'` el
1205# `.*` es glotón y se come el «PART», así que devolvía el nombre de la partición en la tabla
1206# («destino-takana») y el guardián abortaba una instalación correcta. Segunda vez que este mismo
1207# chequeo acusa al inocente por mirar mal el dato. El espacio de ` LABEL="` es lo que los separa.
1208_lbl_post=$(blkid "$DEST" 2>/dev/null | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
1209[ -n "$_lbl_post" ] || _lbl_post=$(blkid -s LABEL -o value "$DEST" 2>/dev/null || true)
1210[ "$_lbl_post" = "hammer-root" ] || {
1211    echo "takana-install-partition: $DEST NO quedó con LABEL=hammer-root (blkid dice '${_lbl_post:-nada}')." >&2
1212    echo "                          El formateo no se completó: no sigo." >&2; exit 1; }
1213if [ -n "$ADOPT" ]; then
1214    echo "    ✓ $DEST adoptada y poblada (LABEL=hammer-root verificada) — el sistema anterior en /$ADOPT_SUB"
1215else
1216    echo "    ✓ $DEST formateada y poblada (LABEL=hammer-root verificada)"
1217fi
1218
1219# ── 3) La ESP: sólo nuestra carpeta de vendor, con mtools y SIN montar ─────────────────────────
1220if [ -n "$ESP" ]; then
1221    # ⚠ **mtools puede QUEDARSE ESPERANDO UNA RESPUESTA, y colgado es indistinguible de lento.**
1222    # Medido en QEMU el 2026-09-23: sobre `/dev/vda1` el instalador se detuvo para siempre justo
1223    # después de imprimir el espacio libre, sin una línea más. Dos defensas, porque una sola tapa el
1224    # síntoma sin quitar la causa:
1225    #   · `MTOOLS_SKIP_CHECK=1` desactiva el chequeo de geometría, que es lo que hace preguntar
1226    #     cuando el device no se parece a un disquete;
1227    #   · `< /dev/null` en cada invocación, para que una pregunta que igual aparezca FALLE en vez de
1228    #     esperar. Un error se lee; un cuelgue en un instalador parece un sistema muerto.
1229    export MTOOLS_SKIP_CHECK=1
1230    "$MDIR" -i "$ESP" :: >/dev/null 2>&1 < /dev/null || { echo "takana-install-partition: $ESP no parece FAT (¿es la ESP?)" >&2; exit 1; }
1231    # Guardián de capacidad: una ESP de fábrica son 100 MiB y Windows ya vive ahí. El margen se
1232    # comprueba ANTES, con los números a la vista, no como un «disk full» a mitad de copiar.
1233    # ⚠ `mdir` imprime el espacio libre con SEPARADOR DE MILES en espacios («60 056 064 bytes
1234    # free»), así que tomar `$1` devuelve **60** y el guardián aborta una instalación perfectamente
1235    # posible. Medido en la primera corrida de este script. Hay que juntar los dígitos de la línea.
1236    _free=$("$MDIR" -i "$ESP" :: 2>/dev/null < /dev/null | awk '/bytes free/ {l=$0; sub(/ bytes free.*/,"",l); gsub(/[^0-9]/,"",l); print l}' | tail -1)
1237    _need=$(( $(wc -c < "$KERNEL") + $(wc -c < "$IRD") ))
1238    [ -n "${_free:-}" ] && [ "$_free" -lt "$_need" ] && {
1239        echo "takana-install-partition: la ESP tiene ${_free}B libres y hacen falta ${_need}B." >&2
1240        echo "                          No se escribió nada en ella." >&2; exit 1; }
1241    # ⚠ **Un FAT puede declarar MÁS espacio del que su partición tiene, y nadie se queja hasta que
1242    # una escritura grande cae más allá del final.** Medido acá el 2026-09-23 fabricando el disco de
1243    # prueba: `mformat -i disco.img@@<offset>` formatea usando el tamaño del FICHERO ENTERO, no el de
1244    # la partición ⇒ una ESP de 64 MiB con un FAT que anuncia 2,5 G. Y no es un artefacto del banco de
1245    # pruebas: cualquier ESP que alguien encogió con una herramienta de particionado sin reformatear
1246    # queda igual. El `df` del kernel repite la mentira —la leyó del mismo sector de arranque— así que
1247    # NO alcanza con preguntarle al filesystem: hay que contrastar contra el TAMAÑO REAL del
1248    # contenedor, que es lo único que no puede mentir.
1249    if [ -b "$ESP" ]; then
1250        _cap=$(blockdev --getsize64 "$ESP" 2>/dev/null || echo "")
1251        [ -z "$_cap" ] && _cap=$(( $(cat "/sys/class/block/$(nombre_kernel "$ESP")/size" 2>/dev/null || echo 0) * 512 ))
1252    else
1253        _cap=$(wc -c < "$ESP")
1254    fi
1255    if [ -n "${_free:-}" ] && [ "${_cap:-0}" -gt 0 ] && [ "$_free" -gt "$_cap" ]; then
1256        echo "    ! el FAT de $ESP dice ${_free}B libres pero la partición mide ${_cap}B: el filesystem" >&2
1257        echo "      declara más espacio del que existe (ESP encogida sin reformatear, o formateada" >&2
1258        echo "      sobre un contenedor mayor). Uso el tamaño REAL como cota." >&2
1259        _free="$_cap"
1260    fi
1261    echo "==> ESP $ESP: \\EFI\\takana\\ (${_need}B sobre ${_free:-?}B libres)"
1262    "$MMD" -i "$ESP" ::/EFI 2>/dev/null < /dev/null || true
1263    "$MMD" -i "$ESP" ::/EFI/takana 2>/dev/null < /dev/null || true
1264    "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/takana/takanax64.efi < /dev/null
1265    "$MCOPY" -o -i "$ESP" "$IRD"    ::/initramfs.cpio.gz < /dev/null
1266    # La ruta fallback es tierra de nadie: si YA hay algo, es de otro sistema y no se pisa sin que
1267    # el usuario lo pida. Si no hay nada, ocuparla es gratis y salva el caso «el firmware ignora la
1268    # entrada NVRAM», que es el modo de fallo más común y el más difícil de diagnosticar.
1269    if "$MDIR" -i "$ESP" ::/EFI/BOOT/BOOTX64.EFI >/dev/null 2>&1 < /dev/null; then
1270        if [ -n "$CLAIM_FALLBACK" ]; then
1271            "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI < /dev/null
1272            echo "    ! \\EFI\\BOOT\\BOOTX64.EFI PISADO (--claim-fallback)"
1273        else
1274            echo "    · \\EFI\\BOOT\\BOOTX64.EFI ya existe y es de otro: NO se toca (--claim-fallback para pisarlo)"
1275        fi
1276    else
1277        "$MMD" -i "$ESP" ::/EFI/BOOT 2>/dev/null < /dev/null || true
1278        "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI < /dev/null
1279        echo "    + \\EFI\\BOOT\\BOOTX64.EFI estaba libre ⇒ ocupada (arranca sin depender de la NVRAM)"
1280    fi
1281    for _p in ::/EFI/takana/takanax64.efi ::/initramfs.cpio.gz; do
1282        "$MDIR" -i "$ESP" "$_p" >/dev/null 2>&1 < /dev/null || { echo "takana-install-partition: falta $_p en la ESP" >&2; exit 1; }
1283    done
1284    echo "    ✓ ESP poblada y verificada (sin montarla)"
1285else
1286    echo "    ! sin --esp: el sistema queda instalado pero NO arrancable todavía"
1287fi
1288
1289# ── 4) La entrada de arranque: convivir con el vecino, con respaldo previo ─────────────────────
1290if [ -z "$NO_NVRAM" ] && [ -n "$ESP" ] && [ -b "$ESP" ] && [ -d /sys/firmware/efi/efivars ] && [ -x "$HAMMER" ]; then
1291    if [ -z "$DISK" ] || [ -z "$ESP_PART" ]; then
1292        _en=$(nombre_kernel "$ESP")
1293        DISK="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$_en/..")")}"
1294        ESP_PART="${ESP_PART:-$(cat "/sys/class/block/$_en/partition" 2>/dev/null || echo 1)}"
1295    fi
1296    echo "==> respaldo del arranque ANTES de tocar la NVRAM (ADR 0018 §5)"
1297    "$HAMMER" boot entry backup || echo "    ! el respaldo falló — seguí sólo si sabés por qué" >&2
1298    echo "==> entrada NVRAM: $DISK p$ESP_PART → \\EFI\\takana\\takanax64.efi"
1299    "$HAMMER" boot entry add --disk "$DISK" --partition "$ESP_PART" --loader '/EFI/takana/takanax64.efi'
1300else
1301    echo "    · NVRAM no tocada (--no-nvram, sin efivarfs, o destino de prueba)"
1302fi
1303
1304if [ -n "$USE_FREE" ]; then
1305    echo "✓ takana instalada en $DEST, una partición NUEVA creada en espacio sin asignar."
1306    echo "  La tabla creció en una entrada; ninguna de las que ya estaban se modificó."
1307else
1308    echo "✓ takana instalada en $DEST. La tabla de particiones NO se tocó y las demás particiones siguen como estaban."
1309fi
1310echo "  Comprobalo antes de reiniciar:  lsblk -o NAME,SIZE,FSTYPE,LABEL  ·  $HAMMER boot entry list"