La NVRAM es estado compartido y el vecino la reescribe sin coordinarse. Contra eso no sirve confiar: sirve converger. `reconcile` descubre su propia ESP, repone la entrada y el BootOrder, y es idempotente. Descubre la ESP por TIPO de partición (GUID de ESP en GPT, 0xEF en MBR), que es el único dato fiable sin montar nada — un reconciliador que monta sistemas de ficheros en el arranque es un efecto secundario que no queremos. Si hay VARIAS ESP no adivina: las lista y pide --disk. Elegir mal significa escribir una entrada que apunta a un disco que puede no estar, y eso es peor que no escribir nada. No rompe el arranque por nada: sin firmware EFI (máquina por BIOS) lo dice y sale 0. Y cuando actúa, GRITA — va a /dev/tty0 además del serial, a diferencia del menú de arranque. Un reconciliador que repara en silencio deja al usuario conviviendo con una rareza intermitente que no entiende; el mensaje dice explícitamente que si se repite en cada arranque es que otro sistema operativo le está reescribiendo la NVRAM. Enganchado al wrapper de PID1 de las dos imágenes, con el mismo timeout y el mismo || true que el menú: corre ANTES del exec de arje-zero y colgarse ahí es un arranque muerto e indistinguible de un kernel colgado. El test que vale es el escenario completo: takana se instala, llega el vecino y se pone primero, y el siguiente arranque lo repone — SIN borrar la entrada del vecino (takana se pone primera, no lo echa) — y el arranque siguiente ya no escribe nada. Más el GUID de ESP, que va en orden DE DISCO y no en el legible: escribirlo "como se lee" es el error clásico y no casaría con ninguna ESP real. 15 tests en el módulo, 74/74 del CLI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
596 lines
28 KiB
Bash
Executable File
596 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
|
|
# Reconciliador del arranque (ADR 0018 §2): la NVRAM es estado compartido y el vecino la reescribe
|
|
# sin avisar. Converger en cada arranque, y DECIRLO en pantalla cuando hubo que actuar — si repara
|
|
# en silencio, el usuario nunca se entera de que algo le está pisando el arranque.
|
|
[ -x /usr/bin/hammer ] && /bin/busybox timeout 15 /usr/bin/hammer boot entry reconcile 2>&1 \
|
|
| /bin/busybox tee /dev/tty0 > /dev/ttyS0 2>/dev/null || 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."
|