#!/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/1 / ext2(montable ext4) + /boot/bzImage + /boot/grub + wrapper /sbin/init # /dev/2 /store ext2 # /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/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: namebytesmodelmounted(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 [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!" # Informe del terreno ANTES de tocar nada (ADR 0018 §4). Esta rama se lleva el disco ENTERO, así # que lo que hay que decir en voz alta no es sólo «cuánto espacio hay»: es **con qué se va a # llevar puesto**. Si en la ESP de este disco hay un `\EFI\Microsoft`, el survey lo nombra, y el # usuario se entera ANTES y no después de que su Windows dejó de arrancar. # # No escribe nada y no puede fallar el instalador: `|| true`. Su trabajo es informar. if [ -x "$PAYLOAD/hammer" ]; then echo "==> terreno actual de $DEV (nada de esto se ha tocado todavía):" "$PAYLOAD/hammer" boot entry survey --disk "$DEV" 2>&1 | "$BB" sed 's/^/ /' || true fi # 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 </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 # efivarfs: sin montarlo la NVRAM del firmware no se ve. El directorio sólo existe si arrancamos por # UEFI, así que el `[ -d ]` distingue UEFI de BIOS sin preguntarle a nadie. # switch_root no arrastra /sys (ni /dev): sin esto el directorio de efivars no existe y el # reconciliador diagnostica «no arrancó por UEFI» en una máquina que sí lo hizo. /bin/busybox mount -t sysfs sys /sys 2>/dev/null || true [ -d /sys/firmware/efi/efivars ] && /bin/busybox mount -t efivarfs efivarfs /sys/firmware/efi/efivars 2>/dev/null || 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 # Respaldo del arranque (ADR 0018 §5). Se hace DESPUÉS de reconciliar, así lo que queda guardado es # el estado bueno y no el que dejó el vecino. El nombre del fichero sale del CONTENIDO: un arranque # que no cambió produce el mismo fichero, así que esto corre en cada arranque sin llenar la # partición de estado con copias idénticas. # # Vive en /var/lib/hammer, que en las imágenes es su propia partición y sobrevive a una reinstalación # del sistema. **No va al store**: el store lo barre `store-gc.sh`, que clasifica por nombre de # receta — un respaldo ahí sería «huérfano» y se borraría en el primer `--huerfanos`. [ -x /usr/bin/hammer ] && /bin/busybox timeout 15 /usr/bin/hammer boot entry backup > /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 # 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 </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" < "$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."