antiXのGRUBメニューにISOブートを追加

antiXが起動しなくなった時にインストールメディアを用意しなくても済むように、ドライブの後半にiso用のパーティションを用意し、そこから起動出来るようにします。

ホストにインストール

末尾にisoパーティションを作ったら、そこにantiXのISOイメージを保存し、下記スクリプトも保存して実行します。なお、スクリプト実行中の上記画面の場面で、起動メニューを表示させる場合には別途パッチを適用する必要があります。表示させないなら、このスクリプトだけで起動出来ますば、antiXの場合は起動メニューがあったほうが便利でしょう。

sudo nano isoboot-grub3.sh
sudo bash isoboot-grub3.sh
#!/usr/bin/env bash
# ============================================================
#  isoboot-grub.sh  —  ISO ループブート GRUB エントリー管理
#
#  機能:
#    1. ISO ループブートエントリーの追加
#       (Ubuntu/casper系、antiX系、汎用live-boot系を自動判定)
#    2. 不要エントリーの削除 (40_custom 由来のみ)
#    3. 何もせず終了
#
#  対応環境:
#    - BIOS (legacy) / UEFI 両対応
#    - パーティションの FS は ext2/3/4, btrfs, xfs, vfat, ntfs,
#      exfat を自動判定して適切な insmod を選択
#
#  注意:
#    バックアップは /root/grub-backups/ に保存する。
#    /etc/grub.d/ 配下に古い .bak ファイルを置いたままにすると
#    update-grub がそれを拾ってエントリーが復活することがあるため
#    削除機能はそれも検出して片付ける。
# ============================================================

set -euo pipefail

CUSTOM_FILE="${CUSTOM_FILE:-/etc/grub.d/40_custom}"
BACKUP_DIR="/root/grub-backups"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die()     { error "$*"; exit 1; }
hr()      { echo -e "${CYAN}$(printf '=%.0s' {1..64})${RESET}"; }
hr_thin() { echo -e "${CYAN}$(printf -- '-%.0s' {1..64})${RESET}"; }

confirm() {
    local ans
    while true; do
        echo -en "${YELLOW}[確認]${RESET} $1 [y/N]: "
        read -r ans
        case "$ans" in
            [yY]*) return 0 ;;
            [nN]*|"") return 1 ;;
            *) echo "  y または n を入力してください。" ;;
        esac
    done
}

check_root() {
    [[ $EUID -eq 0 ]] || die "sudo で実行してください。\n  例: sudo bash $0"
}

check_deps() {
    local missing=()
    for cmd in lsblk blkid mount umount find update-grub; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "コマンドが見つかりません: ${missing[*]}"
}

backup_file() {
    local src="$1" base
    base=$(basename "$src")
    mkdir -p "$BACKUP_DIR"
    local dst="${BACKUP_DIR}/${base}.bak.$(date +%Y%m%d_%H%M%S)"
    cp "$src" "$dst"
    echo "$dst"
}

# ============================================================
#  ファームウェアモードを検出 (uefi / bios)
# ============================================================
detect_firmware() {
    if [[ -d /sys/firmware/efi ]]; then
        echo "uefi"
    else
        echo "bios"
    fi
}

# ============================================================
#  パーティションテーブル種別を検出 (gpt / msdos / unknown)
# ============================================================
detect_partition_table() {
    local dev="$1"
    local disk
    disk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
    [[ -z "$disk" ]] && echo "unknown" && return
    local pttype
    pttype=$(blkid -p -o value -s PTTYPE "/dev/${disk}" 2>/dev/null || true)
    case "$pttype" in
        gpt)   echo "gpt" ;;
        dos)   echo "msdos" ;;
        *)     echo "unknown" ;;
    esac
}

# ============================================================
#  ファイルシステム種別から GRUB insmod 名を返す
#  戻り値: "insmod_fs:insmod_part" の形式
# ============================================================
fstype_to_grub_insmod() {
    local fstype="$1"
    local part_type="$2"   # gpt / msdos / unknown

    local ins_fs
    case "$fstype" in
        ext2|ext3|ext4)        ins_fs="ext2" ;;
        btrfs)                 ins_fs="btrfs" ;;
        xfs)                   ins_fs="xfs" ;;
        vfat|fat16|fat32|fat)  ins_fs="fat" ;;
        ntfs|ntfs-3g)          ins_fs="ntfs" ;;
        exfat)                 ins_fs="exfat" ;;
        *)                     ins_fs="ext2" ;;   # フォールバック
    esac

    local ins_part
    case "$part_type" in
        gpt)    ins_part="part_gpt" ;;
        msdos)  ins_part="part_msdos" ;;
        *)      ins_part="part_gpt part_msdos" ;;  # 判定不能なら両方
    esac

    echo "${ins_fs}:${ins_part}"
}

# ============================================================
#  grub.cfg のパスを返す (BIOS/UEFI 両対応で探索)
# ============================================================
find_grub_cfg() {
    local f
    for f in /boot/grub/grub.cfg /boot/grub2/grub.cfg \
             /boot/efi/EFI/ubuntu/grub.cfg /boot/efi/EFI/debian/grub.cfg \
             /boot/efi/EFI/*/grub.cfg; do
        [[ -f "$f" ]] && echo "$f" && return
    done
    echo ""
}

# ============================================================
#  menuentry/submenu の表示名を1行から抽出
# ============================================================
_SQ="'"
_DQ='"'
extract_entry_name() {
    local line="$1"
    local pat="(menuentry|submenu)[[:space:]]+[${_SQ}${_DQ}]([^${_SQ}${_DQ}]*)[${_SQ}${_DQ}]"
    if [[ "$line" =~ $pat ]]; then
        echo "${BASH_REMATCH[2]}"
    else
        echo ""
    fi
}

# ============================================================
#  grub.cfg をパースしてエントリー情報を配列に格納
# ============================================================
ENTRY_NAMES=()
ENTRY_CLASSES=()
ENTRY_GRUB_IDS=()
ENTRY_SOURCES=()

parse_grub_cfg() {
    local cfg_file="$1"

    declare -gA CUSTOM_ENTRY_MAP=()
    local in_cs=0
    while IFS= read -r line; do
        if [[ "$line" =~ ^###\ BEGIN\ (.+)\ ###$ ]]; then
            [[ "${BASH_REMATCH[1]}" == "$CUSTOM_FILE" ]] && in_cs=1 || in_cs=0
            continue
        fi
        [[ "$line" =~ ^###\ END\ (.+)\ ###$ ]] && { in_cs=0; continue; }
        if [[ $in_cs -eq 1 ]]; then
            local cn; cn=$(extract_entry_name "$line")
            [[ -n "$cn" ]] && CUSTOM_ENTRY_MAP["$cn"]=1
        fi
    done < "$cfg_file"

    ENTRY_NAMES=()
    ENTRY_CLASSES=()
    ENTRY_GRUB_IDS=()
    ENTRY_SOURCES=()

    local top_index=-1 in_submenu=0 sub_index=-1 sub_top_index=-1

    while IFS= read -r line; do

        if [[ $in_submenu -eq 0 ]] && [[ "$line" =~ ^submenu[[:space:]] ]]; then
            ((top_index++)) || true
            sub_top_index=$top_index; in_submenu=1; sub_index=-1
            local sname; sname=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$sname]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$sname"); ENTRY_CLASSES+=("submenu_header")
            ENTRY_GRUB_IDS+=("$top_index"); ENTRY_SOURCES+=("$src")
            continue
        fi

        if [[ $in_submenu -eq 1 ]] && [[ "$line" =~ ^'}'[[:space:]]*$ ]]; then
            in_submenu=0; sub_top_index=-1; continue
        fi

        if [[ $in_submenu -eq 1 ]] && [[ "$line" =~ ^[[:space:]]+menuentry[[:space:]] ]]; then
            ((sub_index++)) || true
            local ename; ename=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$ename]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$ename"); ENTRY_CLASSES+=("sub_entry")
            ENTRY_GRUB_IDS+=("${sub_top_index}>${sub_index}"); ENTRY_SOURCES+=("$src")
            continue
        fi

        if [[ $in_submenu -eq 0 ]] && [[ "$line" =~ ^menuentry[[:space:]] ]]; then
            ((top_index++)) || true
            local ename; ename=$(extract_entry_name "$line")
            local src="auto"; [[ -n "${CUSTOM_ENTRY_MAP[$ename]+_}" ]] && src="custom"
            ENTRY_NAMES+=("$ename"); ENTRY_CLASSES+=("toplevel")
            ENTRY_GRUB_IDS+=("$top_index"); ENTRY_SOURCES+=("$src")
            continue
        fi

    done < "$cfg_file"
}

# ============================================================
#  エントリー一覧を表示
# ============================================================
print_entries() {
    if [[ ${#ENTRY_NAMES[@]} -eq 0 ]]; then
        warn "menuentry が見つかりませんでした。"
        return
    fi

    printf "\n  ${BOLD}%-4s  %-14s  %-12s  %-8s  %s${RESET}\n" \
        "No." "GRUB ID" "種別" "出所" "エントリー名"
    hr_thin

    for i in "${!ENTRY_NAMES[@]}"; do
        local class="${ENTRY_CLASSES[$i]}" src="${ENTRY_SOURCES[$i]}"
        local grub_id="${ENTRY_GRUB_IDS[$i]}" name="${ENTRY_NAMES[$i]}"

        local type_label
        case "$class" in
            toplevel)       type_label="${GREEN}エントリー${RESET}" ;;
            submenu_header) type_label="${YELLOW}サブメニュー${RESET}" ;;
            sub_entry)      type_label="${CYAN}  └子エントリ${RESET}" ;;
        esac

        local src_label
        [[ "$src" == "custom" ]] \
            && src_label="${GREEN}custom${RESET}" \
            || src_label="${CYAN}auto${RESET}  "

        local display_name="$name"
        [[ "$class" == "sub_entry" ]] && display_name="    ${name}"

        printf "  ${BOLD}%3d${RESET}   %-14s  %-22b  %-16b  %s\n" \
            "$i" "$grub_id" "$type_label" "$src_label" "$display_name"
    done

    echo ""
    info "出所: ${GREEN}custom${RESET} = 40_custom(削除可能)  ${CYAN}auto${RESET} = 自動生成(削除不可)"
}

# ============================================================
#  現在の GRUB 設定を表示
# ============================================================
print_current_settings() {
    hr
    echo -e "${BOLD}■ 現在の GRUB 設定${RESET}"
    hr_thin

    local fw; fw=$(detect_firmware)
    if [[ "$fw" == "uefi" ]]; then
        echo -e "  ファームウェア: ${GREEN}${BOLD}UEFI${RESET}"
    else
        echo -e "  ファームウェア: ${YELLOW}${BOLD}BIOS (Legacy)${RESET}"
    fi

    local default_grub="/etc/default/grub"
    if [[ -f "$default_grub" ]]; then
        local dv tv
        dv=$(grep '^GRUB_DEFAULT=' "$default_grub" | cut -d= -f2 | tr -d '"' || echo "未設定")
        tv=$(grep '^GRUB_TIMEOUT='  "$default_grub" | cut -d= -f2 | tr -d '"' || echo "未設定")
        echo -e "  GRUB_DEFAULT  : ${BOLD}${dv}${RESET}"
        echo -e "  GRUB_TIMEOUT  : ${BOLD}${tv}${RESET} 秒"
    fi

    local env_file
    for env_file in /boot/grub/grubenv /boot/grub2/grubenv; do
        [[ -f "$env_file" ]] || continue
        local saved next rf
        saved=$(grub-editenv "$env_file" list 2>/dev/null | grep '^saved_entry=' | cut -d= -f2 || true)
        next=$(grub-editenv  "$env_file" list 2>/dev/null | grep '^next_entry='   | cut -d= -f2 || true)
        rf=$(grub-editenv    "$env_file" list 2>/dev/null | grep '^recordfail='   | cut -d= -f2 || true)
        echo -e "  saved_entry   : ${BOLD}${saved:-(なし)}${RESET}"
        echo -e "  next_entry    : ${BOLD}${next:-(なし)}${RESET}"
        if [[ "${rf}" == "1" ]]; then
            echo -e "  recordfail    : ${RED}${BOLD}1(残存)${RESET}"
            warn "recordfail=1 が残っています。次回起動時に影響する場合があります。"
        else
            echo -e "  recordfail    : ${GREEN}なし${RESET}"
        fi
        break
    done
    echo ""
}

# ============================================================
#  ISO をループマウントして起動方式・カーネル/initrdパスを検出
#
#  対応レイアウト:
#    casper  … Ubuntu / Ubuntu派生 (casper)
#    antix   … antiX (独自の live-init。casper/live-bootとは別物)
#    live    … 汎用 Debian live-boot (live-boot パッケージ)
#    custom  … 上記いずれにも一致しない場合のフォールバック(要手動入力)
#
#  加えて、ISO内に GRUB ループバック用の専用設定ファイル
#  (boot/grub/loopback.cfg など) が同梱されていないか調べる。
#  これが見つかった場合、そのファイルを configfile で読み込むことで
#  実機/実CDから起動したときと同じ起動メニュー(カーネル選択・
#  persistenceオプション等)をそのまま再現できる。
# ============================================================
_detect_boot_paths() {
    local iso_path="$1"
    local tmp="/mnt/_iso_inspect_$$"
    mkdir -p "$tmp"
    if ! mount -o loop,ro "$iso_path" "$tmp" 2>/dev/null; then
        rmdir "$tmp" 2>/dev/null || true
        echo "UNKNOWN:UNKNOWN:custom:NONE"; return
    fi

    local vmlinuz="" initrd="" boot_type=""

    if   [[ -f "$tmp/casper/vmlinuz" ]];     then vmlinuz="/casper/vmlinuz";     initrd="/casper/initrd";     boot_type="casper"
    elif [[ -f "$tmp/casper/vmlinuz.efi" ]]; then vmlinuz="/casper/vmlinuz.efi"; initrd="/casper/initrd.lz";  boot_type="casper"
    elif [[ -f "$tmp/antiX/vmlinuz1" ]];     then vmlinuz="/antiX/vmlinuz1";     initrd="/antiX/initrd.gz";   boot_type="antix"
    elif [[ -f "$tmp/antiX/vmlinuz" ]];      then vmlinuz="/antiX/vmlinuz";      initrd="/antiX/initrd.gz";   boot_type="antix"
    elif [[ -f "$tmp/live/vmlinuz" ]];       then vmlinuz="/live/vmlinuz";       initrd="/live/initrd.img";   boot_type="live"
    elif [[ -f "$tmp/live/vmlinuz.efi" ]];   then vmlinuz="/live/vmlinuz.efi";   initrd="/live/initrd.img";   boot_type="live"
    else
        local found; found=$(find "$tmp" -name "vmlinuz*" | head -1 || true)
        if [[ -n "$found" ]]; then
            vmlinuz="${found##"$tmp"}"
            local dir; dir=$(dirname "$found")
            local ird; ird=$(find "$dir" -name "initrd*" | head -1 || true)
            initrd="${ird##"$tmp"}"
            boot_type="custom"
        else
            vmlinuz="UNKNOWN"; initrd="UNKNOWN"; boot_type="custom"
        fi
    fi

    # ISO本来の起動メニューを再現するための loopback 用 GRUB 設定を探す。
    # antiX / MX Linux / Ubuntu 系の多くは、実機起動用の grub.cfg とは別に
    # loopback boot専用の設定ファイルを同梱している。
    local loopback_cfg="NONE"
    local cfg_candidates=(
        "/boot/grub/loopback.cfg"
        "/isolinux/loopback.cfg"
        "/EFI/BOOT/loopback.cfg"
        "/boot/grub/grub.cfg"
    )
    local c
    for c in "${cfg_candidates[@]}"; do
        if [[ -f "${tmp}${c}" ]]; then
            loopback_cfg="$c"
            break
        fi
    done

    umount "$tmp"; rmdir "$tmp"
    echo "${vmlinuz}:${initrd}:${boot_type}:${loopback_cfg}"
}

# ============================================================
#  1: ISO ループブートエントリーの追加
# ============================================================
action_add_iso() {
    hr
    echo -e "${BOLD}■ ISO ループブートエントリーの追加${RESET}"
    hr_thin
    echo ""

    info "パーティション一覧:"
    echo ""

    mapfile -t part_list < <(
        lsblk -lno NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS \
        | grep -v '^loop' \
        | awk 'NF && $1 ~ /[0-9]$/ { print $0 }'
    )
    [[ ${#part_list[@]} -eq 0 ]] && die "パーティションが見つかりませんでした。"

    printf "  ${BOLD}%-4s  %s${RESET}\n" "No." "NAME  SIZE  FSTYPE  LABEL  MOUNTPOINTS"
    hr_thin
    for i in "${!part_list[@]}"; do
        printf "  ${CYAN}[%2d]${RESET}  %s\n" "$((i+1))" "${part_list[$i]}"
    done
    echo ""

    local part_dev part_uuid part_fstype part_pttype
    while true; do
        echo -en "${BOLD}番号を入力してください${RESET} [1-${#part_list[@]}]: "
        read -r sel
        if ! [[ "$sel" =~ ^[0-9]+$ ]] || (( sel < 1 || sel > ${#part_list[@]} )); then
            error "1〜${#part_list[@]} の番号を入力してください。"; continue
        fi
        local pname; pname=$(echo "${part_list[$((sel-1))]}" | awk '{print $1}')
        part_dev="/dev/${pname}"
        [[ -b "$part_dev" ]] || { error "${part_dev} はブロックデバイスではありません。"; continue; }

        part_fstype=$(lsblk -no FSTYPE "$part_dev" 2>/dev/null | head -1 || true)
        part_pttype=$(detect_partition_table "$part_dev")

        case "$part_fstype" in
            ext2|ext3|ext4|btrfs|xfs|vfat|fat16|fat32|fat|ntfs|ntfs-3g|exfat) ;;
            *)
                warn "${part_dev} のファイルシステム(${part_fstype:-不明})は動作確認済みではありません。"
                confirm "このまま続けますか?" || continue
                ;;
        esac

        echo ""
        lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTS "$part_dev" 2>/dev/null || true
        echo -e "  パーティションテーブル: ${BOLD}${part_pttype}${RESET}"
        echo ""
        confirm "${part_dev} を使用しますか?" && break
    done

    part_uuid=$(blkid -s UUID -o value "$part_dev")
    [[ -n "$part_uuid" ]] || die "UUID を取得できませんでした: ${part_dev}"
    success "パーティション  : ${part_dev}"
    success "UUID           : ${part_uuid}"
    success "FS 種別        : ${part_fstype:-不明}"
    success "パーティション表: ${part_pttype}"
    echo ""

    local insmod_str; insmod_str=$(fstype_to_grub_insmod "$part_fstype" "$part_pttype")
    local ins_fs="${insmod_str%%:*}"
    local ins_part="${insmod_str#*:}"

    local mount_point="/mnt/_isoboot_tmp_$$"
    mkdir -p "$mount_point"
    mount "$part_dev" "$mount_point"
    info "${part_dev} を ${mount_point} にマウントしました"
    echo ""

    mapfile -t iso_candidates < <(find "$mount_point" -maxdepth 2 -name "*.iso" 2>/dev/null)
    if [[ ${#iso_candidates[@]} -eq 0 ]]; then
        umount "$mount_point"; rmdir "$mount_point"
        die "ISO ファイルが見つかりませんでした。"
    fi

    info "見つかった ISO ファイル:"
    for i in "${!iso_candidates[@]}"; do
        local sz; sz=$(du -sh "${iso_candidates[$i]}" 2>/dev/null | cut -f1)
        printf "  ${CYAN}[%d]${RESET}  %s  (%s)\n" "$((i+1))" "${iso_candidates[$i]##"$mount_point"}" "$sz"
    done
    printf "  ${CYAN}[%d]${RESET}  何もせず終了\n" "$((${#iso_candidates[@]}+1))"
    echo ""

    local iso_entries=()
    local sel_input sel_nums=()
    while true; do
        echo -en "${BOLD}追加する番号を入力${RESET}(スペース区切り複数可): "
        read -r sel_input
        local cancel_num=$(( ${#iso_candidates[@]} + 1 ))
        local do_cancel=0
        for n in $sel_input; do
            [[ "$n" == "$cancel_num" ]] && do_cancel=1 && break
        done
        if [[ $do_cancel -eq 1 ]] || [[ -z "$sel_input" ]]; then
            umount "$mount_point"; rmdir "$mount_point"
            info "キャンセルしました。"; return
        fi
        local valid=1
        sel_nums=()
        for n in $sel_input; do
            if ! [[ "$n" =~ ^[0-9]+$ ]] || (( n < 1 || n > ${#iso_candidates[@]} )); then
                error "1〜${#iso_candidates[@]} の番号を入力してください。"
                valid=0; break
            fi
            sel_nums+=("$n")
        done
        [[ $valid -eq 1 ]] && break
    done

    for n in "${sel_nums[@]}"; do
        local iso_full="${iso_candidates[$((n-1))]}"
        local iso_rel="${iso_full##"$mount_point"}"
        local iso_name; iso_name=$(basename "$iso_full")
        echo ""
        info "--- ${iso_name} を検査中 ---"
        local paths; paths=$(_detect_boot_paths "$iso_full")
        local vmlinuz initrd boot_type loopback_cfg
        IFS=':' read -r vmlinuz initrd boot_type loopback_cfg <<< "$paths"
        if [[ "$vmlinuz" == "UNKNOWN" ]]; then
            warn "カーネルパスを自動検出できませんでした。手動入力してください。"
            echo -en "  vmlinuz のパス (例: /casper/vmlinuz): "; read -r vmlinuz
            echo -en "  initrd  のパス (例: /casper/initrd): ";  read -r initrd
            boot_type="custom"
        else
            success "カーネル : ${vmlinuz}"
            success "initrd   : ${initrd}"
            success "起動方式 : ${boot_type}"
        fi

        local use_menu="no"
        if [[ "$loopback_cfg" != "NONE" ]]; then
            success "ISO本来の起動メニュー設定を検出: ${loopback_cfg}"
            if confirm "実CD起動時と同じ起動メニューを表示させますか?(推奨)"; then
                use_menu="yes"
            else
                loopback_cfg="NONE"
            fi
        else
            info "ISO本来の起動メニュー設定は見つかりませんでした(直接カーネル起動になります)。"
        fi

        iso_entries+=("${iso_rel}:${vmlinuz}:${initrd}:${boot_type}:${loopback_cfg}")
    done

    umount "$mount_point"; rmdir "$mount_point"
    success "アンマウント完了"

    [[ ${#iso_entries[@]} -eq 0 ]] && { warn "追加するエントリがありません。"; return; }

    echo ""
    local bak; bak=$(backup_file "$CUSTOM_FILE")
    success "バックアップ: ${bak}"
    echo ""

    for entry in "${iso_entries[@]}"; do
        local iso_rel vmlinuz initrd boot_type loopback_cfg
        IFS=':' read -r iso_rel vmlinuz initrd boot_type loopback_cfg <<< "$entry"
        local menu_label; menu_label=$(basename "$iso_rel" .iso)

        local insmod_lines=""
        for mod in $ins_part; do
            insmod_lines+="    insmod ${mod}"$'\n'
        done
        insmod_lines+="    insmod loopback"$'\n'
        insmod_lines+="    insmod iso9660"$'\n'
        insmod_lines+="    insmod ${ins_fs}"$'\n'
        insmod_lines+="    insmod regexp"

        local grub_entry
        if [[ "$loopback_cfg" != "NONE" ]]; then
            # ISO 内の専用 loopback 設定を configfile で読み込み、
            # 実CD/実機起動と同じメニュー(カーネル選択・言語・
            # Advanced Options 等)をそのまま表示する。
            grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot - 起動メニュー表示)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    set root=(loop)
    configfile (loop)${loopback_cfg}
}
EOF
)
        else
            local params
            case "$boot_type" in
                casper)
                    # Ubuntu / casper 系
                    params="boot=casper iso-scan/filename=\$isofile quiet splash"
                    ;;
                live)
                    # 汎用 Debian live-boot (casper とは別物。findiso= を使う)
                    params="boot=live findiso=\$isofile quiet splash"
                    ;;
                antix)
                    # antiX は casper も live-boot も使わない独自の live-init。
                    # from=hd/all で「どの種類のデバイスを探すか」を絞ると、
                    # 一致するUUIDのデバイスがあってもブロックされてしまう
                    # (antiX側の既知の挙動)。buuid= で直接指定し、from= は
                    # 付けない。VMのvirtioディスクでもこれで検出できる。
                    params="buuid=${part_uuid} fromiso=\$isofile quiet nocheckfs persist"
                    ;;
                *)
                    # 未知のレイアウト。casper 相当のパラメータで試すが
                    # 起動しない場合は手動調整が必要。
                    params="iso-scan/filename=\$isofile quiet splash"
                    ;;
            esac

            grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot)" {
${insmod_lines}
    search --no-floppy --fs-uuid --set=isodev ${part_uuid}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    linux  (loop)${vmlinuz} ${params}
    initrd (loop)${initrd}
}
EOF
)
        fi

        echo -e "${CYAN}追加予定エントリ (検出レイアウト: ${boot_type}, メニュー使用: $([[ "$loopback_cfg" != "NONE" ]] && echo はい || echo いいえ)):${RESET}"
        echo "$grub_entry"
        echo ""
        if confirm "このエントリを追加しますか?"; then
            echo "$grub_entry" >> "$CUSTOM_FILE"
            success "追加しました: ${menu_label}"
        else
            warn "スキップ: ${menu_label}"
        fi
    done

    _run_update_grub
}

# ============================================================
#  2: 不要エントリーの削除
# ============================================================
action_delete() {
    hr
    echo -e "${BOLD}■ 不要エントリーの削除${RESET}"
    hr_thin
    echo ""

    local deletable_indices=()
    for i in "${!ENTRY_SOURCES[@]}"; do
        [[ "${ENTRY_SOURCES[$i]}" == "custom" ]] \
        && [[ "${ENTRY_CLASSES[$i]}" != "submenu_header" ]] \
        && deletable_indices+=("$i")
    done

    if [[ ${#deletable_indices[@]} -eq 0 ]]; then
        warn "削除可能なエントリー(40_custom 由来)が見つかりませんでした。"
        return
    fi

    echo -e "  ${BOLD}削除可能なエントリー一覧:${RESET}"
    echo ""
    printf "  ${BOLD}%-4s  %-14s  %s${RESET}\n" "No." "GRUB ID" "エントリー名"
    hr_thin
    for i in "${deletable_indices[@]}"; do
        printf "  ${BOLD}%3d${RESET}   %-14s  %s\n" \
            "$i" "${ENTRY_GRUB_IDS[$i]}" "${ENTRY_NAMES[$i]}"
    done
    echo ""

    local sel_input to_delete=() valid
    while true; do
        echo -en "${BOLD}削除する番号を入力${RESET}(スペース区切り複数可、Enter でキャンセル): "
        read -r sel_input
        [[ -z "$sel_input" ]] && { info "キャンセルしました。"; return; }

        valid=1; to_delete=()
        for num in $sel_input; do
            if ! [[ "$num" =~ ^[0-9]+$ ]]; then
                error "無効な入力: ${num}"; valid=0; break
            fi
            local found=0
            for di in "${deletable_indices[@]}"; do
                [[ "$di" -eq "$num" ]] && found=1 && break
            done
            if [[ $found -eq 0 ]]; then
                error "番号 ${num} は削除可能なエントリーではありません。"; valid=0; break
            fi
            to_delete+=("$num")
        done
        [[ $valid -eq 1 ]] && break
    done

    echo ""
    echo -e "  ${BOLD}${RED}以下のエントリーを削除します:${RESET}"
    for num in "${to_delete[@]}"; do
        echo -e "    ${RED}・${ENTRY_NAMES[$num]}${RESET}"
    done
    echo ""
    confirm "本当に削除しますか?" || { info "キャンセルしました。"; return; }

    local bak; bak=$(backup_file "$CUSTOM_FILE")
    success "バックアップ: ${bak}"

    local stale_baks=()
    while IFS= read -r -d '' f; do
        stale_baks+=("$f")
    done < <(find /etc/grub.d/ -maxdepth 1 -name '40_custom.bak.*' -print0 2>/dev/null)
    if [[ ${#stale_baks[@]} -gt 0 ]]; then
        warn "/etc/grub.d/ に古いバックアップが残っています(エントリー復活の原因):"
        for f in "${stale_baks[@]}"; do warn "  ${f}"; done
        echo ""
        if confirm "古いバックアップを /etc/grub.d/ から削除しますか?"; then
            for f in "${stale_baks[@]}"; do rm -f "$f" && success "削除: $f"; done
        fi
        echo ""
    fi

    declare -A name_delete_occurrences=()
    for num in "${to_delete[@]}"; do
        local tname="${ENTRY_NAMES[$num]}"
        local occ=0
        for i in "${!ENTRY_NAMES[@]}"; do
            [[ "${ENTRY_SOURCES[$i]}" != "custom" ]] && continue
            [[ "${ENTRY_CLASSES[$i]}" == "submenu_header" ]] && continue
            [[ "${ENTRY_NAMES[$i]}" == "$tname" ]] && ((occ++)) || true
            [[ "$i" -eq "$num" ]] && break
        done
        name_delete_occurrences["$tname"]+="${occ} "
    done

    local tmp; tmp=$(mktemp)
    cp "$CUSTOM_FILE" "$tmp"

    for tname in "${!name_delete_occurrences[@]}"; do
        local sorted_occs
        sorted_occs=$(echo "${name_delete_occurrences[$tname]}" | tr ' ' '\n' | grep -v '^$' | sort -rn)
        for occ in $sorted_occs; do
            awk -v name="$tname" -v target_occ="$occ" '
            BEGIN { skip=0; depth=0; match_count=0 }
            {
                if (!skip && /menuentry/ && index($0, name) > 0) {
                    match_count++
                    if (match_count == target_occ) { skip=1; depth=0 }
                }
                if (skip) {
                    n = split($0, chars, "")
                    for (i=1; i<=n; i++) {
                        if (chars[i] == "{") depth++
                        else if (chars[i] == "}") {
                            depth--
                            if (depth <= 0) { skip=0; break }
                        }
                    }
                    next
                }
                print
            }' "$tmp" > "${tmp}.new"
            mv "${tmp}.new" "$tmp"
            success "削除: ${tname}(${occ}番目)"
        done
    done

    cat "$tmp" > "$CUSTOM_FILE"
    rm -f "$tmp"

    _run_update_grub
}

# ============================================================
#  update-grub を実行してエントリー一覧を再表示
# ============================================================
_run_update_grub() {
    echo ""
    info "GRUB を更新中..."
    if update-grub 2>&1; then
        success "update-grub 完了"
        local grub_cfg; grub_cfg=$(find_grub_cfg)
        if [[ -n "$grub_cfg" ]]; then
            echo ""
            info "エントリー一覧を更新しました:"
            parse_grub_cfg "$grub_cfg"
            print_entries
        fi
    else
        warn "update-grub でエラーが発生しました。手動で確認してください: sudo update-grub"
    fi
}

# ============================================================
#  操作メニュー
# ============================================================
show_action_menu() {
    hr
    echo -e "${BOLD}■ 操作を選択してください${RESET}"
    hr_thin
    echo ""
    echo -e "  ${BOLD}[1]${RESET}  ISO ループブートエントリーを追加"
    echo -e "  ${BOLD}[2]${RESET}  不要エントリーを削除  ${CYAN}(40_custom 由来のみ)${RESET}"
    echo -e "  ${BOLD}[3]${RESET}  何もせず終了"
    echo ""

    local choice
    while true; do
        echo -en "${BOLD}選択${RESET} [1/2/3]: "
        read -r choice
        case "$choice" in
            1) action_add_iso; break ;;
            2) action_delete; break ;;
            3) echo ""; info "変更なしで終了します。"; exit 0 ;;
            *) error "1、2、または 3 を入力してください。" ;;
        esac
    done
}

# ============================================================
#  メイン
# ============================================================
main() {
    clear
    hr
    echo -e "${BOLD}  isoboot-grub.sh — ISO ループブート GRUB エントリー管理ツール${RESET}"
    hr
    echo -e "  実行日時: $(date '+%Y-%m-%d %H:%M:%S')"
    echo ""

    check_root
    check_deps

    local grub_cfg; grub_cfg=$(find_grub_cfg)
    [[ -n "$grub_cfg" ]] || die "grub.cfg が見つかりませんでした。"
    info "設定ファイル: ${BOLD}${grub_cfg}${RESET}"

    parse_grub_cfg "$grub_cfg"
    print_current_settings

    hr
    echo -e "${BOLD}■ GRUB エントリー一覧${RESET}"
    hr_thin
    print_entries

    if [[ $(detect_firmware) == "uefi" ]] && command -v efibootmgr &>/dev/null; then
        hr
        echo -e "${BOLD}■ EFI ブートエントリー(参考)${RESET}"
        hr_thin
        echo ""
        efibootmgr 2>/dev/null | while IFS= read -r line; do
            [[ "$line" =~ ^\* ]] \
                && echo -e "  ${GREEN}${line}${RESET}  ${CYAN}← アクティブ${RESET}" \
                || echo -e "  ${line}"
        done
        echo ""
    fi

    show_action_menu

    echo ""
    hr
    success "完了しました。"
    hr
    echo ""
}

main "$@"

起動メニューを表示させるためのパッチ

先述したように、下記のような起動メニューを表示させるにはパッチを当てる必要があります。

ISOイメージのあるパーティションで下記を実行します。

nano antix-loopboot-patch.sh
sudo bash antix-loopboot-patch.sh
#!/usr/bin/env bash
# ============================================================
#  antix-loopboot-patch.sh
#    — antiX ISO を GRUB からループブートした際に、実CD/実機起動と
#      同じメニュー(configfile方式)を使うと発生する
#        "Fatal Error: Could not find file antiX/linuxfs"
#      を解消するためのパッチスクリプト。
#
#  背景:
#    isoboot-grub.sh で「実CD起動と同じメニューを表示」を選ぶと、
#    ISO内の起動メニュー設定 (grub.cfg / loopback.cfg) をそのまま
#    configfile で読み込みます。この設定ファイルがループブートに
#    対応していない場合、antiX の live-init が usb,cd デバイスしか
#    探索せず、ハードディスク上のISOを見つけられずに Fatal Error に
#    なります。
#
#  対応方式(自動判定、2段階):
#
#    方式A: iso_path 変数方式
#      antiX/MX Linux系の多くは、以下のような専用テンプレートを
#      同梱しています:
#          export iso_path
#          prefix=($root)/boot/grub
#          source $prefix/grub.cfg
#      これは「呼び出し元が iso_path 変数をセットしてから
#      configfile すること」を前提にした設計で、本体の grub.cfg が
#      $iso_path の有無を見てループブート用の起動コードを自動的に
#      付与してくれます。この場合、cfgファイル自体の書き換えは
#      一切不要で、40_custom 側の configfile 呼び出し直前に
#          set iso_path="<ISOの相対パス>"
#          export iso_path
#      を追加するだけで解決します。
#
#    方式B: テキストパッチ方式(フォールバック)
#      iso_path 変数に対応していないメニュー設定の場合、
#      linux/linux16/linuxefi 行に直接 fromiso=/buuid=/persist と
#      いった起動コードを追記したコピーを作成し、そちらを
#      configfile で読み込むように変更します。
#
#  このスクリプトの処理:
#    1. 対象のパーティション/ISOを選択
#    2. ISO内のメニュー設定ファイルを検査し、方式A/Bを自動判定
#    3. 40_custom 内に、この ISO を configfile 方式で読み込んでいる
#       既存エントリーがあれば、それを書き換える
#       (見つからない場合は、そのまま使えるエントリー例を表示)
#    4. update-grub を実行
#
#  既存の isoboot-grub.sh とは独立して動作します。
#  「実CD起動と同じメニュー」で登録した状態で今回のエラーが出た
#  場合にのみ、このスクリプトを実行してください。
# ============================================================

set -euo pipefail

CUSTOM_FILE="${CUSTOM_FILE:-/etc/grub.d/40_custom}"
BACKUP_DIR="/root/grub-backups"
PATCHED_CFG_DIR_NAME="isoboot-patched-cfg"
DEBUG_CFG_PATH="/root/isoboot-debug-loopback.cfg"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die()     { error "$*"; exit 1; }
hr()      { echo -e "${CYAN}$(printf '=%.0s' {1..64})${RESET}"; }
hr_thin() { echo -e "${CYAN}$(printf -- '-%.0s' {1..64})${RESET}"; }

confirm() {
    local ans
    while true; do
        echo -en "${YELLOW}[確認]${RESET} $1 [y/N]: "
        read -r ans
        case "$ans" in
            [yY]*) return 0 ;;
            [nN]*|"") return 1 ;;
            *) echo "  y または n を入力してください。" ;;
        esac
    done
}

check_root() {
    [[ $EUID -eq 0 ]] || die "sudo で実行してください。\n  例: sudo bash $0"
}

check_deps() {
    local missing=()
    for cmd in lsblk blkid mount umount mountpoint find sed awk update-grub; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "コマンドが見つかりません: ${missing[*]}"
}

backup_file() {
    local src="$1" base
    base=$(basename "$src")
    mkdir -p "$BACKUP_DIR"
    local dst="${BACKUP_DIR}/${base}.bak.$(date +%Y%m%d_%H%M%S)"
    cp "$src" "$dst"
    echo "$dst"
}

detect_partition_table() {
    local dev="$1"
    local disk
    disk=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -1)
    [[ -z "$disk" ]] && echo "unknown" && return
    local pttype
    pttype=$(blkid -p -o value -s PTTYPE "/dev/${disk}" 2>/dev/null || true)
    case "$pttype" in
        gpt)   echo "gpt" ;;
        dos)   echo "msdos" ;;
        *)     echo "unknown" ;;
    esac
}

fstype_to_grub_insmod() {
    local fstype="$1" part_type="$2" ins_fs ins_part
    case "$fstype" in
        ext2|ext3|ext4)        ins_fs="ext2" ;;
        btrfs)                 ins_fs="btrfs" ;;
        xfs)                   ins_fs="xfs" ;;
        vfat|fat16|fat32|fat)  ins_fs="fat" ;;
        ntfs|ntfs-3g)          ins_fs="ntfs" ;;
        exfat)                 ins_fs="exfat" ;;
        *)                     ins_fs="ext2" ;;
    esac
    case "$part_type" in
        gpt)    ins_part="part_gpt" ;;
        msdos)  ins_part="part_msdos" ;;
        *)      ins_part="part_gpt part_msdos" ;;
    esac
    echo "${ins_fs}:${ins_part}"
}

insmod_block() {
    local insmod_str; insmod_str=$(fstype_to_grub_insmod "$PART_FSTYPE" "$PART_PTTYPE")
    local ins_fs="${insmod_str%%:*}" ins_part="${insmod_str#*:}"
    local lines="" mod
    for mod in $ins_part; do lines+="    insmod ${mod}"$'\n'; done
    lines+="    insmod loopback"$'\n'
    lines+="    insmod iso9660"$'\n'
    lines+="    insmod ${ins_fs}"$'\n'
    lines+="    insmod regexp"
    echo "$lines"
}

# ============================================================
#  ISO内のメニュー設定ファイルのパスを探す
# ============================================================
find_iso_cfg_path() {
    local tmp="$1" c
    for c in "/boot/grub/loopback.cfg" "/isolinux/loopback.cfg" \
             "/EFI/BOOT/loopback.cfg" "/boot/grub/grub.cfg"; do
        [[ -f "${tmp}${c}" ]] && { echo "$c"; return; }
    done
    echo ""
}

# ============================================================
#  終了時クリーンアップ(自前でマウントしたパーティションのみ解除)
# ============================================================
cleanup_on_exit() {
    if [[ "${PART_MP_OWNED:-0}" -eq 1 && -n "${PART_MP:-}" ]] && mountpoint -q "$PART_MP" 2>/dev/null; then
        umount "$PART_MP" 2>/dev/null || true
        rmdir "$PART_MP" 2>/dev/null || true
    fi
}

# ============================================================
#  対象パーティション / ISO を選択させる
#  (パーティションは処理完了までマウントを維持する。
#    既に別の場所にマウント済みならそれを再利用する)
# ============================================================
select_partition_and_iso() {
    info "パーティション一覧:"
    echo ""
    mapfile -t part_list < <(
        lsblk -lno NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS \
        | grep -v '^loop' \
        | awk 'NF && $1 ~ /[0-9]$/ { print $0 }'
    )
    [[ ${#part_list[@]} -eq 0 ]] && die "パーティションが見つかりませんでした。"

    printf "  ${BOLD}%-4s  %s${RESET}\n" "No." "NAME  SIZE  FSTYPE  LABEL  MOUNTPOINTS"
    hr_thin
    for i in "${!part_list[@]}"; do
        printf "  ${CYAN}[%2d]${RESET}  %s\n" "$((i+1))" "${part_list[$i]}"
    done
    echo ""

    local sel pname
    while true; do
        echo -en "${BOLD}番号を入力してください${RESET} [1-${#part_list[@]}]: "
        read -r sel
        if ! [[ "$sel" =~ ^[0-9]+$ ]] || (( sel < 1 || sel > ${#part_list[@]} )); then
            error "1〜${#part_list[@]} の番号を入力してください。"; continue
        fi
        pname=$(echo "${part_list[$((sel-1))]}" | awk '{print $1}')
        PART_DEV="/dev/${pname}"
        [[ -b "$PART_DEV" ]] || { error "${PART_DEV} はブロックデバイスではありません。"; continue; }
        break
    done

    PART_UUID=$(blkid -s UUID -o value "$PART_DEV")
    [[ -n "$PART_UUID" ]] || die "UUID を取得できませんでした: ${PART_DEV}"
    PART_FSTYPE=$(lsblk -no FSTYPE "$PART_DEV" 2>/dev/null | head -1 || true)
    PART_PTTYPE=$(detect_partition_table "$PART_DEV")
    success "パーティション: ${PART_DEV}  (UUID: ${PART_UUID})"

    local existing_mp
    existing_mp=$(lsblk -no MOUNTPOINT "$PART_DEV" 2>/dev/null | grep -v '^$' | head -1 || true)

    if [[ -n "$existing_mp" ]]; then
        PART_MP="$existing_mp"
        PART_MP_OWNED=0
        info "既にマウント済みのため、そのまま使用します: ${PART_MP}"
    else
        PART_MP="/mnt/_antixpatch_part_$$"
        mkdir -p "$PART_MP"
        mount "$PART_DEV" "$PART_MP"
        PART_MP_OWNED=1
    fi

    mapfile -t iso_candidates < <(find "$PART_MP" -maxdepth 2 -name "*.iso" 2>/dev/null)
    [[ ${#iso_candidates[@]} -eq 0 ]] && die "ISO ファイルが見つかりませんでした。"

    echo ""
    info "見つかった ISO ファイル:"
    for i in "${!iso_candidates[@]}"; do
        printf "  ${CYAN}[%d]${RESET}  %s\n" "$((i+1))" "${iso_candidates[$i]##"$PART_MP"}"
    done
    echo ""

    local isel
    while true; do
        echo -en "${BOLD}パッチ対象の ISO 番号を入力${RESET} [1-${#iso_candidates[@]}]: "
        read -r isel
        if ! [[ "$isel" =~ ^[0-9]+$ ]] || (( isel < 1 || isel > ${#iso_candidates[@]} )); then
            error "1〜${#iso_candidates[@]} の番号を入力してください。"; continue
        fi
        break
    done

    ISO_FULL="${iso_candidates[$((isel-1))]}"
    ISO_REL="${ISO_FULL##"$PART_MP"}"
    ISO_NAME_NOEXT=$(basename "$ISO_REL" .iso)
    success "対象 ISO: ${ISO_REL}"
}

# ============================================================
#  40_custom 内で、この ISO を configfile 方式(実CD同等メニュー)で
#  登録しているエントリーを探す
# ============================================================
has_matching_custom_entry() {
    [[ -f "$CUSTOM_FILE" ]] \
        && grep -qF "set isofile=\"${ISO_REL}\"" "$CUSTOM_FILE" \
        && grep -qE "configfile[[:space:]]+\(loop\)" "$CUSTOM_FILE"
}

# ============================================================
#  方式A: iso_path 変数方式
#    configfile 呼び出し行の直前に
#      set iso_path="<ISO_REL>"
#      export iso_path
#    を挿入する。cfg 自体は書き換えない。
# ============================================================
patch_via_iso_path() {
    local cfg_rel="$1"

    success "iso_path 変数方式のループブート対応 cfg を検出しました。"
    info "cfg 自体の書き換えは不要です。configfile 呼び出し前に iso_path をセットします。"
    echo ""

    if has_matching_custom_entry; then
        info "既存の 40_custom エントリーを検出しました。iso_path の設定を追加します。"
        local bak; bak=$(backup_file "$CUSTOM_FILE")
        success "バックアップ: ${bak}"

        awk -v isofile="${ISO_REL}" -v isopath="${ISO_REL}" '
            BEGIN { in_block = 0; already = 0 }
            {
                if ($0 ~ /^menuentry/) { in_block = 0; already = 0; buf = $0"\n"; capturing = 1 }
                else if (capturing) { buf = buf $0 "\n" }

                if (capturing && index($0, "set isofile=\"" isofile "\"") > 0) { in_block = 1 }
                if (capturing && index($0, "set iso_path=") > 0) { already = 1 }

                if (capturing && $0 ~ /^\}[[:space:]]*$/) {
                    if (in_block == 1 && already == 0) {
                        ins = "    set iso_path=\"" isopath "\"\n    export iso_path\n    "
                        gsub(/configfile[[:space:]]+\(loop\)[^\n]*/, ins "&", buf)
                    }
                    printf "%s", buf
                    capturing = 0; in_block = 0; already = 0
                    next
                }
                if (!capturing) { print }
            }
            END { if (capturing) printf "%s", buf }
        ' "$CUSTOM_FILE" > "${CUSTOM_FILE}.tmp"

        if grep -qF 'set iso_path=' "${CUSTOM_FILE}.tmp"; then
            chmod --reference="$CUSTOM_FILE" "${CUSTOM_FILE}.tmp" 2>/dev/null || chmod 755 "${CUSTOM_FILE}.tmp"
            mv "${CUSTOM_FILE}.tmp" "$CUSTOM_FILE"
            success "40_custom を更新しました(iso_path を追加)。"
            run_update_grub
        else
            rm -f "${CUSTOM_FILE}.tmp"
            warn "自動書き換えに失敗しました。下記のエントリー例を手動で反映してください。"
            print_entry_example_isopath "$cfg_rel"
        fi
    else
        warn "この ISO を configfile 方式で登録している既存エントリーが見つかりませんでした。"
        info "以下のエントリー例を isoboot-grub.sh 側の該当エントリーと差し替えるか、"
        info "手動で ${CUSTOM_FILE} に追加してください。"
        print_entry_example_isopath "$cfg_rel"
    fi
}

# ============================================================
#  方式B: テキストパッチ方式(フォールバック)
#    linux/linux16/linuxefi 行に fromiso=/buuid=/persist を追記した
#    コピーを ISO と同じパーティション上に保存し、configfile の
#    参照先をそちらに書き換える。
# ============================================================
patch_via_text_patch() {
    local cfg_rel="$1" work_cfg="$2"

    local before_count after_count
    before_count=$(grep -Eic '^[[:space:]]*linux(16|efi)?[[:space:]]+.*vmlinuz' "$work_cfg" || true)

    if [[ "$before_count" -eq 0 ]]; then
        cp "$work_cfg" "$DEBUG_CFG_PATH"
        rm -f "$work_cfg"
        error "パッチ対象となる起動行 (linux/linux16/linuxefi ... vmlinuz) が見つかりませんでした。"
        echo ""
        info "検出したメニュー設定ファイルの内容をそのまま表示します(この内容を教えてください):"
        echo ""
        hr_thin
        cat -A "$DEBUG_CFG_PATH" | sed -n '1,80p'
        hr_thin
        echo ""
        info "全文は ${DEBUG_CFG_PATH} に保存しました。"
        die "起動行の自動検出に失敗しました。上記の内容を確認のうえ調整します。"
    fi

    local extra_codes="fromiso=${ISO_REL} buuid=${PART_UUID} nocheckfs persist"

    sed -i -E \
        "/^[[:space:]]*linux(16|efi)?[[:space:]]+.*vmlinuz/I { /fromiso=|findiso=/! s#\$# ${extra_codes}# }" \
        "$work_cfg"

    after_count=$(grep -Fc "fromiso=${ISO_REL}" "$work_cfg" || true)
    if [[ "$after_count" -eq 0 ]]; then
        rm -f "$work_cfg"
        die "起動コードの追記に失敗しました。手動確認が必要です。"
    fi
    success "起動行 ${before_count} 件に起動コードを追記しました:"
    echo -e "  ${CYAN}${extra_codes}${RESET}"

    local dest_dir="${PART_MP}/${PATCHED_CFG_DIR_NAME}"
    mkdir -p "$dest_dir"
    local dest_cfg="${dest_dir}/${ISO_NAME_NOEXT}.cfg"
    cp "$work_cfg" "$dest_cfg"
    rm -f "$work_cfg"

    local dest_cfg_rel="/${PATCHED_CFG_DIR_NAME}/${ISO_NAME_NOEXT}.cfg"
    success "パッチ済みメニュー設定を保存しました: ${PART_DEV}${dest_cfg_rel}"
    echo ""

    if has_matching_custom_entry; then
        info "既存の 40_custom エントリーを検出しました。configfile の参照先を書き換えます。"
        local bak; bak=$(backup_file "$CUSTOM_FILE")
        success "バックアップ: ${bak}"

        awk -v isofile="${ISO_REL}" -v newref="(\$isodev)${dest_cfg_rel}" '
            BEGIN { in_block = 0 }
            {
                if ($0 ~ /^menuentry/) { in_block = 0; buf = $0"\n"; capturing = 1 }
                else if (capturing) { buf = buf $0 "\n" }

                if (capturing && index($0, "set isofile=\"" isofile "\"") > 0) { in_block = 1 }

                if (capturing && $0 ~ /^\}[[:space:]]*$/) {
                    if (in_block == 1) {
                        gsub(/configfile[[:space:]]+\(loop\)[^\n]*/, "configfile " newref, buf)
                    }
                    printf "%s", buf
                    capturing = 0; in_block = 0
                    next
                }
                if (!capturing) { print }
            }
            END { if (capturing) printf "%s", buf }
        ' "$CUSTOM_FILE" > "${CUSTOM_FILE}.tmp"

        if grep -qF "${dest_cfg_rel}" "${CUSTOM_FILE}.tmp"; then
            chmod --reference="$CUSTOM_FILE" "${CUSTOM_FILE}.tmp" 2>/dev/null || chmod 755 "${CUSTOM_FILE}.tmp"
            mv "${CUSTOM_FILE}.tmp" "$CUSTOM_FILE"
            success "40_custom を更新しました(パッチ済みメニューを参照するように変更)。"
            run_update_grub
        else
            rm -f "${CUSTOM_FILE}.tmp"
            warn "自動書き換えに失敗しました。下記のエントリー例を手動で反映してください。"
            print_entry_example_textpatch "$dest_cfg_rel"
        fi
    else
        warn "この ISO を configfile 方式で登録している既存エントリーが見つかりませんでした。"
        info "以下のエントリー例を isoboot-grub.sh 側の該当エントリーと差し替えるか、"
        info "手動で ${CUSTOM_FILE} に追加してください。"
        print_entry_example_textpatch "$dest_cfg_rel"
    fi
}

run_update_grub() {
    if [[ -f "$CUSTOM_FILE" && ! -x "$CUSTOM_FILE" ]]; then
        warn "${CUSTOM_FILE} に実行権限が無かったため付与します(無いと update-grub に無視されます)。"
        chmod 755 "$CUSTOM_FILE"
    fi
    info "GRUB を更新中..."
    if update-grub; then
        success "update-grub 完了"
    else
        warn "update-grub でエラーが発生しました。手動で確認してください: sudo update-grub"
    fi
}

print_entry_example_isopath() {
    local cfg_rel="$1"
    echo ""
    echo -e "${CYAN}--------------------------------------------------------------${RESET}"
    cat <<EOF
menuentry "${ISO_NAME_NOEXT} (ISO Loop Boot - 起動メニュー表示)" {
$(insmod_block)
    search --no-floppy --fs-uuid --set=isodev ${PART_UUID}
    set isofile="${ISO_REL}"
    loopback loop (\$isodev)\$isofile
    set root=(loop)
    set iso_path="${ISO_REL}"
    export iso_path
    configfile (loop)${cfg_rel}
}
EOF
    echo -e "${CYAN}--------------------------------------------------------------${RESET}"
    echo ""
}

print_entry_example_textpatch() {
    local dest_cfg_rel="$1"
    echo ""
    echo -e "${CYAN}--------------------------------------------------------------${RESET}"
    cat <<EOF
menuentry "${ISO_NAME_NOEXT} (ISO Loop Boot - 起動メニュー表示 / パッチ済み)" {
$(insmod_block)
    search --no-floppy --fs-uuid --set=isodev ${PART_UUID}
    set isofile="${ISO_REL}"
    loopback loop (\$isodev)\$isofile
    set root=(loop)
    configfile (\$isodev)${dest_cfg_rel}
}
EOF
    echo -e "${CYAN}--------------------------------------------------------------${RESET}"
    echo ""
}

# ============================================================
#  メイン処理
# ============================================================
main() {
    trap cleanup_on_exit EXIT

    hr
    echo -e "${BOLD}  antix-loopboot-patch.sh — antiX ループブート起動コード修正パッチ${RESET}"
    hr
    echo ""
    warn "対象は antiX(および同系統の live-init を使うディストリ)です。"
    warn "「実CD起動と同じメニュー」方式(configfile)で登録し、"
    warn "  Fatal Error: Could not find file antiX/linuxfs が出た場合に使用してください。"
    echo ""

    check_root
    check_deps

    select_partition_and_iso

    local mp="/mnt/_antixpatch_iso_$$"
    mkdir -p "$mp"
    mount -o loop,ro "$ISO_FULL" "$mp"

    local cfg_rel; cfg_rel=$(find_iso_cfg_path "$mp")
    if [[ -z "$cfg_rel" ]]; then
        umount "$mp"; rmdir "$mp"
        die "ISO内にメニュー設定ファイル(grub.cfg/loopback.cfg)が見つかりませんでした。"
    fi
    success "メニュー設定ファイル: ${cfg_rel}"

    local src_cfg="${mp}${cfg_rel}"
    local work_cfg; work_cfg=$(mktemp)
    cp "$src_cfg" "$work_cfg"

    umount "$mp"; rmdir "$mp"

    echo ""
    if grep -qw 'iso_path' "$work_cfg"; then
        rm -f "$work_cfg"
        patch_via_iso_path "$cfg_rel"
    else
        warn "iso_path 変数方式ではないようです。従来のテキストパッチ方式を試みます。"
        patch_via_text_patch "$cfg_rel" "$work_cfg"
    fi

    echo ""
    hr
    success "完了しました。"
    hr
}

main "$@"

後からISOパーティションを作った場合

nano mount-setup.sh
sudo bash mount-setup.sh
#!/bin/bash
# ============================================================
# 対話式 fstab マウント登録スクリプト
#   - ディスク一覧を表示して番号で選択
#   - 永続マウント(fstab 登録)か一時マウントかを選択
#   - /iso ディレクトリがなければ作成・権限設定
#   - UUID を /etc/fstab に追記してマウント(永続時のみ)
# ============================================================
set -euo pipefail

MOUNT_POINT="/iso"
FSTAB="/etc/fstab"

info()    { echo -e "\033[1;32m[INFO]\033[0m  $*"; }
warn()    { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
error()   { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
confirm() {
  local msg="$1"
  local ans
  read -rp "$(echo -e "\033[1;33m[確認]\033[0m $msg [y/N]: ")" ans
  [[ "${ans,,}" == "y" ]]
}

# root 確認
[[ "$EUID" -ne 0 ]] && error "このスクリプトは sudo で実行してください。"

# ============================================================
# 1. パーティション一覧を表示
# ============================================================
echo ""
echo "======================================================"
echo " 現在のディスク・パーティション一覧"
echo "======================================================"
echo ""
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT,UUID | grep -v "^loop"
echo ""

# パーティションのみ抽出(ディスク本体除く)
mapfile -t PARTS < <(lsblk -lnpo NAME,SIZE,FSTYPE,MOUNTPOINT \
  | awk '$3!="" && $1!~/^(loop|sr)/' \
  | grep -v "^$")

if [[ ${#PARTS[@]} -eq 0 ]]; then
  error "マウント可能なパーティションが見つかりませんでした。"
fi

echo "------------------------------------------------------"
printf " %-3s  %-15s %-8s %-10s %s\n" "No." "デバイス" "サイズ" "FS種別" "現在のマウント先"
echo "------------------------------------------------------"
for i in "${!PARTS[@]}"; do
  read -r dev size fstype mount <<< "${PARTS[$i]}"
  mount="${mount:-(未マウント)}"
  printf " %-3s  %-15s %-8s %-10s %s\n" "$((i+1))" "$dev" "$size" "$fstype" "$mount"
done
echo "------------------------------------------------------"
echo ""

# ============================================================
# 2. パーティション選択
# ============================================================
while true; do
  read -rp "$(echo -e "\033[1;36m[入力]\033[0m マウントするパーティションの番号を入力してください: ")" SEL
  if [[ "$SEL" =~ ^[0-9]+$ ]] && (( SEL >= 1 && SEL <= ${#PARTS[@]} )); then
    break
  fi
  warn "1〜${#PARTS[@]} の番号を入力してください。"
done

read -r SELECTED_DEV _ SELECTED_FS CURRENT_MOUNT <<< "${PARTS[$((SEL-1))]}"
CURRENT_MOUNT="${CURRENT_MOUNT:-(未マウント)}"

echo ""
info "選択: $SELECTED_DEV  FS: $SELECTED_FS  現在: $CURRENT_MOUNT"

# すでに /iso にマウント済みなら確認
if [[ "$CURRENT_MOUNT" == "$MOUNT_POINT" ]]; then
  warn "$SELECTED_DEV はすでに $MOUNT_POINT にマウントされています。"
  confirm "再登録を続けますか?" || exit 0
fi

# 別の場所にマウント済みなら警告
if [[ "$CURRENT_MOUNT" != "(未マウント)" && "$CURRENT_MOUNT" != "$MOUNT_POINT" ]]; then
  warn "$SELECTED_DEV は現在 $CURRENT_MOUNT にマウントされています。"
  confirm "続けますか?(現在のマウントは変更しません)" || exit 0
fi

# ============================================================
# 3. マウント方式の選択
# ============================================================
echo ""
echo "======================================================"
echo " マウント方式を選択してください"
echo "======================================================"
echo ""
echo "  1) 永続マウント  - fstab に登録し、再起動後も自動マウント"
echo "  2) 一時マウント  - 今回のみマウント(再起動で解除)"
echo ""

MOUNT_MODE=""
while true; do
  read -rp "$(echo -e "\033[1;36m[入力]\033[0m 番号を入力してください [1/2]: ")" MODE_SEL
  case "$MODE_SEL" in
    1) MOUNT_MODE="persistent"; break ;;
    2) MOUNT_MODE="temporary";  break ;;
    *) warn "1 または 2 を入力してください。" ;;
  esac
done

echo ""
if [[ "$MOUNT_MODE" == "persistent" ]]; then
  info "モード: 永続マウント(fstab 登録あり)"
else
  info "モード: 一時マウント(fstab 登録なし)"
fi

# ============================================================
# 4. UUID 取得
# ============================================================
UUID=$(blkid -s UUID -o value "$SELECTED_DEV" 2>/dev/null || true)
if [[ -z "$UUID" ]]; then
  error "$SELECTED_DEV の UUID を取得できませんでした。FS が認識されているか確認してください。"
fi
info "UUID: $UUID"

# FS タイプの決定(空なら auto)
FS_TYPE="${SELECTED_FS:-auto}"
# ntfs の場合は ntfs-3g に補正
[[ "$FS_TYPE" == "ntfs" ]] && FS_TYPE="ntfs-3g"

# ============================================================
# 5. /iso ディレクトリの準備
# ============================================================
if [[ ! -d "$MOUNT_POINT" ]]; then
  info "$MOUNT_POINT が存在しないため作成します..."
  mkdir -p "$MOUNT_POINT"
  # 所有者を実行前のユーザーに設定
  REAL_USER="${SUDO_USER:-root}"
  chown "${REAL_USER}:${REAL_USER}" "$MOUNT_POINT"
  chmod 775 "$MOUNT_POINT"
  info "作成完了: $MOUNT_POINT(所有者: ${REAL_USER}, パーミッション: 775)"
else
  info "$MOUNT_POINT はすでに存在します。"
fi

# ============================================================
# 6. fstab の重複チェックと追記(永続マウント時のみ)
# ============================================================
if [[ "$MOUNT_MODE" == "persistent" ]]; then

  # 同じ UUID がすでに fstab にないか確認
  if grep -q "$UUID" "$FSTAB"; then
    warn "UUID=$UUID はすでに $FSTAB に登録されています。"
    grep "$UUID" "$FSTAB"
    confirm "上書き(既存行をコメントアウトして追記)しますか?" || exit 0
    # 既存行をコメントアウト
    sed -i "s|.*${UUID}.*|# &  # mount-setup.sh により無効化 $(date '+%Y-%m-%d')|" "$FSTAB"
  fi

  # fstab に /iso のマウント先がすでにあるか確認
  if grep -qP "^\s*[^#].*\s${MOUNT_POINT}\s" "$FSTAB"; then
    warn "$MOUNT_POINT のエントリがすでに $FSTAB にあります。"
    grep -P "^\s*[^#].*\s${MOUNT_POINT}\s" "$FSTAB"
    confirm "既存行をコメントアウトして上書きしますか?" || exit 0
    sed -i "s|^\([^#].*\s${MOUNT_POINT}\s.*\)|# \1  # mount-setup.sh により無効化 $(date '+%Y-%m-%d')|" "$FSTAB"
  fi

  FSTAB_LINE="UUID=${UUID}  ${MOUNT_POINT}  ${FS_TYPE}  defaults  0  2"

  echo ""
  info "以下の行を $FSTAB に追記します:"
  echo ""
  echo "  $FSTAB_LINE"
  echo ""
  confirm "追記しますか?" || exit 0

  # バックアップ
  cp "$FSTAB" "${FSTAB}.bak.$(date '+%Y%m%d%H%M%S')"
  info "バックアップ: ${FSTAB}.bak.* を作成しました。"

  echo "" >> "$FSTAB"
  echo "# /iso - mount-setup.sh により追加 $(date '+%Y-%m-%d %H:%M:%S')" >> "$FSTAB"
  echo "$FSTAB_LINE" >> "$FSTAB"
  info "fstab への追記が完了しました。"

fi

# ============================================================
# 7. マウント実行
# ============================================================
echo ""
if [[ "$MOUNT_MODE" == "persistent" ]]; then
  info "mount -a でマウントを実行します..."
  if mount -a 2>&1; then
    info "マウント成功!"
  else
    warn "mount -a でエラーが発生しました。fstab の内容を確認してください:"
    tail -5 "$FSTAB"
    exit 1
  fi
else
  info "mount -t ${FS_TYPE} ${SELECTED_DEV} ${MOUNT_POINT} でマウントを実行します..."
  if mount -t "${FS_TYPE}" "${SELECTED_DEV}" "${MOUNT_POINT}" 2>&1; then
    info "マウント成功!"
  else
    warn "マウントに失敗しました。デバイスと FS タイプを確認してください。"
    exit 1
  fi
fi

# ============================================================
# 8. 確認
# ============================================================
echo ""
echo "======================================================"
if [[ "$MOUNT_MODE" == "persistent" ]]; then
  echo " 完了!(永続マウント)マウント状態:"
else
  echo " 完了!(一時マウント・再起動で解除)マウント状態:"
fi
echo "======================================================"
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT | grep -E "NAME|$(basename "$SELECTED_DEV")"
echo ""
df -h "$MOUNT_POINT"
echo "======================================================"
タイトルとURLをコピーしました