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 instalador deja de ser «para esta máquina»: censo del terreno, menú y cmdline
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# (sin destino) modo INTERACTIVO: muestra el censo y te deja elegir. Es lo que hace
36# que un mismo medio sirva en cualquier máquina.
37# --use-free-space no formatear nada existente: crear la partición en el hueco sin
38# asignar más grande de `--disk` e instalar ahí. Al lado, no encima.
39# --on-active-root <modo> qué hacer si `--root-part` ES la raíz viva (auto|kexec|free-space|abort).
40# `auto` (def) = intentar kexec y, si esta máquina no puede, espacio libre.
41# --esp <dev|fichero> ESP existente a poblar (FAT). Sin ella no hay arranque.
42# --disk <dev> --esp-part N para la entrada NVRAM; si no se dan, se deducen del `--esp`.
43# --claim-fallback además, escribir \EFI\BOOT\BOOTX64.EFI (pisa lo que haya ahí).
44# --no-nvram no tocar la NVRAM (pruebas, o firmware que se registra a mano).
45# --yes no preguntar (para guiones; en interactivo pide confirmación).
46# Variables: ROOTFS KERNEL BUSYBOX (como install-image-efi.sh) · STAGE (def work/.install-part-stage)
47set -eu
48ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
49
50DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
51USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""
52while [ $# -gt 0 ]; do
53 case "$1" in
54 --root-part) DEST="${2:?}"; shift 2 ;;
55 --esp) ESP="${2:?}"; shift 2 ;;
56 --disk) DISK="${2:?}"; shift 2 ;;
57 --esp-part) ESP_PART="${2:?}"; shift 2 ;;
58 --claim-fallback) CLAIM_FALLBACK=1; shift ;;
59 --no-nvram) NO_NVRAM=1; shift ;;
60 --survey) SOLO_CENSO=1; shift ;;
61 --use-free-space) USE_FREE=1; shift ;;
62 --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
63 --yes|-y) ASSUME_YES=1; shift ;;
64 -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
65 *) echo "opción desconocida: $1 (--help)" >&2; exit 2 ;;
66 esac
67done
68# Sin destino no se aborta: se PREGUNTA. Eso es lo que separa un instalador de un guion que hay que
69# preparar por máquina. Sólo si tampoco hay con quién hablar (sin terminal) se pide el parámetro.
70NECESITA_ELEGIR=""
71if [ -z "$DEST" ] && [ -z "$USE_FREE" ] && [ -z "$SOLO_CENSO" ]; then NECESITA_ELEGIR=1; fi
72case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root: auto|kexec|free-space|abort" >&2; exit 2 ;; esac
73
74# ── Las herramientas, ANTES que nada ───────────────────────────────────────────────────────────
75# Se resuelven acá arriba y no junto a su uso porque `--use-free-space` necesita `sfdisk` durante
76# las GUARDAS, o sea antes de tocar nada. Salen del store por hash vigente; `TOOL_<binario>=` las
77# inyecta cuando no hay store (dentro del initramfs de un instalador, p. ej.).
78. "$(dirname "$0")/lib/herramientas-desde-store.sh"
79MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
80SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
81# Releer la tabla es su propio problema: tras añadir una entrada, el kernel NO se entera solo y el
82# device nuevo no aparece. `partx -a` se lo dice; `blockdev --rereadpt` es el ioctl crudo, que falla
83# si alguna partición del disco está montada — por eso se intentan los dos, en ese orden. Los dos
84# salen del `util-linux` sellado: en el anfitrión musl no existe ninguno (ni `partprobe`).
85PARTX=$(herramienta util-linux usr/sbin/partx partx 2>/dev/null || echo "")
86BLOCKDEV=$(herramienta util-linux sbin/blockdev blockdev 2>/dev/null || echo "")
87MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1
88MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1
89MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1
90KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
91[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro el kernel EFI-stub (set KERNEL=)" >&2; exit 1; }
92HAMMER="${HAMMER:-$ROOT/target/release/takana}"
93
94# ── LAS GUARDAS VAN PRIMERO, Y SON EL PUNTO DE ESTE SCRIPT ─────────────────────────────────────
95# Un instalador que borra de más falla del lado silencioso: el usuario se entera cuando reinicia y
96# le falta su /home. Cada guarda de abajo existe porque su ausencia es irreversible.
97es_bloque() { [ -b "$1" ]; }
98nombre_kernel() { basename "$(readlink -f "$1")"; }
99disco_de() { echo "/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$1")/..")")"; }
100
101# ── resolver_dev <spec> — NUNCA confíes en `/dev/nvme1n1p6` ───────────────────────────────────
102# ⚠ **Los nombres `nvmeXn1` NO son estables entre arranques, y esto está MEDIDO, no temido**
103# (2026-09-23, QEMU con dos NVMe): el disco declarado primero en la línea de comandos apareció
104# como `nvme1n1` y el segundo como `nvme0n1` — **cruzados**. Salen del orden de sondeo del bus, no
105# del orden físico ni del que vos ves hoy con `lsblk`.
106#
107# Para un instalador eso no es una molestia: es la diferencia entre formatear la raíz vieja y
108# formatear el disco de Windows del usuario. Y el salto por kexec lo agrava, porque el kernel que
109# enumera después del salto NO es el que te dio los nombres antes.
110#
111# Por eso se acepta —y se recomienda— direccionar por identidad y no por posición:
112# PARTUUID=<uuid> el de la TABLA GPT. **No cambia al formatear la partición**, así que es el
113# correcto para nombrar un destino que se va a formatear.
114# UUID=<uuid> el del FILESYSTEM. Sirve para identificar lo que hay HOY (y muere al formatear).
115# LABEL=<etiqueta> cómodo, pero puede estar repetido en dos discos.
116resolver_dev() {
117 case "$1" in
118 /dev/*) echo "$1"; return 0 ;;
119 PARTUUID=*|partuuid=*) _rd_v="${1#*=}"; _rd_por=partuuid ;;
120 UUID=*|uuid=*) _rd_v="${1#*=}"; _rd_por=uuid ;;
121 LABEL=*|label=*) _rd_v="${1#*=}"; _rd_por=label ;;
122 *) echo "$1"; return 0 ;; # un fichero (pruebas)
123 esac
124 # `findfs` resuelve UUID y LABEL en busybox y en util-linux; para PARTUUID no se puede contar
125 # con él, así que ese caso se resuelve leyendo la TABLA con sfdisk — que es donde vive el dato.
126 if [ "$_rd_por" != partuuid ] && command -v findfs >/dev/null 2>&1; then
127 _rd_d=$(findfs "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=$_rd_v" 2>/dev/null || true)
128 [ -n "${_rd_d:-}" ] && { echo "$_rd_d"; return 0; }
129 fi
130 # `/dev/loop?` está en la lista porque el banco de pruebas trabaja sobre loop devices: sin él,
131 # la resolución por identidad quedaba SIN COBERTURA en el único sitio donde se puede probar sin
132 # arriesgar un disco de verdad. Lo detectó el propio banco al pasar a PARTUUID.
133 for _rd_disco in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
134 [ -b "$_rd_disco" ] || continue
135 case "$_rd_por" in
136 partuuid)
137 _rd_hit=$("$SFDISK" -d "$_rd_disco" 2>/dev/null \
138 | awk -v u="$(printf '%s' "$_rd_v" | tr 'A-Z' 'a-z')" \
139 'tolower($0) ~ ("uuid=" u) {print $1; exit}')
140 ;;
141 *)
142 _rd_hit=$(for _rd_p in "$_rd_disco"*; do
143 [ -b "$_rd_p" ] || continue
144 blkid "$_rd_p" 2>/dev/null | grep -qi "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=\"$_rd_v\"" && { echo "$_rd_p"; break; }
145 done)
146 ;;
147 esac
148 [ -n "${_rd_hit:-}" ] && { echo "$_rd_hit"; return 0; }
149 done
150 echo "takana-install-partition: no encuentro ninguna partición con $1" >&2
151 echo " (mirá 'lsblk -o NAME,SIZE,PARTUUID,LABEL' en el sistema vivo)" >&2
152 return 1
153}
154
155# ── releer_tabla <disco> — que el KERNEL se entere de la entrada nueva ────────────────────────
156# Medido el 2026-09-23 en el banco: `sfdisk --append` escribió la entrada perfectamente y
157# `/dev/loop0p4` NO APARECIÓ. El script lo detectó y abortó —bien— pero el motivo estaba fuera de
158# él: en el anfitrión musl no existen `partprobe`, `partx` ni `blockdev`, así que los tres intentos
159# de releer la tabla eran no-ops silenciosos. La tabla en disco y la que el kernel tiene en memoria
160# son dos cosas distintas, y nada avisa cuando divergen.
161# ⚠ Y el comando es `partx -u`, NO `partx -a`: medido en el banco el mismo día. `-a` («añadir»)
162# intenta registrar TODAS las particiones y falla entero —«error adding partitions 1-4»— cuando las
163# viejas ya están, que es siempre en este flujo. `-u` («actualizar») añade lo que falte y deja en paz
164# lo que ya hay. El nombre de la opción sugiere justo lo contrario de lo que sirve acá.
165releer_tabla() {
166 [ -n "${PARTX:-}" ] && "$PARTX" -u "$1" >/dev/null 2>&1 && return 0
167 [ -n "${PARTX:-}" ] && "$PARTX" -a "$1" >/dev/null 2>&1 && return 0
168 [ -n "${BLOCKDEV:-}" ] && "$BLOCKDEV" --rereadpt "$1" >/dev/null 2>&1 && return 0
169 partprobe "$1" 2>/dev/null || true
170}
171
172# ── EL CENSO DEL TERRENO ───────────────────────────────────────────────────────────────────────
173# Un instalador que hay que preparar a mano para cada máquina no es un instalador. Esto enumera lo
174# que hay —en CUALQUIER máquina— y dice de cada partición si se puede usar, si no se debe tocar y
175# por qué. Es la base del modo interactivo y de la recomendación automática.
176#
177# Dos fuentes, porque ninguna sola alcanza:
178# · `sfdisk -d` da el TIPO de la tabla (GUID) y el **PARTUUID**, que es la identidad estable;
179# · `blkid` da lo que hay ADENTRO (ntfs, crypto_LUKS, ext4, swap…), que el tipo GPT miente
180# a menudo: media instalación de Windows vive en «datos básicos», y un LUKS puede
181# estar en una partición marcada como Linux.
182# Y `/proc/mounts` dice qué está en uso AHORA, que es lo único que distingue «vacía» de «tu raíz».
183
184# ── raiz_viva_majmin — quién es «/» de verdad ─────────────────────────────────────────────────
185# ⚠ **`/proc/mounts` NO alcanza para saber cuál es la raíz viva, y esto está MEDIDO** (2026-09-23,
186# sobre el anfitrión de este hub): la línea de «/» dice `/dev/root`, un nombre que no existe en
187# `/dev` ni casa con ninguna partición. Comparar por nombre daba «ninguna partición es la raíz» y el
188# censo **ofrecía formatear la raíz del sistema que estaba corriendo**. Pasa también con `UUID=…`
189# en la cmdline, con initramfs que montan por label, y con `/dev/mapper/*`.
190#
191# Lo que sí es inequívoco es el NÚMERO de dispositivo: `stat` lo da para «/» y cada partición lo
192# publica en `/sys/class/block/<n>/dev`. La descomposición es la de Linux (12 bits de major, minor
193# partido en dos), no el `/256` ingenuo, que se rompe justo en NVMe (major 259).
194raiz_viva_majmin() {
195 _rv_hex=$(stat -c %D / 2>/dev/null) || return 1
196 [ -n "${_rv_hex:-}" ] || return 1
197 _rv_d=$(( 0x$_rv_hex )) 2>/dev/null || return 1
198 _rv_maj=$(( (_rv_d >> 8) & 0xfff ))
199 _rv_min=$(( (_rv_d & 0xff) | ((_rv_d >> 12) & 0xfff00) ))
200 echo "${_rv_maj}:${_rv_min}"
201}
202RAIZ_MAJMIN="$(raiz_viva_majmin || true)"
203
204es_la_raiz_viva() { # <dev>
205 [ -n "${RAIZ_MAJMIN:-}" ] || return 1
206 _er_n=$(nombre_kernel "$1")
207 [ "$(cat "/sys/class/block/$_er_n/dev" 2>/dev/null)" = "$RAIZ_MAJMIN" ]
208}
209
210GUID_ESP="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
211GUID_MSR="e3c9e316-0b5c-4db8-817d-f92df00215ae"
212GUID_WINRE="de94bba4-06d1-4d40-a16a-bfd50179d6ac"
213GUID_MSBASIC="ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"
214GUID_LINUX="0fc63daf-8483-4772-8e79-3d69d8477de4"
215GUID_LUKS="ca7d7ccb-63ed-4c53-861c-1742536059cc"
216GUID_SWAP="0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"
217
218discos_del_sistema() {
219 for _dd in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
220 [ -b "$_dd" ] || continue
221 # Tamaño 0 = un `loop` libre sin fichero detrás. Existen por docenas y no son discos: si se
222 # listan, el censo se llena de ruido y lo que importa deja de verse. Un guardián ruidoso se
223 # ignora igual que uno mudo.
224 [ "$(cat "/sys/class/block/$(basename "$(readlink -f "$_dd")")/size" 2>/dev/null || echo 0)" -gt 0 ] || continue
225 echo "$_dd"
226 done
227}
228
229mib_de_sectores() { echo $(( ${1:-0} / 2048 )); }
230
231# clasificar <guid-tipo> <fstype> <montaje> → VEREDICTO|ETIQUETA
232# El veredicto es lo que decide el menú: SOLO_ESP, NO_TOCAR, CANDIDATA, RAIZ_VIVA, EN_USO.
233clasificar() {
234 _cl_guid=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
235 _cl_fs="${2:-}"; _cl_mnt="${3:-}"
236 case "$_cl_guid" in
237 "$GUID_ESP") echo "SOLO_ESP|ESP (partición de arranque EFI)"; return ;;
238 "$GUID_MSR") echo "NO_TOCAR|reservada de Microsoft"; return ;;
239 "$GUID_WINRE") echo "NO_TOCAR|recuperación de Windows"; return ;;
240 "$GUID_SWAP") echo "NO_TOCAR|swap"; return ;;
241 # «Datos básicos de Microsoft» es el tipo que Windows le pone a sus volúmenes. Aunque el
242 # filesystem no se reconozca —puede estar cerrado por BitLocker, o simplemente vacío— no se
243 # ofrece: el coste de equivocarse acá es el disco de otro sistema operativo.
244 "$GUID_MSBASIC") echo "NO_TOCAR|volumen de datos de Windows (tipo GPT «basic data»)"; return ;;
245 # ⚠ El tipo LUKS de la TABLA tiene que bastar por sí solo. Estaba definido y **no se
246 # consultaba**: en la prueba con el layout del usuario, una partición marcada como LUKS en
247 # GPT salió ofrecida como «se puede usar» porque `blkid` no veía cabecera dentro. Un
248 # contenedor cifrado es justo el caso donde no se puede mirar adentro para decidir — el
249 # único dato disponible es el tipo, así que el tipo manda.
250 "$GUID_LUKS") echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS, por tipo de la tabla)"; return ;;
251 esac
252 case "$_cl_fs" in
253 crypto_LUKS) echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS) — lo que hay dentro no se puede ver desde acá"; return ;;
254 ntfs) echo "NO_TOCAR|NTFS (Windows u otro sistema)"; return ;;
255 vfat|msdos) echo "NO_TOCAR|FAT — puede ser una ESP o datos compartidos"; return ;;
256 swap) echo "NO_TOCAR|swap"; return ;;
257 BitLocker|bitlocker) echo "NO_TOCAR|BitLocker"; return ;;
258 esac
259 if [ "$_cl_mnt" = "/" ]; then
260 echo "RAIZ_VIVA|la raíz del sistema que está corriendo AHORA"; return
261 fi
262 if [ -n "$_cl_mnt" ]; then
263 echo "EN_USO|montada en $_cl_mnt"; return
264 fi
265 case "$_cl_fs" in
266 ext2|ext3|ext4|xfs|btrfs|f2fs) echo "CANDIDATA|Linux ($_cl_fs) sin montar"; return ;;
267 "") echo "CANDIDATA|sin filesystem reconocible"; return ;;
268 esac
269 echo "NO_TOCAR|$_cl_fs (desconocido para este instalador)"
270}
271
272# censo [--solo-candidatas] → una línea por partición:
273# <dev>|<partuuid>|<mib>|<fstype>|<label>|<montaje>|<VEREDICTO>|<explicación>
274censo() {
275 _ce_solo="${1:-}"
276 for _ce_d in $(discos_del_sistema); do
277 # ⚠ Un disco SIN TABLA no es un disco vacío: puede estar formateado directo y montado —así
278 # vive el store de este hub, `/dev/sdb` con un ext4 encima y nada más—. Sin esta rama el
279 # censo lo OMITE por completo, que es la peor forma de presentar algo que existe y está en
280 # uso: invisible. Se lista, se dice que no tiene tabla, y no se ofrece.
281 if [ -z "$("$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}")" ]; then
282 _ce_bl=$(blkid "$_ce_d" 2>/dev/null || true)
283 _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"' | head -1 | sed 's/^ TYPE="//; s/"$//')
284 _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
285 _ce_mn=$(awk -v d="$_ce_d" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
286 es_la_raiz_viva "$_ce_d" && _ce_mn="/"
287 _ce_sz=$(( $(cat "/sys/class/block/$(nombre_kernel "$_ce_d")/size" 2>/dev/null || echo 0) ))
288 if [ -n "$_ce_mn" ]; then _ce_v="EN_USO"; _ce_e="disco SIN TABLA de particiones, montado en $_ce_mn"
289 else _ce_v="NO_TOCAR"; _ce_e="disco SIN TABLA de particiones (${_ce_fs:-sin filesystem})"; fi
290 echo "$_ce_d|-|$(mib_de_sectores "$_ce_sz")|${_ce_fs}|${_ce_lb}|${_ce_mn}|$_ce_v|$_ce_e"
291 continue
292 fi
293 "$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}" | while IFS= read -r _ce_l; do
294 _ce_dev=$(printf '%s' "$_ce_l" | awk '{print $1}')
295 _ce_sz=$(printf '%s' "$_ce_l" | sed -n 's/.*size=[ ]*\([0-9]*\).*/\1/p')
296 _ce_ty=$(printf '%s' "$_ce_l" | sed -n 's/.*type=\([0-9A-Fa-f-]*\).*/\1/p')
297 _ce_uu=$(printf '%s' "$_ce_l" | sed -n 's/.*uuid=\([0-9A-Fa-f-]*\).*/\1/p')
298 _ce_bl=$(blkid "$_ce_dev" 2>/dev/null || true)
299 _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"' | head -1 | sed 's/^ TYPE="//; s/"$//')
300 _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
301 _ce_mn=$(awk -v d="$_ce_dev" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
302 # La raíz viva se decide por número de dispositivo, no por el nombre de /proc/mounts.
303 if es_la_raiz_viva "$_ce_dev"; then _ce_mn="/"; fi
304 _ce_cl=$(clasificar "$_ce_ty" "$_ce_fs" "$_ce_mn")
305 _ce_ver="${_ce_cl%%|*}"; _ce_exp="${_ce_cl#*|}"
306 [ "$_ce_solo" = "--solo-candidatas" ] && [ "$_ce_ver" != "CANDIDATA" ] && continue
307 echo "$_ce_dev|$_ce_uu|$(mib_de_sectores "$_ce_sz")|$_ce_fs|$_ce_lb|$_ce_mn|$_ce_ver|$_ce_exp"
308 done
309 # El espacio SIN ASIGNAR es un candidato más, y el único que no destruye nada.
310 "$SFDISK" -F "$_ce_d" 2>/dev/null | awk -v d="$_ce_d" 'NF==4 && $1 ~ /^[0-9]+$/ {print d"|-|"int($3/2048)"|-|-|-|LIBRE|espacio sin asignar en "d}'
311 done
312}
313
314# mostrar_censo — el censo en forma de tabla, con los NO TOCAR dichos en voz alta.
315mostrar_censo() {
316 _mc_f="${1:-}"
317 echo "── el terreno de esta máquina ─────────────────────────────────────────────────"
318 [ -n "${RAIZ_MAJMIN:-}" ] || echo " ⚠ NO pude determinar cuál es la raíz viva de esta máquina: tratá TODO con sospecha."
319 printf " %-3s %-16s %-9s %-11s %s\n" "nº" "partición" "tamaño" "qué es" "veredicto"
320 _mc_n=0
321 { if [ -n "$_mc_f" ]; then cat "$_mc_f"; else censo; fi; } | while IFS='|' read -r _d _u _m _fs _lb _mn _ver _exp; do
322 _mc_n=$(( _mc_n + 1 ))
323 case "$_ver" in
324 CANDIDATA) _mc_marca="✓ se puede usar (SE BORRA lo que tenga)" ;;
325 LIBRE) _mc_marca="✓ instalar AL LADO sin borrar nada" ;;
326 RAIZ_VIVA) _mc_marca="⚡ sólo con el salto (kexec)" ;;
327 SOLO_ESP) _mc_marca="· se reusa como ESP, no se formatea" ;;
328 EN_USO) _mc_marca="· en uso" ;;
329 *) _mc_marca="✗ NO TOCAR" ;;
330 esac
331 printf " %-3s %-16s %6s M %-11s %s\n" "$_mc_n" "$_d" "$_m" "${_fs:-—}" "$_mc_marca"
332 printf " %s%s\n" "$_exp" "$( [ -n "$_lb" ] && [ "$_lb" != "-" ] && printf ' · etiqueta «%s»' "$_lb")"
333 [ -n "$_u" ] && [ "$_u" != "-" ] && printf " PARTUUID=%s\n" "$_u"
334 done
335 echo "───────────────────────────────────────────────────────────────────────────────"
336}
337
338# ── partuuid_de <dev> — la identidad estable de una partición, leída de la TABLA ──────────────
339partuuid_de() {
340 "$SFDISK" -d "$(disco_de "$1")" 2>/dev/null \
341 | awk -v d="$1" '$1==d {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i; exit}}'
342}
343
344# ── elegir_destino — el modo interactivo, que es lo que hace de esto un instalador ────────────
345# Sin esto hay que saber de antemano el PARTUUID de la máquina de destino, o sea preparar un medio
346# distinto por máquina. Con esto el MISMO medio sirve en cualquiera: arranca, mira y ofrece.
347# Deja la elección en DEST / USE_FREE / DISK del llamador.
348# ⚠ `/tmp` puede NO EXISTIR. Medido dentro del initramfs: el censo no pudo escribir su fichero y el
349# mensaje que salió fue «no veo ningún disco en esta máquina» — con once discos a la vista dos
350# líneas más abajo. Un diagnóstico que culpa al hardware cuando falta un directorio manda a
351# desarmar la máquina equivocada. Ahora se busca un sitio escribible y, si no hay ninguno, se dice
352# ESO.
353dir_temporal() {
354 for _dt in "${TMPDIR:-}" /tmp /run /dev/shm .; do
355 [ -n "$_dt" ] || continue
356 [ -d "$_dt" ] || continue
357 if ( : > "$_dt/.takana-escritura.$$" ) 2>/dev/null; then
358 rm -f "$_dt/.takana-escritura.$$"; echo "$_dt"; return 0
359 fi
360 done
361 return 1
362}
363
364elegir_destino() {
365 _ed_dir=$(dir_temporal) || {
366 echo "takana-install-partition: no encuentro ningún directorio donde escribir (/tmp, /run…)" >&2
367 return 1; }
368 _ed_f="$_ed_dir/.takana-censo.$$"
369 censo > "$_ed_f" 2>/dev/null
370 [ -s "$_ed_f" ] || { echo "takana-install-partition: no veo ningún disco en esta máquina" >&2; rm -f "$_ed_f"; return 1; }
371 mostrar_censo "$_ed_f"
372 echo " Lo marcado ✗ NO TOCAR no se ofrece: son sistemas de otros, volúmenes cifrados o swap."
373 echo " Elegir una ✓ BORRA lo que tenga. «Al lado» no borra nada."
374 _ed_n=0
375 printf " opciones: "
376 while IFS='|' read -r _d _u _m _fs _lb _mn _ver _exp; do
377 _ed_n=$(( _ed_n + 1 ))
378 case "$_ver" in CANDIDATA|LIBRE|RAIZ_VIVA) printf "%s " "$_ed_n" ;; esac
379 done < "$_ed_f"
380 echo
381 printf " nº (o 'no' para salir sin tocar nada): "
382 read -r _ed_sel || _ed_sel=no
383 case "$_ed_sel" in
384 ''|no|NO|n) echo " cancelado: no se tocó nada."; rm -f "$_ed_f"; return 1 ;;
385 esac
386 _ed_linea=$(sed -n "${_ed_sel}p" "$_ed_f" 2>/dev/null)
387 rm -f "$_ed_f"
388 [ -n "$_ed_linea" ] || { echo "takana-install-partition: «$_ed_sel» no es una de las opciones" >&2; return 1; }
389 _ed_dev=$(printf '%s' "$_ed_linea" | cut -d'|' -f1)
390 _ed_uu=$(printf '%s' "$_ed_linea" | cut -d'|' -f2)
391 _ed_ver=$(printf '%s' "$_ed_linea" | cut -d'|' -f7)
392 case "$_ed_ver" in
393 LIBRE) USE_FREE=1; DISK="$_ed_dev"; DEST=""
394 echo " ⇒ instalar AL LADO, en el espacio libre de $_ed_dev" ;;
395 CANDIDATA|RAIZ_VIVA)
396 # Por PARTUUID SIEMPRE que se pueda: el nombre de device puede cambiar entre
397 # este momento y el arranque siguiente (ver `resolver_dev`).
398 if [ -n "$_ed_uu" ] && [ "$_ed_uu" != "-" ]; then DEST="PARTUUID=$_ed_uu"; else DEST="$_ed_dev"; fi
399 echo " ⇒ destino: $_ed_dev ($DEST)" ;;
400 *) echo "takana-install-partition: esa partición está marcada como NO TOCAR y no se ofrece" >&2; return 1 ;;
401 esac
402 return 0
403}
404
405# ── esp_automatica [disco-del-destino] — la ESP no se pregunta si se puede deducir ────────────
406# Regla: la ESP del MISMO disco que el destino; si ahí no hay, la única del sistema. Si hay varias y
407# ninguna en ese disco, se pregunta: elegir entre las ESP de dos sistemas operativos distintos es
408# justo la clase de decisión que un instalador no debe tomar solo.
409esp_automatica() {
410 _ea_disco="${1:-}"
411 _ea_todas=$(censo | awk -F'|' '$7=="SOLO_ESP" {print $1}')
412 [ -n "$_ea_todas" ] || return 1
413 if [ -n "$_ea_disco" ]; then
414 for _ea_e in $_ea_todas; do
415 case "$_ea_e" in "$_ea_disco"*) echo "$_ea_e"; return 0 ;; esac
416 done
417 fi
418 _ea_n=$(printf '%s\n' "$_ea_todas" | grep -c .)
419 if [ "$_ea_n" = "1" ]; then printf '%s\n' "$_ea_todas"; return 0; fi
420 echo "takana-install-partition: hay $_ea_n ESP y ninguna en el disco del destino. Pasá --esp:" >&2
421 printf '%s\n' "$_ea_todas" | sed 's/^/ /' >&2
422 return 1
423}
424
425# ── mib_necesarios — cuánto pedirle al hueco ──────────────────────────────────────────────────
426# El rootfs medido + 40 %, con piso de 2 GiB. El margen no es superstición: `mke2fs` se queda con
427# ~1,5 % en metadatos y el sistema instalado tiene que poder escribir algo antes de que alguien lo
428# agrande. Un destino que entra JUSTO produce un sistema que arranca y se llena a la semana.
429mib_necesarios() {
430 _mn_r="${ROOTFS:-}"
431 [ -z "$_mn_r" ] && _mn_r=$(ls -dt store/*-product-rootfs /store/*-product-rootfs 2>/dev/null | head -1)
432 _mn_m=$(du -sm "$_mn_r" 2>/dev/null | cut -f1)
433 [ -z "${_mn_m:-}" ] && _mn_m=1024
434 _mn_m=$(( _mn_m * 14 / 10 ))
435 [ "$_mn_m" -lt 2048 ] && _mn_m=2048
436 echo "$_mn_m"
437}
438
439# ── crear_en_hueco <disco> <mib> — la ÚNICA vez que este script escribe la tabla ───────────────
440# `sfdisk --append` añade una entrada y **no reescribe las existentes**: es la diferencia entre
441# «instalar al lado» y el `o` de `takana-live-install.sh`, que empieza por tirar la tabla entera.
442# Aun así es la operación más delicada de acá, así que: se elige el hueco MÁS GRANDE, se dice cuál
443# es con sus sectores a la vista, y se pide confirmación aparte de la del formateo.
444crear_en_hueco() {
445 _ch_disco="$1"; _ch_mib="$2"
446 _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}')
447 [ -n "$_ch_libre" ] || { echo "takana-install-partition: $_ch_disco no tiene espacio SIN ASIGNAR" >&2; return 1; }
448 _ch_start=$(echo "$_ch_libre" | cut -d' ' -f1)
449 _ch_sect=$(echo "$_ch_libre" | cut -d' ' -f3)
450 _ch_libre_mib=$(( _ch_sect / 2048 ))
451 echo " hueco más grande: ${_ch_libre_mib} MiB desde el sector ${_ch_start}" >&2
452 [ "$_ch_libre_mib" -ge "$_ch_mib" ] || {
453 echo "takana-install-partition: el hueco da ${_ch_libre_mib} MiB y hacen falta ${_ch_mib} MiB." >&2
454 echo " Nada que hacer sin encoger algo, y encoger pide otro medio." >&2; return 1; }
455 _ch_size=$(( _ch_mib * 2048 ))
456 if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
457 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
458 read -r _ch_r; [ "$_ch_r" = "si" ] || { echo " cancelado." >&2; return 1; }
459 fi
460 # La lista de ANTES, para saber después cuál es la nueva por DIFERENCIA y no por su nombre.
461 # ⚠ El primer intento buscaba la línea por `start=<sector>` y nunca la encontraba: `sfdisk -d`
462 # **alinea los campos con espacios** (`start= 3614720`), así que el patrón sin espacios no
463 # casa jamás. El síntoma era idéntico al de un device que no aparece — y mandó a buscar el fallo
464 # en `partx` y en udev, que estaban bien. Comparar conjuntos no depende del formato.
465 _ch_lista_antes=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
466 _ch_antes=$(printf '%s\n' "$_ch_lista_antes" | grep -c . || true)
467 echo "start=$_ch_start, size=$_ch_size, type=linux, name=\"takana-root\"" \
468 | "$SFDISK" --append --quiet "$_ch_disco" >/dev/null 2>&1 \
469 || { echo "takana-install-partition: sfdisk --append falló sobre $_ch_disco" >&2; return 1; }
470 # El device nuevo tarda en aparecer (udev, o `partx` en un loop): esperarlo es obligatorio, y el
471 # nombre se LEE de la tabla en vez de construirlo a mano — `nvme0n1p4` y `sda4` no se escriben igual.
472 releer_tabla "$_ch_disco"
473 _ch_nuevo=""
474 _ch_i=0
475 while [ "$_ch_i" -lt 15 ]; do
476 _ch_lista_post=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}')
477 for _ch_d in $_ch_lista_post; do
478 case " $(printf '%s ' $_ch_lista_antes)" in
479 *" $_ch_d "*) ;;
480 *) _ch_nuevo="$_ch_d" ;;
481 esac
482 done
483 [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] && break
484 _ch_nuevo=""
485 _ch_i=$(( _ch_i + 1 )); sleep 1
486 releer_tabla "$_ch_disco"
487 done
488 [ -n "$_ch_nuevo" ] && [ -b "$_ch_nuevo" ] || {
489 echo "takana-install-partition: la partición se añadió pero su device no apareció (¿hace falta un partprobe?)" >&2; return 1; }
490 _ch_despues=$("$SFDISK" -d "$_ch_disco" 2>/dev/null | awk '/^\/dev/ {print $1}' | grep -c . || true)
491 [ "$_ch_despues" = "$(( _ch_antes + 1 ))" ] || {
492 echo "takana-install-partition: la tabla pasó de $_ch_antes a $_ch_despues entradas — esperaba UNA más" >&2; return 1; }
493 echo "$_ch_nuevo"
494}
495
496# `--survey`: mirar y no tocar. Es el comando con el que se empieza en una máquina desconocida.
497if [ -n "$SOLO_CENSO" ]; then
498 mostrar_censo
499 exit 0
500fi
501
502if [ -n "$NECESITA_ELEGIR" ]; then
503 if [ -t 0 ]; then
504 elegir_destino || exit 1
505 else
506 echo "takana-install-partition: no me diste destino y no hay terminal para preguntarte." >&2
507 echo " Pasá --root-part PARTUUID=… o --use-free-space --disk …" >&2
508 echo " (y mirá el terreno con --survey)" >&2
509 exit 2
510 fi
511fi
512
513# La identidad se resuelve ANTES de cualquier guarda: a partir de acá se trabaja con el device real,
514# y si el `PARTUUID` no existe en esta máquina se aborta sin haber tocado nada.
515if [ -n "$DEST" ]; then DEST=$(resolver_dev "$DEST") || exit 1; fi
516if [ -n "$ESP" ]; then ESP=$(resolver_dev "$ESP") || exit 1; fi
517
518# `--use-free-space` sin `--root-part`: el destino todavía no existe, se crea en un hueco. Esto es
519# «instalar al lado», y es el camino que la política de la raíz viva usa cuando no hay kexec.
520if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
521 _ufs_disco="${DISK:-}"
522 if [ -z "$_ufs_disco" ] && [ -n "$ESP" ] && [ -b "$ESP" ]; then _ufs_disco=$(disco_de "$ESP"); fi
523 [ -n "$_ufs_disco" ] || { echo "takana-install-partition: --use-free-space necesita --disk (o un --esp real)" >&2; exit 2; }
524 echo "==> buscando espacio SIN ASIGNAR en $_ufs_disco"
525 DEST=$(crear_en_hueco "$_ufs_disco" "$(mib_necesarios)") || exit 1
526 echo " partición creada: $DEST"
527fi
528
529# La ESP: si no se dijo cuál, se deduce del disco del destino. Una pregunta menos que hacerle al
530# usuario, y la respuesta correcta en el caso normal (un disco, una ESP).
531if [ -z "$ESP" ] && [ -n "$DEST" ] && [ -b "$DEST" ]; then
532 _esp_auto=$(esp_automatica "$(disco_de "$DEST")" 2>/dev/null || true)
533 [ -n "${_esp_auto:-}" ] && { ESP="$_esp_auto"; echo "==> ESP deducida: $ESP (del mismo disco que el destino)"; }
534fi
535
536if es_bloque "$DEST"; then
537 _n=$(nombre_kernel "$DEST")
538 # 1) DESTINO TIENE QUE SER UNA PARTICIÓN, NO UN DISCO. `/sys/class/block/<x>/partition` sólo
539 # existe en particiones: es la diferencia entre formatear 200 G y formatear 1,8 T.
540 [ -r "/sys/class/block/$_n/partition" ] || {
541 echo "takana-install-partition: $DEST NO es una partición (¿es el disco entero?)." >&2
542 echo " Este instalador nunca escribe un disco completo." >&2; exit 1; }
543 # 2) NO PUEDE ESTAR MONTADA. mke2fs sobre un fs montado corrompe el que está en uso.
544 if grep -q "^$(readlink -f "$DEST") " /proc/mounts 2>/dev/null; then
545 echo "takana-install-partition: $DEST está MONTADA. Desmontala primero (o elegí otra)." >&2
546 grep "^$(readlink -f "$DEST") " /proc/mounts >&2; exit 1
547 fi
548 # 3) NO PUEDE SER LA RAÍZ VIVA. Es el caso que el usuario pregunta siempre: «¿puedo instalar
549 # encima del Linux que estoy usando?». No desde él: hay que saltar antes a otro kernel/medio.
550 # Por número de dispositivo, no por nombre: ver `raiz_viva_majmin`. El fallback por nombre se
551 # conserva porque no estorba y cubre el caso en que `stat` no exista.
552 _rootdev=$(awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null || true)
553 if es_la_raiz_viva "$DEST" || \
554 [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
555 politica_raiz_viva # kexec → espacio libre → abortar, según --on-active-root
556 fi
557 # 4) NO PUEDE SER LA ESP que vamos a reusar.
558 [ -n "$ESP" ] && [ "$(readlink -f "$DEST")" = "$(readlink -f "$ESP")" ] && {
559 echo "takana-install-partition: --root-part y --esp son la MISMA partición" >&2; exit 1; }
560else
561 [ -f "$DEST" ] || { echo "takana-install-partition: $DEST no existe (ni bloque ni fichero)" >&2; exit 1; }
562fi
563
564# ── politica_raiz_viva — el «saltito»: qué hacer si el destino ES el sistema que corre ────────
565# La pregunta que todo el mundo hace es «¿puedo poner takana encima del Linux que estoy usando, sin
566# USB?». La respuesta honesta tiene tres partes y este bloque las implementa en orden:
567#
568# 1. Desde adentro, NO. Un filesystem montado y en uso no se formatea; eso es el kernel, no una
569# política nuestra.
570# 2. Pero se puede SALIR de adentro sin reiniciar por el firmware: `kexec` salta a otro kernel con
571# un instalador en RAM, y en ese salto el sistema viejo deja de correr ⇒ su disco queda libre.
572# Es el ADR 0017 §1, y el syscall ya estaba implementado (`kexec_file_load`, sin kexec-tools).
573# 3. Y si esta máquina no puede saltar —kernel sin `CONFIG_KEXEC_FILE`, o lockdown por Secure
574# Boot— no se fuerza nada: se instala AL LADO, en espacio sin asignar, y el usuario elige en el
575# arranque. Peor camino, cero riesgo.
576#
577# `--on-active-root` permite fijar uno de los tres a mano; `auto` es la política de arriba.
578politica_raiz_viva() {
579 echo "⚠ $DEST es la RAÍZ VIVA: el sistema que está corriendo ahora mismo."
580 echo " Un filesystem montado y en uso no se puede formatear desde adentro."
581 case "$ON_ACTIVE" in
582 abort)
583 echo " --on-active-root=abort ⇒ no hago nada más." >&2; exit 1 ;;
584 esac
585 _prv_kexec=no
586 if [ "$ON_ACTIVE" = auto ] || [ "$ON_ACTIVE" = kexec ]; then
587 echo "==> ¿puede esta máquina saltar a otro kernel sin pasar por el firmware?"
588 if _prv_out=$("$HAMMER" kernel kexec --probe 2>&1); then _prv_kexec=si; fi
589 printf '%s\n' "$_prv_out" | sed 's/^/ /'
590 fi
591 if [ "$_prv_kexec" = si ]; then
592 camino_kexec # no vuelve: salta
593 exit 0
594 fi
595 if [ "$ON_ACTIVE" = kexec ]; then
596 echo " --on-active-root=kexec y esta máquina no puede saltar (motivos arriba)." >&2; exit 1
597 fi
598 echo "==> sin kexec ⇒ INSTALO AL LADO, en espacio sin asignar (la raíz viva no se toca)"
599 _prv_disco=$(disco_de "$DEST")
600 DEST=$(crear_en_hueco "$_prv_disco" "$(mib_necesarios)") || {
601 echo "takana-install-partition: no hay hueco donde instalar al lado, y sin kexec no hay otra" >&2
602 echo " desde este sistema. Queda el camino del medio externo (USB)." >&2
603 exit 1; }
604 echo " destino nuevo: $DEST (la raíz viva sigue intacta)"
605}
606
607# ── camino_kexec — armar el instalador en RAM y saltar. NO VUELVE ─────────────────────────────
608camino_kexec() {
609 echo "==> camino KEXEC: el instalador viaja en RAM y el sistema viejo muere en el salto"
610 # ⚠ **La elección viaja por la CMDLINE, no horneada en el initramfs.** Un initramfs con el
611 # destino adentro sirve para una máquina y hay que rehacerlo para la siguiente; el mismo fichero
612 # con la elección en la cmdline sirve para todas. Y va por **PARTUUID**: el kernel de después
613 # del salto puede enumerar los discos con otros nombres (medido), así que `/dev/nvme1n1p6` es
614 # justamente lo que NO hay que pasar.
615 _ck_ru=$(partuuid_de "$DEST" 2>/dev/null || true)
616 _ck_eu=""; [ -n "$ESP" ] && _ck_eu=$(partuuid_de "$ESP" 2>/dev/null || true)
617 _ck_cl="${CMDLINE_SALTO:-console=ttyS0,115200 console=tty0}"
618 if [ -n "$_ck_ru" ]; then _ck_cl="$_ck_cl takana.root=PARTUUID=$_ck_ru"
619 else _ck_cl="$_ck_cl takana.root=$DEST"; fi
620 if [ -n "$_ck_eu" ]; then _ck_cl="$_ck_cl takana.esp=PARTUUID=$_ck_eu"
621 elif [ -n "$ESP" ]; then _ck_cl="$_ck_cl takana.esp=$ESP"; fi
622 [ -n "$ASSUME_YES" ] && _ck_cl="$_ck_cl takana.yes"
623 _ck_ird="${IRD_INSTALADOR:-work/takana-instalador.cpio.gz}"
624 OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh || {
625 echo "takana-install-partition: no pude armar el instalador en RAM" >&2; exit 1; }
626 _ck_art=$(dirname "$(dirname "$KERNEL")") # …/<hash>-linux-metal/boot/bzImage → el artefacto
627 echo
628 echo " kernel: $_ck_art"
629 echo " initrd: $_ck_ird ($(du -h "$_ck_ird" | cut -f1)) — genérico, sin esta máquina adentro"
630 echo " destino: $DEST"
631 echo " cmdline: $_ck_cl"
632 if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
633 printf " ⚡ ESTE COMANDO MATA EL SISTEMA QUE ESTÁS USANDO y arranca el instalador. [escribí saltar] "
634 read -r _ck_r; [ "$_ck_r" = "saltar" ] || { echo " cancelado — no se saltó y no se tocó nada."; exit 1; }
635 fi
636 # ⚠ **El orden de las `console=` decide cuál es `/dev/console`: gana la ÚLTIMA.** Con
637 # `console=tty0 console=ttyS0,115200` la consola es el SERIAL, y un portátil no tiene puerto
638 # serie: el instalador arrancaría y la pantalla se quedaría en negro con todo pasando en un
639 # cable que no existe. Es el bug que este repo ya pagó dos veces (ver `arranque-visible-cada-capa`).
640 # Por eso acá tty0 va ÚLTIMO: pantalla primero, serial de regalo para quien lo tenga.
641 "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" --cmdline "$_ck_cl" --jump
642 echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
643 exit 1
644}
645
646# ── El terreno, DICHO EN VOZ ALTA antes de tocar nada (ADR 0018 §4) ───────────────────────────
647echo "==> destino: $DEST ($( [ -b "$DEST" ] && echo 'partición real' || echo 'fichero de prueba'))"
648if [ -b "$DEST" ] && command -v blkid >/dev/null 2>&1; then
649 echo " lo que hay HOY ahí (se va a PERDER):"
650 blkid "$DEST" 2>/dev/null | sed 's/^/ /' || echo " (sin filesystem reconocible)"
651fi
652if [ -n "$ESP" ] && [ -b "$ESP" ] && [ -x "$HAMMER" ]; then
653 _d="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$ESP")/..")")}"
654 echo " terreno de $_d (nada de esto se toca):"
655 "$HAMMER" boot entry survey --disk "$_d" 2>&1 | sed 's/^/ /' || true
656fi
657if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
658 printf " ¿formateo %s y dejo el resto del disco INTACTO? [escribí si] " "$DEST"
659 read -r _r; [ "$_r" = "si" ] || { echo " cancelado."; exit 1; }
660fi
661
662# ── 1) Los árboles (initramfs de pivote + rootfs con su /sbin/init) los arma el script VALIDADO ──
663# No se copian acá: se PIDEN. Ver el comentario de `STAGE_ONLY` en install-image-efi.sh.
664STAGE="${STAGE:-work/.install-part-stage}"
665# `STAGE_LISTO=1` — el árbol ya está armado y no hay que (ni se puede) rearmarlo. Es lo que usa el
666# instalador cuando corre DENTRO de un initramfs: allí no hay repo, ni store, ni `install-image-efi.sh`;
667# el stage viajó horneado en el propio initramfs. Se comprueba que esté completo, no se confía.
668if [ -n "${STAGE_LISTO:-}" ]; then
669 echo "==> stage ya armado: $STAGE (STAGE_LISTO=1)"
670else
671 echo "==> armando árboles (STAGE_ONLY sobre install-image-efi.sh)"
672 STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh
673fi
674ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
675[ -d "$ROOT_TREE" ] && [ -r "$IRD" ] || { echo "takana-install-partition: el stage no quedó completo" >&2; exit 1; }
676
677# ── 2) Formatear SÓLO la partición destino, y poblarla SIN MONTARLA ────────────────────────────
678# `mke2fs -d` escribe el árbol dentro del filesystem nuevo sin `mount` y sin loop. Que no haya
679# montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
680# este script existe para no tener. Si no somos root, `unshare -r` da los dueños root sin sudo.
681echo "==> ext4 LABEL=hammer-root en $DEST (única escritura destructiva de todo el script)"
682# ⚠ La detección de root NO puede fallar en silencio. Medido dentro del initramfs el 2026-09-23: sin
683# el applet `id` el `$(id -u)` quedaba VACÍO, la comparación daba falso, el script se iba a la rama
684# de `unshare` —que tampoco estaba— y **siguió adelante sin formatear nada**. Un `if` cuyo dato de
685# entrada no existe elige una rama igual, y elige mal.
686_uid=$(id -u 2>/dev/null || echo "")
687[ -n "$_uid" ] || { echo "takana-install-partition: no hay 'id' en este entorno y no puedo saber si soy root" >&2; exit 1; }
688if [ "$_uid" = "0" ]; then
689 "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
690else
691 command -v unshare >/dev/null 2>&1 || {
692 echo "takana-install-partition: no soy root (uid $_uid) y no hay 'unshare' para suplirlo" >&2; exit 1; }
693 unshare -r "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
694fi
695# Y no se da por hecho: se mira. Regla 3 del CLAUDE.md — un ausente falla ruidosamente, un vacío
696# llega hasta el final diciendo que todo fue bien. Acá el vacío sería un destino sin filesystem.
697# ⚠ La verificación tiene que funcionar con LOS DOS `blkid`, y la primera versión no lo hacía:
698# `-s LABEL -o value` es de util-linux y **busybox lo ignora**, devolviendo la línea entera
699# (`/dev/vda2: LABEL="hammer-root" UUID=…`). Resultado medido en QEMU: el formateo había salido
700# PERFECTO y el guardián abortó igual, con un mensaje que mostraba la label correcta dentro del
701# texto de «no quedó con LABEL». Tercer caso hoy del mismo patrón: un chequeo que valida algo
702# distinto de lo que cree validar acusa al inocente, y un guardián así se desactiva a la semana.
703# ⚠ Y el patrón tiene que distinguir `LABEL` de **`PARTLABEL`**, que es otra cosa: el primero es la
704# etiqueta del FILESYSTEM y el segundo el nombre de la ENTRADA GPT. Con `sed 's/.*LABEL="…"/'` el
705# `.*` es glotón y se come el «PART», así que devolvía el nombre de la partición en la tabla
706# («destino-takana») y el guardián abortaba una instalación correcta. Segunda vez que este mismo
707# chequeo acusa al inocente por mirar mal el dato. El espacio de ` LABEL="` es lo que los separa.
708_lbl_post=$(blkid "$DEST" 2>/dev/null | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
709[ -n "$_lbl_post" ] || _lbl_post=$(blkid -s LABEL -o value "$DEST" 2>/dev/null || true)
710[ "$_lbl_post" = "hammer-root" ] || {
711 echo "takana-install-partition: $DEST NO quedó con LABEL=hammer-root (blkid dice '${_lbl_post:-nada}')." >&2
712 echo " El formateo no se completó: no sigo." >&2; exit 1; }
713echo " ✓ $DEST formateada y poblada (LABEL=hammer-root verificada)"
714
715# ── 3) La ESP: sólo nuestra carpeta de vendor, con mtools y SIN montar ─────────────────────────
716if [ -n "$ESP" ]; then
717 # ⚠ **mtools puede QUEDARSE ESPERANDO UNA RESPUESTA, y colgado es indistinguible de lento.**
718 # Medido en QEMU el 2026-09-23: sobre `/dev/vda1` el instalador se detuvo para siempre justo
719 # después de imprimir el espacio libre, sin una línea más. Dos defensas, porque una sola tapa el
720 # síntoma sin quitar la causa:
721 # · `MTOOLS_SKIP_CHECK=1` desactiva el chequeo de geometría, que es lo que hace preguntar
722 # cuando el device no se parece a un disquete;
723 # · `< /dev/null` en cada invocación, para que una pregunta que igual aparezca FALLE en vez de
724 # esperar. Un error se lee; un cuelgue en un instalador parece un sistema muerto.
725 export MTOOLS_SKIP_CHECK=1
726 "$MDIR" -i "$ESP" :: >/dev/null 2>&1 < /dev/null || { echo "takana-install-partition: $ESP no parece FAT (¿es la ESP?)" >&2; exit 1; }
727 # Guardián de capacidad: una ESP de fábrica son 100 MiB y Windows ya vive ahí. El margen se
728 # comprueba ANTES, con los números a la vista, no como un «disk full» a mitad de copiar.
729 # ⚠ `mdir` imprime el espacio libre con SEPARADOR DE MILES en espacios («60 056 064 bytes
730 # free»), así que tomar `$1` devuelve **60** y el guardián aborta una instalación perfectamente
731 # posible. Medido en la primera corrida de este script. Hay que juntar los dígitos de la línea.
732 _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)
733 _need=$(( $(wc -c < "$KERNEL") + $(wc -c < "$IRD") ))
734 [ -n "${_free:-}" ] && [ "$_free" -lt "$_need" ] && {
735 echo "takana-install-partition: la ESP tiene ${_free}B libres y hacen falta ${_need}B." >&2
736 echo " No se escribió nada en ella." >&2; exit 1; }
737 # ⚠ **Un FAT puede declarar MÁS espacio del que su partición tiene, y nadie se queja hasta que
738 # una escritura grande cae más allá del final.** Medido acá el 2026-09-23 fabricando el disco de
739 # prueba: `mformat -i disco.img@@<offset>` formatea usando el tamaño del FICHERO ENTERO, no el de
740 # la partición ⇒ una ESP de 64 MiB con un FAT que anuncia 2,5 G. Y no es un artefacto del banco de
741 # pruebas: cualquier ESP que alguien encogió con una herramienta de particionado sin reformatear
742 # queda igual. El `df` del kernel repite la mentira —la leyó del mismo sector de arranque— así que
743 # NO alcanza con preguntarle al filesystem: hay que contrastar contra el TAMAÑO REAL del
744 # contenedor, que es lo único que no puede mentir.
745 if [ -b "$ESP" ]; then
746 _cap=$(blockdev --getsize64 "$ESP" 2>/dev/null || echo "")
747 [ -z "$_cap" ] && _cap=$(( $(cat "/sys/class/block/$(nombre_kernel "$ESP")/size" 2>/dev/null || echo 0) * 512 ))
748 else
749 _cap=$(wc -c < "$ESP")
750 fi
751 if [ -n "${_free:-}" ] && [ "${_cap:-0}" -gt 0 ] && [ "$_free" -gt "$_cap" ]; then
752 echo " ! el FAT de $ESP dice ${_free}B libres pero la partición mide ${_cap}B: el filesystem" >&2
753 echo " declara más espacio del que existe (ESP encogida sin reformatear, o formateada" >&2
754 echo " sobre un contenedor mayor). Uso el tamaño REAL como cota." >&2
755 _free="$_cap"
756 fi
757 echo "==> ESP $ESP: \\EFI\\takana\\ (${_need}B sobre ${_free:-?}B libres)"
758 "$MMD" -i "$ESP" ::/EFI 2>/dev/null < /dev/null || true
759 "$MMD" -i "$ESP" ::/EFI/takana 2>/dev/null < /dev/null || true
760 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/takana/takanax64.efi < /dev/null
761 "$MCOPY" -o -i "$ESP" "$IRD" ::/initramfs.cpio.gz < /dev/null
762 # La ruta fallback es tierra de nadie: si YA hay algo, es de otro sistema y no se pisa sin que
763 # el usuario lo pida. Si no hay nada, ocuparla es gratis y salva el caso «el firmware ignora la
764 # entrada NVRAM», que es el modo de fallo más común y el más difícil de diagnosticar.
765 if "$MDIR" -i "$ESP" ::/EFI/BOOT/BOOTX64.EFI >/dev/null 2>&1 < /dev/null; then
766 if [ -n "$CLAIM_FALLBACK" ]; then
767 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI < /dev/null
768 echo " ! \\EFI\\BOOT\\BOOTX64.EFI PISADO (--claim-fallback)"
769 else
770 echo " · \\EFI\\BOOT\\BOOTX64.EFI ya existe y es de otro: NO se toca (--claim-fallback para pisarlo)"
771 fi
772 else
773 "$MMD" -i "$ESP" ::/EFI/BOOT 2>/dev/null < /dev/null || true
774 "$MCOPY" -o -i "$ESP" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI < /dev/null
775 echo " + \\EFI\\BOOT\\BOOTX64.EFI estaba libre ⇒ ocupada (arranca sin depender de la NVRAM)"
776 fi
777 for _p in ::/EFI/takana/takanax64.efi ::/initramfs.cpio.gz; do
778 "$MDIR" -i "$ESP" "$_p" >/dev/null 2>&1 < /dev/null || { echo "takana-install-partition: falta $_p en la ESP" >&2; exit 1; }
779 done
780 echo " ✓ ESP poblada y verificada (sin montarla)"
781else
782 echo " ! sin --esp: el sistema queda instalado pero NO arrancable todavía"
783fi
784
785# ── 4) La entrada de arranque: convivir con el vecino, con respaldo previo ─────────────────────
786if [ -z "$NO_NVRAM" ] && [ -n "$ESP" ] && [ -b "$ESP" ] && [ -d /sys/firmware/efi/efivars ] && [ -x "$HAMMER" ]; then
787 if [ -z "$DISK" ] || [ -z "$ESP_PART" ]; then
788 _en=$(nombre_kernel "$ESP")
789 DISK="${DISK:-/dev/$(basename "$(readlink -f "/sys/class/block/$_en/..")")}"
790 ESP_PART="${ESP_PART:-$(cat "/sys/class/block/$_en/partition" 2>/dev/null || echo 1)}"
791 fi
792 echo "==> respaldo del arranque ANTES de tocar la NVRAM (ADR 0018 §5)"
793 "$HAMMER" boot entry backup || echo " ! el respaldo falló — seguí sólo si sabés por qué" >&2
794 echo "==> entrada NVRAM: $DISK p$ESP_PART → \\EFI\\takana\\takanax64.efi"
795 "$HAMMER" boot entry add --disk "$DISK" --partition "$ESP_PART" --loader '/EFI/takana/takanax64.efi'
796else
797 echo " · NVRAM no tocada (--no-nvram, sin efivarfs, o destino de prueba)"
798fi
799
800if [ -n "$USE_FREE" ]; then
801 echo "✓ takana instalada en $DEST, una partición NUEVA creada en espacio sin asignar."
802 echo " La tabla creció en una entrada; ninguna de las que ya estaban se modificó."
803else
804 echo "✓ takana instalada en $DEST. La tabla de particiones NO se tocó y las demás particiones siguen como estaban."
805fi
806echo " Comprobalo antes de reiniciar: lsblk -o NAME,SIZE,FSTYPE,LABEL · $HAMMER boot entry list"