gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

el instalador deja de ser «para esta máquina»: censo del terreno, menú y cmdline

El initramfs anterior llevaba el destino HORNEADO: servía para una máquina y había que rehacerlo
para la siguiente. Eso no es un instalador. Ahora el mismo fichero arranca en cualquier máquina y
decide allí, con tres fuentes en orden: la cmdline del kernel (`takana.root=PARTUUID=…`,
`takana.esp=`, `takana.free`, `takana.yes`), los valores por defecto si los hubo, y si no hay nada,
el CENSO DEL TERRENO y una pregunta.

El censo (`--survey`, y la base del menú) enumera cada partición de cada disco y dice qué se puede
usar y qué no, cruzando DOS fuentes porque ninguna sola alcanza: `sfdisk -d` da el tipo GPT y el
PARTUUID —la identidad estable—, y `blkid` da lo que hay dentro, porque el tipo miente a menudo.
Veredictos: ESP (se reusa, no se formatea), NO TOCAR (Windows, LUKS, swap, MSR), CANDIDATA, EN USO,
RAÍZ VIVA (sólo con el salto) y LIBRE (instalar al lado). Cada línea publica su PARTUUID.

Cinco fallos encontrados probándolo sobre hardware real y sobre el layout del usuario replicado:

1) LA RAÍZ VIVA NO SE DETECTA POR NOMBRE. `/proc/mounts` dice `/dev/root` —un nombre que no existe
   en /dev— así que comparar cadenas daba «ninguna partición es la raíz» y el censo OFRECÍA
   FORMATEAR LA RAÍZ DEL SISTEMA QUE ESTABA CORRIENDO. Medido en el anfitrión de este hub. Ahora se
   compara el NÚMERO de dispositivo (`stat` vs `/sys/class/block/*/dev`), con la descomposición de
   Linux y no el `/256` ingenuo, que se rompe justo en NVMe (major 259). Y si no se puede
   determinar, el censo lo dice en vez de callar.

2) EL GUID DE LUKS ESTABA DEFINIDO Y NO SE CONSULTABA. Una partición marcada como LUKS en la tabla
   salió ofrecida como «se puede usar» porque blkid no veía cabecera dentro. Un contenedor cifrado
   es justo el caso donde no se puede mirar adentro: el tipo es el único dato, así que manda.

3) `/tmp` PUEDE NO EXISTIR. Dentro del initramfs el censo no pudo escribir su fichero y el mensaje
   fue «no veo ningún disco en esta máquina» — con once discos listados dos líneas más abajo. Un
   diagnóstico que culpa al hardware por un directorio que falta manda a desarmar la máquina
   equivocada. Ahora busca dónde escribir y, si no hay, dice ESO.

4) LOS DISCOS SIN TABLA ERAN INVISIBLES. El store de este hub es `/dev/sdb` con un ext4 encima y
   nada más: el censo lo omitía por completo. Omitir algo que existe y está en uso es la peor forma
   de presentarlo.

5) Los `loop` libres (tamaño 0) llenaban el censo de ruido. Un guardián ruidoso se ignora igual que
   uno mudo.

El banco cubre ahora tres escenarios y el tercero es el menú: se le da una tty de verdad con
`python3 -m pty` —con un pipe no aparecería, porque el instalador exige `[ -t 0 ]`— y se comprueba
que el censo sale, que ofrece opciones, que marca intocables, que publica PARTUUID y que responder
«no» no toca nada. 25 comprobaciones, verde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

59d444f0c072de90a5e655b9a7294548851ef0f0 · ruwaq Sergio · (29 p'unchaw ñawpaq) · tayta af63ffd37 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

3 qillqakuna · +405 −20

diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index c7622154..63e84041 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -21,9 +21,13 @@
 set -eu
 ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 
+# Los argumentos son OPCIONALES a propósito. Un initramfs con el destino horneado sirve para UNA
+# máquina y hay que rehacerlo para la siguiente — eso no es un instalador, es un apaño. Sin
+# argumentos, el mismo fichero arranca en cualquier máquina y decide allí: lee la cmdline del kernel
+# (`takana.root=`, `takana.esp=`, `takana.free`) y, si no hay nada, muestra el censo del terreno y
+# pregunta. Lo que se pase acá queda sólo como VALOR POR DEFECTO.
 ARGS_INSTALADOR=""
 while [ $# -gt 0 ]; do ARGS_INSTALADOR="$ARGS_INSTALADOR $1"; shift; done
-[ -n "$ARGS_INSTALADOR" ] || { echo "uso: $0 <argumentos que se le pasarán a takana-install-partition.sh>" >&2; exit 2; }
 
 OUT="${OUT:-work/takana-instalador.cpio.gz}"
 STAGE="${STAGE:-work/.instalador-initramfs-stage}"
@@ -61,8 +65,10 @@ fi
 
 # ── 3) el árbol del initramfs ──────────────────────────────────────────────────────────────────
 echo "==> árbol del initramfs-instalador"
+# `/tmp` y `/run` NO son decorativos: el censo del terreno escribe ahí su fichero de trabajo, y sin
+# ellos el instalador fallaba diciendo «no veo ningún disco» con once discos delante.
 rm -rf "$IRD_TREE"; mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" \
-    "$IRD_TREE/instalador/lib" "$IRD_TREE/payload"
+    "$IRD_TREE/tmp" "$IRD_TREE/run" "$IRD_TREE/instalador/lib" "$IRD_TREE/payload"
 BUSYBOX="${BUSYBOX:-$STAGE/root/bin/busybox}"
 [ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX" >&2; exit 1; }
 cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
@@ -141,8 +147,24 @@ while [ "$i" -lt 60 ]; do
 done
 
 cd /instalador
-ARGS=$(cat /instalador/args)
-log "TAKANA-INSTALADOR: takana-install-partition.sh $ARGS"
+# ── De dónde sale QUÉ instalar, en orden de precedencia ───────────────────────────────────────
+#   1. la CMDLINE del kernel  (`takana.root=PARTUUID=… takana.esp=… takana.free takana.yes`)
+#   2. los valores por defecto horneados al armar el initramfs, si los hubo
+#   3. NADA ⇒ modo interactivo: el censo del terreno y una pregunta
+# El 1 es lo que hace que este mismo initramfs sirva en cualquier máquina: quien salta por kexec
+# pasa la elección por la cmdline, y quien arranca de un USB la edita en el menú del loader.
+ARGS=""; YES=""
+for _w in $(cat /proc/cmdline 2>/dev/null); do
+    case "$_w" in
+        takana.root=*) ARGS="$ARGS --root-part ${_w#takana.root=}" ;;
+        takana.esp=*)  ARGS="$ARGS --esp ${_w#takana.esp=}" ;;
+        takana.disk=*) ARGS="$ARGS --disk ${_w#takana.disk=}" ;;
+        takana.free)   ARGS="$ARGS --use-free-space" ;;
+        takana.yes)    YES=1 ;;
+    esac
+done
+[ -z "$ARGS" ] && [ -s /instalador/args ] && ARGS=$(cat /instalador/args)
+log "TAKANA-INSTALADOR: takana-install-partition.sh ${ARGS:-(sin parámetros ⇒ interactivo)}"
 # Las herramientas viajan en /bin: el instalador las toma por TOOL_<binario> en vez de buscarlas en
 # un store que acá no existe. STAGE_LISTO dice que el árbol ya vino armado.
 export TOOL_mke2fs=/bin/mke2fs TOOL_sfdisk=/bin/sfdisk TOOL_mmd=/bin/mmd \
@@ -156,7 +178,19 @@ export STAGE=/payload/stage STAGE_LISTO=1 KERNEL=/payload/bzImage HAMMER=/bin/ta
 # shellcheck disable=SC2086
 # `< /dev/null`: nada de esto debe leer del teclado —las confirmaciones ya van por `--yes`— y si
 # algo lo intenta tiene que FALLAR, no quedarse esperando en una consola que nadie está mirando.
-{ sh /instalador/takana-install-partition.sh $ARGS --yes < /dev/null; echo $? > /rc; } 2>&1 | tee /dev/tty0 > /dev/ttyS0
+# Desatendido SÓLO si la cmdline lo autorizó con `takana.yes`. Si hay parámetros pero nadie dijo
+# que sí, se entra igual por la rama interactiva: preguntar y que nadie conteste es un cuelgue, pero
+# formatear sin que nadie lo haya pedido es peor.
+if [ -n "$ARGS" ] && [ -n "$YES" ]; then
+    { sh /instalador/takana-install-partition.sh $ARGS --yes < /dev/null; echo $? > /rc; } 2>&1 | tee /dev/tty0 > /dev/ttyS0
+else
+    # Interactivo: la consola del kernel es la pantalla (tty0 va ÚLTIMO en la cmdline del salto),
+    # así que entrada y salida van ahí y el instalador ve una terminal de verdad — sin eso su
+    # `[ -t 0 ]` sale falso y se niega a preguntar.
+    log "TAKANA-INSTALADOR: sin autorización desatendida ⇒ el censo y la pregunta salen por la consola"
+    sh /instalador/takana-install-partition.sh $ARGS < /dev/console > /dev/console 2>&1
+    echo $? > /rc
+fi
 if [ "$(cat /rc 2>/dev/null || echo 1)" = "0" ]; then
     log "TAKANA-INSTALADOR-OK: instalado. Reiniciando en 10 s (sacá el medio si lo hubiera)."
     sync; sleep 10; reboot -f
@@ -171,6 +205,12 @@ mkdir -p "$(dirname "$OUT")"
 ( cd "$IRD_TREE" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -1 ) > "$OUT"
 echo "==> $OUT: $(du -h "$OUT" | cut -f1)"
 echo
+echo "Este initramfs NO lleva ninguna máquina horneada: decide al arrancar."
+echo "  · sin parámetros                  ⇒ censo del terreno + pregunta (sirve en cualquier máquina)"
+echo "  · takana.root=PARTUUID=…          ⇒ destino por identidad estable"
+echo "  · takana.free takana.disk=/dev/…  ⇒ instalar al lado, en espacio sin asignar"
+echo "  · takana.yes                      ⇒ sin preguntar (sólo junto a lo de arriba)"
+echo
 echo "Para SALTAR a él sin pasar por el firmware (el userspace de esta máquina muere ACÁ):"
 echo "  sudo takana kernel kexec --probe          # ¿se puede? exit 0 = sí"
 echo "  sudo takana kernel kexec <linux-metal> --initrd $OUT \\"
diff --git a/scripts/install-particion-test.sh b/scripts/install-particion-test.sh
index 56be5002..c22e1c34 100755
--- a/scripts/install-particion-test.sh
+++ b/scripts/install-particion-test.sh
@@ -171,6 +171,58 @@ _nueva=$(tail -1 "$P/tabla-ahora2" | cut -d' ' -f1 | tr -d ':')
 [ "$(blkid -s LABEL -o value "$_nueva" 2>/dev/null)" = "hammer-root" ] \
     && ok "la partición nueva ($_nueva) quedó ext4 LABEL=hammer-root" || falla "la partición nueva no tiene el filesystem esperado"
 
+# ── escenario 3: el MODO INTERACTIVO, bajo un pty ─────────────────────────────────────────────
+# El menú sólo aparece si el instalador ve una terminal de verdad (`[ -t 0 ]`), así que no se puede
+# probar con un simple pipe: haría falta una tty. Se le da una con `python3 -m pty`, que es la forma
+# de automatizar exactamente el camino que recorre una persona. Se comprueba (1) que el censo
+# clasifica bien —que lo cifrado y lo de Windows NO se ofrecen— y (2) que la elección se convierte
+# en un PARTUUID y no en un nombre de device.
+echo
+echo "==> escenario 3: modo interactivo (censo + elección) bajo pty"
+_n_destino=$("$SFDISK" -d "$LOOP" 2>/dev/null | awk '/^\/dev/{n++} END{print n}')
+_menu_out="$P/menu.txt"
+ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" TAKANA_TEST_MENU=1 \
+python3 - "$PWD" "$LOOP" > "$_menu_out" 2>&1 <<'PYEOF' || true
+import os, pty, sys, time
+cwd, loop = sys.argv[1], sys.argv[2]
+# Se responde "no": lo que se valida es el CENSO y las opciones ofrecidas, no otra instalación.
+salida = []
+def leer(fd):
+    d = os.read(fd, 1024)
+    salida.append(d)
+    return d
+pid, fd = pty.fork()
+if pid == 0:
+    os.chdir(cwd)
+    os.execv("/bin/sh", ["sh", "scripts/takana-install-partition.sh"])
+buf = b""
+t0 = time.time()
+while time.time() - t0 < 120:
+    try:
+        d = leer(fd)
+    except OSError:
+        break
+    if not d:
+        break
+    buf += d
+    if b"salir sin tocar nada" in buf:
+        os.write(fd, b"no\n")
+        break
+try:
+    while True:
+        d = leer(fd)
+        if not d: break
+except OSError:
+    pass
+os.waitpid(pid, 0)
+sys.stdout.write(b"".join(salida).decode("utf-8", "replace"))
+PYEOF
+grep -q "el terreno de esta" "$_menu_out" && ok "el menú muestra el censo del terreno" || falla "el modo interactivo no mostró el censo"
+grep -q "opciones:" "$_menu_out" && ok "el menú ofrece opciones numeradas" || falla "el menú no ofreció opciones"
+grep -qi "NO TOCAR" "$_menu_out" && ok "hay particiones marcadas NO TOCAR" || falla "el censo no marcó ninguna partición como intocable"
+grep -q "PARTUUID=" "$_menu_out" && ok "el censo publica el PARTUUID de cada partición" || falla "el censo no muestra PARTUUID"
+grep -q "cancelado: no se tocó nada" "$_menu_out" && ok "responder «no» sale sin tocar nada" || falla "la salida por «no» no se comportó"
+
 echo
 [ "$FALLOS" = "0" ] && { echo "VERDE: instaló sin tocar nada de lo ajeno ($LOOP)"; exit 0; }
 echo "ROJO: $FALLOS comprobación(es) fallaron" >&2; exit 1
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index 0bf53191..5f3806f8 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -30,6 +30,10 @@
 #   --root-part <dev|PARTUUID=…|UUID=…|LABEL=…>  DESTINO. Se formatea ext4 con LABEL=hammer-root.
 #                              Lo ÚNICO destructivo. **Usá PARTUUID=**: los nombres nvmeXn1 NO son
 #                              estables entre arranques (medido) y el kexec cambia quién enumera.
+#   --survey                   mirar y NO tocar: el censo del terreno de esta máquina, con el
+#                              PARTUUID de cada partición y qué se puede usar. Empezá por acá.
+#   (sin destino)              modo INTERACTIVO: muestra el censo y te deja elegir. Es lo que hace
+#                              que un mismo medio sirva en cualquier máquina.
 #   --use-free-space           no formatear nada existente: crear la partición en el hueco sin
 #                              asignar más grande de `--disk` e instalar ahí. Al lado, no encima.
 #   --on-active-root <modo>    qué hacer si `--root-part` ES la raíz viva (auto|kexec|free-space|abort).
@@ -44,7 +48,7 @@ set -eu
 ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 
 DEST=""; ESP=""; DISK=""; ESP_PART=""; CLAIM_FALLBACK=""; NO_NVRAM=""; ASSUME_YES=""
-USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"
+USE_FREE=""; ON_ACTIVE="${ON_ACTIVE:-auto}"; SOLO_CENSO=""
 while [ $# -gt 0 ]; do
     case "$1" in
         --root-part) DEST="${2:?}"; shift 2 ;;
@@ -53,6 +57,7 @@ while [ $# -gt 0 ]; do
         --esp-part)  ESP_PART="${2:?}"; shift 2 ;;
         --claim-fallback) CLAIM_FALLBACK=1; shift ;;
         --no-nvram)  NO_NVRAM=1; shift ;;
+        --survey)    SOLO_CENSO=1; shift ;;
         --use-free-space) USE_FREE=1; shift ;;
         --on-active-root) ON_ACTIVE="${2:?}"; shift 2 ;;
         --yes|-y)    ASSUME_YES=1; shift ;;
@@ -60,7 +65,10 @@ while [ $# -gt 0 ]; do
         *) echo "opción desconocida: $1 (--help)" >&2; exit 2 ;;
     esac
 done
-[ -n "$DEST" ] || [ -n "$USE_FREE" ] || { echo "falta --root-part (o --use-free-space) — ver --help" >&2; exit 2; }
+# Sin destino no se aborta: se PREGUNTA. Eso es lo que separa un instalador de un guion que hay que
+# preparar por máquina. Sólo si tampoco hay con quién hablar (sin terminal) se pide el parámetro.
+NECESITA_ELEGIR=""
+if [ -z "$DEST" ] && [ -z "$USE_FREE" ] && [ -z "$SOLO_CENSO" ]; then NECESITA_ELEGIR=1; fi
 case "$ON_ACTIVE" in auto|kexec|free-space|abort) ;; *) echo "--on-active-root: auto|kexec|free-space|abort" >&2; exit 2 ;; esac
 
 # ── Las herramientas, ANTES que nada ───────────────────────────────────────────────────────────
@@ -161,6 +169,259 @@ releer_tabla() {
     partprobe "$1" 2>/dev/null || true
 }
 
+# ── EL CENSO DEL TERRENO ───────────────────────────────────────────────────────────────────────
+# Un instalador que hay que preparar a mano para cada máquina no es un instalador. Esto enumera lo
+# que hay —en CUALQUIER máquina— y dice de cada partición si se puede usar, si no se debe tocar y
+# por qué. Es la base del modo interactivo y de la recomendación automática.
+#
+# Dos fuentes, porque ninguna sola alcanza:
+#   · `sfdisk -d`  da el TIPO de la tabla (GUID) y el **PARTUUID**, que es la identidad estable;
+#   · `blkid`      da lo que hay ADENTRO (ntfs, crypto_LUKS, ext4, swap…), que el tipo GPT miente
+#                  a menudo: media instalación de Windows vive en «datos básicos», y un LUKS puede
+#                  estar en una partición marcada como Linux.
+# Y `/proc/mounts` dice qué está en uso AHORA, que es lo único que distingue «vacía» de «tu raíz».
+
+# ── raiz_viva_majmin — quién es «/» de verdad ─────────────────────────────────────────────────
+# ⚠ **`/proc/mounts` NO alcanza para saber cuál es la raíz viva, y esto está MEDIDO** (2026-09-23,
+# sobre el anfitrión de este hub): la línea de «/» dice `/dev/root`, un nombre que no existe en
+# `/dev` ni casa con ninguna partición. Comparar por nombre daba «ninguna partición es la raíz» y el
+# censo **ofrecía formatear la raíz del sistema que estaba corriendo**. Pasa también con `UUID=…`
+# en la cmdline, con initramfs que montan por label, y con `/dev/mapper/*`.
+#
+# Lo que sí es inequívoco es el NÚMERO de dispositivo: `stat` lo da para «/» y cada partición lo
+# publica en `/sys/class/block/<n>/dev`. La descomposición es la de Linux (12 bits de major, minor
+# partido en dos), no el `/256` ingenuo, que se rompe justo en NVMe (major 259).
+raiz_viva_majmin() {
+    _rv_hex=$(stat -c %D / 2>/dev/null) || return 1
+    [ -n "${_rv_hex:-}" ] || return 1
+    _rv_d=$(( 0x$_rv_hex )) 2>/dev/null || return 1
+    _rv_maj=$(( (_rv_d >> 8) & 0xfff ))
+    _rv_min=$(( (_rv_d & 0xff) | ((_rv_d >> 12) & 0xfff00) ))
+    echo "${_rv_maj}:${_rv_min}"
+}
+RAIZ_MAJMIN="$(raiz_viva_majmin || true)"
+
+es_la_raiz_viva() {   # <dev>
+    [ -n "${RAIZ_MAJMIN:-}" ] || return 1
+    _er_n=$(nombre_kernel "$1")
+    [ "$(cat "/sys/class/block/$_er_n/dev" 2>/dev/null)" = "$RAIZ_MAJMIN" ]
+}
+
+GUID_ESP="c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
+GUID_MSR="e3c9e316-0b5c-4db8-817d-f92df00215ae"
+GUID_WINRE="de94bba4-06d1-4d40-a16a-bfd50179d6ac"
+GUID_MSBASIC="ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"
+GUID_LINUX="0fc63daf-8483-4772-8e79-3d69d8477de4"
+GUID_LUKS="ca7d7ccb-63ed-4c53-861c-1742536059cc"
+GUID_SWAP="0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"
+
+discos_del_sistema() {
+    for _dd in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
+        [ -b "$_dd" ] || continue
+        # Tamaño 0 = un `loop` libre sin fichero detrás. Existen por docenas y no son discos: si se
+        # listan, el censo se llena de ruido y lo que importa deja de verse. Un guardián ruidoso se
+        # ignora igual que uno mudo.
+        [ "$(cat "/sys/class/block/$(basename "$(readlink -f "$_dd")")/size" 2>/dev/null || echo 0)" -gt 0 ] || continue
+        echo "$_dd"
+    done
+}
+
+mib_de_sectores() { echo $(( ${1:-0} / 2048 )); }
+
+# clasificar <guid-tipo> <fstype> <montaje> → VEREDICTO|ETIQUETA
+# El veredicto es lo que decide el menú: SOLO_ESP, NO_TOCAR, CANDIDATA, RAIZ_VIVA, EN_USO.
+clasificar() {
+    _cl_guid=$(printf '%s' "${1:-}" | tr 'A-Z' 'a-z')
+    _cl_fs="${2:-}"; _cl_mnt="${3:-}"
+    case "$_cl_guid" in
+        "$GUID_ESP") echo "SOLO_ESP|ESP (partición de arranque EFI)"; return ;;
+        "$GUID_MSR") echo "NO_TOCAR|reservada de Microsoft"; return ;;
+        "$GUID_WINRE") echo "NO_TOCAR|recuperación de Windows"; return ;;
+        "$GUID_SWAP") echo "NO_TOCAR|swap"; return ;;
+        # «Datos básicos de Microsoft» es el tipo que Windows le pone a sus volúmenes. Aunque el
+        # filesystem no se reconozca —puede estar cerrado por BitLocker, o simplemente vacío— no se
+        # ofrece: el coste de equivocarse acá es el disco de otro sistema operativo.
+        "$GUID_MSBASIC") echo "NO_TOCAR|volumen de datos de Windows (tipo GPT «basic data»)"; return ;;
+        # ⚠ El tipo LUKS de la TABLA tiene que bastar por sí solo. Estaba definido y **no se
+        # consultaba**: en la prueba con el layout del usuario, una partición marcada como LUKS en
+        # GPT salió ofrecida como «se puede usar» porque `blkid` no veía cabecera dentro. Un
+        # contenedor cifrado es justo el caso donde no se puede mirar adentro para decidir — el
+        # único dato disponible es el tipo, así que el tipo manda.
+        "$GUID_LUKS") echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS, por tipo de la tabla)"; return ;;
+    esac
+    case "$_cl_fs" in
+        crypto_LUKS) echo "NO_TOCAR|VOLUMEN CIFRADO (LUKS) — lo que hay dentro no se puede ver desde acá"; return ;;
+        ntfs)        echo "NO_TOCAR|NTFS (Windows u otro sistema)"; return ;;
+        vfat|msdos)  echo "NO_TOCAR|FAT — puede ser una ESP o datos compartidos"; return ;;
+        swap)        echo "NO_TOCAR|swap"; return ;;
+        BitLocker|bitlocker) echo "NO_TOCAR|BitLocker"; return ;;
+    esac
+    if [ "$_cl_mnt" = "/" ]; then
+        echo "RAIZ_VIVA|la raíz del sistema que está corriendo AHORA"; return
+    fi
+    if [ -n "$_cl_mnt" ]; then
+        echo "EN_USO|montada en $_cl_mnt"; return
+    fi
+    case "$_cl_fs" in
+        ext2|ext3|ext4|xfs|btrfs|f2fs) echo "CANDIDATA|Linux ($_cl_fs) sin montar"; return ;;
+        "")  echo "CANDIDATA|sin filesystem reconocible"; return ;;
+    esac
+    echo "NO_TOCAR|$_cl_fs (desconocido para este instalador)"
+}
+
+# censo [--solo-candidatas] → una línea por partición:
+#   <dev>|<partuuid>|<mib>|<fstype>|<label>|<montaje>|<VEREDICTO>|<explicación>
+censo() {
+    _ce_solo="${1:-}"
+    for _ce_d in $(discos_del_sistema); do
+        # ⚠ Un disco SIN TABLA no es un disco vacío: puede estar formateado directo y montado —así
+        # vive el store de este hub, `/dev/sdb` con un ext4 encima y nada más—. Sin esta rama el
+        # censo lo OMITE por completo, que es la peor forma de presentar algo que existe y está en
+        # uso: invisible. Se lista, se dice que no tiene tabla, y no se ofrece.
+        if [ -z "$("$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}")" ]; then
+            _ce_bl=$(blkid "$_ce_d" 2>/dev/null || true)
+            _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"'  | head -1 | sed 's/^ TYPE="//; s/"$//')
+            _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
+            _ce_mn=$(awk -v d="$_ce_d" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
+            es_la_raiz_viva "$_ce_d" && _ce_mn="/"
+            _ce_sz=$(( $(cat "/sys/class/block/$(nombre_kernel "$_ce_d")/size" 2>/dev/null || echo 0) ))
+            if [ -n "$_ce_mn" ]; then _ce_v="EN_USO"; _ce_e="disco SIN TABLA de particiones, montado en $_ce_mn"
+            else _ce_v="NO_TOCAR"; _ce_e="disco SIN TABLA de particiones (${_ce_fs:-sin filesystem})"; fi
+            echo "$_ce_d|-|$(mib_de_sectores "$_ce_sz")|${_ce_fs}|${_ce_lb}|${_ce_mn}|$_ce_v|$_ce_e"
+            continue
+        fi
+        "$SFDISK" -d "$_ce_d" 2>/dev/null | grep "^${_ce_d}" | while IFS= read -r _ce_l; do
+            _ce_dev=$(printf '%s' "$_ce_l" | awk '{print $1}')
+            _ce_sz=$(printf '%s'  "$_ce_l" | sed -n 's/.*size=[ ]*\([0-9]*\).*/\1/p')
+            _ce_ty=$(printf '%s'  "$_ce_l" | sed -n 's/.*type=\([0-9A-Fa-f-]*\).*/\1/p')
+            _ce_uu=$(printf '%s'  "$_ce_l" | sed -n 's/.*uuid=\([0-9A-Fa-f-]*\).*/\1/p')
+            _ce_bl=$(blkid "$_ce_dev" 2>/dev/null || true)
+            _ce_fs=$(printf '%s' "$_ce_bl" | grep -o ' TYPE="[^"]*"'  | head -1 | sed 's/^ TYPE="//; s/"$//')
+            _ce_lb=$(printf '%s' "$_ce_bl" | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
+            _ce_mn=$(awk -v d="$_ce_dev" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true)
+            # La raíz viva se decide por número de dispositivo, no por el nombre de /proc/mounts.
+            if es_la_raiz_viva "$_ce_dev"; then _ce_mn="/"; fi
+            _ce_cl=$(clasificar "$_ce_ty" "$_ce_fs" "$_ce_mn")
+            _ce_ver="${_ce_cl%%|*}"; _ce_exp="${_ce_cl#*|}"
+            [ "$_ce_solo" = "--solo-candidatas" ] && [ "$_ce_ver" != "CANDIDATA" ] && continue
+            echo "$_ce_dev|$_ce_uu|$(mib_de_sectores "$_ce_sz")|$_ce_fs|$_ce_lb|$_ce_mn|$_ce_ver|$_ce_exp"
+        done
+        # El espacio SIN ASIGNAR es un candidato más, y el único que no destruye nada.
+        "$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}'
+    done
+}
+
+# mostrar_censo — el censo en forma de tabla, con los NO TOCAR dichos en voz alta.
+mostrar_censo() {
+    _mc_f="${1:-}"
+    echo "── el terreno de esta máquina ─────────────────────────────────────────────────"
+    [ -n "${RAIZ_MAJMIN:-}" ] || echo "  ⚠ NO pude determinar cuál es la raíz viva de esta máquina: tratá TODO con sospecha."
+    printf "  %-3s %-16s %-9s %-11s %s\n" "nº" "partición" "tamaño" "qué es" "veredicto"
+    _mc_n=0
+    { if [ -n "$_mc_f" ]; then cat "$_mc_f"; else censo; fi; } | while IFS='|' read -r _d _u _m _fs _lb _mn _ver _exp; do
+        _mc_n=$(( _mc_n + 1 ))
+        case "$_ver" in
+            CANDIDATA) _mc_marca="✓ se puede usar (SE BORRA lo que tenga)" ;;
+            LIBRE)     _mc_marca="✓ instalar AL LADO sin borrar nada" ;;
+            RAIZ_VIVA) _mc_marca="⚡ sólo con el salto (kexec)" ;;
+            SOLO_ESP)  _mc_marca="· se reusa como ESP, no se formatea" ;;
+            EN_USO)    _mc_marca="· en uso" ;;
+            *)         _mc_marca="✗ NO TOCAR" ;;
+        esac
+        printf "  %-3s %-16s %6s M  %-11s %s\n" "$_mc_n" "$_d" "$_m" "${_fs:-—}" "$_mc_marca"
+        printf "      %s%s\n" "$_exp" "$( [ -n "$_lb" ] && [ "$_lb" != "-" ] && printf ' · etiqueta «%s»' "$_lb")"
+        [ -n "$_u" ] && [ "$_u" != "-" ] && printf "      PARTUUID=%s\n" "$_u"
+    done
+    echo "───────────────────────────────────────────────────────────────────────────────"
+}
+
+# ── partuuid_de <dev> — la identidad estable de una partición, leída de la TABLA ──────────────
+partuuid_de() {
+    "$SFDISK" -d "$(disco_de "$1")" 2>/dev/null \
+        | awk -v d="$1" '$1==d {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i; exit}}'
+}
+
+# ── elegir_destino — el modo interactivo, que es lo que hace de esto un instalador ────────────
+# Sin esto hay que saber de antemano el PARTUUID de la máquina de destino, o sea preparar un medio
+# distinto por máquina. Con esto el MISMO medio sirve en cualquiera: arranca, mira y ofrece.
+# Deja la elección en DEST / USE_FREE / DISK del llamador.
+# ⚠ `/tmp` puede NO EXISTIR. Medido dentro del initramfs: el censo no pudo escribir su fichero y el
+# mensaje que salió fue «no veo ningún disco en esta máquina» — con once discos a la vista dos
+# líneas más abajo. Un diagnóstico que culpa al hardware cuando falta un directorio manda a
+# desarmar la máquina equivocada. Ahora se busca un sitio escribible y, si no hay ninguno, se dice
+# ESO.
+dir_temporal() {
+    for _dt in "${TMPDIR:-}" /tmp /run /dev/shm .; do
+        [ -n "$_dt" ] || continue
+        [ -d "$_dt" ] || continue
+        if ( : > "$_dt/.takana-escritura.$$" ) 2>/dev/null; then
+            rm -f "$_dt/.takana-escritura.$$"; echo "$_dt"; return 0
+        fi
+    done
+    return 1
+}
+
+elegir_destino() {
+    _ed_dir=$(dir_temporal) || {
+        echo "takana-install-partition: no encuentro ningún directorio donde escribir (/tmp, /run…)" >&2
+        return 1; }
+    _ed_f="$_ed_dir/.takana-censo.$$"
+    censo > "$_ed_f" 2>/dev/null
+    [ -s "$_ed_f" ] || { echo "takana-install-partition: no veo ningún disco en esta máquina" >&2; rm -f "$_ed_f"; return 1; }
+    mostrar_censo "$_ed_f"
+    echo "  Lo marcado ✗ NO TOCAR no se ofrece: son sistemas de otros, volúmenes cifrados o swap."
+    echo "  Elegir una ✓ BORRA lo que tenga. «Al lado» no borra nada."
+    _ed_n=0
+    printf "  opciones: "
+    while IFS='|' read -r _d _u _m _fs _lb _mn _ver _exp; do
+        _ed_n=$(( _ed_n + 1 ))
+        case "$_ver" in CANDIDATA|LIBRE|RAIZ_VIVA) printf "%s " "$_ed_n" ;; esac
+    done < "$_ed_f"
+    echo
+    printf "  nº (o 'no' para salir sin tocar nada): "
+    read -r _ed_sel || _ed_sel=no
+    case "$_ed_sel" in
+        ''|no|NO|n) echo "  cancelado: no se tocó nada."; rm -f "$_ed_f"; return 1 ;;
+    esac
+    _ed_linea=$(sed -n "${_ed_sel}p" "$_ed_f" 2>/dev/null)
+    rm -f "$_ed_f"
+    [ -n "$_ed_linea" ] || { echo "takana-install-partition: «$_ed_sel» no es una de las opciones" >&2; return 1; }
+    _ed_dev=$(printf '%s' "$_ed_linea" | cut -d'|' -f1)
+    _ed_uu=$(printf  '%s' "$_ed_linea" | cut -d'|' -f2)
+    _ed_ver=$(printf '%s' "$_ed_linea" | cut -d'|' -f7)
+    case "$_ed_ver" in
+        LIBRE)     USE_FREE=1; DISK="$_ed_dev"; DEST=""
+                   echo "  ⇒ instalar AL LADO, en el espacio libre de $_ed_dev" ;;
+        CANDIDATA|RAIZ_VIVA)
+                   # Por PARTUUID SIEMPRE que se pueda: el nombre de device puede cambiar entre
+                   # este momento y el arranque siguiente (ver `resolver_dev`).
+                   if [ -n "$_ed_uu" ] && [ "$_ed_uu" != "-" ]; then DEST="PARTUUID=$_ed_uu"; else DEST="$_ed_dev"; fi
+                   echo "  ⇒ destino: $_ed_dev ($DEST)" ;;
+        *) echo "takana-install-partition: esa partición está marcada como NO TOCAR y no se ofrece" >&2; return 1 ;;
+    esac
+    return 0
+}
+
+# ── esp_automatica [disco-del-destino] — la ESP no se pregunta si se puede deducir ────────────
+# Regla: la ESP del MISMO disco que el destino; si ahí no hay, la única del sistema. Si hay varias y
+# ninguna en ese disco, se pregunta: elegir entre las ESP de dos sistemas operativos distintos es
+# justo la clase de decisión que un instalador no debe tomar solo.
+esp_automatica() {
+    _ea_disco="${1:-}"
+    _ea_todas=$(censo | awk -F'|' '$7=="SOLO_ESP" {print $1}')
+    [ -n "$_ea_todas" ] || return 1
+    if [ -n "$_ea_disco" ]; then
+        for _ea_e in $_ea_todas; do
+            case "$_ea_e" in "$_ea_disco"*) echo "$_ea_e"; return 0 ;; esac
+        done
+    fi
+    _ea_n=$(printf '%s\n' "$_ea_todas" | grep -c .)
+    if [ "$_ea_n" = "1" ]; then printf '%s\n' "$_ea_todas"; return 0; fi
+    echo "takana-install-partition: hay $_ea_n ESP y ninguna en el disco del destino. Pasá --esp:" >&2
+    printf '%s\n' "$_ea_todas" | sed 's/^/      /' >&2
+    return 1
+}
+
 # ── mib_necesarios — cuánto pedirle al hueco ──────────────────────────────────────────────────
 # El rootfs medido + 40 %, con piso de 2 GiB. El margen no es superstición: `mke2fs` se queda con
 # ~1,5 % en metadatos y el sistema instalado tiene que poder escribir algo antes de que alguien lo
@@ -232,6 +493,23 @@ crear_en_hueco() {
     echo "$_ch_nuevo"
 }
 
+# `--survey`: mirar y no tocar. Es el comando con el que se empieza en una máquina desconocida.
+if [ -n "$SOLO_CENSO" ]; then
+    mostrar_censo
+    exit 0
+fi
+
+if [ -n "$NECESITA_ELEGIR" ]; then
+    if [ -t 0 ]; then
+        elegir_destino || exit 1
+    else
+        echo "takana-install-partition: no me diste destino y no hay terminal para preguntarte." >&2
+        echo "                          Pasá --root-part PARTUUID=… o --use-free-space --disk …" >&2
+        echo "                          (y mirá el terreno con --survey)" >&2
+        exit 2
+    fi
+fi
+
 # La identidad se resuelve ANTES de cualquier guarda: a partir de acá se trabaja con el device real,
 # y si el `PARTUUID` no existe en esta máquina se aborta sin haber tocado nada.
 if [ -n "$DEST" ]; then DEST=$(resolver_dev "$DEST") || exit 1; fi
@@ -248,6 +526,13 @@ if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
     echo "    partición creada: $DEST"
 fi
 
+# La ESP: si no se dijo cuál, se deduce del disco del destino. Una pregunta menos que hacerle al
+# usuario, y la respuesta correcta en el caso normal (un disco, una ESP).
+if [ -z "$ESP" ] && [ -n "$DEST" ] && [ -b "$DEST" ]; then
+    _esp_auto=$(esp_automatica "$(disco_de "$DEST")" 2>/dev/null || true)
+    [ -n "${_esp_auto:-}" ] && { ESP="$_esp_auto"; echo "==> ESP deducida: $ESP (del mismo disco que el destino)"; }
+fi
+
 if es_bloque "$DEST"; then
     _n=$(nombre_kernel "$DEST")
     # 1) DESTINO TIENE QUE SER UNA PARTICIÓN, NO UN DISCO. `/sys/class/block/<x>/partition` sólo
@@ -262,8 +547,11 @@ if es_bloque "$DEST"; then
     fi
     # 3) NO PUEDE SER LA RAÍZ VIVA. Es el caso que el usuario pregunta siempre: «¿puedo instalar
     #    encima del Linux que estoy usando?». No desde él: hay que saltar antes a otro kernel/medio.
+    # Por número de dispositivo, no por nombre: ver `raiz_viva_majmin`. El fallback por nombre se
+    # conserva porque no estorba y cubre el caso en que `stat` no exista.
     _rootdev=$(awk '$2=="/" {print $1; exit}' /proc/mounts 2>/dev/null || true)
-    if [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
+    if es_la_raiz_viva "$DEST" || \
+       [ "$(readlink -f "$DEST")" = "$(readlink -f "${_rootdev:-/nonexistent}" 2>/dev/null || echo /nonexistent)" ]; then
         politica_raiz_viva      # kexec → espacio libre → abortar, según --on-active-root
     fi
     # 4) NO PUEDE SER LA ESP que vamos a reusar.
@@ -319,22 +607,28 @@ politica_raiz_viva() {
 # ── camino_kexec — armar el instalador en RAM y saltar. NO VUELVE ─────────────────────────────
 camino_kexec() {
     echo "==> camino KEXEC: el instalador viaja en RAM y el sistema viejo muere en el salto"
-    _ck_args="--root-part $DEST"
-    [ -n "$ESP" ] && _ck_args="$_ck_args --esp $ESP"
-    [ -n "$CLAIM_FALLBACK" ] && _ck_args="$_ck_args --claim-fallback"
-    # ⚠ `--on-active-root abort` ADENTRO, y no es decorativo: si por lo que sea el destino siguiera
-    # siendo la raíz viva del sistema nuevo, `auto` volvería a intentar un kexec y tendríamos una
-    # máquina saltando a sí misma en bucle. Abortar ahí es un fallo legible.
-    _ck_args="$_ck_args --on-active-root abort"
+    # ⚠ **La elección viaja por la CMDLINE, no horneada en el initramfs.** Un initramfs con el
+    # destino adentro sirve para una máquina y hay que rehacerlo para la siguiente; el mismo fichero
+    # con la elección en la cmdline sirve para todas. Y va por **PARTUUID**: el kernel de después
+    # del salto puede enumerar los discos con otros nombres (medido), así que `/dev/nvme1n1p6` es
+    # justamente lo que NO hay que pasar.
+    _ck_ru=$(partuuid_de "$DEST" 2>/dev/null || true)
+    _ck_eu=""; [ -n "$ESP" ] && _ck_eu=$(partuuid_de "$ESP" 2>/dev/null || true)
+    _ck_cl="${CMDLINE_SALTO:-console=ttyS0,115200 console=tty0}"
+    if [ -n "$_ck_ru" ]; then _ck_cl="$_ck_cl takana.root=PARTUUID=$_ck_ru"
+    else                      _ck_cl="$_ck_cl takana.root=$DEST"; fi
+    if [ -n "$_ck_eu" ]; then _ck_cl="$_ck_cl takana.esp=PARTUUID=$_ck_eu"
+    elif [ -n "$ESP" ];  then _ck_cl="$_ck_cl takana.esp=$ESP"; fi
+    [ -n "$ASSUME_YES" ] && _ck_cl="$_ck_cl takana.yes"
     _ck_ird="${IRD_INSTALADOR:-work/takana-instalador.cpio.gz}"
-    # shellcheck disable=SC2086
-    OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh $_ck_args || {
+    OUT="$_ck_ird" KERNEL="$KERNEL" ./scripts/instalador-initramfs.sh || {
         echo "takana-install-partition: no pude armar el instalador en RAM" >&2; exit 1; }
     _ck_art=$(dirname "$(dirname "$KERNEL")")     # …/<hash>-linux-metal/boot/bzImage → el artefacto
     echo
     echo "  kernel:  $_ck_art"
-    echo "  initrd:  $_ck_ird ($(du -h "$_ck_ird" | cut -f1))"
+    echo "  initrd:  $_ck_ird ($(du -h "$_ck_ird" | cut -f1)) — genérico, sin esta máquina adentro"
     echo "  destino: $DEST"
+    echo "  cmdline: $_ck_cl"
     if [ -z "$ASSUME_YES" ] && [ -t 0 ]; then
         printf "  ⚡ ESTE COMANDO MATA EL SISTEMA QUE ESTÁS USANDO y arranca el instalador. [escribí saltar] "
         read -r _ck_r; [ "$_ck_r" = "saltar" ] || { echo "  cancelado — no se saltó y no se tocó nada."; exit 1; }
@@ -344,8 +638,7 @@ camino_kexec() {
     # serie: el instalador arrancaría y la pantalla se quedaría en negro con todo pasando en un
     # cable que no existe. Es el bug que este repo ya pagó dos veces (ver `arranque-visible-cada-capa`).
     # Por eso acá tty0 va ÚLTIMO: pantalla primero, serial de regalo para quien lo tenga.
-    "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" \
-        --cmdline "${CMDLINE_SALTO:-console=ttyS0,115200 console=tty0}" --jump
+    "$HAMMER" kernel kexec "$_ck_art" --initrd "$_ck_ird" --cmdline "$_ck_cl" --jump
     echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
     exit 1
 }