From 489af9b38705c8a1b75834cd407fa2e717894c9d Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Sat, 6 Jun 2026 16:55:22 -0300 Subject: [PATCH] feat: initial multiboot manager script and research doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script interactivo para configurar multiboot desde disco interno. Auto-detecta sistema, crea partición MULTIBOOT en espacio libre, gestiona ISOs con entradas GRUB automáticas (loopback + fallback). Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 + investigacion-plan.md | 253 +++++++++++++++ multiboot.sh | 709 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 964 insertions(+) create mode 100644 .gitignore create mode 100644 investigacion-plan.md create mode 100755 multiboot.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5daa0e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Memoria interna de Claude Code — no va al repo +.claude/ diff --git a/investigacion-plan.md b/investigacion-plan.md new file mode 100644 index 0000000..6f02af6 --- /dev/null +++ b/investigacion-plan.md @@ -0,0 +1,253 @@ +# Multiboot Nativo desde HD — Investigación y Plan + +## Objetivo + +Reemplazar el pendrive booteable con ISOs por una partición del disco duro interno que permita arrancar distintas ISOs de instalación de Linux directamente, sin depender de hardware externo. + +**Flujo deseado:** +``` +Reiniciar PC → Menú GRUB → Elegir ISO → Instalar distro en la PC de turno +``` + +--- + +## Panorama de Enfoques + +### Opción A — GRUB2 Loopback Manual +**¿Qué es?** GRUB2 incluye soporte nativo para montar ISOs en tiempo de arranque (loopback) y pasar el kernel/initrd directamente desde el archivo `.iso` sin descomprimirlo. + +**Cómo funciona:** +1. La ISO vive en cualquier partición que GRUB pueda leer (ext4, NTFS, FAT32) +2. GRUB monta la ISO como loop device en tiempo de boot +3. Se carga el kernel + initrd extraídos de adentro de la ISO +4. El kernel recibe un parámetro que le indica dónde está la ISO para continuar + +**Requisito clave:** La ISO debe incluir un archivo `boot/grub/loopback.cfg` — la mayoría de distros modernas lo tienen (Ubuntu, Debian, Fedora, Arch, etc.). + +**Ejemplo de entrada GRUB:** +```grub +menuentry "Ubuntu 24.04 LTS" { + set isofile="/isos/ubuntu-24.04-desktop-amd64.iso" + loopback loop (hd0,3)$isofile + linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$isofile quiet splash + initrd (loop)/casper/initrd +} +``` + +**Pros:** +- Sin dependencias externas — GRUB puro +- Control total sobre el menú +- Funciona con BIOS/Legacy y UEFI (con Secure Boot desactivado) + +**Contras:** +- Cada ISO requiere una entrada manual en grub.cfg +- Sintaxis varía entre distros (Ubuntu usa `casper`, Fedora usa `rd.live.image`, Arch es diferente) +- Mantenimiento manual al agregar ISOs + +--- + +### Opción B — GLIM (GRUB2 Live ISO Multiboot) +**Repo:** https://github.com/thias/glim + +**¿Qué es?** Colección de scripts y configuraciones GRUB prehechas para arrancar múltiples ISOs. Detecta la distro por nombre de archivo y aplica la configuración correcta automáticamente. + +**Estructura de uso:** +``` +/boot/isos/ + ubuntu/ ← Ubuntu ISOs van aquí + debian/ ← Debian ISOs van aquí + fedora/ ← Fedora ISOs van aquí + arch/ ← Arch Linux ISOs van aquí + ... +``` + +**Soporte de particiones:** +- Partición 1: FAT32, etiqueta `GLIM` (100MB mínimo, para el bootloader) +- Partición 2: ext4, etiqueta `GLIMISO` (para ISOs > 4GB que no caben en FAT32) + +**Pros:** +- Agregar ISO = copiar archivo a la carpeta correcta +- Soporta > 60 distros out-of-the-box +- Maneja automáticamente las diferencias de sintaxis por distro + +**Contras:** +- Diseñado para USB, adaptarlo a HD interno requiere ajustes +- Requiere partición FAT32 separada para el bootloader GLIM +- Puede coexistir con un OS existente, pero necesita configuración adicional + +--- + +### Opción C — Ventoy en Disco Interno +**Repo:** https://github.com/ventoy/Ventoy + +**¿Qué es?** Herramienta que convierte un disco (o partición) en un multiboot que simplemente copia ISOs y listo. + +**Instalación en HD interno:** +```bash +# Instalar Ventoy en un disco dedicado o partición reservada +sudo sh Ventoy2Disk.sh -I /dev/sdX +# Luego copiar ISOs a la partición Ventoy +``` + +**Pros:** +- La más simple: copiar ISO → aparece en el menú al reiniciar +- Soporta > 1300 sistemas operativos +- Funciona con BIOS y UEFI (incluyendo Secure Boot) +- Soporta ISO, WIM, IMG, VHD, EFI + +**Contras / Riesgo importante:** +- Ventoy toma control del MBR/GPT del disco donde se instala +- Si se instala en el disco del OS actual: **riesgo de romper el boot existente** +- La solución más segura es un **disco físico separado** o un disco dedicado solo a Ventoy +- Durante la instalación de un OS destino, el installer puede confundirse y seleccionar el disco Ventoy + +**Mitigación:** Reservar un disco SSD secundario o una segunda unidad NVMe solo para Ventoy. + +--- + +### Opción D — grml-rescueboot (paquete Debian/Ubuntu) +**¿Qué es?** Paquete que se integra con `update-grub`. Escanea `/boot/grml/` y agrega entradas automáticamente al GRUB del sistema. + +```bash +sudo apt install grml-rescueboot +# Copiar ISO a /boot/grml/ +sudo update-grub # agrega la entrada automáticamente +``` + +**Pros:** Extremadamente simple, integrado con el sistema + +**Contras:** Diseñado principalmente para ISOs Grml, soporte limitado para otras distros + +--- + +## Comparativa Rápida + +| Criterio | A: GRUB Manual | B: GLIM | C: Ventoy HD | D: grml-rescueboot | +|---|---|---|---|---| +| Simplicidad de uso | Baja | Media | Alta | Alta | +| Agregar nueva ISO | Manual | Copiar a carpeta | Copiar a partición | Copiar + update-grub | +| Soporte de distros | Todas* | ~60 | +1300 | Grml + pocas | +| Riesgo para OS actual | Bajo | Bajo | **Alto** | Bajo | +| UEFI Secure Boot | No** | No** | Sí | No** | +| Scripteabilidad | Total | Alta | Alta | Media | + +*con entrada GRUB correcta +**requiere desactivar Secure Boot + +--- + +## Recomendación: Plan en 3 Fases + +### Fase 1 — Estructura Base (mínimo viable) + +**Arquitectura elegida:** Partición dedicada ext4 en el disco actual + GRUB2 Loopback + script de gestión + +**¿Por qué esta combinación?** +- Sin riesgo para el OS actual (es solo una partición nueva) +- Control total (no dependemos de proyectos externos) +- Scripteabilidad completa para automatizar el flujo +- Compatible con BIOS y UEFI + +**Pasos:** + +1. **Crear partición dedicada** (20–50 GB, ext4, etiqueta `MULTIBOOT`) + ```bash + # Identificar disco y espacio libre + lsblk + sudo fdisk /dev/sdX # o parted / gparted + sudo mkfs.ext4 -L MULTIBOOT /dev/sdXN + ``` + +2. **Punto de montaje permanente** en `/etc/fstab`: + ``` + LABEL=MULTIBOOT /mnt/multiboot ext4 defaults,noatime 0 2 + ``` + +3. **Estructura de carpetas:** + ``` + /mnt/multiboot/ + isos/ + ubuntu/ + debian/ + fedora/ + arch/ + grub/ + grub.cfg ← configuración principal + entries/ ← un .cfg por ISO + ``` + +4. **Entradas GRUB:** Agregar a `/etc/grub.d/40_custom` o crear `/etc/grub.d/41_multiboot`: + ```grub + menuentry "--- Multiboot ISOs ---" { echo } + source (hd0,X)/grub/grub.cfg + ``` + +### Fase 2 — Script de Gestión (`add-iso.sh`) + +Script que dado un archivo ISO: +1. Lo copia a la partición en la carpeta correcta según la distro detectada +2. Genera automáticamente la entrada GRUB para esa ISO +3. Ejecuta `sudo update-grub` para registrarla + +```bash +#!/bin/bash +# add-iso.sh +# Uso: ./add-iso.sh ~/downloads/ubuntu-24.04.iso +``` + +Detección de distro por nombre: `ubuntu*` → carpeta ubuntu, `debian*` → debian, etc. + +### Fase 3 — Mejoras + +- Script `list-isos.sh`: listar ISOs disponibles con info de tamaño y fecha +- Script `remove-iso.sh`: eliminar ISO y su entrada GRUB +- Soporte para ISOs con `loopback.cfg` nativo (delegar la config a la propia ISO) +- Considerar migrar a **GLIM** si el número de distros crece mucho + +--- + +## Consideraciones Importantes + +### UEFI vs BIOS Legacy +- Si la PC usa **UEFI**, hay que asegurarse de que **Secure Boot esté desactivado** para que GRUB cargue ISOs de terceros +- En UEFI, el GRUB de la partición EFI existente es el que controla el arranque + +### ISOs que soportan loopback.cfg (recomendadas) +- Ubuntu / Xubuntu / Kubuntu / Lubuntu +- Debian (live e installer) +- Linux Mint +- Fedora Workstation +- openSUSE +- Manjaro +- Kali Linux + +### ISOs con soporte limitado o especial +- Arch Linux: no tiene loopback.cfg, requiere entrada GRUB personalizada +- Windows ISOs: requieren enfoque diferente (memdisk o Ventoy) +- Alpine Linux: funciona pero con parámetros especiales + +### Riesgo de confusión del installer +Cuando se arranca una ISO de instalación desde el HD interno y se va a instalar en **otra PC** (escenario típico del usuario), no hay problema porque la ISO corre en la PC fuente como live environment. El riesgo existe si se intenta instalar en la **misma PC** del multiboot. + +--- + +## Próximos Pasos Concretos + +1. **Determinar la máquina host:** ¿Qué disco/partición tiene espacio libre? (`lsblk -f`) +2. **Elegir modo boot:** ¿BIOS/Legacy o UEFI? (`[ -d /sys/firmware/efi ] && echo UEFI || echo BIOS`) +3. **Crear la partición MULTIBOOT** con el espacio disponible +4. **Implementar `add-iso.sh`** con soporte inicial para Ubuntu, Debian y Fedora +5. **Probar el boot** con una ISO conocida antes de ampliar + +--- + +## Fuentes y Referencias + +- [Ubuntu Wiki — GRUB2 ISOBoot](https://help.ubuntu.com/community/Grub2/ISOBoot) +- [ArchWiki — Multiboot USB Drive](https://wiki.archlinux.org/title/Multiboot_USB_drive) +- [GLIM en GitHub (thias)](https://github.com/thias/glim) +- [Ventoy — GitHub oficial](https://github.com/ventoy/Ventoy) +- [LinuxBabe — Boot ISO desde GRUB2](https://www.linuxbabe.com/desktop-linux/boot-from-iso-files-using-grub2-boot-loader) +- [DEV.to — Boot desde ISO sin USB](https://dev.to/oryaacov/how-to-boot-from-iso-file-using-grub-internal-hdd-without-external-usbcd-4a09) +- [grml-rescueboot — GitHub](https://github.com/grml/grml-rescueboot) +- [Loopback.cfg — Super Grub Disk Wiki](https://www.supergrubdisk.org/wiki/Loopback.cfg) diff --git a/multiboot.sh b/multiboot.sh new file mode 100755 index 0000000..7499dcd --- /dev/null +++ b/multiboot.sh @@ -0,0 +1,709 @@ +#!/usr/bin/env bash +# ============================================================================= +# multiboot.sh — Gestor de Multiboot desde disco interno +# Proyecto: rm-MULTIBOOT | Uso: sudo ./multiboot.sh +# ============================================================================= +set -euo pipefail + +# ─── Constantes ─────────────────────────────────────────────────────────────── +readonly SCRIPT_VERSION="1.0.0" +readonly MULTIBOOT_LABEL="MULTIBOOT" +readonly MULTIBOOT_MOUNT="/mnt/multiboot" +readonly ISO_DIR="$MULTIBOOT_MOUNT/isos" +readonly ENTRIES_DIR="$MULTIBOOT_MOUNT/grub/entries" +readonly GRUB_HOOK="/etc/grub.d/41_multiboot" + +# ─── Colores ────────────────────────────────────────────────────────────────── +R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' +B='\033[0;34m' C='\033[0;36m' W='\033[1m' N='\033[0m' + +# ─── Estado global (poblado por detect_system) ──────────────────────────────── +BOOT_MODE="" OS_NAME="" ROOT_DISK_DEV="" ROOT_DISK_SIZE="" +MULTIBOOT_DEV="" MULTIBOOT_SIZE="" MULTIBOOT_READY=false + + +# ============================================================================= +# UI +# ============================================================================= + +banner() { + clear + echo -e "${W}${B}" + echo " ╔═══════════════════════════════════════════════════╗" + echo " ║ MULTIBOOT MANAGER · rm-MULTIBOOT ║" + echo " ╚═══════════════════════════════════════════════════╝" + echo -e "${N}" +} + +ok() { echo -e " ${G}✓${N} $*"; } +warn() { echo -e " ${Y}⚠${N} $*"; } +err() { echo -e " ${R}✗${N} $*"; } +step() { echo -e "\n ${W}${C}→${N} ${W}$*${N}"; } +divider() { echo -e "\n ${B}──────────────────────────────────────────────────${N}\n"; } + +ask() { + local prompt="$1" default="${2:-}" answer + if [[ -n "$default" ]]; then + read -rp " ${W}$prompt${N} [$default]: " answer + echo "${answer:-$default}" + else + read -rp " ${W}$prompt${N}: " answer + echo "$answer" + fi +} + +confirm() { + local answer + read -rp " ${W}$* [s/N]:${N} " answer + [[ "${answer,,}" == "s" ]] +} + +pause() { echo ""; read -rp " Presioná Enter para continuar..." _; } + +grub_update() { + if command -v update-grub &>/dev/null; then + update-grub 2>/dev/null && ok "GRUB actualizado" || warn "No se pudo actualizar GRUB" + elif command -v grub-mkconfig &>/dev/null; then + grub-mkconfig -o /boot/grub/grub.cfg 2>/dev/null && ok "GRUB actualizado" || warn "No se pudo actualizar GRUB" + else + warn "No se encontró update-grub ni grub-mkconfig — actualizá el GRUB manualmente" + fi +} + + +# ============================================================================= +# DETECCIÓN DEL SISTEMA +# ============================================================================= + +detect_system() { + step "Detectando sistema..." + echo "" + + # Modo boot + [[ -d /sys/firmware/efi ]] && BOOT_MODE="UEFI" || BOOT_MODE="BIOS Legacy" + ok "Modo de arranque: ${W}$BOOT_MODE${N}" + + # OS actual + [[ -f /etc/os-release ]] && { . /etc/os-release; OS_NAME="${PRETTY_NAME:-Linux}"; } || OS_NAME="Linux" + ok "Sistema operativo: ${W}$OS_NAME${N}" + + # Disco raíz + local root_src root_name + root_src=$(findmnt -n -o SOURCE / 2>/dev/null | head -1) + root_name=$(lsblk -no PKNAME "$root_src" 2>/dev/null | head -1) + if [[ -z "$root_name" ]]; then + root_name=$(echo "$root_src" | sed 's|/dev/||;s|p\?[0-9]*$||') + fi + ROOT_DISK_DEV="/dev/$root_name" + ROOT_DISK_SIZE=$(lsblk -dno SIZE "$ROOT_DISK_DEV" 2>/dev/null || echo "?") + ok "Disco principal: ${W}$ROOT_DISK_DEV${N} ($ROOT_DISK_SIZE)" + + # ¿Ya existe partición MULTIBOOT? + local mb_name + mb_name=$(lsblk -o NAME,LABEL -rn 2>/dev/null \ + | awk -v lbl="$MULTIBOOT_LABEL" '$2==lbl {print $1}' | head -1) + if [[ -n "$mb_name" ]]; then + MULTIBOOT_DEV="/dev/$mb_name" + MULTIBOOT_SIZE=$(lsblk -dno SIZE "$MULTIBOOT_DEV" 2>/dev/null || echo "?") + MULTIBOOT_READY=true + ok "Partición MULTIBOOT: ${W}$MULTIBOOT_DEV${N} ($MULTIBOOT_SIZE) ${G}lista${N}" + else + MULTIBOOT_READY=false + warn "Partición MULTIBOOT: ${Y}no encontrada${N}" + fi +} + + +# ============================================================================= +# MENÚ PRINCIPAL +# ============================================================================= + +menu_principal() { + divider + + if [[ "$MULTIBOOT_READY" == false ]]; then + echo -e " Hola. Este equipo aún no tiene partición MULTIBOOT." + echo -e " Voy a crearla en espacio libre del disco ${W}$ROOT_DISK_DEV${N}" + echo -e " para que puedas bootear ISOs sin pendrive.\n" + echo " 1) Preparar este equipo para multiboot" + echo " 2) Salir" + echo "" + local opt; opt=$(ask "Opción [1-2]") + case "$opt" in + 1) setup_wizard ;; + 2) echo ""; exit 0 ;; + *) menu_principal ;; + esac + else + mount_multiboot + local iso_count + iso_count=$(find "$ISO_DIR" -maxdepth 2 -name "*.iso" 2>/dev/null | wc -l) + [[ "$iso_count" -gt 0 ]] && show_iso_mini_list || echo -e " ${Y}Todavía no hay ISOs cargadas.${N}\n" + echo " 1) Agregar una ISO" + echo " 2) Ver ISOs disponibles" + echo " 3) Eliminar una ISO" + echo " 4) Salir" + echo "" + local opt; opt=$(ask "Opción [1-4]") + case "$opt" in + 1) add_iso ;; + 2) list_isos; pause; banner; detect_system; menu_principal ;; + 3) remove_iso ;; + 4) echo ""; exit 0 ;; + *) menu_principal ;; + esac + fi +} + +show_iso_mini_list() { + local count + count=$(find "$ISO_DIR" -maxdepth 2 -name "*.iso" 2>/dev/null | wc -l) + echo -e " ISOs disponibles: ${W}$count${N}\n" + find "$ISO_DIR" -maxdepth 2 -name "*.iso" 2>/dev/null | sort | while read -r iso; do + local sz; sz=$(du -h "$iso" | cut -f1) + printf " ${C}•${N} %-50s %s\n" "$(basename "$iso")" "($sz)" + done + echo "" +} + + +# ============================================================================= +# SETUP WIZARD — crear la partición MULTIBOOT +# ============================================================================= + +setup_wizard() { + banner + step "Asistente de configuración inicial" + echo "" + + # ── Detectar espacios libres ────────────────────────────────────────────── + local free_raw + free_raw=$(parted -s "$ROOT_DISK_DEV" unit MiB print free 2>/dev/null \ + | awk '/Free Space/ && $3+0 >= 10240 {print $1, $2, $3}') + + if [[ -z "$free_raw" ]]; then + err "No se encontró espacio libre ≥ 10 GB sin asignar en $ROOT_DISK_DEV" + echo "" + echo -e " Para continuar necesitás liberar espacio con GParted u otra herramienta.\n" + pause; exit 1 + fi + + # ── Mostrar regiones disponibles ───────────────────────────────────────── + echo -e " Espacio libre sin asignar en ${W}$ROOT_DISK_DEV${N}:\n" + local -a starts ends sizes labels + local i=1 + while read -r start end size; do + local size_gib + size_gib=$(echo "$size" | sed 's/MiB//' | awk '{printf "%.0f", $1/1024}') + starts+=("$start"); ends+=("$end"); sizes+=("$size_gib") + labels+=("${size_gib} GB libres (desde $start hasta $end)") + printf " ${C}%d)${N} %s\n" "$i" "${labels[$((i-1))]}" + i=$((i+1)) + done <<< "$free_raw" + echo "" + + local region_opt=1 + if [[ "${#starts[@]}" -gt 1 ]]; then + region_opt=$(ask "¿En qué región crear la partición?" "1") + if ! [[ "$region_opt" =~ ^[0-9]+$ ]] || \ + [[ "$region_opt" -lt 1 ]] || [[ "$region_opt" -gt "${#starts[@]}" ]]; then + err "Opción inválida."; pause; setup_wizard; return + fi + fi + + local idx=$((region_opt - 1)) + local free_start="${starts[$idx]}" + local max_gib="${sizes[$idx]}" + local recommended=$((max_gib > 100 ? 50 : max_gib)) + + # ── Pedir tamaño ───────────────────────────────────────────────────────── + echo -e " ${Y}Nota:${N} cada ISO pesa entre 600 MB y 5 GB." + echo -e " Recomendado: ≥ 30 GB para tener varias ISOs.\n" + local size_gb + size_gb=$(ask "¿Cuántos GB asignar a MULTIBOOT? (máx $max_gib)" "$recommended") + + if ! [[ "$size_gb" =~ ^[0-9]+$ ]] || [[ "$size_gb" -lt 10 ]] || [[ "$size_gb" -gt "$max_gib" ]]; then + err "Tamaño inválido. Debe ser un número entre 10 y $max_gib." + pause; setup_wizard; return + fi + + # ── Calcular extremo de la nueva partición ──────────────────────────────── + local start_mib end_mib + start_mib=$(echo "$free_start" | sed 's/MiB//') + end_mib=$((start_mib + size_gb * 1024)) + + # ── Confirmar ───────────────────────────────────────────────────────────── + echo "" + echo -e " ${W}Resumen:${N}" + echo -e " Disco: $ROOT_DISK_DEV" + echo -e " Inicio: ${start_mib} MiB" + echo -e " Tamaño: ${size_gb} GB" + echo -e " Etiqueta: $MULTIBOOT_LABEL" + echo -e " Sistema arch: ext4" + echo -e " Punto montaje: $MULTIBOOT_MOUNT" + echo "" + + if ! confirm "¿Crear la partición ahora?"; then + echo ""; warn "Operación cancelada."; pause; exit 0 + fi + + # ── Crear partición ─────────────────────────────────────────────────────── + echo "" + step "Creando partición..." + + local parts_before + parts_before=$(lsblk -lno NAME "$ROOT_DISK_DEV" \ + | grep -v "^$(basename "$ROOT_DISK_DEV")$" | sort) + + # Tabla GPT: mkpart sin nombre de tipo; MBR: mkpart primary + local part_table + part_table=$(parted -s "$ROOT_DISK_DEV" print 2>/dev/null | awk '/Partition Table/{print $3}') + + if [[ "$part_table" == "gpt" ]]; then + parted -s "$ROOT_DISK_DEV" mkpart "$MULTIBOOT_LABEL" ext4 \ + "${start_mib}MiB" "${end_mib}MiB" 2>/dev/null + else + parted -s "$ROOT_DISK_DEV" mkpart primary ext4 \ + "${start_mib}MiB" "${end_mib}MiB" 2>/dev/null + fi + + sleep 1 + partprobe "$ROOT_DISK_DEV" 2>/dev/null || true + sleep 1 + + local new_part + new_part=$(comm -13 \ + <(echo "$parts_before") \ + <(lsblk -lno NAME "$ROOT_DISK_DEV" | grep -v "^$(basename "$ROOT_DISK_DEV")$" | sort) \ + | head -1) + + if [[ -z "$new_part" ]]; then + err "No se pudo identificar la nueva partición. Revisá con 'lsblk'." + pause; exit 1 + fi + MULTIBOOT_DEV="/dev/$new_part" + ok "Partición creada: $MULTIBOOT_DEV" + + # ── Formatear ───────────────────────────────────────────────────────────── + step "Formateando como ext4..." + mkfs.ext4 -L "$MULTIBOOT_LABEL" -q "$MULTIBOOT_DEV" + ok "Formato aplicado con etiqueta '$MULTIBOOT_LABEL'" + + # ── Montar ──────────────────────────────────────────────────────────────── + mkdir -p "$MULTIBOOT_MOUNT" + mount "$MULTIBOOT_DEV" "$MULTIBOOT_MOUNT" + mkdir -p "$ISO_DIR" "$MULTIBOOT_MOUNT/grub/entries" + ok "Montada en $MULTIBOOT_MOUNT" + + # ── fstab ───────────────────────────────────────────────────────────────── + local part_uuid + part_uuid=$(lsblk -no UUID "$MULTIBOOT_DEV" | head -1) + if ! grep -q "$part_uuid" /etc/fstab 2>/dev/null; then + printf '\n# Partición MULTIBOOT — rm-MULTIBOOT\nUUID=%s %s ext4 defaults,noatime 0 2\n' \ + "$part_uuid" "$MULTIBOOT_MOUNT" >> /etc/fstab + ok "Agregado a /etc/fstab (montaje automático en cada inicio)" + fi + + # ── Hook de GRUB ────────────────────────────────────────────────────────── + install_grub_hook "$part_uuid" + + # ── Actualizar GRUB ─────────────────────────────────────────────────────── + step "Actualizando GRUB..." + grub_update + + MULTIBOOT_READY=true + MULTIBOOT_SIZE="${size_gb}G" + + divider + echo -e " ${W}${G}¡Listo! El equipo está preparado para multiboot.${N}" + echo -e " Ahora podés agregar ISOs con la opción 1 del menú.\n" + pause + banner; detect_system; menu_principal +} + + +# ============================================================================= +# HOOK DE GRUB +# ============================================================================= + +install_grub_hook() { + local part_uuid="$1" + cat > "$GRUB_HOOK" </dev/null; then + mount UUID="$part_uuid" "\$MPOINT" 2>/dev/null || exit 0 +fi + +for cfg in "\$ENTRIES"/*.cfg; do + [ -f "\$cfg" ] || continue + cat "\$cfg" +done +HOOK + chmod +x "$GRUB_HOOK" + ok "Hook de GRUB instalado: $GRUB_HOOK" +} + + +# ============================================================================= +# MONTAJE +# ============================================================================= + +mount_multiboot() { + if ! mountpoint -q "$MULTIBOOT_MOUNT" 2>/dev/null; then + mkdir -p "$MULTIBOOT_MOUNT" + mount -L "$MULTIBOOT_LABEL" "$MULTIBOOT_MOUNT" 2>/dev/null \ + || mount "$MULTIBOOT_DEV" "$MULTIBOOT_MOUNT" 2>/dev/null \ + || { err "No se pudo montar la partición MULTIBOOT."; exit 1; } + fi + mkdir -p "$ISO_DIR" "$ENTRIES_DIR" +} + + +# ============================================================================= +# AGREGAR ISO +# ============================================================================= + +add_iso() { + banner + step "Agregar una ISO al multiboot" + echo "" + echo -e " Ingresá la ruta al archivo .iso" + echo -e " ${Y}Tip:${N} podés arrastrar el archivo directamente a la terminal\n" + + local iso_path + iso_path=$(ask "Ruta al .iso") + iso_path="${iso_path/#\~/$HOME}" + iso_path="${iso_path//\'/}"; iso_path="${iso_path//\"/}" + + if [[ ! -f "$iso_path" ]]; then + err "Archivo no encontrado: $iso_path" + pause; banner; detect_system; menu_principal; return + fi + + local iso_name size distro + iso_name=$(basename "$iso_path") + size=$(du -h "$iso_path" | cut -f1) + distro=$(detect_distro "$iso_name") + + echo "" + echo -e " ${W}ISO detectada:${N}" + ok "Nombre: ${W}$iso_name${N}" + ok "Tamaño: $size" + ok "Distro: ${W}$distro${N}" + echo "" + + # Verificar espacio disponible + local avail_k iso_k + avail_k=$(df "$MULTIBOOT_MOUNT" --output=avail | tail -1) + iso_k=$(du -k "$iso_path" | cut -f1) + if [[ "$iso_k" -ge "$avail_k" ]]; then + err "Espacio insuficiente en la partición MULTIBOOT." + echo -e " Disponible: $(( avail_k / 1024 / 1024 )) GB" + echo -e " Necesario: $(( iso_k / 1024 / 1024 )) GB" + pause; banner; detect_system; menu_principal; return + fi + + # Verificar si ya existe + if [[ -f "$ISO_DIR/$iso_name" ]]; then + warn "Ya existe una ISO con ese nombre." + if ! confirm "¿Sobreescribir?"; then + banner; detect_system; menu_principal; return + fi + fi + + if ! confirm "¿Copiar esta ISO al multiboot?"; then + banner; detect_system; menu_principal; return + fi + + # Copiar con progreso + echo "" + step "Copiando ISO..." + local total + total=$(stat -c%s "$iso_path") + local dest="$ISO_DIR/$iso_name" + + cp "$iso_path" "$dest" & + local cp_pid=$! + while kill -0 "$cp_pid" 2>/dev/null; do + local done_bytes=0 + [[ -f "$dest" ]] && done_bytes=$(stat -c%s "$dest" 2>/dev/null || echo 0) + local pct=$(( total > 0 ? done_bytes * 100 / total : 0 )) + local done_mb=$(( done_bytes / 1024 / 1024 )) + local total_mb=$(( total / 1024 / 1024 )) + printf "\r ${C}→${N} %3d%% (%d MB de %d MB)" "$pct" "$done_mb" "$total_mb" + sleep 0.4 + done + wait "$cp_pid" + printf "\r ${G}✓${N} 100%% (%d MB) \n" "$(( total / 1024 / 1024 ))" + + # Generar entrada GRUB + step "Generando entrada de arranque..." + generate_grub_entry "$iso_name" "$distro" + + # Actualizar GRUB + step "Actualizando menú de arranque..." + grub_update + + divider + echo -e " ${W}${G}¡ISO agregada!${N}" + echo -e " Al reiniciar, aparecerá en el menú de arranque.\n" + pause; banner; detect_system; menu_principal +} + + +# ============================================================================= +# DETECCIÓN DE DISTRO (por nombre de archivo) +# ============================================================================= + +detect_distro() { + local n="${1,,}" + case "$n" in + *kubuntu*) echo "Kubuntu" ;; + *xubuntu*) echo "Xubuntu" ;; + *lubuntu*) echo "Lubuntu" ;; + *ubuntu*) echo "Ubuntu" ;; + *linuxmint*|*mint*) echo "Linux Mint" ;; + *debian*) echo "Debian" ;; + *fedora*) echo "Fedora" ;; + *arch*) echo "Arch Linux" ;; + *manjaro*) echo "Manjaro" ;; + *kali*) echo "Kali Linux" ;; + *parrot*) echo "Parrot OS" ;; + *opensuse*) echo "openSUSE" ;; + *pop*|*popos*) echo "Pop!_OS" ;; + *elementary*) echo "elementary OS" ;; + *zorin*) echo "Zorin OS" ;; + *mxlinux*|*mx-*) echo "MX Linux" ;; + *tails*) echo "Tails" ;; + *alpine*) echo "Alpine Linux" ;; + *rockylinux*|*rocky*) echo "Rocky Linux" ;; + *almalinux*|*alma*) echo "AlmaLinux" ;; + *centosstream*|*centos*) echo "CentOS" ;; + *) echo "Linux" ;; + esac +} + + +# ============================================================================= +# GENERAR ENTRADA GRUB +# ============================================================================= + +generate_grub_entry() { + local iso_name="$1" distro="$2" + local label="${iso_name%.iso}" + local part_uuid + part_uuid=$(lsblk -no UUID "$MULTIBOOT_DEV" 2>/dev/null | head -1) + local entry_file="$ENTRIES_DIR/${label}.cfg" + + # ── Verificar si la ISO tiene loopback.cfg nativo ───────────────────────── + local tmp_mnt has_loopback=false + tmp_mnt=$(mktemp -d) + if modprobe loop 2>/dev/null; then + if mount -o loop,ro "$ISO_DIR/$iso_name" "$tmp_mnt" 2>/dev/null; then + [[ -f "$tmp_mnt/boot/grub/loopback.cfg" ]] && has_loopback=true + umount "$tmp_mnt" 2>/dev/null || true + fi + fi + rmdir "$tmp_mnt" 2>/dev/null || true + + if [[ "$has_loopback" == true ]]; then + # Usar loopback.cfg nativo de la ISO (máxima compatibilidad) + cat > "$entry_file" < "$entry_file" </dev/null | sort) + + if [[ "$count" -eq 0 ]]; then + echo -e " ${Y}No hay ISOs cargadas todavía.${N}\n" + else + echo "" + local avail + avail=$(df -h "$MULTIBOOT_MOUNT" --output=avail | tail -1 | tr -d ' ') + ok "Espacio libre en MULTIBOOT: ${W}$avail${N}" + fi + echo "" +} + + +# ============================================================================= +# ELIMINAR ISO +# ============================================================================= + +remove_iso() { + banner + step "Eliminar una ISO" + echo "" + + local -a isos + while IFS= read -r iso; do + [[ -f "$iso" ]] && isos+=("$iso") + done < <(find "$ISO_DIR" -maxdepth 2 -name "*.iso" 2>/dev/null | sort) + + if [[ "${#isos[@]}" -eq 0 ]]; then + echo -e " No hay ISOs disponibles para eliminar.\n" + pause; banner; detect_system; menu_principal; return + fi + + local i=1 + for iso in "${isos[@]}"; do + local sz; sz=$(du -h "$iso" | cut -f1) + printf " ${C}%2d)${N} %-50s %6s\n" "$i" "$(basename "$iso")" "$sz" + i=$((i+1)) + done + echo " 0) Cancelar" + echo "" + + local opt; opt=$(ask "¿Cuál eliminás? [0-${#isos[@]}]") + + if [[ "$opt" == "0" ]]; then + banner; detect_system; menu_principal; return + fi + + if ! [[ "$opt" =~ ^[0-9]+$ ]] || \ + [[ "$opt" -lt 1 ]] || [[ "$opt" -gt "${#isos[@]}" ]]; then + err "Opción inválida."; pause; remove_iso; return + fi + + local target="${isos[$((opt-1))]}" + local tname; tname=$(basename "$target") + local entry="$ENTRIES_DIR/${tname%.iso}.cfg" + + echo "" + warn "Vas a eliminar: ${W}$tname${N}" + if ! confirm "¿Confirmar?"; then + banner; detect_system; menu_principal; return + fi + + rm -f "$target" + [[ -f "$entry" ]] && rm -f "$entry" + + step "Actualizando GRUB..."; grub_update + + echo "" + ok "ISO eliminada: $tname" + pause; banner; detect_system; menu_principal +} + + +# ============================================================================= +# PUNTO DE ENTRADA +# ============================================================================= + +main() { + if [[ "$EUID" -ne 0 ]]; then + echo "" + echo -e " ${R}Error:${N} Este script necesita permisos de administrador." + echo -e " Ejecutalo con: ${W}sudo $0${N}\n" + exit 1 + fi + + banner + detect_system + menu_principal +} + +main "$@"