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…
la imagen USB, para cuando el salto no se puede — y tres bugs que sólo se ven por ese camino
1#!/bin/sh
2# instalador-usb.sh — la imagen USB del instalador, para cuando el salto NO se puede.
3#
4# ══ POR QUÉ EXISTE ════════════════════════════════════════════════════════════════════════════
5# `takana metal install` reemplaza el Linux que corre sin USB, saltando con `kexec`. Eso pide tres
6# cosas del kernel de ESA máquina —`CONFIG_KEXEC_FILE`, ser root y que Secure Boot no esté en
7# lockdown— y cualquiera de las tres puede faltar. Cuando falta, `metal check` lo dice y manda acá.
8#
9# **El instalador es el mismo**: el mismo árbol, el mismo censo, las mismas guardas. Lo único que
10# cambia es cómo se llega a él.
11#
12# ── EL DISEÑO, Y LA MEDICIÓN QUE LO OBLIGA ────────────────────────────────────────────────────
13# La forma obvia sería poner el initramfs del instalador en la ESP y dejar que el EFI-stub lo
14# cargue. **No se puede, y está medido:** el cuelgue del primer viaje a metal fue exactamente eso,
15# un initrd de ~100 MiB cargado por el stub (ADR 0010 paso 2). El del instalador son ~122 MiB.
16#
17# Entonces el USB usa el MISMO patrón que un disco instalado, que sí está probado:
18#
19# firmware → \EFI\BOOT\BOOTX64.EFI (= bzImage, EFI-stub)
20# → initramfs de PIVOTE (~800 KiB, el que sí entra)
21# → findfs LABEL=takana-medio → switch_root
22# → /init del instalador, ya sobre un filesystem de verdad
23#
24# ⚠ La etiqueta es `takana-medio` y **no** `hammer-root` a propósito: si el medio se llamara igual
25# que la raíz de un sistema instalado, el pivote podría montar el disco del usuario en vez del USB
26# —o al revés— y el que pierde es el disco. Dos cosas distintas, dos nombres distintos.
27#
28# Uso: scripts/instalador-usb.sh [--salida <img>]
29# Vars: ROOTFS KERNEL (como instalador-paquete.sh) · IMG · ESP_MB (def 96) · HOLGURA_MB (def 128)
30#
31# Grabarlo: sudo dd if=work/takana-instalador-usb.img of=/dev/sdX bs=4M oflag=sync status=progress
32set -eu
33ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
34
35IMG="${IMG:-work/takana-instalador-usb.img}"
36ESP_MB="${ESP_MB:-96}"
37HOLGURA_MB="${HOLGURA_MB:-128}"
38ETIQUETA="takana-medio"
39[ "${1:-}" = "--salida" ] && { IMG="${2:?}"; shift 2; }
40
41. "$(dirname "$0")/lib/herramientas-desde-store.sh"
42SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1
43MKE2FS=$(herramienta e2fsprogs sbin/mke2fs mke2fs) || exit 1
44MFORMAT=$(herramienta mtools usr/bin/mformat mformat) || exit 1
45MMD=$(herramienta mtools usr/bin/mmd mmd) || exit 1
46MCOPY=$(herramienta mtools usr/bin/mcopy mcopy) || exit 1
47MDIR=$(herramienta mtools usr/bin/mdir mdir) || exit 1
48CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio) || exit 1
49GZIP=$(herramienta gzip bin/gzip gzip) || exit 1
50
51# ── 1) el árbol del instalador — el MISMO que viaja por kexec ─────────────────────────────────
52TREE="${IRD_TREE:-work/.instalador-initramfs-tree}"
53echo "==> árbol del instalador (TREE_ONLY: lo arma el generador de siempre)"
54TREE_ONLY=1 IRD_TREE="$TREE" ROOTFS="${ROOTFS:-}" KERNEL="${KERNEL:-}" ./scripts/instalador-initramfs.sh
55[ -x "$TREE/init" ] && [ -d "$TREE/payload/stage/root" ] || {
56 echo "el árbol no quedó completo en $TREE" >&2; exit 1; }
57KERNEL="${KERNEL:-$TREE/payload/bzImage}"
58[ -r "$KERNEL" ] || { echo "no encuentro el bzImage (set KERNEL=)" >&2; exit 1; }
59
60TREE_MB=$(du -sm "$TREE" | cut -f1)
61EXT_MB=$(( TREE_MB + HOLGURA_MB ))
62TOTAL_MB=$(( ESP_MB + EXT_MB + 2 ))
63echo " árbol ${TREE_MB} MiB · ext4 ${EXT_MB} MiB · imagen ${TOTAL_MB} MiB"
64
65# ── 2) el initramfs de PIVOTE: lo único que carga el EFI-stub ─────────────────────────────────
66# Es hermano del pivote de `install-image-efi.sh` y comparte sus dos lecciones caras: el log va a
67# tty0 **y** al serial porque el cmdline horneado deja /dev/console en el serial, y la espera del
68# disco es LARGA porque el USB enumera de forma asíncrona y tarda segundos (en QEMU nunca se ve).
69P="work/.instalador-usb"; rm -rf "$P"; mkdir -p "$P/pivote/bin" "$P/pivote/dev" "$P/pivote/proc" \
70 "$P/pivote/sys" "$P/pivote/newroot"
71BUSYBOX="${BUSYBOX:-$TREE/bin/busybox}"
72[ -r "$BUSYBOX" ] || { echo "no encuentro busybox en $BUSYBOX" >&2; exit 1; }
73cp "$BUSYBOX" "$P/pivote/bin/busybox"; chmod 0755 "$P/pivote/bin/busybox"
74for a in sh mount umount findfs switch_root mkdir echo sleep ls cat setsid dmesg blkid; do
75 ln -sf busybox "$P/pivote/bin/$a"
76done
77cat > "$P/pivote/init" <<'PIVOTE'
78#!/bin/busybox sh
79# Pivote del MEDIO de instalación: encuentra el USB por etiqueta y le entrega el control al
80# instalador. No toca ningún disco: sólo monta el medio del que se arrancó.
81export PATH=/bin
82mount -t proc proc /proc 2>/dev/null || true
83mount -t sysfs sys /sys 2>/dev/null || true
84mount -t devtmpfs dev /dev 2>/dev/null || true
85# El cmdline horneado deja /dev/console en el SERIAL: sin escribir explícitamente en tty0, en un
86# portátil sin puerto serie todo esto es invisible y el arranque parece congelado.
87log() { for d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$d" 2>/dev/null || true; done; }
88rescate() {
89 log "TAKANA-MEDIO-FALLO: $*"
90 log " bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
91 log " shell de rescate en tty1 — probá: blkid ; dmesg | tail -30"
92 setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
93 exec sh
94}
95log "TAKANA-MEDIO: buscando el medio de instalación (LABEL=takana-medio)"
96# ⏱ 60 s y no 5: el bus USB enumera asíncrono —reset del hub, 1 s de settling por dispositivo, scan
97# SCSI— y tarda segundos. En QEMU el disco está desde el instante cero, así que esta carrera NO se
98# ve en validación: aparece la primera vez que alguien arranca de un pendrive de verdad.
99DEV=""; i=0
100while [ "$i" -lt 60 ]; do
101 DEV="$(findfs LABEL=takana-medio 2>/dev/null || true)"
102 [ -n "$DEV" ] && break
103 i=$(( i + 1 )); [ $(( i % 5 )) -eq 0 ] && log "TAKANA-MEDIO: esperando el medio… ${i}s (el USB tarda en enumerar)"
104 sleep 1
105done
106[ -n "$DEV" ] || rescate "no encuentro LABEL=takana-medio tras ${i}s"
107log "TAKANA-MEDIO: $DEV → montando"
108mount -t ext4 "$DEV" /newroot || rescate "no pude montar $DEV"
109[ -x /newroot/init ] || rescate "el medio no trae /init"
110log "TAKANA-MEDIO-OK: switch_root → el instalador"
111# El instalador tiene que SABER que vino de un medio: con eso su raíz viva es este USB —y no se
112# ofrece— mientras que por el salto la raíz viva es el sistema del usuario, que sí se reemplaza.
113# El entorno sobrevive al `exec` de switch_root, así que esto llega intacto al otro lado.
114export TAKANA_MEDIO=1
115exec switch_root /newroot /init
116PIVOTE
117chmod 0755 "$P/pivote/init"
118( cd "$P/pivote" && find . -print0 | "$CPIO" --null -o -H newc --owner=root:root 2>/dev/null | "$GZIP" -9 ) > "$P/initramfs.cpio.gz"
119echo "==> pivote: $(( $(wc -c < "$P/initramfs.cpio.gz") / 1024 )) KiB"
120
121# ── 3) la tabla ───────────────────────────────────────────────────────────────────────────────
122rm -f "$IMG"; truncate -s "${TOTAL_MB}M" "$IMG"
123ESP_SECT=$(( ESP_MB * 2048 ))
124"$SFDISK" --quiet "$IMG" <<EOF
125label: gpt
126start=2048, size=$ESP_SECT, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="EFI System Partition"
127start=$(( 2048 + ESP_SECT )), type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="takana-medio"
128EOF
129
130# ── 4) la ESP, poblada SIN montarla ───────────────────────────────────────────────────────────
131# Se formatea aparte y se empalma en su offset: formatear in-situ con `@@` hace que el FAT declare
132# el tamaño del fichero ENTERO, y después `df` repite la mentira.
133"$MFORMAT" -F -v TAKANA -i "$P/esp.img" -C -T $(( ESP_MB * 2048 )) ::
134"$MMD" -i "$P/esp.img" ::/EFI ::/EFI/BOOT ::/EFI/takana
135# Las DOS rutas al MISMO bzImage: la de vendor y la de medios removibles (ADR 0018 §3). De un USB
136# manda la segunda, que es la que el firmware busca sin que nadie registre nada en la NVRAM.
137"$MCOPY" -i "$P/esp.img" "$KERNEL" ::/EFI/takana/takanax64.efi
138"$MCOPY" -i "$P/esp.img" "$KERNEL" ::/EFI/BOOT/BOOTX64.EFI
139# El nombre NO es libre: el cmdline horneado del kernel dice `initrd=/initramfs.cpio.gz`.
140"$MCOPY" -i "$P/esp.img" "$P/initramfs.cpio.gz" ::/initramfs.cpio.gz
141for f in ::/EFI/takana/takanax64.efi ::/EFI/BOOT/BOOTX64.EFI ::/initramfs.cpio.gz; do
142 "$MDIR" -i "$P/esp.img" "$f" >/dev/null 2>&1 < /dev/null || {
143 echo "la ESP no quedó con $f" >&2; exit 1; }
144done
145dd if="$P/esp.img" of="$IMG" bs=1M seek=1 conv=notrunc status=none
146
147# ── 5) el ext4 con el árbol adentro, sin montar nada ──────────────────────────────────────────
148echo "==> ext4 «$ETIQUETA» con el instalador adentro (${EXT_MB} MiB, sin montar)"
149"$MKE2FS" -q -t ext4 -L "$ETIQUETA" -d "$TREE" "$P/medio.img" "${EXT_MB}M"
150dd if="$P/medio.img" of="$IMG" bs=1M seek=$(( ESP_MB + 1 )) conv=notrunc status=none
151
152# ── 6) el control: que la imagen diga lo que creemos ──────────────────────────────────────────
153echo "==> control"
154FALLOS=0
155ok() { echo " ✓ $*"; }
156falla(){ echo " ✗ $*" >&2; FALLOS=$(( FALLOS + 1 )); }
157"$SFDISK" -d "$IMG" | grep -q 'C12A7328' && ok "la tabla trae una ESP" || falla "sin ESP en la tabla"
158"$MDIR" -i "$IMG@@1048576" ::/EFI/BOOT >/dev/null 2>&1 < /dev/null \
159 && ok "la ESP se lee desde su offset en la imagen" || falla "la ESP no se lee"
160dd if="$IMG" bs=1M skip=$(( ESP_MB + 1 )) count="$EXT_MB" status=none > "$P/verif-ext.img"
161BL=$(herramienta util-linux sbin/blkid blkid) || exit 1
162"$BL" "$P/verif-ext.img" 2>/dev/null | grep -q "LABEL=\"$ETIQUETA\"" \
163 && ok "el ext4 quedó etiquetado «$ETIQUETA»" || falla "el ext4 no tiene la etiqueta"
164rm -rf "$P/verif-ext.img" "$P/medio.img" "$P/esp.img"
165[ "$FALLOS" = 0 ] || exit 1
166
167echo
168echo "✓ $IMG ($(( $(wc -c < "$IMG") / 1048576 )) MiB)"
169echo " sudo dd if=$IMG of=/dev/sdX bs=4M oflag=sync status=progress"