#!/bin/sh # instalador-initramfs.sh — empaqueta el instalador ENTERO en un initramfs que corre en RAM. # # ── PARA QUÉ ─────────────────────────────────────────────────────────────────────────────────── # Es lo que hace posible reemplazar el Linux que está corriendo **sin un USB**: se salta a este # initramfs por `kexec` (`takana kernel kexec --jump`, ADR 0017), el sistema viejo muere en el salto # y su disco queda libre ⇒ recién entonces su raíz se puede formatear. El mismo initramfs sirve # arrancado por QEMU o por un medio cualquiera; el salto es sólo una forma de llegar. # # ── POR QUÉ EL PAYLOAD VIAJA ADENTRO Y NO EN EL DISCO ────────────────────────────────────────── # Porque el disco es justamente lo que se va a formatear. Dejar el rootfs en la partición destino y # formatearla después es morderse la cola; dejarlo en otra partición ata la instalación a que exista # esa otra partición. En RAM no depende de nada: el precio es que el initramfs pesa lo que pese el # rootfs, y por eso el script COMPRUEBA que entre en la memoria de la máquina antes de armarlo. # # Uso: # ROOTFS= scripts/instalador-initramfs.sh --root-part /dev/nvme0n1p2 --esp /dev/nvme0n1p1 # # o sin destino fijo: lo decide el instalador al arrancar (--use-free-space) # # Salida: work/takana-instalador.cpio.gz (+ el `takana kernel kexec` que lo salta, impreso al final) 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 OUT="${OUT:-work/takana-instalador.cpio.gz}" STAGE="${STAGE:-work/.instalador-initramfs-stage}" IRD_TREE="${IRD_TREE:-work/.instalador-initramfs-tree}" KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal/boot/bzImage /store/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}" [ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro un bzImage de linux-metal (set KERNEL=)" >&2; exit 1; } . "$(dirname "$0")/lib/herramientas-desde-store.sh" MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1 SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1 MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1 MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1 MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1 CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio) || exit 1 GZIP=$(herramienta gzip bin/gzip gzip) || exit 1 # ── 1) el stage (rootfs a instalar + initramfs de pivote) lo arma el script validado ─────────── echo "==> stage del sistema a instalar" STAGE_ONLY=1 KEEP_STORE=1 STAGE="$STAGE" ./scripts/install-image-efi.sh [ -d "$STAGE/root" ] && [ -r "$STAGE/initramfs.cpio.gz" ] || { echo "el stage no quedó completo" >&2; exit 1; } # ── 2) ¿entra en la RAM de esta máquina? ─────────────────────────────────────────────────────── # Un initramfs vive ENTERO en memoria, y el kernel no lo avisa con elegancia: se queda sin RAM al # desempaquetar y el arranque muere sin un mensaje que diga por qué. Medirlo antes es la diferencia # entre «no entra, usá el USB» y un cuelgue mudo en la máquina del usuario. # ⚠ **Esta guarda es del camino en RAM, y sólo de ése.** Con `TREE_ONLY=1` el árbol se va a # escribir en el ext4 de un USB, donde no hay ningún límite de memoria: aplicarla ahí rechazaba la # variante `full` por no caber en la RAM de una máquina que no es la que va a instalar. Medido el # 2026-09-23 armando las tres variantes. # # ⚠ Y hay un segundo defecto, que se deja anotado y no barrido: mide la RAM de **esta** máquina, no # la de aquella donde se va a instalar. Acá acierta por casualidad —el hub tiene 7,6 GiB, parecido a # un portátil—, pero construir en una caja grande dejaría pasar una imagen que allá no arranca. if [ -n "${TREE_ONLY:-}" ]; then echo " (TREE_ONLY: el árbol va a un disco, no a RAM — la guarda de memoria no aplica)" else _mib_payload=$(du -sm "$STAGE/root" | cut -f1) _mib_ram=$(( $(awk '/^MemTotal:/ {print $2}' /proc/meminfo) / 1024 )) echo " payload ${_mib_payload} MiB · RAM de esta máquina ${_mib_ram} MiB" if [ "$(( _mib_payload * 100 / _mib_ram ))" -gt 40 ]; then echo "instalador-initramfs: el payload es más del 40 % de la RAM ($_mib_payload de $_mib_ram MiB)." >&2 echo " En RAM van el initramfs comprimido Y su contenido desempaquetado." >&2 echo " Este camino no sirve para este rootfs: instalá desde un medio." >&2 exit 1 fi 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/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" # ⚠ `id` estaba fuera de esta lista y el instalador eligió la rama EQUIVOCADA por eso: sin `id -u` # concluyó «no soy root» y se fue a `unshare`, que tampoco existía. Los applets que el instalador usa # 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 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 # del initramfs no hay store del que resolverlas por hash, así que viajan copiadas y el instalador # las recibe por `TOOL_` — el override que existe exactamente para esto. for t in "$MKE2FS" "$SFDISK" "$MMD" "$MCOPY" "$MDIR"; do cp "$t" "$IRD_TREE/bin/$(basename "$t")"; chmod 0755 "$IRD_TREE/bin/$(basename "$t")" done TAKANA_MUSL="${TAKANA_MUSL:-$ROOT/target/x86_64-unknown-linux-musl/release/takana}" [ -x "$TAKANA_MUSL" ] || TAKANA_MUSL="$ROOT/target/release/takana" cp "$TAKANA_MUSL" "$IRD_TREE/bin/takana"; chmod 0755 "$IRD_TREE/bin/takana" cp scripts/takana-install-partition.sh "$IRD_TREE/instalador/" cp scripts/lib/herramientas-desde-store.sh "$IRD_TREE/instalador/lib/" cp "$KERNEL" "$IRD_TREE/payload/bzImage" mkdir -p "$IRD_TREE/payload/stage" cp -a "$STAGE/root" "$IRD_TREE/payload/stage/root" cp "$STAGE/initramfs.cpio.gz" "$IRD_TREE/payload/stage/initramfs.cpio.gz" printf '%s\n' "$ARGS_INSTALADOR" > "$IRD_TREE/instalador/args" # ── 4) /init — lo que corre cuando el kernel nuevo toma el control ───────────────────────────── cat > "$IRD_TREE/init" <<'INIT' #!/bin/busybox sh # PID 1 del instalador. No pivota a ningún disco: instala y reinicia. export PATH=/bin mount -t proc proc /proc 2>/dev/null || true mount -t sysfs sys /sys 2>/dev/null || true mount -t devtmpfs dev /dev 2>/dev/null || true # ⚠ El log va a tty0 **y** al serial, por lo mismo que el initramfs de pivote: el cmdline horneado # deja /dev/console en ttyS0 y en un metal sin puerto serie todo esto sería invisible. # El log va SÓLO a los devices: el stdout de PID 1 ya es /dev/console (= ttyS0 por el cmdline # horneado), así que un `echo` suelto duplicaba cada línea en el serial. log() { for d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$d" 2>/dev/null || true; done; } rescate() { log "TAKANA-INSTALADOR-FALLO: $*" log " bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')" log " shell de rescate en tty1 — probá: blkid ; dmesg | tail -30" setsid -c sh -c 'exec /bin/sh /dev/tty1 2>&1' >/dev/null 2>&1 & exec sh } # efivarfs: sin esto el instalador concluye «esta máquina no arrancó por UEFI» y NO registra la # entrada de arranque — en una máquina que sí arrancó por UEFI. Es el mismo diagnóstico falso que ya # costó una vuelta en el `/sbin/init` del rootfs: `switch_root` y un initramfs fresco no arrastran # /sys, y sin sysfs el directorio no existe. Tras un kexec los servicios EFI de runtime siguen ahí, # así que acá esto es lo único que falta. if [ -d /sys/firmware/efi ]; then mkdir -p /sys/firmware/efi/efivars 2>/dev/null || true mount -t efivarfs efivarfs /sys/firmware/efi/efivars 2>/dev/null || true fi # ── ¿POR DÓNDE SE LLEGÓ? ────────────────────────────────────────────────────────────────────── # Lo dice el pivote del medio, que exporta `TAKANA_MEDIO=1` antes de su `switch_root` — y el # entorno sobrevive al exec. No es un detalle: la diferencia cambia QUÉ ES LA RAÍZ VIVA (por el # salto, el sistema del usuario; por el medio, el propio USB) y con eso qué se ofrece formatear. # # ⚠ **La primera versión miraba `rdinit=` en /proc/cmdline y estaba MAL.** Con `CONFIG_CMDLINE_BOOL` # y sin `CONFIG_CMDLINE_OVERRIDE`, el cmdline horneado del kernel se CONCATENA con el que pasa quien # lo arranca: `rdinit=` aparece SIEMPRE, también tras un kexec, así que el instalador se creía en un # medio estando en el salto. Medido el 2026-09-23 — y el síntoma fue benigno de casualidad (un # mensaje raro), pero la consecuencia real era que la raíz viva del usuario habría quedado marcada # «es el MEDIO desde el que arrancaste» y NO SE HABRÍA PODIDO ELEGIR, que es justo el caso que el # salto existe para resolver. MEDIO="${TAKANA_MEDIO:-}" if [ -n "$MEDIO" ]; then log "TAKANA-INSTALADOR: arrancado desde el MEDIO de instalación. Ningún disco de esta máquina está en uso." else log "TAKANA-INSTALADOR: arrancado en RAM. El sistema viejo YA NO CORRE: su disco está libre." fi [ -d /sys/firmware/efi/efivars ] && log "TAKANA-INSTALADOR: firmware UEFI visible (efivarfs montado)" \ || log "TAKANA-INSTALADOR: sin efivarfs ⇒ no se registrará entrada NVRAM (BIOS, o kernel sin EFI)" # Los discos tardan en enumerar (USB y NVMe sobre todo). Misma espera larga que el pivote, y por el # mismo motivo: en QEMU aparecen al instante y la carrera sólo se ve en metal. # ⚠ Esto era `ls /dev/nvme* /dev/sd* /dev/vd* … && break` y **esperaba los 60 s completos incluso # con el disco presente**: `ls` sale ≠ 0 si CUALQUIERA de sus argumentos no existe, y los globs sin # coincidencia quedan literales. O sea que bastaba con no tener NVMe para que un disco virtio ya # enumerado no contara. Medido en QEMU: 60 s de «esperando discos» con /dev/vda ahí desde el # segundo uno. En metal eso se lee como un arranque colgado. i=0 while [ "$i" -lt 60 ]; do hay="" for d in /dev/vd? /dev/sd? /dev/nvme?n? /dev/mmcblk?; do [ -b "$d" ] && hay=1; done [ -n "$hay" ] && break i=$(( i + 1 )); [ $(( i % 5 )) -eq 0 ] && log "TAKANA-INSTALADOR: esperando discos… ${i}s" sleep 1 done cd /instalador # ── 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=""; CONSOLA_PEDIDA="" 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 ;; takana.console=*) CONSOLA_PEDIDA="${_w#takana.console=}" ;; esac done # ── DÓNDE SE PREGUNTA ───────────────────────────────────────────────────────────────────────── # ⚠ `/dev/console` es la ÚLTIMA `console=` del cmdline, y **el cmdline HORNEADO del kernel pone # `tty0` PRIMERO y `ttyS0` último** ⇒ arrancado de un USB, `/dev/console` es el SERIAL. En un # portátil sin puerto serie eso significa que el censo y la pregunta salen por un sitio que no # existe: pantalla muerta y un instalador que parece colgado esperando una tecla que nadie puede # escribir. Por el salto el cmdline lo ponemos nosotros y `tty0` va último, pero por USB no se # puede elegir sin recompilar el kernel. # # Regla: si hay una VT de verdad, se pregunta en la PANTALLA. Y se dice en voz alta por los dos # lados, porque el caso contrario —una máquina sin monitor, administrada por serie— existe y tiene # que poder corregirlo sin adivinar: `takana.console=/dev/ttyS0` en la cmdline. # ⚠ Y no alcanza con que `tty0` EXISTA: con `console=tty0` en el cmdline el kernel lo registra # igual en una máquina sin pantalla, con el driver `dummy` detrás — o sea un dispositivo que acepta # todo lo que le escribas y no lo muestra en ningún lado. La pregunta se iría a un agujero. Lo que # distingue una pantalla de verdad es el vtconsole: `dummy device` contra `frame buffer device` o # `VGA+`. Medido en QEMU, que da las dos según se le pida `-vga std` o `-vga none`. hay_pantalla() { for _v in /sys/class/vtconsole/vtcon*/name; do [ -r "$_v" ] || continue grep -qi 'dummy' "$_v" || return 0 done return 1 } CONSOLA=/dev/console if [ -n "$CONSOLA_PEDIDA" ]; then CONSOLA="$CONSOLA_PEDIDA" elif [ -c /dev/tty0 ] && hay_pantalla; then CONSOLA=/dev/tty0 fi [ -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_ 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 \ TOOL_mcopy=/bin/mcopy TOOL_mdir=/bin/mdir export STAGE=/payload/stage STAGE_LISTO=1 KERNEL=/payload/bzImage HAMMER=/bin/takana # El instalador necesita saberlo para clasificar bien la raíz viva: por el salto es el sistema del # usuario (se puede reemplazar); por el medio es el propio USB (no se toca nunca). [ -n "$MEDIO" ] && export TAKANA_MEDIO=1 # ⚠ **El estado de un PIPE es el del ÚLTIMO comando, no el del primero.** Esto era # `if sh instalador… | tee …; then OK else rescate fi` y el `if` evaluaba el `tee`, que siempre sale # 0 ⇒ **el instalador falló, no formateó NADA, y esta pantalla dijo «instalado»**. Medido en QEMU el # 2026-09-23, y es el modo de fallo más caro posible: el usuario reinicia confiando y se queda sin # sistema. El estado del instalador se guarda aparte y se lee después; el pipe ya no decide nada. # 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. # 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: el censo y la pregunta salen por $CONSOLA" log "TAKANA-INSTALADOR: si estás mirando esto por el puerto serie y la pregunta no aparece acá," log " rearrancá con takana.console=/dev/ttyS0 en la cmdline." sh /instalador/takana-install-partition.sh $ARGS < "$CONSOLA" > "$CONSOLA" 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 else rescate "el instalador salió con error — NADA se reinicia solo, mirá el log de arriba" fi INIT chmod +x "$IRD_TREE/init" # ── 5) empaquetar ────────────────────────────────────────────────────────────────────────────── mkdir -p "$(dirname "$OUT")" # `TREE_ONLY=1` corta acá: el ÁRBOL es la pieza, y tiene dos medios. Comprimido en un cpio viaja # por kexec y dentro del `.run`; escrito en un ext4 es el USB, que existe para las máquinas donde el # salto no se puede. Duplicar el armado del árbol en el script del USB sería tener dos instaladores # que se parecen — y el que se olvide de actualizar es el que alguien va a arrancar. if [ -n "${TREE_ONLY:-}" ]; then echo "==> TREE_ONLY: árbol listo en $IRD_TREE — no se comprime" exit 0 fi ( 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Á), lo normal es NO tocar esto y usar el verbo, que comprueba, avisa y arma la cmdline solo (ADR 0021): sudo takana metal check # ¿se puede? sudo takana metal survey # el terreno sudo takana metal install # el viaje entero El motor a la vista, por si hace falta: sudo takana kernel kexec --initrd work/instalador-salida/takana-instalador.cpio.gz \ --cmdline 'console=ttyS0,115200 console=tty0' --jump # tty0 ÚLTIMO = la pantalla Para probarlo SIN saltar (y sin riesgo), en QEMU:" echo " qemu-system-x86_64 -m 4096 -kernel $KERNEL -initrd $OUT \\" echo " -append 'console=ttyS0' -drive file=,format=raw -nographic"