Files
takana/scripts/takana-live-install.sh
T
SergioandClaude Opus 5 294959c1da arranque y GC: las dos rutas EFI, guardián de ESP y el kernel vivo como raíz
Primera tanda de los ADR 0017/0018, con las mediciones que la corrigieron.

ADR 0018 §3 — install-image-efi.sh y takana-live-install.sh escriben el
kernel en \EFI\takana\takanax64.efi ADEMAS de la ruta fallback, con un
guardián de capacidad que comprueba ANTES y con los números a la vista
(duplicar el kernel cuesta, y el costo se dice). En el live-install la
verificación es por TAMAÑO, no por presencia: un cp truncado en FAT32
deja el fichero ahí y test -e diría que todo salió bien.

Verificado con imagen real en OVMF: las dos copias dan el mismo sha256
que el bzImage del store, y la imagen arranca hasta arje-zero PID1.
CONTROL NEGATIVO: pisando la fallback con 4K de basura el firmware dice
"No bootable option or device was found" y no aparece HAMMER-EFI ni una
vez ⇒ el escenario de Windows, reproducido.

Y lo que NO se pudo verificar cambia el alcance, así que va en el ADR:
sin entrada NVRAM el firmware NO busca el vendor path. La copia vendor
es hoy un seguro que no se cobra solo — el §3 es necesario y NO
suficiente sin el §1. Eso asciende la receta efibootmgr a bloqueante.

ADR 0017 §4 — store-gc.sh suma como raíz el kernel EN EJECUCIÓN,
identificado por .config byte a byte. Sin esto el artefacto del kernel
vivo cae en "superados" cuando la receta se movió, y se borra: el
sistema sigue andando perfecto hasta el día que hace falta volver atrás.
Probado en los tres sentidos, incluido el control que TIENE que seguir
condenado.

Además, dos bugs preexistentes que aparecieron al ir a medir:

- install-image-efi.sh abortaba con "ROOTFS sin /sbin/init" en TODO
  rootfs sano: /sbin/init es un symlink ABSOLUTO (→/usr/bin/arje-zero) y
  [ -e ] lo sigue contra la raíz del HOST. Un chequeo que validaba algo
  distinto de lo que creía validar. Arreglado resolviendo el destino
  dentro del rootfs.
- (no arreglado, es del entorno) el cp -al del staging da EXDEV si
  ROOTFS y STAGE no están en el MISMO MOUNT — y el bind-mount del store
  cuenta como otro mount aunque sea el mismo /dev/sdb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
2026-09-11 20:40:34 +00:00

591 lines
28 KiB
Bash
Executable File

#!/bin/sh
# takana-install — instalador a disco DESDE el medio live (Etapa E5 / refinamiento #1).
#
# Se inyecta en el rootfs del producto como /usr/bin/takana-install (ver scripts/iso-image.sh
# INSTALLER=1). Corre DENTRO del live, como root real (arje-zero es PID1) ⇒ NO necesita unshare -r
# ni los rodeos rootless de install-image.sh: particiona, formatea, monta y copia con privilegios
# reales, usando sólo herramientas que el producto ya trae (busybox fdisk/mke2fs/mount/dd + uutils cp).
#
# El sistema que se instala es EL PROPIO LIVE: la raíz en RAM se copia al disco (autoinstalador). El
# payload de arranque (kernel + GRUB) viaja en /usr/lib/hammer/install/, armado en build-time (el host
# tiene grub-mkimage; el live no lo necesita).
#
# Layout en disco (MBR, no GPT — más simple y busybox-friendly; GRUB va en el hueco post-MBR):
# /dev/<dev>1 / ext2(montable ext4) + /boot/bzImage + /boot/grub + wrapper /sbin/init
# /dev/<dev>2 /store ext2
# /dev/<dev>3 /var/lib/hammer ext2
#
# Cadena de arranque del disco resultante: BIOS → MBR (boot.img) → core.img (hueco post-MBR) →
# (hd0,msdos1)/boot/grub/grub.cfg → linux /boot/bzImage root=/dev/<dev>1 → wrapper /sbin/init.
#
# Modo de uso:
# INTERACTIVO (sin argumentos, con TTY): takana-install
# TUI en busybox sh: detecta discos, el usuario elige destino + confirma el borrado, recoge
# hostname / credencial de root / zona horaria / teclado / tamaños de partición, y recién ahí
# particiona+instala. Es la cara humana del instalador (el /init del ISO installer lo lanza).
# DESATENDIDO (con device): takana-install /dev/vdb [ROOT_MB] [STORE_MB]
# Autoinstalador (lo usa iso-install-test.sh con AUTO_INSTALL). La config sale de env:
# TAKANA_HOSTNAME / TAKANA_ROOT_PW / TAKANA_ROOT_AUTHKEYS / TAKANA_TZ / TAKANA_KEYMAP.
# Los nombres viejos HAMMER_* se siguen aceptando (ADR 0016); gana el nuevo si están los dos.
set -eu
DEV="${1:-}"
ROOT_MB="${2:-}"
STORE_MB="${3:-}" # /var/lib/hammer toma el resto
PAYLOAD=/usr/lib/hammer/install
BB="${BB:-/bin/busybox}"
SYS_BLOCK="${SYS_BLOCK:-/sys/block}" # override para tests
MOUNTS="${MOUNTS:-/proc/mounts}" # override para tests
# bb: usa el busybox del live si existe; si no (p.ej. tests en el host), cae a las herramientas del PATH.
# Sólo lo usan las funciones NUEVAS (config/TUI). Las ramas EFI/BIOS validadas siguen con "$BB" literal
# y jamás corren en el host (el dry-run sale antes).
bb() { if [ -x "$BB" ]; then "$BB" "$@"; else "$@"; fi; }
_human() { # bytes → humano
_b="$1"
if [ "$_b" -ge 1073741824 ]; then printf '%s.%s GiB' "$((_b/1073741824))" "$(( (_b%1073741824)*10/1073741824 ))"
elif [ "$_b" -ge 1048576 ]; then printf '%s MiB' "$((_b/1048576))"
else printf '%s B' "$_b"; fi
}
_is_mounted() { # ¿alguna partición del disco $1 aparece en MOUNTS? (heurística "es el medio live")
_d="$1"
while read -r _src _rest; do
case "$_src" in "/dev/$_d"|"/dev/$_d"[0-9]*|"/dev/$_d"p[0-9]*) return 0 ;; esac
done < "$MOUNTS" 2>/dev/null
return 1
}
list_disks() { # emite por disco candidato: name<TAB>bytes<TAB>model<TAB>mounted(0|1)
for _p in "$SYS_BLOCK"/*; do
[ -d "$_p" ] || continue
_n=${_p##*/}
case "$_n" in loop*|ram*|sr*|fd*|dm-*|md*|zram*) continue ;; esac
[ -r "$_p/size" ] || continue
_sz=$(cat "$_p/size" 2>/dev/null || echo 0); [ "$_sz" -gt 0 ] || continue
_model=$(cat "$_p/device/model" 2>/dev/null | tr -d '\n'); _model=$(printf '%s' "$_model" | sed 's/[[:space:]]*$//')
_m=0; _is_mounted "$_n" && _m=1
printf '%s\t%s\t%s\t%s\n' "$_n" "$((_sz*512))" "${_model:-disco}" "$_m"
done
}
ask() { # ask VAR "prompt" "default"
if [ -n "$3" ]; then printf '%s [%s]: ' "$2" "$3"; else printf '%s: ' "$2"; fi
IFS= read -r _ans || _ans=""
[ -n "$_ans" ] || _ans="$3"
eval "$1=\$_ans"
}
read_secret() { # read_secret VAR "prompt" (sin eco si hay stty)
printf '%s: ' "$2"
bb stty -echo 2>/dev/null || true
IFS= read -r _sec || _sec=""
bb stty echo 2>/dev/null || true
echo
eval "$1=\$_sec"
}
have_cryptpw() { bb 2>&1 | grep -qw cryptpw; }
# Reescribe el campo de contraseña de un usuario en $mnt/etc/shadow (o crea el fichero).
_set_shadow() {
_u="$1"; _h="$2"; _f="$3/etc/shadow"
if [ ! -f "$_f" ]; then
bb mkdir -p "$3/etc"
printf '%s:%s:19000:0:99999:7:::\n' "$_u" "$_h" > "$_f"; bb chmod 600 "$_f"; return
fi
_t="$_f.hammer-tmp"
bb awk -F: -v u="$_u" -v h="$_h" 'BEGIN{OFS=":"}
{ if ($1==u){ $2=h; seen=1 } print }
END{ if(!seen) print u,h,"19000","0","99999","7","","","" }' "$_f" > "$_t" && bb mv "$_t" "$_f"
bb chmod 600 "$_f"
}
# Aplica la config recogida al rootfs YA copiado y montado en $1 (llamada por ambas ramas tras la copia).
apply_post_install_config() {
_mnt="$1"
bb mkdir -p "$_mnt/etc/hammer"
if [ -n "${CFG_HOSTNAME:-}" ]; then
printf '%s\n' "$CFG_HOSTNAME" > "$_mnt/etc/hostname"
if ! grep -q "[[:space:]]$CFG_HOSTNAME\$" "$_mnt/etc/hosts" 2>/dev/null; then
printf '127.0.0.1\tlocalhost %s\n::1\t\tlocalhost %s\n' "$CFG_HOSTNAME" "$CFG_HOSTNAME" >> "$_mnt/etc/hosts"
fi
fi
if [ -n "${CFG_ROOT_HASH:-}" ]; then _set_shadow root "$CFG_ROOT_HASH" "$_mnt"; fi
if [ -n "${CFG_ROOT_AUTHKEYS:-}" ]; then
bb mkdir -p "$_mnt/root/.ssh"; bb chmod 700 "$_mnt/root/.ssh"
printf '%s\n' "$CFG_ROOT_AUTHKEYS" >> "$_mnt/root/.ssh/authorized_keys"
bb chmod 600 "$_mnt/root/.ssh/authorized_keys"
fi
if [ -n "${CFG_TZ:-}" ] && [ -e "$_mnt/usr/share/zoneinfo/$CFG_TZ" ]; then
bb ln -sf "/usr/share/zoneinfo/$CFG_TZ" "$_mnt/etc/localtime"
printf '%s\n' "$CFG_TZ" > "$_mnt/etc/timezone"
fi
_ts=$(bb date -u +%FT%TZ 2>/dev/null || echo unknown)
{
echo "# escrito por takana-install en $_ts"
echo "HOSTNAME=${CFG_HOSTNAME:-}"
echo "TIMEZONE=${CFG_TZ:-}"
echo "KEYMAP=${CFG_KEYMAP:-}" # best-effort: el producto aún no trae kbd/loadkeys
echo "TARGET=${DEV:-}"
} > "$_mnt/etc/hammer/install.conf"
}
# ---- recolección de config: interactiva (TUI) o desde env (desatendido) ----
CFG_HOSTNAME="${TAKANA_HOSTNAME:-${HAMMER_HOSTNAME:-takana}}"
CFG_ROOT_AUTHKEYS="${TAKANA_ROOT_AUTHKEYS:-${HAMMER_ROOT_AUTHKEYS:-}}"
CFG_ROOT_HASH="${TAKANA_ROOT_HASH:-${HAMMER_ROOT_HASH:-}}"
CFG_TZ="${TAKANA_TZ:-${HAMMER_TZ:-}}"
CFG_KEYMAP="${TAKANA_KEYMAP:-${HAMMER_KEYMAP:-us}}"
interactive_setup() {
echo
echo " # instalador de hammer"
echo " ─────────────────────────"
echo
_disks=$(list_disks)
[ -n "$_disks" ] || { echo "No detecté discos instalables (¿el medio live es el único dispositivo?)."; exit 1; }
echo "Discos detectados:"
OLDIFS=$IFS; IFS='
'; set --
for _l in $_disks; do set -- "$@" "$_l"; done
IFS=$OLDIFS
_i=1
for _l in "$@"; do
_n=$(printf '%s' "$_l" | cut -f1); _by=$(printf '%s' "$_l" | cut -f2)
_mo=$(printf '%s' "$_l" | cut -f3); _mm=$(printf '%s' "$_l" | cut -f4)
_w=""; [ "$_mm" = 1 ] && _w=" ⚠ montado (¿medio live?)"
printf ' %s) /dev/%-8s %10s %s%s\n' "$_i" "$_n" "$(_human "$_by")" "$_mo" "$_w"
_i=$((_i+1))
done
_total=$#
while :; do
ask _sel "Disco destino (1-$_total)" ""
case "$_sel" in ''|*[!0-9]*) echo " número inválido"; continue ;; esac
[ "$_sel" -ge 1 ] && [ "$_sel" -le "$_total" ] && break
echo " fuera de rango"
done
eval "_line=\${$_sel}"
_name=$(printf '%s' "$_line" | cut -f1); _bytes=$(printf '%s' "$_line" | cut -f2)
DEV="/dev/$_name"
_disk_mb=$((_bytes/1048576))
# --- tamaños de partición ---
_esp=64; _slack=16; _avail=$((_disk_mb - _esp - _slack))
[ "$_avail" -ge 600 ] || { echo "Disco demasiado chico ($(_human "$_bytes")); se necesitan ~700 MiB."; exit 1; }
_defr=4096; [ "$_defr" -gt $((_avail*6/10)) ] && _defr=$((_avail*6/10))
_defs=1024; [ "$_defs" -gt $((_avail/5)) ] && _defs=$((_avail/5))
while :; do
ask ROOT_MB "Tamaño de / en MB" "$_defr"
ask STORE_MB "Tamaño de /store en MB" "$_defs"
case "$ROOT_MB$STORE_MB" in *[!0-9]*) echo " usá sólo números"; continue ;; esac
if [ $((ROOT_MB + STORE_MB + _esp + _slack)) -le "$_disk_mb" ]; then break; fi
echo " no entra en el disco ($_disk_mb MiB) — / + /store + ESP deben sumar menos"
done
# --- identidad ---
ask CFG_HOSTNAME "Hostname" "${CFG_HOSTNAME:-hammer}"
echo "Credencial de root:"
echo " 1) contraseña"
echo " 2) clave SSH (authorized_keys)"
echo " 3) omitir"
ask _cred "Opción" "1"
case "$_cred" in
1) if have_cryptpw; then
while :; do
read_secret _pw1 "Contraseña de root"
read_secret _pw2 "Repetir"
[ "$_pw1" = "$_pw2" ] && [ -n "$_pw1" ] && break
echo " no coinciden (o vacía) — reintentá"
done
CFG_ROOT_HASH=$(printf '%s' "$_pw1" | bb cryptpw -m sha512 2>/dev/null || true)
[ -n "$CFG_ROOT_HASH" ] || echo " ⚠ no pude generar el hash; root queda como está"
else
echo " (cryptpw no disponible en este busybox — usá la opción de clave SSH)"
ask CFG_ROOT_AUTHKEYS "Pegá una línea authorized_keys" ""
fi ;;
2) ask CFG_ROOT_AUTHKEYS "Pegá una línea authorized_keys (o ruta a un fichero .pub)" ""
if [ -n "$CFG_ROOT_AUTHKEYS" ] && [ -f "$CFG_ROOT_AUTHKEYS" ]; then
CFG_ROOT_AUTHKEYS=$(cat "$CFG_ROOT_AUTHKEYS")
fi ;;
*) echo " (root sin credencial nueva)" ;;
esac
# --- locale (best-effort) ---
_zi="${ZONEINFO:-/usr/share/zoneinfo}"
if [ -d "$_zi" ]; then
ask CFG_TZ "Zona horaria (ej. America/La_Paz; vacío = UTC)" "${CFG_TZ:-}"
if [ -n "$CFG_TZ" ] && [ ! -e "$_zi/$CFG_TZ" ]; then
echo " ⚠ zona desconocida, se ignora"; CFG_TZ=""
fi
else
echo "(tzdata no instalado — la zona horaria se omite)"; CFG_TZ=""
fi
ask CFG_KEYMAP "Layout de teclado" "${CFG_KEYMAP:-us}"
# --- resumen + confirmación fuerte ---
_cd="sin cambios"; [ -n "$CFG_ROOT_HASH" ] && _cd="contraseña"; [ -n "$CFG_ROOT_AUTHKEYS" ] && _cd="clave SSH"
echo
echo "Resumen de instalación:"
echo " destino : $DEV ($(_human "$_bytes"))"
echo " / = ${ROOT_MB} MiB /store = ${STORE_MB} MiB /var/lib/hammer = resto"
echo " hostname: $CFG_HOSTNAME"
echo " root : $_cd"
echo " zona : ${CFG_TZ:-UTC} teclado: $CFG_KEYMAP"
echo
echo " ⚠ ESTO BORRA TODO EN $DEV."
ask _ok "Escribí el nombre del disco ($DEV) para confirmar, o Enter para cancelar" ""
[ "$_ok" = "$DEV" ] || { echo "Cancelado — no se tocó ningún disco."; exit 1; }
}
# --- resolución del modo: interactivo (sin device + TTY) vs desatendido (device dado) ---
if [ -z "$DEV" ]; then
if [ -t 0 ] || [ "${TAKANA_INSTALL_INTERACTIVE:-${HAMMER_INSTALL_INTERACTIVE:-}}" = 1 ]; then
interactive_setup
else
echo "uso: takana-install <device> [ROOT_MB] [STORE_MB] (o sin args con un TTY para el modo interactivo)" >&2
exit 2
fi
fi
ROOT_MB="${ROOT_MB:-1500}"
STORE_MB="${STORE_MB:-400}"
# En modo desatendido con contraseña por env, derivar el hash acá (interactivo ya lo hizo).
if [ -z "$CFG_ROOT_HASH" ] && [ -n "${TAKANA_ROOT_PW:-${HAMMER_ROOT_PW:-}}" ] && have_cryptpw; then
CFG_ROOT_HASH=$(printf '%s' "${TAKANA_ROOT_PW:-$HAMMER_ROOT_PW}" | bb cryptpw -m sha512 2>/dev/null || true)
fi
# --- gancho de prueba: valida TUI + config sin tocar disco (TAKANA_INSTALL_DRYRUN=1) ---
if [ "${TAKANA_INSTALL_DRYRUN:-${HAMMER_INSTALL_DRYRUN:-}}" = 1 ]; then
echo "== DRYRUN: plan de instalación =="
echo " DEV=$DEV ROOT_MB=$ROOT_MB STORE_MB=$STORE_MB"
echo " HOSTNAME=$CFG_HOSTNAME TZ=${CFG_TZ:-UTC} KEYMAP=$CFG_KEYMAP"
echo " ROOT_HASH=$([ -n "$CFG_ROOT_HASH" ] && echo set || echo -) AUTHKEYS=$([ -n "$CFG_ROOT_AUTHKEYS" ] && echo set || echo -)"
_testroot="${TAKANA_INSTALL_TESTROOT:-${HAMMER_INSTALL_TESTROOT:-}}"
if [ -n "$_testroot" ]; then
apply_post_install_config "$_testroot"
echo " config aplicada a $_testroot"
fi
exit 0
fi
[ -b "$DEV" ] || { echo "takana-install: $DEV no es un block device" >&2; exit 1; }
[ -d "$PAYLOAD" ] || { echo "takana-install: falta el payload $PAYLOAD (¿ISO sin INSTALLER=1?)" >&2; exit 1; }
# --- Detección de firmware: UEFI ⇒ rama EFI-stub SOBERANA (ADR 0010 paso 5); BIOS ⇒ MBR+GRUB (abajo) ---
# La rama EFI usa una ESP tipo 0xEF en tabla MBR (UEFI arranca \EFI\BOOT\BOOTX64.EFI de una ESP MBR igual
# que de GPT) ⇒ alcanza con busybox fdisk/mkfs.vfat/mount, sin GPT-tool ni mtools. El bzImage metal ES el
# binario EFI (EFI-stub, cmdline horneada carga el initrd); un initramfs mínimo pivota por LABEL. Mismos
# artefactos que scripts/install-image-efi.sh (validado en OVMF), armados desde dentro del live.
if [ -d /sys/firmware/efi ]; then
[ -r "$PAYLOAD/kernel/bzImage-efi" ] || { echo "takana-install: live en UEFI pero falta $PAYLOAD/kernel/bzImage-efi (ISO sin kernel metal)" >&2; exit 1; }
ESP_MB="${ESP_MB:-64}"
echo "==> takana-install (UEFI): EFI-stub soberano en $DEV (ESP ${ESP_MB}M, root ${ROOT_MB}M, store ${STORE_MB}M, estado=resto)"
echo " ¡esto BORRA $DEV!"
# MBR, 4 primarias: p1=ESP(0xEF) p2=root p3=store p4=estado. Sectores explícitos (como la rama BIOS).
SPM=2048
P1S=2048; P1E=$(( P1S + ESP_MB * SPM - 1 ))
P2S=$(( P1E + 1 )); P2E=$(( P2S + ROOT_MB * SPM - 1 ))
P3S=$(( P2E + 1 )); P3E=$(( P3S + STORE_MB* SPM - 1 ))
P4S=$(( P3E + 1 )) # estado = resto del disco
echo "==> particionando $DEV (MBR: ESP ${P1S}-${P1E} tipo ef, root ${P2S}-${P2E}, store ${P3S}-${P3E}, estado ${P4S}-fin)"
"$BB" fdisk "$DEV" >/dev/null 2>&1 <<FDISK || true
o
n
p
1
$P1S
$P1E
n
p
2
$P2S
$P2E
n
p
3
$P3S
$P3E
n
p
4
$P4S
t
1
ef
a
1
w
FDISK
"$BB" partprobe "$DEV" 2>/dev/null || true
"$BB" sync; sleep 1
P1="${DEV}1"; P2="${DEV}2"; P3="${DEV}3"; P4="${DEV}4"
for p in "$P1" "$P2" "$P3" "$P4"; do
[ -b "$p" ] || { echo "takana-install: no apareció la partición $p tras particionar" >&2; exit 1; }
done
echo "==> formateando (ESP FAT32 + 3 ext2 etiquetadas para el pivote por LABEL)"
"$BB" mkfs.vfat -F 32 -n HAMMER "$P1" >/dev/null 2>&1
"$BB" mke2fs -F -L hammer-root "$P2" >/dev/null 2>&1
"$BB" mke2fs -F -L hammer-store "$P3" >/dev/null 2>&1
"$BB" mke2fs -F -L hammer-state "$P4" >/dev/null 2>&1
# initramfs de pivote (busybox estático del live + /init: findfs LABEL=takana-root → switch_root).
echo "==> armando el initramfs de pivote (busybox + /init switch_root a hammer-root)"
IRD_TREE=/run/hammer-efi-ird
"$BB" rm -rf "$IRD_TREE"
"$BB" mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" "$IRD_TREE/newroot"
cp "$BB" "$IRD_TREE/bin/busybox"; "$BB" chmod 0755 "$IRD_TREE/bin/busybox"
for a in sh mount umount findfs switch_root mkdir echo sleep; do "$BB" ln -sf busybox "$IRD_TREE/bin/$a"; done
cat > "$IRD_TREE/init" <<'INIT'
#!/bin/busybox sh
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
log() { echo "$*"; echo "$*" > /dev/ttyS0 2>/dev/null || true; }
log "HAMMER-EFI: initramfs de pivote — resolviendo hammer-root por LABEL"
ROOTDEV=""
for _try in 1 2 3 4 5; do
ROOTDEV="$(findfs LABEL=hammer-root 2>/dev/null || true)"
[ -n "$ROOTDEV" ] && break
log "HAMMER-EFI: hammer-root aún no visible, reintento $_try…"; sleep 1
done
[ -n "$ROOTDEV" ] || { log "HAMMER-EFI-FALLO: no encuentro LABEL=hammer-root"; exec sh; }
log "HAMMER-EFI: hammer-root = $ROOTDEV → montando en /newroot"
mount -t ext4 "$ROOTDEV" /newroot || { log "HAMMER-EFI-FALLO: no pude montar $ROOTDEV"; exec sh; }
STOREDEV="$(findfs LABEL=hammer-store 2>/dev/null || true)"
STATEDEV="$(findfs LABEL=hammer-state 2>/dev/null || true)"
[ -n "$STOREDEV" ] && mount -t ext4 "$STOREDEV" /newroot/store 2>/dev/null || true
[ -n "$STATEDEV" ] && mount -t ext4 "$STATEDEV" /newroot/var/lib/hammer 2>/dev/null || true
log "HAMMER-EFI-DISK-PIVOT-OK: switch_root → /sbin/init"
exec switch_root /newroot /sbin/init
INIT
"$BB" chmod +x "$IRD_TREE/init"
( cd "$IRD_TREE" && "$BB" find . | "$BB" cpio -o -H newc 2>/dev/null | "$BB" gzip -1 ) > /run/hammer-efi-ird.cpio.gz
# Poblar la ESP con el kernel en DOS rutas (ADR 0018 §3), porque una sola es un punto único de
# fallo que NO controlamos:
# \EFI\takana\takanax64.efi el vendor path — nuestro, no lo reclama nadie más.
# \EFI\BOOT\BOOTX64.EFI la ruta FALLBACK de medios removibles (spec UEFI): tierra de nadie
# y lo único que arranca SIN entrada NVRAM, que es lo que hay hoy
# (takana todavía no crea entradas: hace falta efibootmgr/efivar).
# Es el mismo fichero dos veces: cuesta bytes de ESP, no complejidad.
echo "==> poblando la ESP (\\EFI\\takana\\takanax64.efi + \\EFI\\BOOT\\BOOTX64.EFI = kernel metal + \\initramfs.cpio.gz)"
ESPMNT=/run/hammer-esp
"$BB" mkdir -p "$ESPMNT"
"$BB" mount -t vfat "$P1" "$ESPMNT"
"$BB" mkdir -p "$ESPMNT/EFI/BOOT" "$ESPMNT/EFI/takana"
# Guardián de capacidad (ADR 0018 §4): comprobar ANTES, con los números a la vista. Un `cp` que
# se queda sin sitio en FAT32 deja un fichero TRUNCADO y devuelve error tarde — y un BOOTX64.EFI
# a medias no falla: arranca y muere en el firmware, que es la peor forma de enterarse.
_k_kb=$( "$BB" du -k "$PAYLOAD/kernel/bzImage-efi" | "$BB" awk '{print $1}' )
_i_kb=$( "$BB" du -k /run/hammer-efi-ird.cpio.gz | "$BB" awk '{print $1}' )
_free_kb=$( "$BB" df -k "$ESPMNT" | "$BB" awk 'NR==2{print $4}' )
_need_kb=$(( _k_kb * 2 + _i_kb + 256 ))
echo " kernel $(( _k_kb / 1024 ))M x2 + initramfs $(( _i_kb / 1024 ))M = $(( _need_kb / 1024 ))M sobre $(( _free_kb / 1024 ))M libres en la ESP"
if [ "$_need_kb" -gt "$_free_kb" ]; then
"$BB" umount "$ESPMNT"
echo "TAKANA-INSTALL-FAIL: no entra en la ESP — hacen falta $(( _need_kb / 1024 ))M y hay $(( _free_kb / 1024 ))M libres." >&2
echo " subí ESP_MB (actual ${ESP_MB}M) o liberá la ESP." >&2
exit 1
fi
cp "$PAYLOAD/kernel/bzImage-efi" "$ESPMNT/EFI/takana/takanax64.efi"
cp "$PAYLOAD/kernel/bzImage-efi" "$ESPMNT/EFI/BOOT/BOOTX64.EFI"
cp /run/hammer-efi-ird.cpio.gz "$ESPMNT/initramfs.cpio.gz"
"$BB" sync
# Verificar las DOS rutas y el initramfs por TAMAÑO, no por presencia: un `cp` truncado deja el
# fichero ahí y `test -e` diría que todo salió bien (regla 3 de CLAUDE.md).
for _pair in "EFI/takana/takanax64.efi:$_k_kb" "EFI/BOOT/BOOTX64.EFI:$_k_kb" "initramfs.cpio.gz:$_i_kb"; do
_f="${_pair%:*}"; _want="${_pair##*:}"
_got=$( "$BB" du -k "$ESPMNT/$_f" 2>/dev/null | "$BB" awk '{print $1}' )
[ "${_got:-0}" = "$_want" ] || {
"$BB" umount "$ESPMNT"
echo "TAKANA-INSTALL-FAIL: $_f quedó en ${_got:-0}K, se esperaban ${_want}K (ESP llena o cp truncado)" >&2
exit 1
}
done
echo " ESP verificada: las dos rutas del kernel y el initramfs con su tamaño completo"
"$BB" umount "$ESPMNT"
# Poblar la root (copia del live) — sin wrapper que monte particiones (el pivote ya montó store/estado).
echo "==> copiando la raíz del live → $P2 (excluye virtuales/efímeros, /store, /var/lib/hammer, payload)"
MNT=/run/takana-install
"$BB" mkdir -p "$MNT"
"$BB" mount -t ext4 "$P2" "$MNT"
for entry in /* /.[!.]*; do
[ -e "$entry" ] || continue
name="${entry##*/}"
case "$name" in
proc|sys|dev|run|tmp|store|init) continue ;;
*) cp -a "$entry" "$MNT/" 2>/dev/null || true ;;
esac
done
"$BB" rm -rf "$MNT/var/lib/hammer" 2>/dev/null || true
"$BB" rm -rf "$MNT/usr/lib/hammer/install" 2>/dev/null || true
"$BB" mkdir -p "$MNT/store" "$MNT/var/lib/hammer" "$MNT/proc" "$MNT/sys" "$MNT/dev" "$MNT/run" "$MNT/tmp"
# /sbin/init: wrapper que auto-sana un upgrade interrumpido y arranca arje-zero (el pivote ya montó
# store/estado por LABEL, así que acá NO hace falta montar nada — sólo hammer-recover + exec).
if [ -x "$PAYLOAD/hammer-recover" ]; then
"$BB" mkdir -p "$MNT/usr/sbin"
cp "$PAYLOAD/hammer-recover" "$MNT/usr/sbin/hammer-recover"; "$BB" chmod 0755 "$MNT/usr/sbin/hammer-recover"
fi
# CLI takana (static musl) para el menú de arranque por grafo (ADR 0010): lo copia al disco desde el
# payload; el wrapper lo invoca como `takana boot menu`.
if [ -x "$PAYLOAD/hammer" ]; then
cp "$PAYLOAD/hammer" "$MNT/usr/bin/hammer"; "$BB" chmod 0755 "$MNT/usr/bin/hammer"
fi
"$BB" rm -f "$MNT/sbin/init"
cat > "$MNT/sbin/init" <<'INIT'
#!/bin/sh
# Wrapper PID1 (rama UEFI): el initramfs de pivote ya montó / /store /var/lib/hammer por LABEL antes del
# switch_root; sólo auto-sanamos un upgrade interrumpido y arrancamos el init real. El marcador a
# /dev/ttyS0 sobrevive `quiet` (que suprime el printk kernel-side) ⇒ prueba serie de que llegó a PID1.
# busybox switch_root NO mueve /dev ⇒ montamos devtmpfs antes (si no, /dev/ttyS0 aún no existe).
/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true
[ -x /usr/sbin/hammer-recover ] && /usr/sbin/hammer-recover || true
# Menú de arranque por grafo (ADR 0010): emite /run/hammer/boot-graph.json y, si hay compositor (mirada),
# lo pinta sobre KMS. Graceful (sin compositor sigue) y al serial (no pinta tty0 ⇒ cero-parpadeo).
[ -x /usr/bin/hammer ] && /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
exec /usr/bin/arje-zero
INIT
"$BB" chmod +x "$MNT/sbin/init"
# config del usuario (hostname / root / zona / teclado) recogida al inicio
apply_post_install_config "$MNT"
"$BB" sync
"$BB" umount "$MNT"
echo "✓ takana-install (UEFI): $DEV instalado con EFI-stub SOBERANO (sin GRUB ni systemd-boot). Reiniciá sin el medio live."
exit 0
fi
# ===================== rama BIOS (MBR + GRUB) — el firmware NO es UEFI ==============================
[ -r "$PAYLOAD/grub/boot.img" ] && [ -r "$PAYLOAD/grub/core.img" ] || { echo "takana-install: payload GRUB incompleto" >&2; exit 1; }
[ -r "$PAYLOAD/kernel/bzImage" ] || { echo "takana-install: falta el kernel en el payload" >&2; exit 1; }
echo "==> takana-install: instalando el sistema live en $DEV (root ${ROOT_MB}M, store ${STORE_MB}M, estado=resto)"
echo " ¡esto BORRA $DEV!"
# --- particionar MBR con busybox fdisk (3 primarias; p1 bootable) ---
# Pasamos sectores de inicio/fin EXPLÍCITOS para las tres: busybox fdisk, al pedir el inicio por
# defecto, ofrece el sector LIBRE MÁS BAJO (= el hueco 63..2047 que dejamos antes de p1) ⇒ con <enter>
# colocaría p2 en ese hueco. Por eso computamos los sectores y los tecleamos.
# p1 ARRANCA EN 2048 (1 MiB) A PROPÓSITO: deja un hueco post-MBR de 2047 sectores para el core.img de
# GRUB (~281 sectores). El default CHS de busybox (sector 63 ⇒ hueco de 62) lo DESBORDA, pisando el
# filesystem de p1 ⇒ GRUB cae a `grub>`.
SPM=2048
P1S=2048; P1E=$(( P1S + ROOT_MB * SPM - 1 ))
P2S=$(( P1E + 1 )); P2E=$(( P2S + STORE_MB * SPM - 1 ))
P3S=$(( P2E + 1 )) # p3 toma hasta el final del disco (último sector = default)
echo "==> particionando $DEV (MBR: p1 ${P1S}-${P1E}, p2 ${P2S}-${P2E}, p3 ${P3S}-fin)"
"$BB" fdisk "$DEV" >/dev/null 2>&1 <<FDISK || true
o
n
p
1
$P1S
$P1E
n
p
2
$P2S
$P2E
n
p
3
$P3S
a
1
w
FDISK
"$BB" partprobe "$DEV" 2>/dev/null || true
"$BB" sync; sleep 1
P1="${DEV}1"; P2="${DEV}2"; P3="${DEV}3"
for p in "$P1" "$P2" "$P3"; do
[ -b "$p" ] || { echo "takana-install: no apareció la partición $p tras particionar" >&2; exit 1; }
done
# --- formatear (busybox mke2fs = ext2; el driver ext4 del kernel lo monta) ---
echo "==> formateando particiones (ext2, montables como ext4)"
"$BB" mke2fs -F -L hammer-root "$P1" >/dev/null 2>&1
"$BB" mke2fs -F -L hammer-store "$P2" >/dev/null 2>&1
"$BB" mke2fs -F -L hammer-state "$P3" >/dev/null 2>&1
# --- copiar la raíz del live a la partición root, excluyendo virtuales + el payload ---
MNT=/run/takana-install
"$BB" mkdir -p "$MNT"
"$BB" mount -t ext4 "$P1" "$MNT"
echo "==> copiando la raíz del live → $P1 (excluye virtuales/efímeros, /store, /var/lib/hammer y el payload)"
# Enumeramos TODO el top-level de / y copiamos salvo lo virtual/efímero — así no se nos escapa ningún
# dir del producto (p.ej. /ente con la seed.card, /linuxrc). uutils cp -a preserva modos/symlinks.
for entry in /* /.[!.]*; do
[ -e "$entry" ] || continue
name="${entry##*/}"
case "$name" in
proc|sys|dev|run|tmp|store|init) continue ;; # virtuales/efímeros + el /init marcador del live
*) cp -a "$entry" "$MNT/" 2>/dev/null || true ;;
esac
done
# /var sin /var/lib/hammer (estado) — se recrea vacío como mountpoint.
"$BB" rm -rf "$MNT/var/lib/hammer" 2>/dev/null || true
# Quitar el payload del disco instalado (sólo sirve en el live) y recrear mountpoints.
"$BB" rm -rf "$MNT/usr/lib/hammer/install" 2>/dev/null || true
"$BB" mkdir -p "$MNT/store" "$MNT/var/lib/hammer" "$MNT/proc" "$MNT/sys" "$MNT/dev" "$MNT/run" "$MNT/tmp" "$MNT/boot/grub/i386-pc"
# --- /boot: kernel + módulos GRUB + grub.cfg, y wrapper /sbin/init (monta las particiones dedicadas) ---
echo "==> poblando /boot (kernel + GRUB) y wrapper /sbin/init"
cp "$PAYLOAD/kernel/bzImage" "$MNT/boot/bzImage"; "$BB" chmod 0644 "$MNT/boot/bzImage"
cp "$PAYLOAD"/grub/i386-pc/*.mod "$MNT/boot/grub/i386-pc/" 2>/dev/null || true
cp "$PAYLOAD"/grub/i386-pc/*.lst "$MNT/boot/grub/i386-pc/" 2>/dev/null || true
ROOTDEV="${P1##*/}" # p.ej. vda1 cuando el disco instalado sea el único
cat > "$MNT/boot/grub/grub.cfg" <<CFG
set timeout=2
set default=0
serial --unit=0 --speed=115200
terminal_input serial console
terminal_output serial console
insmod part_msdos
insmod ext2
menuentry "hammer" {
linux /boot/bzImage console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init
}
CFG
# hammer-recover: auto-sana un upgrade interrumpido al arranque (E4 #4b). Lo dejamos en /usr/sbin del
# disco; el wrapper lo corre tras montar /store y /var/lib/hammer (que necesita) y antes de incarnar.
if [ -x "$PAYLOAD/hammer-recover" ]; then
"$BB" mkdir -p "$MNT/usr/sbin"
cp "$PAYLOAD/hammer-recover" "$MNT/usr/sbin/hammer-recover"; "$BB" chmod 0755 "$MNT/usr/sbin/hammer-recover"
fi
"$BB" rm -f "$MNT/sbin/init"
cat > "$MNT/sbin/init" <<'INIT'
#!/bin/sh
# Wrapper PID1: el kernel monta la partición root; montamos las dedicadas, auto-sanamos un upgrade
# interrumpido (si lo hay) y arrancamos el init real.
/bin/busybox mount -t ext4 /dev/vda2 /store || echo "init: no pude montar /store (/dev/vda2)"
/bin/busybox mount -t ext4 /dev/vda3 /var/lib/hammer || echo "init: no pude montar /var/lib/hammer (/dev/vda3)"
[ -x /usr/sbin/hammer-recover ] && /usr/sbin/hammer-recover || true
exec /usr/bin/arje-zero
INIT
"$BB" chmod +x "$MNT/sbin/init"
# config del usuario (hostname / root / zona / teclado) recogida al inicio
apply_post_install_config "$MNT"
"$BB" sync
"$BB" umount "$MNT"
# --- instalar GRUB: boot.img→MBR (440 B, sin pisar la tabla de particiones) + core.img→hueco post-MBR.
# Con install contiguo MBR los punteros por defecto de GRUB (kernel_sector=1, blocklist=2) ya valen
# ⇒ NO hace falta el parcheo binario que sí pide el caso GPT de install-image.sh.
echo "==> instalando GRUB (boot.img→MBR + core.img→hueco post-MBR)"
"$BB" dd if="$PAYLOAD/grub/boot.img" of="$DEV" bs=1 count=440 conv=notrunc 2>/dev/null
"$BB" dd if="$PAYLOAD/grub/core.img" of="$DEV" bs=512 seek=1 conv=notrunc 2>/dev/null
"$BB" sync
echo "✓ takana-install: $DEV instalado. Reiniciá sin el medio live para arrancar del disco."