los nombres de device NO son estables: el destino se nombra por PARTUUID
Hallazgo de la prueba con el layout REAL de la laptop del usuario replicado en QEMU (dos NVMe: uno
con Windows, otro con ESP + /home + la raíz de Artix en los slots 1, 3 y 6):
· el disco declarado PRIMERO en la línea de comandos apareció como `nvme1n1` y el segundo como
`nvme0n1` — cruzados;
· y entre dos arranques del MISMO QEMU se cruzaron otra vez: la raíz de Artix fue `nvme0n1p6` en
una corrida y `nvme1n1p6` en la siguiente.
Los nombres salen del orden de sondeo del bus. Para un instalador eso es la diferencia entre
formatear la raíz vieja y formatear el disco de Windows del usuario — y el kexec lo agrava, porque
quien enumera después del salto es otro kernel, no el que te dio los nombres.
⇒ `--root-part` y `--esp` aceptan `PARTUUID=`, `UUID=` y `LABEL=`. El correcto para un destino que
se va a formatear es **PARTUUID**: vive en la tabla GPT y no cambia al formatear; el UUID es del
filesystem y muere ahí. `PARTUUID` se resuelve leyendo la tabla con sfdisk, que es donde está el
dato, porque `findfs` no se puede dar por hecho para ese caso.
El banco instala por PARTUUID a propósito: si esa parte se cambia a un device suelto, se pierde
justo la cobertura que importa. Y el banco encontró dos cosas más al hacerlo:
· `/dev/loop?` faltaba en la lista de discos a recorrer ⇒ la resolución por identidad quedaba sin
cobertura en el único sitio donde se puede probar sin arriesgar un disco real;
· el guardián de la LABEL capturaba **PARTLABEL** en vez de LABEL: con `sed 's/.*LABEL="…"/'` el
`.*` es glotón y se come el «PART», así que leía el nombre de la entrada GPT («destino-takana»)
y abortaba una instalación correcta. Segunda vez que ese mismo chequeo acusa al inocente por
mirar mal el dato; ahora distingue por el espacio de ` LABEL="`.
Y el salto ahora pasa `console=ttyS0,115200 console=tty0` — tty0 ÚLTIMO, porque gana la última y un
portátil no tiene puerto serie: con el orden anterior el instalador arrancaba y la pantalla se
quedaba negra con todo pasando por un cable que no existe.
Verificado por hashes sobre el layout replicado: disco de Windows IDÉNTICO byte a byte (8 GiB),
/home idéntica, la raíz de Artix reemplazada, el grub de la ESP intacto, el kernel exacto en
\EFI\takana\ y la tabla sin cambios. Banco local verde en sus dos escenarios (20 comprobaciones).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
4 qillqakuna · +120 −7
diff --git a/docs/runbooks/instalador-laptop-tigerlake.md b/docs/runbooks/instalador-laptop-tigerlake.md
index ab5002d1..a8b6ed7c 100644
--- a/docs/runbooks/instalador-laptop-tigerlake.md
+++ b/docs/runbooks/instalador-laptop-tigerlake.md
@@ -215,6 +215,41 @@ sudo takana kernel kexec --probe # el veredicto completo
cat /sys/kernel/security/lockdown # [none] = Secure Boot no está estorbando
```
+## 5 ter. ⚠ Los nombres `/dev/nvme1n1p6` NO son estables — se direcciona por PARTUUID
+
+**Medido el 2026-09-23 en QEMU con dos NVMe**, replicando el layout real de la laptop (un disco con
+Windows, otro con ESP + `/home` + la raíz de Artix en los slots 1, 3 y 6):
+
+- el disco declarado **primero** en la línea de comandos apareció como `nvme1n1` y el segundo como
+ `nvme0n1` — cruzados;
+- y **entre dos arranques del mismo QEMU se volvieron a cruzar**: en una corrida la raíz de Artix
+ fue `nvme0n1p6` y en la siguiente `nvme1n1p6`.
+
+Los nombres salen del orden de sondeo del bus, no del orden físico ni del que muestra `lsblk` hoy.
+Para un instalador eso no es una molestia: es la diferencia entre formatear la raíz vieja y
+formatear el disco de Windows. Y **el salto por kexec lo agrava**, porque quien enumera después del
+salto es un kernel distinto del que te dio los nombres antes.
+
+⇒ `takana-install-partition.sh` acepta `PARTUUID=`, `UUID=` y `LABEL=` además de un `/dev/…`, y
+**la forma correcta de nombrar el destino es `PARTUUID=`**: es el identificador de la TABLA GPT y
+**no cambia al formatear la partición**. El `UUID=` es del filesystem y muere en el formateo.
+
+```sh
+lsblk -o NAME,SIZE,LABEL,PARTUUID /dev/nvme1n1 # de dónde salen
+```
+
+La prueba con el layout replicado terminó así, verificado por hashes y no por la salida del propio
+instalador:
+
+| qué | resultado |
+|---|---|
+| disco entero de Windows | **idéntico byte a byte** (8 GiB comparados) |
+| `/home` | idéntica |
+| la raíz de Artix (p6) | reemplazada: `LABEL=hammer-root` en el superbloque |
+| `\EFI\grub\` y `\EFI\BOOT\` de la ESP de Artix | intactos |
+| `\EFI\takana\takanax64.efi` | = el bzImage, hash exacto |
+| tabla de particiones | 3 entradas, sin cambios |
+
## 6. Instalar desde OTRO sistema operativo, sin medio live
La pregunta del usuario (2026-09-23) es si se puede romper con la instalación tradicional y tener un
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index d8d89a16..c7622154 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -71,7 +71,7 @@ cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
# se enumeran acá y su ausencia ya no es silenciosa (el script aborta si le falta `id`).
for a in sh mount umount mkdir echo sleep ls cat sed awk grep cut tr basename dirname readlink \
find sync reboot dmesg blkid setsid tee du wc head tail sort printf test sha256sum \
- id sh chmod rm cp mv stat; do
+ id sh chmod rm cp mv stat findfs; do
ln -sf busybox "$IRD_TREE/bin/$a"
done
# Las herramientas de disco NO salen de busybox: son las SELLADAS del corpus, estáticas musl. Dentro
@@ -174,7 +174,7 @@ 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 \\"
-echo " --cmdline 'console=tty0 console=ttyS0,115200' --jump"
+echo " --cmdline 'console=ttyS0,115200 console=tty0' --jump # tty0 ÚLTIMO = la pantalla"
echo
echo "Para probarlo SIN saltar (y sin riesgo), en QEMU:"
echo " qemu-system-x86_64 -m 4096 -kernel $KERNEL -initrd $OUT \\"
diff --git a/scripts/install-particion-test.sh b/scripts/install-particion-test.sh
index 86711926..56be5002 100755
--- a/scripts/install-particion-test.sh
+++ b/scripts/install-particion-test.sh
@@ -102,9 +102,16 @@ corre --root-part "${LOOP}p1" --esp "${LOOP}p1" --no-nvram --yes && falla "acept
# inocente se desactiva a la semana, que es peor que no tenerlo. Lo que este test tiene que probar
# es que **la instalación** no la toca, y eso es exactamente lo que mide desde acá.
sha256sum "${LOOP}p3" | cut -d' ' -f1 > "$P/p3-antes"
-echo "==> instalando en ${LOOP}p2"
+# ⚠ Se instala por **PARTUUID**, no por `/dev/…`, y el banco lo hace así a propósito: los nombres
+# `nvmeXn1` NO son estables entre arranques (medido en QEMU con dos NVMe: el disco declarado primero
+# apareció como el segundo, y al rearrancar se cruzaron otra vez). Un instalador que confía en el
+# nombre puede formatear el disco equivocado. Si esta parte del test se cambia a un device suelto,
+# se pierde justamente la cobertura que importa.
+_pu=$("$SFDISK" -d "$LOOP" 2>/dev/null | awk -v d="${LOOP}p2" '$1==d {for(i=1;i<=NF;i++) if ($i ~ /^uuid=/) {sub(/,$/,"",$i); sub(/^uuid=/,"",$i); print $i}}')
+[ -n "$_pu" ] && ok "PARTUUID del destino: $_pu" || falla "no pude leer el PARTUUID de ${LOOP}p2"
+echo "==> instalando en PARTUUID=$_pu (que hoy es ${LOOP}p2)"
ROOTFS="$ROOTFS" KERNEL="$KERNEL" STAGE="$P/stage" \
- ./scripts/takana-install-partition.sh --root-part "${LOOP}p2" --esp "${LOOP}p1" --no-nvram --yes 2>"$P/stderr" | tail -4
+ ./scripts/takana-install-partition.sh --root-part "PARTUUID=$_pu" --esp "${LOOP}p1" --no-nvram --yes 2>"$P/stderr" | tail -4
if [ "$ESP_SANA" = "0" ]; then
grep -q "declara más espacio del que existe" "$P/stderr" \
&& ok "el guardián de capacidad NO se cree al FAT mentiroso" \
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index f776b141..0bf53191 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -27,7 +27,9 @@
# scripts/takana-install-partition.sh --root-part work/p-root.img --esp work/p-esp.img --no-nvram
#
# Opciones:
-# --root-part <dev|fichero> DESTINO. Se formatea ext4 con LABEL=hammer-root. Lo ÚNICO destructivo.
+# --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.
# --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).
@@ -88,6 +90,60 @@ es_bloque() { [ -b "$1" ]; }
nombre_kernel() { basename "$(readlink -f "$1")"; }
disco_de() { echo "/dev/$(basename "$(readlink -f "/sys/class/block/$(nombre_kernel "$1")/..")")"; }
+# ── resolver_dev <spec> — NUNCA confíes en `/dev/nvme1n1p6` ───────────────────────────────────
+# ⚠ **Los nombres `nvmeXn1` NO son estables entre arranques, y esto está MEDIDO, no temido**
+# (2026-09-23, QEMU con dos NVMe): el disco declarado primero en la línea de comandos apareció
+# como `nvme1n1` y el segundo como `nvme0n1` — **cruzados**. Salen del orden de sondeo del bus, no
+# del orden físico ni del que vos ves hoy con `lsblk`.
+#
+# Para un instalador eso no es una molestia: es la diferencia entre formatear la raíz vieja y
+# formatear el disco de Windows del usuario. Y el salto por kexec lo agrava, porque el kernel que
+# enumera después del salto NO es el que te dio los nombres antes.
+#
+# Por eso se acepta —y se recomienda— direccionar por identidad y no por posición:
+# PARTUUID=<uuid> el de la TABLA GPT. **No cambia al formatear la partición**, así que es el
+# correcto para nombrar un destino que se va a formatear.
+# UUID=<uuid> el del FILESYSTEM. Sirve para identificar lo que hay HOY (y muere al formatear).
+# LABEL=<etiqueta> cómodo, pero puede estar repetido en dos discos.
+resolver_dev() {
+ case "$1" in
+ /dev/*) echo "$1"; return 0 ;;
+ PARTUUID=*|partuuid=*) _rd_v="${1#*=}"; _rd_por=partuuid ;;
+ UUID=*|uuid=*) _rd_v="${1#*=}"; _rd_por=uuid ;;
+ LABEL=*|label=*) _rd_v="${1#*=}"; _rd_por=label ;;
+ *) echo "$1"; return 0 ;; # un fichero (pruebas)
+ esac
+ # `findfs` resuelve UUID y LABEL en busybox y en util-linux; para PARTUUID no se puede contar
+ # con él, así que ese caso se resuelve leyendo la TABLA con sfdisk — que es donde vive el dato.
+ if [ "$_rd_por" != partuuid ] && command -v findfs >/dev/null 2>&1; then
+ _rd_d=$(findfs "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=$_rd_v" 2>/dev/null || true)
+ [ -n "${_rd_d:-}" ] && { echo "$_rd_d"; return 0; }
+ fi
+ # `/dev/loop?` está en la lista porque el banco de pruebas trabaja sobre loop devices: sin él,
+ # la resolución por identidad quedaba SIN COBERTURA en el único sitio donde se puede probar sin
+ # arriesgar un disco de verdad. Lo detectó el propio banco al pasar a PARTUUID.
+ for _rd_disco in /dev/nvme?n? /dev/sd? /dev/vd? /dev/mmcblk? /dev/loop? /dev/loop??; do
+ [ -b "$_rd_disco" ] || continue
+ case "$_rd_por" in
+ partuuid)
+ _rd_hit=$("$SFDISK" -d "$_rd_disco" 2>/dev/null \
+ | awk -v u="$(printf '%s' "$_rd_v" | tr 'A-Z' 'a-z')" \
+ 'tolower($0) ~ ("uuid=" u) {print $1; exit}')
+ ;;
+ *)
+ _rd_hit=$(for _rd_p in "$_rd_disco"*; do
+ [ -b "$_rd_p" ] || continue
+ blkid "$_rd_p" 2>/dev/null | grep -qi "$(printf '%s' "$_rd_por" | tr 'a-z' 'A-Z')=\"$_rd_v\"" && { echo "$_rd_p"; break; }
+ done)
+ ;;
+ esac
+ [ -n "${_rd_hit:-}" ] && { echo "$_rd_hit"; return 0; }
+ done
+ echo "takana-install-partition: no encuentro ninguna partición con $1" >&2
+ echo " (mirá 'lsblk -o NAME,SIZE,PARTUUID,LABEL' en el sistema vivo)" >&2
+ return 1
+}
+
# ── releer_tabla <disco> — que el KERNEL se entere de la entrada nueva ────────────────────────
# Medido el 2026-09-23 en el banco: `sfdisk --append` escribió la entrada perfectamente y
# `/dev/loop0p4` NO APARECIÓ. El script lo detectó y abortó —bien— pero el motivo estaba fuera de
@@ -176,6 +232,11 @@ crear_en_hueco() {
echo "$_ch_nuevo"
}
+# 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
+if [ -n "$ESP" ]; then ESP=$(resolver_dev "$ESP") || exit 1; fi
+
# `--use-free-space` sin `--root-part`: el destino todavía no existe, se crea en un hueco. Esto es
# «instalar al lado», y es el camino que la política de la raíz viva usa cuando no hay kexec.
if [ -n "$USE_FREE" ] && [ -z "$DEST" ]; then
@@ -278,8 +339,13 @@ camino_kexec() {
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; }
fi
+ # ⚠ **El orden de las `console=` decide cuál es `/dev/console`: gana la ÚLTIMA.** Con
+ # `console=tty0 console=ttyS0,115200` la consola es el SERIAL, y un portátil no tiene puerto
+ # 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 "console=tty0 console=ttyS0,115200" --jump
+ --cmdline "${CMDLINE_SALTO:-console=ttyS0,115200 console=tty0}" --jump
echo "takana-install-partition: el salto volvió, o sea que NO saltó. Nada se tocó." >&2
exit 1
}
@@ -341,7 +407,12 @@ fi
# PERFECTO y el guardián abortó igual, con un mensaje que mostraba la label correcta dentro del
# texto de «no quedó con LABEL». Tercer caso hoy del mismo patrón: un chequeo que valida algo
# distinto de lo que cree validar acusa al inocente, y un guardián así se desactiva a la semana.
-_lbl_post=$(blkid "$DEST" 2>/dev/null | sed -n 's/.*LABEL="\([^"]*\)".*/\1/p')
+# ⚠ Y el patrón tiene que distinguir `LABEL` de **`PARTLABEL`**, que es otra cosa: el primero es la
+# etiqueta del FILESYSTEM y el segundo el nombre de la ENTRADA GPT. Con `sed 's/.*LABEL="…"/'` el
+# `.*` es glotón y se come el «PART», así que devolvía el nombre de la partición en la tabla
+# («destino-takana») y el guardián abortaba una instalación correcta. Segunda vez que este mismo
+# chequeo acusa al inocente por mirar mal el dato. El espacio de ` LABEL="` es lo que los separa.
+_lbl_post=$(blkid "$DEST" 2>/dev/null | grep -o ' LABEL="[^"]*"' | head -1 | sed 's/^ LABEL="//; s/"$//')
[ -n "$_lbl_post" ] || _lbl_post=$(blkid -s LABEL -o value "$DEST" 2>/dev/null || true)
[ "$_lbl_post" = "hammer-root" ] || {
echo "takana-install-partition: $DEST NO quedó con LABEL=hammer-root (blkid dice '${_lbl_post:-nada}')." >&2