#!/bin/bash

#==============================================================================
# live-kernel-updater
# A robust program to change kernels on antiX/MX live systems
#
# (C) 2016 -- 2017 Paul Banham <antiX@operamail.com>
# License: GPLv3 or later
#==============================================================================

#     VERSION="2.30.04"
#VERSION_DATE="Wed 23 Oct 2019 09:39:52 PM MDT"
#      VERSION="2.30.04-2302"
# VERSION_DATE="Sat, 25 Feb 2023 12:45:10 -0500"

      VERSION="26.05.1"
 VERSION_DATE="Mon, 18 May 2026 19:42:08 -0400"

        ME="live-kernel-updater"
    MY_DIR=$(dirname "$(readlink -f $0)")
MY_LIB_DIR=$(readlink -f "$MY_DIR/../cli-shell-utils")
   LIB_DIR="/usr/lib/cli-shell-utils"
   if [ -d "/usr/local/lib/cli-shell-utils" ]; then
       LIB_DIR="/usr/local/lib/cli-shell-utils"
   fi

export TEXTDOMAIN="cli-shell-utils"
domain_dir="$MY_DIR/../cli-shell-utils/locale"
test -d "$domain_dir" && export TEXTDOMAINDIR=$domain_dir

#== BEGIN_CONFIG
         MAX_FILES="20"
         #boot mount point
           LIVE_MP="/live/boot-dev"
         if [ -L "/live" ]; then
         	LIVE_MP="/$(readlink /live)/boot-dev"
         fi
          LIVE_DIR="antiX"
          BOOT_DIR="boot"

   VM_VERSION_PROG="vmlinuz-version"
      LINUXFS_NAME="linuxfs"
      VMLINUZ_NAME="vmlinuz"
    VMLINUZ_2_NAME="vmlinuz1"
       INITRD_NAME="initrd.gz"
 TEMPLATE_INITRD_1="/usr/lib/iso-template/template-initrd.gz"
 TEMPLATE_INITRD_2="/usr/lib/iso-template/initrd.gz"

  MIN_LINUXFS_SIZE="100M"

          # for kernel and initrd.gz files
          KOLD_EXT=".kold"
          KBAD_EXT=".kbad"
          IOLD_EXT=".iold"
          IBAD_EXT=".ibad"
           MD5_EXT=".md5"

          # For linuxfs file (right after a remaster)
           NEW_EXT=".new"

         SHELL_LIB="cli-shell-utils.bash"

         LUKS_NAME="live-kernel-updater"
       CRYPT_FNAME="crypt"

      COLOR_SCHEME="high"
   GRAPHICAL_MENUS="true"

# --------------------------------------------------------------------------
# FUTURE: multi-kernel + initrd-mode support for antiX
#
# Two independent axes are planned:
#
# 1) Slot count
#    Currently antiX is hard-limited to 2 kernel slots (vmlinuz + vmlinuz1).
#    MX already supports up to 10 slots (vmlinuz...vmlinuz9).
#    Lifting the antiX limit requires only removing the VMLINUZ_2_NAME guard
#    and widening the both_vm regex -- the slot machinery is already in place.
#
# 2) Initrd sharing mode  (controlled by MULTI_KERNEL below)
#
#    MULTI_KERNEL=false -- shared initrd mode:
#      vmlinuz and vmlinuz1 share one initrd.gz.  The initrd contains modules
#      for both active slots (plus their .kold backups).  Low disk usage;
#      simpler update path.
#
#    MULTI_KERNEL=true -- per-kernel initrd mode:
#      Each slot gets its own initrd (vmlinuzN <-> initrdN.gz).  Each initrd
#      carries only the modules for its own kernel.  Independent updates.
#
#    Migration path (shared -> per-kernel):
#      During transition, the primary initrd.gz may still hold modules for
#      multiple kernels while new slots already have their own initrdN.gz.
#      The 'add' action handles this by unpacking initrd.gz as the source
#      and installing a fresh per-slot initrd, so both models can coexist
#      temporarily on the same live-usb.
#
#    MULTI_KERNEL (set here or via --multi-kernel / --shared-initrd on the command line):
#      (empty)      auto-detect: per-kernel if /etc/mx-version exists, shared otherwise
#      true         force per-kernel initrd mode  (--multi-kernel)
#      false        force shared initrd mode      (--shared-initrd)
#
   MULTI_KERNEL=

# --------------------------------------------------------------------------
#== END_CONFIG

        WORK_DIR="/run/$ME"
            FIFO="/run/$ME/fifo"
    THE_LOG_FILE="/var/log/$ME.log"
    THE_ERR_FILE="/var/log/$ME.error"
        LOG_FILE="/dev/null"
        ERR_FILE="/dev/null"
     CONFIG_FILE="/etc/$ME/$ME.conf"

        ALL_CMDS="rollback all unpack copy-modules copy-programs repack install defrag"
       ALL_FORCE="all,flock,clear,compress,usb,umount"
       ALL_PAUSE="all,fifo,mount,unpack,copy,repack,clean,exit"

        LIB_PATH="$MY_LIB_DIR:$LIB_DIR"
            PATH="$MY_LIB_DIR/bin:$LIB_DIR/bin:$PATH"


#------------------------------------------------------------------------------
# True when using per-kernel initrd mode (each slot has its own initrd).
# Overrideable via MULTI_KERNEL config var or --multi-kernel / --shared-initrd flags.
#------------------------------------------------------------------------------
multi_kernel() {
    case $MULTI_KERNEL in
        true)  return 0 ;;
        false) return 1 ;;
    esac
    test -e /etc/mx-version
}

#------------------------------------------------------------------------------
# Map vmlinuz slot name to its initrd name:
#   vmlinuz  -> initrd.gz
#   vmlinuzN -> initrdN.gz
#------------------------------------------------------------------------------
kernel_to_initrd() {
    local suffix=${1#vmlinuz}
    echo "initrd${suffix}.gz"
}

#------------------------------------------------------------------------------
# Echo the first unoccupied vmlinuz slot name in $1 (live dir).
# Returns 1 if all 10 slots (vmlinuz ... vmlinuz9) are occupied.
#------------------------------------------------------------------------------
find_next_free_slot() {
    local dir=$1
    test -e "$dir/vmlinuz" || { echo "vmlinuz" ; return 0 ; }
    local n
    for n in 1 2 3 4 5 6 7 8 9; do
        test -e "$dir/vmlinuz$n" || { echo "vmlinuz$n" ; return 0 ; }
    done
    return 1
}

#------------------------------------------------------------------------------
# Parse build date from the second field of "file -b" output (after the '#'
# separator).  Handles three formats:
#   DPA1  ISO YYYY-MM-DD (Debian/MX kernels)
#   DPA2  verbose C-locale "Wed May 14 12:34:56 UTC 2026" (antiX self-built)
#   DPA3  compact YYYYMMDD embedded in version tag (xanmod: ~20260511.)
# Echoes YYYY-MM-DD, or nothing if no date is found.
#------------------------------------------------------------------------------
parse_kernel_build_date() {
    local fver=$1
    local DPA1='20[[:digit:]]{2}(-[[:digit:]]{2}){2}'
    local DPA2='(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[[:space:]]+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[[:space:]]+[0-9]{1,2}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}[[:space:]][A-Z]{2,5}[[:space:]]+20[0-9]{2}'
    local kdat
    kdat=$(sed -n -r "s/.*($DPA1|$DPA2).*/\1/p" <<<"$fver")
    if [ -z "$kdat" ]; then
        # DPA3: compact YYYYMMDD with non-digit boundary on each side
        kdat=$(grep -oE '(^|[^[:digit:]])20[2-9][0-9][01][0-9][0-3][0-9]([^[:digit:]]|$)' \
            <<<"$fver" | grep -oE '20[2-9][0-9][01][0-9][0-3][0-9]' | head -1)
    fi
    [ -n "$kdat" ] && kdat=$(LC_ALL=C TZ=UTC date --date="$kdat" +'%Y-%m-%d' 2>/dev/null)
    echo "$kdat"
}

#------------------------------------------------------------------------------
# Extract build date from a kernel binary path via "file -b".
# Echoes YYYY-MM-DD, or nothing if the file is not a Linux kernel or has no
# recognisable date string.
#------------------------------------------------------------------------------
get_kernel_build_date() {
    command -v file >/dev/null 2>&1 || return
    local kernel_path=$1
    local frel fver
    IFS='#' read -r frel fver < <(file -b "$kernel_path" 2>/dev/null)
    [ -n "$frel" ] && [ -z "${frel%%Linux kernel*}" ] || return
    parse_kernel_build_date "$fver"
}

#------------------------------------------------------------------------------
# Replace file-mtime dates in a kernel list with the build date embedded in
# the kernel binary (from "file -b"), when available.  Falls back to the
# original mtime date if "file" is absent or the kernel has no date string.
#------------------------------------------------------------------------------
fix_kernel_dates() {
    local var=$1  dir=$2
    command -v file >/dev/null 2>&1 || return
    local list="${!var}"
    local IFS=$K_IFS
    local out="" version fname date file_date full_fname
    while read -r version fname date; do
        [ ${#version} -eq 0 ] && continue
        full_fname=$fname
        [ "${fname#/}" = "$fname" ] && full_fname="$dir/$fname"
        file_date=$(get_kernel_build_date "$full_fname")
        [ -n "$file_date" ] && date=$file_date
        out="${out}${out:+
}${version}${K_IFS}${fname}${K_IFS}${date}"
    done <<List
${list}
List
    printf -v "$var" '%s' "$out"
}

#------------------------------------------------------------------------------
# Write vmlinuzX.ver sidecar file for GRUB menu display (MX mode only).
# KERNEL_VERSION holds a human-readable version+date string; KERNEL_LABEL
# is "signed" when sbverify detects a Secure Boot signature, else empty.
#------------------------------------------------------------------------------
write_vmlinuz_ver() {
    local kernel_path=$1
    [ -f "$kernel_path" ] || return

    # Split file -b output on '#' to separate the release line from the build line.
    local frel fver
    IFS='#' read -r frel fver < <(file -b "$kernel_path" 2>/dev/null)
    [ -n "$frel" ] && [ -z "${frel%%Linux kernel*}" ] || return   # not a Linux kernel binary

    local KPAT='(([[:digit:]]+[.]){1,3}[[:digit:]]+[[:alnum:].~+-]+)'
    local krel kdat
    krel=$(sed -n -r "s/.*[[:space:]]($KPAT).*/\1/p" <<<"$frel")
    krel=$(sed -r 's/-(amd64|i[36]86|686-pae|686|arm64|armhf|armel)$//' <<<"$krel")

    kdat=$(parse_kernel_build_date "$fver")
    [ -z "$kdat" ] && kdat=$(TZ=UTC date --date=@$(stat -L -c %Y "$kernel_path") +'%Y-%m-%d' 2>/dev/null)

    # Build human-readable version string
    local version="$krel"
    [ -n "$kdat" ] && version+=" - $kdat"

    # Secure Boot signature check
    local label=""
    if command -v sbverify >/dev/null 2>&1; then
        local sbout
        sbout=$(LC_ALL=C.UTF-8 sbverify --list "$kernel_path" 2>&1)
        [ -z "${sbout%%No signature*}" ] || label="signed"
    fi

    printf 'KERNEL_VERSION="%s"\nKERNEL_LABEL="%s"\n' "$version" "$label" \
        > "${kernel_path}.ver"
}

#------------------------------------------------------------------------------
# (Re)write .ver sidecars for all slots in a live_vm list (MX mode only).
#------------------------------------------------------------------------------
sync_vmlinuz_ver() {
    local dir=$1  list=$2
    local IFS=$K_IFS
    local version fname date
    while read -r version fname date; do
        [ -n "$fname" ] && write_vmlinuz_ver "$dir/$fname"
    done <<List
${list}
List
}

#------------------------------------------------------------------------------
# Remove all files belonging to a kernel slot (kernel, initrd, backups, md5).
#------------------------------------------------------------------------------
do_remove_slot() {
    local dir=$1  vmlinuz=$2
    local initrd=$(kernel_to_initrd "$vmlinuz")
    local f ext
    for f in "$vmlinuz" "$initrd"; do
        test -e "$dir/$f" && cmd rm -f "$dir/$f"
        for ext in "$KOLD_EXT" "$KBAD_EXT" "$MD5_EXT" "$IOLD_EXT"; do
            test -e "$dir/$f$ext" && cmd rm -f "$dir/$f$ext"
        done
    done
    cmd rm -f "$dir/$vmlinuz.ver"
}

#------------------------------------------------------------------------------
# Return 0 if any syslinux *.cfg stanza that boots $1 (e.g. vmlinuz1)
# references the shared initrd.gz rather than a per-slot initrdN.gz.
# Reads $live_mp/boot/syslinux/*.cfg (dynamic scope).
#------------------------------------------------------------------------------
syslinux_uses_shared_for_slot() {
    local slot=$1
    local syslinux_dir="$live_mp/boot/syslinux"
    local live_path="/${rel_live_dir#/}"
    [ -d "$syslinux_dir" ] || return 1
    local f
    for f in "$syslinux_dir"/*.cfg; do
        [ -f "$f" ] || continue
        awk -v live="$live_path" -v slot="$slot" '
            BEGIN { in_slot=0; found=0 }
            /^[[:space:]]*(LABEL)[[:space:]]/ { in_slot=0 }
            /^[[:space:]]*(KERNEL|LINUX)[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                in_slot=0
                for (i=2; i<=n; i++)
                    if (a[i] == live "/" slot) { in_slot=1; break }
            }
            in_slot && /^[[:space:]]*INITRD[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                for (i=2; i<=n; i++)
                    if (a[i] ~ /\/initrd\.gz$/) { found=1; exit }
                in_slot=0
            }
            END { exit (found ? 0 : 1) }
        ' "$f" && return 0
    done
    return 1
}

#------------------------------------------------------------------------------
# Print paths (relative to live_mp) of syslinux *.cfg files whose stanza for
# slot $1 references the shared initrd.gz.  Emits one line per matching file.
# Reads $live_mp/boot/syslinux/*.cfg (dynamic scope).
#------------------------------------------------------------------------------
syslinux_shared_cfg_files() {
    local slot=$1
    local syslinux_dir="$live_mp/boot/syslinux"
    local live_path="/${rel_live_dir#/}"
    [ -d "$syslinux_dir" ] || return
    local f
    for f in "$syslinux_dir"/*.cfg; do
        [ -f "$f" ] || continue
        awk -v live="$live_path" -v slot="$slot" '
            BEGIN { in_slot=0; found=0 }
            /^[[:space:]]*(LABEL)[[:space:]]/ { in_slot=0 }
            /^[[:space:]]*(KERNEL|LINUX)[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                in_slot=0
                for (i=2; i<=n; i++)
                    if (a[i] == live "/" slot) { in_slot=1; break }
            }
            in_slot && /^[[:space:]]*INITRD[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                for (i=2; i<=n; i++)
                    if (a[i] ~ /\/initrd\.gz$/) { found=1; exit }
                in_slot=0
            }
            END { exit (found ? 0 : 1) }
        ' "$f" && printf '%s\n' "${f#$live_mp}"
    done
}

#------------------------------------------------------------------------------
# Return 0 if any syslinux *.cfg stanza that boots $1 (e.g. vmlinuz1)
# explicitly references the per-slot initrd (e.g. initrd1.gz).
# Reads $live_mp/boot/syslinux/*.cfg (dynamic scope).
#------------------------------------------------------------------------------
syslinux_needs_slot_initrd() {
    local slot=$1
    local syslinux_dir="$live_mp/boot/syslinux"
    local live_path="/${rel_live_dir#/}"
    local slot_initrd
    slot_initrd=$(kernel_to_initrd "$slot")
    [ -d "$syslinux_dir" ] || return 1
    local f
    for f in "$syslinux_dir"/*.cfg; do
        [ -f "$f" ] || continue
        awk -v live="$live_path" -v slot="$slot" -v si="$slot_initrd" '
            BEGIN { in_slot=0; found=0 }
            /^[[:space:]]*(LABEL)[[:space:]]/ { in_slot=0 }
            /^[[:space:]]*(KERNEL|LINUX)[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                in_slot=0
                for (i=2; i<=n; i++)
                    if (a[i] == live "/" slot) { in_slot=1; break }
            }
            in_slot && /^[[:space:]]*INITRD[[:space:]]/ {
                line=$0; gsub(/,/, " ", line); n=split(line, a)
                for (i=2; i<=n; i++)
                    if (a[i] ~ "/" si "$") { found=1; exit }
                in_slot=0
            }
            END { exit (found ? 0 : 1) }
        ' "$f" && return 0
    done
    return 1
}

#------------------------------------------------------------------------------
# Shared-initrd mode: after initrd.gz is updated, handle any leftover
# initrd1.gz from a previous multi-kernel setup.
# - Copy initrd.gz -> initrd1.gz only when syslinux has a stanza for vmlinuz1
#   that explicitly needs initrd1.gz.
# - Otherwise remove initrd1.gz: GRUB falls back to initrd.gz automatically,
#   and keeping a duplicate serves no purpose.
# Accesses full_live_dir, INITRD_NAME, IOLD_EXT via dynamic scope.
#------------------------------------------------------------------------------
sync_slot_initrds_with_shared() {
    local slot_initrd="$full_live_dir/initrd1.gz"
    [ -f "$slot_initrd" ] || return 0
    if syslinux_needs_slot_initrd "vmlinuz1"; then
        msg $"Copying updated %s to %s (syslinux stanza requires it)" \
            "$(pq "$INITRD_NAME")" "$(pq "initrd1.gz")"
        do_install "$full_live_dir/$INITRD_NAME" "$slot_initrd" "$IOLD_EXT"
    else
        msg $"Removing redundant %s (modules are in shared %s; GRUB falls back)" \
            "$(pq "initrd1.gz")" "$(pq "$INITRD_NAME")"
        cmd rm -f "$slot_initrd" "$slot_initrd$IOLD_EXT" "$slot_initrd$MD5_EXT"
    fi
}

#------------------------------------------------------------------------------
# List kernel module version(s) packed inside an initrd image.
# Stage 1: try each decompressor directly (no microcode preamble).
# Stage 2: scan for CPIO TRAILER!!! boundaries, try decompressors from each
#          boundary -- handles multiple microcode CPIO streams and any
#          compression format.  Technique from live-slot/_get_initrd_module_vers.
# Echoes one module version per line (sort -uV); nothing on failure.
#------------------------------------------------------------------------------
initrd_modules_ver() {
    local ipath=$1
    [ -f "$ipath" ] || return

    local MOD_SED='s|lib/modules/\([^/]*\).*|\1|'
    local decomp vers

    for decomp in zcat xzcat lz4cat zstdcat bzcat; do
        command -v "$decomp" >/dev/null 2>&1 || continue
        vers=$($decomp "$ipath" 2>/dev/null \
            | cpio -t 2>/dev/null \
            | grep '^lib/modules/[^/]' \
            | sed "$MOD_SED" \
            | sort -uV)
        [ -n "$vers" ] && { printf '%s\n' "$vers"; return; }
    done

    local off bnd
    while IFS=: read -r off _; do
        bnd=$(( (off + 121 + 511) / 512 * 512 ))
        for decomp in zcat xzcat lz4cat zstdcat bzcat; do
            command -v "$decomp" >/dev/null 2>&1 || continue
            vers=$(dd if="$ipath" bs=512 skip=$((bnd / 512)) status=none 2>/dev/null \
                | $decomp 2>/dev/null \
                | cpio -t 2>/dev/null \
                | grep '^lib/modules/[^/]' \
                | sed "$MOD_SED" \
                | sort -uV)
            [ -n "$vers" ] && { printf '%s\n' "$vers"; return; }
        done
    done < <(grep -boa 'TRAILER!!!' "$ipath" 2>/dev/null)
}

#------------------------------------------------------------------------------
# -VV debug: print a per-slot state table showing kernel version, initrd
# filename, size/mtime, and the module version(s) packed inside each initrd.
# Args: $1=full_live_dir  $2=live_vm (active slot records)  $3=live_old (.kold)
#------------------------------------------------------------------------------
debug_slot_table() {
    [ "$VERY_VERBOSE" ] || return 0
    local dir=$1 live_list=$2 old_list=$3
    local kfmt="  %-14s  %-42s  %s\n"
    local ifmt="  %1s %-16s  %s\n"   # col1: '!' on module mismatch, else blank

    msg $"Slot state:"
    printf "$kfmt" "Slot" "Kernel version" "Built"

    local ver fname kdate iname ipath isz idate iinfo mods mark
    while IFS="$K_IFS" read -r ver fname kdate; do
        [ -z "$fname" ] && continue
        printf "$kfmt" "$fname" "$ver" "${kdate:--}"
        iname=$(kernel_to_initrd "$fname")
        ipath="$dir/$iname"
        if [ -f "$ipath" ]; then
            isz=$(du -h "$ipath" 2>/dev/null | cut -f1)
            idate=$(TZ=UTC date --date=@$(stat -c %Y "$ipath") +'%Y-%m-%d' 2>/dev/null)
            mods=$(initrd_modules_ver "$ipath" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g')
            iinfo="$isz  $idate${mods:+   modules: $mods}"
            grep -qF "$ver" <<<"$mods" && mark="" || mark="!"
        else
            iinfo="(missing)"
            mark=""
        fi
        printf "$ifmt" "$mark" "$iname" "$iinfo"
    done <<< "$live_list"

    if [ -n "$old_list" ]; then
        while IFS="$K_IFS" read -r ver fname kdate; do
            [ -z "$fname" ] && continue
            printf "$kfmt" "$fname" "$ver" "${kdate:--}"
            local base="${fname%$KOLD_EXT}"
            # Rollback backup initrd uses KOLD_EXT (created by do_install default)
            local iold_name="$(kernel_to_initrd "$base")$KOLD_EXT"
            ipath="$dir/$iold_name"
            if [ -f "$ipath" ]; then
                isz=$(du -h "$ipath" 2>/dev/null | cut -f1)
                idate=$(TZ=UTC date --date=@$(stat -c %Y "$ipath") +'%Y-%m-%d' 2>/dev/null)
                mods=$(initrd_modules_ver "$ipath" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g')
                iinfo="$isz  $idate${mods:+   modules: $mods}"
                grep -qF "$ver" <<<"$mods" && mark="" || mark="!"
            else
                iinfo="(missing)"
                mark=""
            fi
            printf "$ifmt" "$mark" "$iold_name" "$iinfo"
        done <<< "$old_list"
    fi
}

#------------------------------------------------------------------------------
# Show usage and exit
#------------------------------------------------------------------------------
usage() {
    local ret=${1:-0}

cat<<Usage
Usage: $ME [options] [command]

Update the kernel on a running antiX/MX live-usb or on an antiX/MX live-usb
that is plugged into another system.  The new kernel must already be installed.
You will be prompted for information that is needed but was not given in the
command line arguments.

Commands:
   all         All commands below
   unpack      Unpack the old initrd
   copy        Copy kernel modules into initrd
   repack      Repack the new initrd
   install     Copy new initrd and vmlinuz to the live boot directory

Options:
  -C --color=<xxx>      Set color scheme to off|low|low1|bw|dark|high
  -d --device=<device>  live-usb device to update the kernel on
                        (use "live" to force updating a running live system)
  -F --force=XXXX       Force the options specfied:
                             flock:  ignore missing flock program
                               usb:  Allow non-usb devices (dangerous!)
                             clear:  remove previous initrd directory
  -G --graphic-ui       Use the new graphics user interface (default)
  -h --help             Show this usage
  -i --initrd           Only update the initrd using the file:
                            $TEMPLATE_INITRD_1
                        If that file is not found, use:
                            $TEMPLATE_INITRD_2
     --initrd=<file>    Only update the initrd using file <file>
                        leading / then treated as full path to alternate initrd
  -I --ignore-config    Ignore the configuration file
  -k --kernel=<kernel>  The version (uname -r) of the new kernel
  -K --keep-old         Keep the old module directory in the initrd
  -m --modules=<list>   Only add listed modules to the existing initrd
  -N --numeric-ui       Use the legacy numerical user interface
     --no-ucode         Skip microcode refresh in the repacked initrd
  -U                    Same as --no-ucode
     --live-dir=<dir>   Subdir holding live boot files (normally "antiX"); skips auto-detection
     --multi-kernel     Force per-kernel initrd mode (one initrd per kernel slot)
     --shared-initrd    Force shared initrd mode (one initrd shared by vmlinuz + vmlinuz1)
     --pause            Wait for user input before exiting
  -p --pretend          Don't actually install the new kernel or initrd.gz
  -q --quiet            Print less
  -R --reset-config     Write a config template (all entries commented out) and exit
  -v --version          Show version information
  -V --verbose          Print more, show output of commands
  -VV --very-verbose    Also show the commands
  -W --write-config     Write a config template (all entries commented out) and exit

Notes:
  - short options stack. Example: -pq instead of --pretend --quiet
  - options can be intermingled with commands and parameters
  - config file: $CONFIG_FILE
  - the config file will be sourced if it exists
  - it will be created if it doesn't exist
Usage
    exit $ret
}

#------------------------------------------------------------------------------
# Callback routine to evaluate arguments after the root user check.  We also
# need to include the early args to avoid unknown argument errors.
#------------------------------------------------------------------------------
eval_argument() {
    local arg=$1 val=$2
    case $arg in
              -auto|a)  AUTO_MODE=true                  ;;
             -color|C)  COLOR_SCHEME=$val               ;;
             -color=*)  COLOR_SCHEME=$val               ;;
            -device|d)  DEVICE=$val                     ;;
            -device=*)  DEVICE=$val                     ;;
             -force|F)  FORCE="$FORCE${FORCE:+,}$val"   ;;
             -force=*)  FORCE="$FORCE${FORCE:+,}$val"   ;;
#               -fifo)  FIFO_MODE=true                  ;;
       --graphic-ui|G)  GRAPHICAL_MENUS=true            ;;
              -help|h)  usage                           ;;
            -initrd|i)  CMD_INITRD=$TEMPLATE_INITRD     ;;
            -initrd=*)  CMD_INITRD=${arg#*=}; CMD_INITRD_EXPLICIT=true ;;
            -kernel|k)  NEW_KERNEL=$val                 ;;
            -kernel=*)  NEW_KERNEL=$val                 ;;
          -keep-old|K)  KEEP_OLD=true                   ;;
           -modules|m)  MODS="$MODS${MODS:+,}$val"      ;;
           -modules=*)  MODS="$MODS${MODS:+,}$val"      ;;
       --numeric-ui|N)  GRAPHICAL_MENUS=                ;;
          -no-ucode|U)  NO_UCODE=true                  ;;
           -pretend|p)  PRETEND_MODE=true ;             ;;
               -pause)  PAUSE="$PAUSE${PAUSE:+,}exit"   ;;
             -pause=*)  PAUSE="$PAUSE${PAUSE:+,}$val"   ;;
             -quiet|q)  QUIET=true                      ;;
           -verbose|V)  VERBOSITY=$((VERBOSITY + 1))    ;;
        -very-verbose)  VERBOSITY=$((VERBOSITY + 2))    ;;

    -multi-kernel)  MULTI_KERNEL=true                ;;   # force per-kernel initrd mode
   -shared-initrd)  MULTI_KERNEL=false               ;;   # force shared initrd mode
     -live-dir=*)  CMD_LIVE_DIR=$val               ;;   # specify live subdir non-interactively

       # These are read early.  They are not unknown
     -ignore-config|I)                                  ;;
      -reset-config|R)                                  ;;
      -write-config|W)                                  ;;

               *)  fatal "Unknown parameter %s" "-$arg" ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to evalute some command line args before the root user test.
#------------------------------------------------------------------------------
eval_early_argument() {
    case $1 in
      -ignore-config|I) IGNORE_CONFIG=true    ;;
       -reset-config|R) RESET_CONFIG=true     ;;
       -write-config|W) WRITE_CONFIG=true     ;;
               -help|h) usage                 ;;
            -version|v) show_version          ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine for command line arguments that don't start with "-"
#------------------------------------------------------------------------------
assign_parameter() {
    local cnt=$1 param=$2
   case $cnt in
        *) CMDS="$CMDS${CMD:+ }$param" ;;
    esac
}

#------------------------------------------------------------------------------
# Callback routine to see if an argument requires a value to follow it.
#------------------------------------------------------------------------------
takes_param() {
    case $1 in
           -color|C) return 0 ;;
          -device|d) return 0 ;;
           -force|F) return 0 ;;
          -kernel|k) return 0 ;;
         -modules|m) return 0 ;;
    esac
    return 1
}

#------------------------------------------------------------------------------
# The main routine.  Called from the very bottom of this script.
#------------------------------------------------------------------------------
main() {
    local SHIFT SHORT_STACK="CdDFGhiIkKmNpqRUvVW"
    local BE_VERBOSE VERY_VERBOSE VERBOSITY=0
    local orig_args="$*"

    # Bashism: put the original arguments into an array to deal w/ spaces
    declare -a ORIG_ARGS=("$@")

    # Let non-root users get usage.  Need to get --ignore-config early.
    read_early_params "$@"

    need_root

    EXIT_NUM=100

    read_reset_config_file "$CONFIG_FILE"

    # Needs to be before we read cmdline
    TEMPLATE_INITRD=$TEMPLATE_INITRD_1
    test -f "$TEMPLATE_INITRD" || TEMPLATE_INITRD=$TEMPLATE_INITRD_2

    read_all_cmdline_mingled "$@"
    set_colors $COLOR_SCHEME
    check_cmds  CMDS  "$ALL_CMDS"
    check_force FORCE "$ALL_FORCE"
    check_pause PAUSE "$ALL_PAUSE"

    case $VERBOSITY in
        0) ;;
        1) BE_VERBOSE=true                     ;;
        *) BE_VERBOSE=true ; VERY_VERBOSE=true ;;
    esac

    local new_initrd
    if [ ${#CMD_INITRD} -gt 0 ]; then
        if multi_kernel && [ -z "$CMD_INITRD_EXPLICIT" ]; then
            warn $"-i/--initrd option is not supported in multi-kernel mode, ignoring"
            CMD_INITRD=""
        else
            test -f "$CMD_INITRD" || fatal "The initrd file %s was not found" "$CMD_INITRD"
            multi_kernel || new_initrd=true
        fi
    fi

    local template_initrd=${CMD_INITRD:=$TEMPLATE_INITRD}

    need_prog $VM_VERSION_PROG copy-initrd-modules

    [ "$WRITE_CONFIG" ] && write_config "$CONFIG_FILE" && exit 0

    trap clean_up EXIT

    do_flock

    : ${CMDS:=all}

    # starting <program name>
    shout_title $"Starting %s" "live-kernel-updater"
    msg $"live-kernel-updater version: %s" "$(pq "$VERSION")"

    set_window_title "$ME"

    start_log "$THE_LOG_FILE" "$orig_args"
    ERR_FILE=$THE_ERR_FILE
    test -f $ERR_FILE && rm -f $ERR_FILE

    type -t find_man_page &>/dev/null && find_man_page

    shout_pretend

    USB_DIR=$WORK_DIR/usb
    SQFS_DIR=$WORK_DIR/linux
    BIOS_DIR=$WORK_DIR/bios

    local root_dir=$SQFS_DIR

    mkdir -p $WORK_DIR || fatal 120 "Could not make a work directory under /run"
    mount -t tmpfs tmpfs $WORK_DIR || fatal 121 "Could not mount tmpfs at %s" $WORK_DIR

    local we_are_live  live_dev  bios_uuid  bios_dev
    if its_alive_usb; then
        we_are_live=true

        # This reads initrd.out file
        read_initrd_config

        bios_uuid=$INITRD_BIOS_UUID
        [ -n "$bios_uuid" ] && bios_dev=$(blkid -U "$bios_uuid")

        live_dev=$(get_live_dev)
        warn_z "$live_dev" $"The live media is not mounted"
        msg $"Current running kernel is %s" "$(pq $(uname -r))"
    fi

    #==============================================================================
    # The First Menu
    #==============================================================================
    [ ${#DEVICE} -eq 0 ] && select_live_usb_device DEVICE "$live_dev" "$bios_dev"

    # Are we going to update a running live system or an plugged in live-usb?
    local usb_dev live_mp

    #----- RUNNING LIVE -------------------------------------------------------
    if update_live; then
        fatal_z "$we_are_live" $"This is not a live-usb system"
        live_mp=$LIVE_MP
        msg $"Will use running live system"
        msg $"Live mount point: %s" "$(pq "$LIVE_MP")"

        IS_ENCRYPTED=$INITRD_ENCRYPTED

        if encrypted ; then
            mount_if_needed "$bios_dev" $BIOS_DIR
            shout "Encryption detected"
            msg "Made sure %s was mounted at %s" "$(pq $bios_dev)" "$(pq $BIOS_DIR)"

            # copy_programs passes --encrypt to copy-initrd-programs when IS_ENCRYPTED=true
        fi

    #----- ALREADY-MOUNTED DIRECTORY ------------------------------------------
    elif [ -d "$DEVICE" ]; then
        live_mp=$DEVICE
        msg $"Will use mounted directory %s" "$(pq $DEVICE)"

    #----- LIVE-USB -----------------------------------------------------------
    else

        fatal_z "$DEVICE" "Must specify a device if not running live"
        usb_dev=$(expand_device $DEVICE)
        fatal_z "$usb_dev" $"Could not find device %s" "$DEVICE"

        force usb || is_usb_or_removable $usb_dev \
            || fatal usb $"The device %s does not appear to be a usb device" "$usb_dev"

        local dev_type=$(lsblk -no TYPE --nodeps $usb_dev)

        # Allow user to specify disk device instead of partition
        if [ "$dev_type" = "disk"  ]; then
            local part_dev=$(get_partition $usb_dev 1)
            dev_type=$(lsblk -no TYPE --nodeps $part_dev)
            if [ "$dev_type" = "part" ]; then
                usb_dev=$part_dev
            else
                fatal $"Can't find first partition on device %s" $usb_dev
            fi
        fi
        [ "$dev_type" = "part" ] || fatal $"Device %s is not a disk partition" $usb_dev

        local usb_drive=$(get_drive $usb_dev)
        umount_all $usb_drive

        my_mount $usb_dev $USB_DIR
        msg $"Will use live-usb device %s" "$(pq $usb_dev)"
        live_mp=$USB_DIR

        local crypt_file=$live_mp/$LIVE_DIR/$CRYPT_FNAME
        if test -e $crypt_file; then
            shout "Encrypted live-usb detected"
            local main_uuid=$(cat $crypt_file 2>/dev/null)

            my_mkdir $BIOS_DIR
            always_cmd mount --move $live_mp $BIOS_DIR || fatal "Could not move mount to %s" $(basename $BIOS_DIR)

            fatal_z "$main_uuid" "Could not find uuid of encrypted partition"

            local main_dev=$(blkid -U "$main_uuid")
            echo "main uuid: $main_uuid  main dev: $main_dev" >> $LOG_FILE

            fatal_z "$main_dev" "Could not find encrypted partition with uuid %s" "$boot_uuid"

            shout "When asked, please enter the password for this encrypted live-usb"
            LUKS_DEV=/dev/mapper/$LUKS_NAME

            always_cmd cryptsetup open --type=luks $main_dev $LUKS_NAME \
                || fatal "Was unable to open the encrypted partition"

            always_cmd mount $LUKS_DEV $live_mp || fatal "Was unable to mount the encrypted device"

            IS_ENCRYPTED=true
        fi

    fi

    is_mountpoint "$live_mp" || fatal "Expected %s to be a mountpoint" "$live_mp"

    show_distro_version "$live_mp"

    # local full_live_dir=$live_mp/${LIVE_DIR#/}
    local full_live_dir rel_live_dir

    if [ -n "$CMD_LIVE_DIR" ]; then
        rel_live_dir="${CMD_LIVE_DIR#/}"
        [ -d "$live_mp/$rel_live_dir" ] \
            || fatal $"Live directory %s not found under %s" "$CMD_LIVE_DIR" "$live_mp"
    else
        find_live_boot_dir rel_live_dir "$live_mp" "$LINUXFS_NAME" \
            || fatal $"No %s file found in any directory in %s" "$LINUXFS_NAME" "$live_mp"
    fi

    local full_live_dir=$live_mp/${rel_live_dir#/}

    check_writable "$full_live_dir" "live boot"

    # Find the linuxfs file (where we get new kernels and modules from)
    # Use the remastered linuxfs.new if it is available.
    local ext file linuxfs_name
    for ext in "$NEW_EXT" ""; do
        file=$full_live_dir/$LINUXFS_NAME$ext
        test -r $file || continue
        linuxfs_name=$file
        break
    done

    # Could not find '<linuxfs.new>' or '<linuxfs>' on the live-usb
    fatal_z "$linuxfs_name" $"Could not find %s or %s on the live-usb" \
        $(pqh "$LINUXFS_NAME$NEW_EXT") $(pqh "$LINUXFS_NAME")

    local linuxfs_base=$(basename "$linuxfs_name")
    local live_remaster
    if [ -z "${linuxfs_name%%*$NEW_EXT}" ]; then
        live_remaster=true
        # Found <type> file <file-name> in directory <dir-name>
        msg $"Found %s file %s in directory %s" "$(pq live-remaster)" "$(pq $linuxfs_base)" "$(pq $rel_live_dir)"
    else
        msg $"Found %s file %s in directory %s" "$(pq linuxfs)" "$(pq $linuxfs_base)" "$(pq $rel_live_dir)"
    fi

    local sq_compress=$(unsquashfs -s "$linuxfs_name" | awk '/^Compression/ {print $2}')
    # Squashfs file <filename> uses <type> compression"
    msg $"Squashfs file %s uses %s compression" "$(pq $linuxfs_base)" "$(pq $sq_compress)"

    my_mount "$linuxfs_name" "$root_dir" -t squashfs -o loop,ro
    local full_boot_dir="$root_dir/${BOOT_DIR#/}"

    # When targeting an external USB, prefer the template from inside the squashfs:
    # the host template may be from a different/older release.
    # When live-booted and updating the running system, keep the host template --
    # it may be newer than the squashfs if the package was updated in the live session.
    # A user-supplied --initrd=file is never overridden.
    if [ -z "$new_initrd" ] && [ -z "$CMD_INITRD_EXPLICIT" ] && ! update_live; then
        local t
        for t in "$TEMPLATE_INITRD_1" "$TEMPLATE_INITRD_2"; do
            test -f "$root_dir$t" && template_initrd="$root_dir$t" && break
        done
    fi
    # Fallback: host template not found (e.g. package not installed on live system)
    # -- try inside the mounted squashfs regardless of update_live
    if [ -z "$new_initrd" ] && [ -z "$CMD_INITRD_EXPLICIT" ] && ! test -f "$template_initrd"; then
        local t
        for t in "$TEMPLATE_INITRD_1" "$TEMPLATE_INITRD_2"; do
            test -f "$root_dir$t" && template_initrd="$root_dir$t" && break
        done
    fi

    # Resolve MULTI_KERNEL auto-detect from the TARGET, not the running system.
    # When targeting an external USB from a host (e.g. MX host -> antiX USB),
    # the running system's /etc/mx-version must not influence the mode.
    # For live-boot updating its own system, running system IS the target.
    if [ -z "$MULTI_KERNEL" ]; then
        if update_live; then
            test -e /etc/mx-version && MULTI_KERNEL=true || MULTI_KERNEL=false
        else
            test -e "$root_dir/etc/mx-version" && MULTI_KERNEL=true || MULTI_KERNEL=false
        fi
    fi

    # Use firmware from the target squashfs for microcode injection when targeting
    # an external USB; the host may be a different/older release with older microcode.
    # For live-boot updating own system, host firmware may be newer (apt-updated).
    local uc_firmware_arg=""
    ! update_live && [ -d "$root_dir/lib/firmware" ] \
        && uc_firmware_arg="-F $root_dir/lib/firmware"

    pause mount

    # NOTE: *after* mounting the linuxfs file we switch to the BIOS_DIR directory (sneaky)
    # if we are working on an encrypted live-usb
    encrypted && live_mp=$BIOS_DIR
    full_live_dir=$live_mp/${rel_live_dir#/}

    local full_initrd targ_initrd="$full_live_dir/$INITRD_NAME"
    if [ "$new_initrd" ]; then
        full_initrd=$CMD_INITRD
    elif [ ${#INITRD_NAME} -eq 0 ]; then
        fatal "An empty initrd name was given"
    else
        full_initrd=$targ_initrd
    fi

    test -e "$full_initrd" || fatal $"Could not find initrd file %s" "$full_initrd"

    # default live kernels: vmlinuz and vmlinuz1 (antiX), or vmlinuz[1-9] (MX)
    local both_vm=$VMLINUZ_NAME
    if multi_kernel; then
        both_vm="${VMLINUZ_NAME}|${VMLINUZ_NAME}[1-9]"
    elif [ -n "$VMLINUZ_2_NAME" ]; then
        both_vm="$both_vm|$VMLINUZ_2_NAME"
    fi

    local live_all boot_all
    get_all_kernel live_all "$full_live_dir"                          # all in live dir
    get_all_kernel boot_all "$full_boot_dir"                          # all in boot dir

    # Filter out Memtest-kernels
    live_all=$(sed /memtest/Id <<<"$live_all")
    boot_all=$(sed /memtest/Id <<<"$boot_all")

    fix_kernel_dates live_all "$full_live_dir"
    fix_kernel_dates boot_all "$full_boot_dir"

    #get_all_kernel boot_mod "$full_boot_dir" --mod-dir="$root_dir"   # boot dir with module dir

    # Filter out kernels that can't handle the squashfs compression
    filter_on_compression boot_all "$full_boot_dir" "$sq_compress"

    # Sort by version descending (highest first); date descending as tiebreaker
    boot_all=$(echo "$boot_all" | sort -t"$K_IFS" -k1,1Vr -k3,3r)

    local live_vm=$(find_kernel_fname "$both_vm" "$live_all")         # live with default vmlinuz names
    local vm_cnt=$(count_lines "$live_vm")
    local live_old=$(find_kernel_fname "($both_vm)$KOLD_EXT" "$live_all" )

    # Sort live kernels by slot order: vmlinuz, vmlinuz1, vmlinuz2, ...
    live_vm=$(echo "$live_vm" | sort -t"$K_IFS" -k2,2)

    # Keep .ver sidecars in sync with current live slot contents (MX only)
    multi_kernel && sync_vmlinuz_ver "$full_live_dir" "$live_vm"

    local live_both=$(echo -e "$live_vm\n$live_old")
    # In MX mode .kold is a safety backup only -- don't exclude its version from new-kernel list;
    # in antiX shared mode .kold means "rolled back from this", so keep it excluded.
    local _live_for_new; multi_kernel && _live_for_new="$live_vm" || _live_for_new="$live_both"
    # Escape + (regex metachar) so version strings match literally in grep patterns
    local live_versions=$(get_kernel_version "$_live_for_new" | tr "\n" "|" | sed 's/+/[+]/g')
    live_versions=${live_versions%|}

    local boot_new=$(find_kernel_version "$live_versions" "$boot_all" -v)
    local new_cnt=$(count_lines "$boot_new")
    local live_old_cnt=$(count_lines "$live_old")
    local action_cnt=$((new_cnt + live_old_cnt))

    # In live-boot mode, running /boot may have kernels installed on the overlay that
    # are not yet in the squashfs.  They need a remaster before lku can use them.
    # Skip when working with linuxfs.new: remaster already done, overlay check is irrelevant.
    local overlay_new="" overlay_cnt=0
    local real_boot_all=""
    if update_live && [ -z "$live_remaster" ]; then
        get_all_kernel real_boot_all "/boot"
        real_boot_all=$(sed /memtest/Id <<<"$real_boot_all")
        fix_kernel_dates real_boot_all "/boot"
        real_boot_all=$(echo "$real_boot_all" | sort -t"$K_IFS" -k1,1Vr -k3,3r)
        local sq_versions=$(get_kernel_version "$boot_all" | tr "\n" "|" | sed 's/+/[+]/g')
        sq_versions=${sq_versions%|}
        if [ -n "$sq_versions" ]; then
            overlay_new=$(find_kernel_version "$sq_versions" "$real_boot_all" -v) || true
        else
            overlay_new="$real_boot_all"
        fi
        overlay_cnt=$(count_lines "$overlay_new")
    fi

    # Slots holding kernels no longer available in the (possibly remastered) squashfs
    local stale_vm="" stale_cnt=0
    if [ -n "$boot_all" ]; then
        local boot_versions=$(get_kernel_version "$boot_all" | tr "\n" "|" | sed 's/+/[+]/g')
        boot_versions=${boot_versions%|}
        if [ -n "$boot_versions" ]; then
            stale_vm=$(find_kernel_version "$boot_versions" "$live_vm" -v) || true
        fi
        stale_cnt=$(count_lines "$stale_vm")
    fi

    # Slots whose kernel exists in squashfs but was removed from the persistence overlay
    # (in squashfs /boot but not in running /boot -- may fail to boot with persistence)
    local persist_removed_vm="" persist_removed_cnt=0
    if live_persist && [ -n "$real_boot_all" ]; then
        local real_versions=$(get_kernel_version "$real_boot_all" | tr "\n" "|" | sed 's/+/[+]/g')
        real_versions=${real_versions%|}
        if [ -n "$real_versions" ]; then
            persist_removed_vm=$(find_kernel_version "$real_versions" "$live_vm" -v) || true
        else
            persist_removed_vm="$live_vm"
        fi
        persist_removed_cnt=$(count_lines "$persist_removed_vm")
    fi

    # [a table of what was found will follow]
    msg $"Found:"
    if multi_kernel; then
        # MX: .kold backups have no rollback action; only active slots are relevant
        msg_kernel "$live_vm"  $"default live kernel"  $"default live kernels"
    else
        msg_kernel "$live_all" $"total live kernel"    $"total live kernels"
        msg_kernel "$live_vm"  $"default live kernel"  $"default live kernels"
        msg_kernel "$live_old" $"old live kernel"      $"old live kernels"
    fi
    msg_kernel "$boot_all"  $"total installed kernel"   $"total installed kernels"
    msg_kernel "$boot_new"  $"new available kernel"     $"new available kernels"
    if [ $overlay_cnt -gt 0 ]; then
        msg_kernel "$overlay_new" $"new installed kernel" $"new installed kernels"
    fi
    if [ $stale_cnt -gt 0 ]; then
        local _stale_label
        [ -n "$live_remaster" ] \
            && _stale_label=$"not in remastered linuxfs" \
            || _stale_label=$"not available in linuxfs"
        local _stale_names=""
        while IFS="$K_IFS" read -r _sver _sfname _sdate; do
            [ -z "$_sfname" ] && continue
            warn "%-9s %s -- %s" "$_sfname:" "$(vq "$_sver")" "$_stale_label"
            _stale_names="${_stale_names:+$_stale_names, }$_sfname"
        done <<<"$stale_vm"
        warn $"Please update or remove: %s" "$_stale_names"
    fi

    show_kernel_3 $"Live kernels:" "$live_all" | strip_color >> $LOG_FILE
    show_kernel_3 $"Boot kernels:" "$boot_all" | strip_color >> $LOG_FILE

    case $new_cnt in
        0) ;;
        1) log_it show_kernel_2 $"Only one new available kernel was found:" "$boot_new" ;;
        *) log_it show_kernel_2 $"New available kernels:" "$boot_new" ;;
    esac
    [ $overlay_cnt -gt 0 ] \
        && log_it show_kernel_2 $"New installed kernels:" "$overlay_new"

    if [ $new_cnt -gt $MAX_FILES ]; then
        warn $"There are %s new kernels.  Only showing the most recent %s." "$new_cnt" "$MAX_FILES"
        boot_new=$(echo "$boot_new" | grep . | head -n $MAX_FILES)
    fi

    fatal_k0 "$boot_all" $"No kernels were found in the boot directory"
    fatal_k0 "$live_all" $"No kernels were found in the live boot directory"

    local boot_all_cnt=$(count_lines "$boot_all")

    # Offer remaster if overlay has kernels not yet baked into squashfs
    if [ $overlay_cnt -gt 0 ]; then
        shout $"You should do a remaster before doing a kernel update"
        do_remaster "${ORIG_ARGS[@]}"
    fi

    # MX: add (free slot) or reassign (any slot) are available even when new_cnt=0
    local mk_can_add=
    multi_kernel && [ $boot_all_cnt -gt 0 ] && mk_can_add=true

    if [ $action_cnt -eq 0 -a ${#MODS} -eq 0 -a -z "$mk_can_add" ]; then

        # Aha, they just need to remaster first
        if [ $overlay_cnt -gt 0 ]; then
            shout_subtitle $"You MUST do a remaster before doing a kernel update"
            do_remaster "${ORIG_ARGS[@]}"
            exit

        elif test -f "$template_initrd" ; then
            shout
            shout $"No new kernels were found"
            shout $"Can only update the initrd file"
            shout $"You must install a kernel and then remaster before doing a kernel update"
        else
            error $"No new kernels were found and there's no template initrd file"
            fatal $"You must install a kernel and then remaster before doing a kernel update"
        fi
    fi

    debug_slot_table "$full_live_dir" "$live_vm" "$live_old"

    # MX mode: warn if syslinux boots any slot via shared initrd.gz
    if multi_kernel; then
        local _sysl_shared="" _sysl_files="" _sv _sf _sd _flist
        while IFS="$K_IFS" read -r _sv _sf _sd; do
            [ -z "$_sf" ] && continue
            [ "$_sf" = "$VMLINUZ_NAME" ] && continue  # primary slot always uses initrd.gz
            # Only warn if per-slot initrd exists -- if missing, initrd.gz fallback is correct
            test -f "$full_live_dir/$(kernel_to_initrd "$_sf")" || continue
            _flist=$(syslinux_shared_cfg_files "$_sf")
            if [ -n "$_flist" ]; then
                _sysl_shared="${_sysl_shared:+$_sysl_shared }$_sf"
                _sysl_files="${_sysl_files}${_flist}"$'\n'
            fi
        done <<< "$live_vm"
        if [ -n "$_sysl_shared" ]; then
            local _dflt=""
            test -e "$root_dir/etc/mx-version" \
                || _dflt=" ($"default mode for antiX")"
            local _wfmt1=$"Syslinux uses shared %s for %s%s"
            local _wfmt2=$"Run with %s and select %s to fix"
            warn "${m_co}${_wfmt1}" "$(pq 'initrd.gz')" "$(pq "$_sysl_shared")" "$_dflt"
            warn "${m_co}${_wfmt2}" "$(pq '--shared-initrd')" "$(pq $"Update initrd using file")"
            local _f
            while IFS= read -r _f; do
                [ -n "$_f" ] || continue
                msg "  %s" "$(pq "$_f")"
            done <<< "$(printf '%s' "$_sysl_files" | sort -u)"
        fi
    fi

    #==============================================================================
    # The Second Menu (if needed, no --initrd  and no --modules)
    #==============================================================================
    if multi_kernel && [ -n "$NEW_KERNEL" ]; then
        warn $"-k/--kernel option is not supported in multi-kernel mode, ignoring"
        NEW_KERNEL=""
    fi
    local action_target
    if ! multi_kernel && [ ${#MODS} -gt 0 ]; then
        action_target="modules@$MODS"
    elif [ "$new_initrd" ];then
        action_target=initrd@$CMD_INITRD
    else
        select_update_rollback_action action_target "$live_vm" "$live_old" "$new_cnt" "$template_initrd" "$full_live_dir" "$boot_all_cnt"
    fi

    local action=${action_target%@*}
    local target_fname=${action_target#*@}

    local new_kernel new_version new_fname stat_list action_name blurb
    local initrd_only add_modules
    local current_versions=$(get_kernel_version "$live_both")

    #----- Update Action -------------------------------------------------------------------------
    if [ "$action" = "update" ]; then
        action_name=$"live kernel update"

        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local target_date=$(get_kernel_date "$target")

        #==========================================================================
        # The Third Menu (only if needed)
        #==========================================================================
        # Shared mode: offer only kernels not already in an active slot (same
        # kernel in both slots is pointless and makes rollback a no-op).
        # Multi-kernel mode: offer all -- slots are independent.
        local _update_pool; multi_kernel && _update_pool="$boot_all" || _update_pool="$boot_new"
        select_new_kernel new_kernel "$NEW_KERNEL" "$_update_pool" "$target_version" $"kernel being replaced"

        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")
        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel %s" "$new_version"

        # Current kernel and New/Old kernel specs
        # Multi-kernel: no .kold backup (do_install_direct); shared: backup shown
        local _kold_dest; multi_kernel || _kold_dest="$target_fname$KOLD_EXT"
        blurb=$(kernel_stats \
            $"Current" "$target_version" "$target_date" "$target_fname" "$_kold_dest" \
            $"New"     "$new_version"    "$new_date"    $"(new)"        "$target_fname")
        if multi_kernel && [ -n "$CMD_INITRD_EXPLICIT" ]; then
            local _lbl_initrd=$"Using initrd template"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_initrd" "$(pq "$CMD_INITRD")")"
            action_name=$"live kernel update with initrd template"
        fi
        if multi_kernel && [ -n "$MODS" ]; then
            local _lbl_mods=$"Add modules"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_mods" "$(pq "$MODS")")"
            if [ -n "$CMD_INITRD_EXPLICIT" ]; then
                action_name=$"live kernel update with initrd template and modules"
            else
                action_name=$"live kernel update with modules"
            fi
        fi

    #---- Rollback Action ---------------------------------------------------------
    elif [ "$action" = "rollback" ]; then
        action_name=$"live kernel rollback"
        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local target_date=$(get_kernel_date "$target")

        new_kernel=$(find_kernel_fname "$target_fname$KOLD_EXT" "$live_old")
        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")

        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel %s" "$new_fname"

        blurb=$(kernel_stats \
            $"Current" "$target_version" "$target_date" "$target_fname"          "$target_fname$KBAD_EXT" \
            $"Old"     "$new_version"    "$new_date"    "$target_fname$KOLD_EXT" "$target_fname")

    #---- Update Initrd Action ----------------------------------------------------
    elif [ "$action" = "initrd" ]; then
        action_name=$"live initrd update"
        initrd_only=true
        local blurb_fmt="$m_co%s: %s"
        # Update initrd using file <filename> [NOTE: no "%s" here!]
        blurb=$(printf "$blurb_fmt" $"Update initrd using file" "$(pq "$target_fname")")

    elif [ "$action" = "modules" ]; then
        action_name=$"live modules update"
        initrd_only=true
        add_modules=true
        local blurb_fmt="$m_co%s: %s"
        # Update initrd using file <filename> [NOTE: no "%s" here!]
        blurb=$(printf "$blurb_fmt" $"Add initrd modules" "$(pq "$target_fname")")

    elif [ "$action" = "update-initrd" ]; then
        action_name=$"live initrd update"
        initrd_only=true
        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local blurb_fmt="${m_co}%s: %s"
        blurb=$(printf "$blurb_fmt" $"Update initrd for" "$(pq "$target_fname")")

    elif [ "$action" = "reassign" ]; then
        action_name=$"live kernel reassign"
        target=$(find_kernel_fname "$target_fname" "$live_vm")
        local target_version=$(get_kernel_version "$target")
        local target_date=$(get_kernel_date "$target")
        select_new_kernel new_kernel "$NEW_KERNEL" "$boot_all" "$target_version" $"kernel being replaced"
        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")
        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel %s" "$new_version"
        local _kold_dest; multi_kernel || _kold_dest="$target_fname$KOLD_EXT"
        blurb=$(kernel_stats \
            $"Current" "$target_version" "$target_date" "$target_fname" "$_kold_dest" \
            $"New"     "$new_version"    "$new_date"    $"(new)"        "$target_fname")
        if multi_kernel && [ -n "$CMD_INITRD_EXPLICIT" ]; then
            local _lbl_initrd=$"Using initrd template"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_initrd" "$(pq "$CMD_INITRD")")"
            action_name=$"live kernel reassign with initrd template"
        fi
        if multi_kernel && [ -n "$MODS" ]; then
            local _lbl_mods=$"Add modules"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_mods" "$(pq "$MODS")")"
            if [ -n "$CMD_INITRD_EXPLICIT" ]; then
                action_name=$"live kernel reassign with initrd template and modules"
            else
                action_name=$"live kernel reassign with modules"
            fi
        fi

    elif [ "$action" = "add" ]; then
        action_name=$"live kernel add"
        local _add_mark=""
        update_live && _add_mark=$(uname -r)
        select_new_kernel new_kernel "$NEW_KERNEL" "$boot_all" "$_add_mark" $"running live kernel"
        new_fname=$(get_kernel_fname "$new_kernel")
        new_version=$(get_kernel_version "$new_kernel")
        new_date=$(get_kernel_date "$new_kernel")
        test -d $root_dir/lib/modules/$new_version \
            || fatal "Missing modules directory for kernel %s" "$new_version"
        blurb=$(printf "${m_co}%s %s: %s ($date_co%s$nc_co)" \
            $"Add new kernel to" "$(pq "$target_fname")" "$(vq "$new_version")" "$new_date")
        if [ -n "$CMD_INITRD_EXPLICIT" ]; then
            local _lbl_initrd=$"Using initrd template"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_initrd" "$(pq "$CMD_INITRD")")"
            action_name=$"live kernel add with initrd template"
        fi
        if [ -n "$MODS" ]; then
            local _lbl_mods=$"Add modules"
            blurb="$blurb
$(printf "${m_co}%s: %s${nc_co}" "$_lbl_mods" "$(pq "$MODS")")"
            if [ -n "$CMD_INITRD_EXPLICIT" ]; then
                action_name=$"live kernel add with initrd template and modules"
            else
                action_name=$"live kernel add with modules"
            fi
        fi

    elif [ "$action" = "remove" ]; then
        action_name=$"live kernel remove"
        local target_version=$(get_kernel_version "$(find_kernel_fname "$target_fname" "$live_vm")")
        blurb=$(printf "${m_co}%s: %s" $"Remove kernel" "$(pq "$target_fname")")

    elif [ "$action" = 'quit' ]; then
        offer_restart

    else
        internal_error "update/rollback menu" "$action"
    fi

    msg
    # Ready to make the following <live kernel update>
    shout_subtitle $"Ready to make the following %s" "$action_name"
    log_it echo "$blurb"

    if update_live && [ "$target_version" = "$(uname -r)" ]; then
        local _running_ver="$target_version"
        if [ "$action" = "remove" ] || [ "$action" = "update" ] || [ "$action" = "reassign" ]; then
            local _safe="" _ver _fn _dt
            while IFS="$K_IFS" read -r _ver _fn _dt; do
                [ "$_fn" = "$target_fname" ] && continue
                [ "$_ver" = "$_running_ver" ] && _safe=true && break
            done <<< "$live_vm"
            if [ -z "$_safe" ]; then
                local _wfmt
                [ "$action" = "remove" ] \
                    && _wfmt=$"You are about to remove the currently running live kernel %s" \
                    || _wfmt=$"You are about to replace the currently running live kernel %s"
                warn "${m_co}${_wfmt}" "${warn_co}${_running_ver}${m_co}"
            fi
        fi
    fi

    YES_no_pretend || offer_restart

    if [ "$action" = "remove" ]; then
        do_remove_slot "$full_live_dir" "$target_fname"
        my_done "$action_name"
    fi

    if multi_kernel && [ "$action" = "update"   -o "$action" = "rollback" \
               -o "$action" = "add"      -o "$action" = "update-initrd" \
               -o "$action" = "reassign" ]; then
        local slot_initrd_name=$(kernel_to_initrd "$target_fname")
        targ_initrd="$full_live_dir/$slot_initrd_name"
        if [ "$action" = "add" ]; then
            # Prefer template (guaranteed clean); fall back to primary initrd.gz
            full_initrd=$template_initrd
            test -e "$full_initrd" || full_initrd=$full_live_dir/$INITRD_NAME
            test -e "$full_initrd" || fatal $"Could not find initrd file %s" "$full_initrd"
        else
            # update / update-initrd / reassign: always rebuild from template (clean per-slot initrd)
            full_initrd=$template_initrd
            test -e "$full_initrd" || fatal $"Could not find template initrd %s" "$full_initrd"
        fi
    fi

    if [ "$action" = "rollback" ]; then
        local rollback_initrd=$INITRD_NAME
        multi_kernel && rollback_initrd=$(kernel_to_initrd "$target_fname")
        do_rollback "$full_live_dir" "$target_fname" "$rollback_initrd"
        my_done "$action_name"
    fi

    local initrd_dir=$WORK_DIR/initrd
    local tmp_initrd_file="$WORK_DIR/$(basename "$targ_initrd").tmp"
    local compression_file="$WORK_DIR/compression"
    local cpio_files="$WORK_DIR/cpio.{in,out}"

    if [ ${#CMDS} -le 0 ]; then
        echo -e "No command(s) given.  Try 'all'."
        exit 0
    fi

    # NEED: $new_version  $new_fname $targ_fname
    #--------------------------------------------------------------------------
    #----- REAL WORK STARTS HERE ----------------------------------------------
    #--------------------------------------------------------------------------
    if need unpack; then

        if force clear; then
            cmd rm -rf "$initrd_dir/../initrd"
            cmd rm -f "$tmp_initrd_file" $compression_file $cpio_files
        else
            test -d "$initrd_dir" && yes_NO_fatal "clear" \
                "Do you want to delete it now"            \
                "Use --force=clear to always delete it"   \
                "Will not over-write an existing initrd directory."
        fi
        unpack-initrd --from "$full_initrd" --dir "$initrd_dir"  --quiet
    fi

    pause unpack

    copy_initrd_release "$root_dir" "$initrd_dir" "$initrd_only"

    if need copy-modules; then
        if ! multi_kernel && [ ${#MODS} -gt 0 ]; then
            : # Don't delete any exiting modules
            local mod_file=$WORK_DIR/modules.list
            echo "$MODS" | sed -r "s/[ ,]+/\n/g" > $mod_file
            copy_modules "$root_dir" "$initrd_dir" "$current_versions" "$mod_file"
            need copy-programs && copy_programs "$root_dir"  "$initrd_dir"
        elif [ "$action" = "update-initrd" ]; then
            delete_all_modules $initrd_dir
            copy_modules "$root_dir" "$initrd_dir" "$target_version"
            need copy-programs && copy_programs "$root_dir" "$initrd_dir"
        elif [ "$initrd_only" ]; then
            delete_all_modules $initrd_dir
            copy_modules  "$root_dir"  "$initrd_dir"  "$(get_kernel_version "$live_vm")"
            need copy-programs && copy_programs "$root_dir"  "$initrd_dir"
        elif [ "$action" = "add" ]; then
            delete_all_modules $initrd_dir
            copy_modules "$root_dir" "$initrd_dir" "$new_version"
            if [ -n "$MODS" ]; then
                local mod_file=$WORK_DIR/modules.list  no_banner=true
                echo "$MODS" | sed -r "s/[ ,]+/\n/g" > $mod_file
                copy_modules "$root_dir" "$initrd_dir" "$new_version" "$mod_file" $no_banner
            fi
            need copy-programs && copy_programs "$root_dir" "$initrd_dir"
        else
            if multi_kernel; then
                delete_all_modules "$initrd_dir"
            else
                [ "$KEEP_OLD" ] || remove_old_mods $initrd_dir $target_version
            fi
            copy_modules "$root_dir" "$initrd_dir" "$new_version"
            if multi_kernel && [ -n "$MODS" ]; then
                local mod_file=$WORK_DIR/modules.list  no_banner=true
                echo "$MODS" | sed -r "s/[ ,]+/\n/g" > $mod_file
                copy_modules "$root_dir" "$initrd_dir" "$new_version" "$mod_file" $no_banner
            fi
            # Shared mode: also keep modules for every other active slot
            if ! multi_kernel; then
                local _shr_vers
                _shr_vers=$(while IFS="$K_IFS" read -r _v _f _d; do
                    [ "$_f" = "$target_fname" ] && continue
                    echo "$_v"
                done <<< "$live_vm" | grep -vxF "$new_version")
                [ -n "$_shr_vers" ] && copy_modules "$root_dir" "$initrd_dir" "$_shr_vers"
            fi
            need copy-programs && copy_programs "$root_dir"  "$initrd_dir"
        fi
    fi

    pause copy

    if need repack; then
        unpack-initrd --dir "$initrd_dir" --from "$tmp_initrd_file" --repack --quiet
        if [ -z "$NO_UCODE" ]; then
            if command -v uc-tool >/dev/null 2>&1; then
                local uc_vflag="-s"; [ "$BE_VERBOSE" ] && uc_vflag="-V"
                msg "Adding microcode to initrd ..."
                uc-tool $uc_vflag $uc_firmware_arg -u amd,intel -i "$tmp_initrd_file" \
                    || warn "uc-tool failed -- initrd may lack current microcode"
            else
                warn "uc-tool not found -- skipping microcode refresh"
            fi
        fi
    fi
    pause repack

    if need install; then
        if multi_kernel; then
            # MX mode: no backups -- each slot IS its own rollback; install directly
            do_install_direct "$tmp_initrd_file" "$targ_initrd"
            if [ -z "$initrd_only" ]; then
                do_install_direct "$full_boot_dir/$new_fname" "$full_live_dir/$target_fname"
                need defrag && do_defrag "$full_live_dir"
            fi
        elif [ "$action" = "add" ]; then
            do_install "$tmp_initrd_file" "$targ_initrd" "$KOLD_EXT" no_warn
            do_install "$full_boot_dir/$new_fname" "$full_live_dir/$target_fname" "$KOLD_EXT" no_warn
            need defrag && do_defrag "$full_live_dir"
        elif [ "$initrd_only" ]; then
            do_install "$tmp_initrd_file" "$targ_initrd"  "$IOLD_EXT"
        else
            do_install "$tmp_initrd_file" "$targ_initrd"
            do_install "$full_boot_dir/$new_fname" "$full_live_dir/$target_fname"
            need defrag && do_defrag "$full_live_dir"
        fi
        multi_kernel && [ -z "$initrd_only" ] \
            && write_vmlinuz_ver "$full_live_dir/$target_fname"
        # Feature 2: shared mode -- propagate updated initrd.gz to any stale per-slot initrdN.gz
        if ! multi_kernel; then
            sync_slot_initrds_with_shared
        fi
    fi
    pause install

    my_done "$action_name"
}

#==============================================================================
# END OF MAIN
#==============================================================================

#==============================================================================
# Menu Routines
#==============================================================================

#------------------------------------------------------------------------------
# This is the first menu.  It is used to select the current live system or a
# plugged in usb or removable device.
#------------------------------------------------------------------------------
select_live_usb_device() {
    local var=$1  live_dev=$2  bios_dev=$3
    local menu=$(cli_drive_menu "$live_dev" "$bios_dev")

    if [ "$live_dev" ]; then
        local title=$(printf $"The current Live System on %s" "$(pq $live_dev)")
        menu=$(printf "live$P_IFS$m_co%s$nc_co\n%s" "$title" "$menu")
    fi

    fatal_0 $(count_lines "$menu") "usb" $"The system is not a live-usb and no usb drives were detected"

    my_select "$var" $"Please select the system to update" "$menu"
}

#------------------------------------------------------------------------------
# This is the second menu.  It selects which action to take: update or rollback
# and which vmlinuz file (vmlinuz or vmlinuz1) to perform it on.
#------------------------------------------------------------------------------
select_update_rollback_action() {
    local var=$1

    local menu=$(make_update_menu "$@")

    #echo "$menu"
    local cnt=$(count_lines "$menu")
    [ $cnt -lt 1 ] && fatal $"No update or rollback actions are available"

    if update_live; then
        local _rv=$(uname -r)
        local _lbl_run=$"running live kernel"
        local _lbl_persist=$"kernel uninstalled under persistence - may not boot"
        msg "${hi_co}*${nc_co} ${version_co}${_rv}${nc_co}  ${m_co}${_lbl_run}${nc_co}"
        live_persist \
            && msg "${warn_co}?${nc_co}  ${m_co}${_lbl_persist}${nc_co}"
    fi
    local BACK_TO_MAIN=$"quit"
    my_select $var $"Please select an action to perform" "$menu"
}

#------------------------------------------------------------------------------
# Make the main menu for changing kernel or rolling back
#------------------------------------------------------------------------------
_slot_is_orphaned() {
    local ver=$1 _sv _sf _sd
    [ -n "$stale_vm" ] && while IFS="$K_IFS" read -r _sv _sf _sd; do
        [ "$_sv" = "$ver" ] && return 0
    done <<< "$stale_vm"
    [ -n "$persist_removed_vm" ] && while IFS="$K_IFS" read -r _sv _sf _sd; do
        [ "$_sv" = "$ver" ] && return 0
    done <<< "$persist_removed_vm"
    return 1
}

make_update_menu() {
    local var=$1  live_vm=$2  old_vm=$3  new_cnt=$4  initrd=$5  live_dir=$6  boot_cnt=$7  orig_ifs=$IFS

    local IFS=$K_IFS

    local running_ver=""
    update_live && running_ver=$(uname -r)

    # FIXME: go through twice to get widths?
    local fmt="%s$bold_co %s$m_co %s %s ($date_co%s$nc_co)%s"
    local live_k  action  action_name

    local version fname date run_mark stale_mark
    if [ $new_cnt -gt 0 ]; then
        action=update; action_name=$"Update"
        while read version fname date; do
            [ ${#version} -eq 0 ] && continue
            [ -n "$running_ver" ] && [ "$version" = "$running_ver" ] \
                && run_mark="${hi_co}*${nc_co}" || run_mark=""
            _slot_is_orphaned "$version" \
                && stale_mark="${warn_co}?${nc_co}" || stale_mark=""
            menu_printf "$action@$fname" "$fmt" "$action_name" "$(printf '%-8s' "$fname")" $"from" "$(vq $version)" "$date" "${run_mark}${stale_mark}"
        done <<Live_VM
$(echo "$live_vm")
Live_VM
    fi

    if ! multi_kernel; then
        action="rollback" ; action_name=$"Rollback"
        fmt="%s$bold_co %s$m_co %s %s ($date_co%s$nc_co)"
        while read version fname date; do
            [ ${#version} -eq 0 ] && continue
            fname=${fname%$KOLD_EXT}
            menu_printf "$action@$fname" "$fmt" "$action_name" "$fname" $"to" "$(vq $version)" "$date"
        done <<Old_VM
$(echo "$old_vm")
Old_VM
    fi

    if multi_kernel && [ -n "$live_dir" ]; then
        local vmver vmslot vmdate
        if [ ${boot_cnt:-0} -gt 0 -a $new_cnt -eq 0 ]; then
            while read vmver vmslot vmdate; do
                [ ${#vmslot} -eq 0 ] && continue
                [ -n "$running_ver" ] && [ "$vmver" = "$running_ver" ] \
                    && run_mark="${hi_co}*${nc_co}" || run_mark=""
                _slot_is_orphaned "$vmver" \
                    && stale_mark="${warn_co}?${nc_co}" || stale_mark=""
                menu_printf "reassign@$vmslot" "$fmt" $"Update" "$(printf '%-8s' "$vmslot")" $"from" "$(vq $vmver)" "$vmdate" "${run_mark}${stale_mark}"
            done <<Reassign_VMs
$(echo "$live_vm")
Reassign_VMs
        fi
        local next_slot
        next_slot=$(find_next_free_slot "$live_dir") \
            && [ ${boot_cnt:-0} -gt 0 ] \
            && menu_printf "add@$next_slot" $"Add new kernel to %s" "$(pq "$next_slot")"
        local slot_cnt=$(count_lines "$live_vm")
        if [ $slot_cnt -gt 1 ]; then
            while read vmver vmslot vmdate; do
                [ ${#vmslot} -eq 0 ] && continue
                [ "$vmslot" = "$VMLINUZ_NAME" ] && continue
                [ -n "$running_ver" ] && [ "$vmver" = "$running_ver" ] \
                    && run_mark="${hi_co}*${nc_co}" || run_mark=""
                _slot_is_orphaned "$vmver" \
                    && stale_mark="${warn_co}?${nc_co}" || stale_mark=""
                menu_printf "remove@$vmslot" $"Remove kernel %s%s%s" "$(pq "$vmslot")" "$run_mark" "$stale_mark"
            done <<Remove_VMs
$(echo "$live_vm")
Remove_VMs
        fi
    else
        [ ${#initrd} -gt 0 ] && test -f "$initrd" \
            && menu_printf "initrd@$initrd" $"Update initrd using file %s" "$(pq "$initrd")"
    fi

    menu_printf 'quit' $"Quit"
    IFS=$orig_ifs
}


#------------------------------------------------------------------------------
# This is the third menu.  It lets the user select the new kernel that will
# be used when they do an update.  If there is only one new kernel then that
# kernel is selected and there is no menu.
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# Clone of select_kernel_2 with a marker (*) on the currently installed kernel.
# Payload returned on selection is always the clean version|fname|date record.
#------------------------------------------------------------------------------
select_kernel_2_marked() {
    local title=$1 var=$2 list=$3 current_version=$4 orig_ifs=$IFS
    IFS=$K_IFS
    local f1 f2 f3  w1=5
    while read f1 f2 f3; do
        [ $w1 -lt ${#f1} ] && w1=${#f1}
    done <<< "$list"
    local fmt="$hi_co%s$version_co%-${w1}s $date_co%s$nc_co\n"
    local hfmt="$head_co  %s %s$nc_co\n"
    local data="$P_IFS$(printf "$hfmt" "$(rpad $w1 $"Version")" $"Date")\n"
    local payload marker
    while read f1 f2 f3; do
        [ ${#f1} -gt 0 ] || continue
        payload="$f1$IFS$f2$IFS$f3"
        [ "$f1" = "$current_version" ] && marker="* " || marker="  "
        data="$data$payload$P_IFS$(printf "$fmt" "$marker" "$f1" "$f3")\n"
    done <<< "$list"
    IFS=$orig_ifs
    my_select $var "$title" "$data" ""
}

select_new_kernel() {
    local var=$1  cmd_version=$2  list=$3  current_version=$4  current_label=$5  cnt=$(count_lines "$3")

    fatal_0 $cnt $"Did not find any new kernels in the boot directory"

    if [ ${#cmd_version} -gt 0 ]; then
        local cmd_kernel=$(find_kernel_version "$cmd_version" "$list")
        local cmd_cnt=$(count_lines "$cmd_kernel")
        case $cmd_cnt in
            0) fatal "No new kernels match the requested version %s" "$cmd_version" ;;

            1) msg $"Found one kernel matching the requested version %s" "$(pq $cmd_version)"
               eval "$var=\$cmd_kernel"
               return ;;

            *) fatal "Found multiple kernels matching the requested version %s" "$cmd_version" ;;
        esac
    fi

    case $cnt in
        1)  msg $"Found one new kernel %s" "$(pq $(get_kernel_version $list))"
            eval "$var=\$list"
            return ;;
    esac

    local BACK_TO_MAIN=$"quit"
    if [ -n "$current_version" ]; then
        msg "${hi_co}*${nc_co} ${version_co}${current_version}${nc_co}  ${m_co}${current_label}${nc_co}"
        select_kernel_2_marked $"Please select a kernel from this list" $var "$list" "$current_version"
    else
        select_kernel_2 $"Please select a kernel from this list" $var "$list"
    fi
    [ "${!var}" = "quit" ] && offer_restart
}

#==============================================================================
# The Routines that do the "real" work.
#==============================================================================

#------------------------------------------------------------------------------
# Copy the initrd-release file (if needed)
#------------------------------------------------------------------------------
copy_initrd_release() {
    local from=$1  to=$2  always=$3
    local from_release to_release release
    for release in initrd_release initrd-release; do
        from_release=$from/etc/$release
        to_release=$to/etc/$release

        [ "$always" -o ! -e "$to_release" ] || continue
        test -e "$from_release"             || continue
        always_cmd cp "$from_release" "$to_release"
    done
}

#------------------------------------------------------------------------------
# Remove all kernel modules from the initrd
#------------------------------------------------------------------------------
delete_all_modules() {
    local to=$1
    local mod_dir="$to/lib/modules"
    test -e "$mod_dir" && always_cmd rm -r "$mod_dir"
}

#------------------------------------------------------------------------------
# Remove modules directory for the old kernel
#------------------------------------------------------------------------------
remove_old_mods() {
    local root=$1  kernel=$2
    local dir=$root/lib/modules/$kernel

    if test -d "$dir"; then
        always_cmd rm -r "$dir"
    else
        warn "Module directory for old kernel %s not found" "$kernel"
    fi
}

#------------------------------------------------------------------------------
# Copy in modules directory for the new kernel
#------------------------------------------------------------------------------
copy_modules() {
    local from=$1  to=$2  versions=$3  mod_file=$4  no_banner=$5

    local options="--count --quiet --from=$from --to=$to"
    [ ${#mod_file} -gt 0 ] && options="$options --verbose"

    local version success
    for version in $versions; do
        [ -z "$no_banner" ] && msg "  -> %s %s" $"kernel" "$(pq $version)"
        if always_cmd copy-initrd-modules $options --encrypt --kernel="$version" $mod_file; then
            success=true
            continue
        fi
        warn $"Copy initrd modules failed for kernel %s" "$version"
    done

    [ "$success" ] && return
    fatal $"No modules were copied to the initrd"
}

#------------------------------------------------------------------------------
# Copy programs and libs from linuxfs into the initrd directory
# Should only be needed when we use a template initrd.gz
#------------------------------------------------------------------------------
copy_programs() {
    local from=$1  to=$2  prog=${3:-copy-initrd-programs}
    local encrypt_option=""
    if [ "$IS_ENCRYPTED" = true ]; then
        encrypt_option="--encrypt"
    fi
    need_prog $prog
    cmd $prog --from="$from" --to="$to" $encrypt_option --clean
    cmd $prog --from="$from" --to="$to" $encrypt_option
}

#------------------------------------------------------------------------------
# Installs a file from "$from" to "$to".  Makes a backup of the original and
# also makes an md5sum of the new file.
#------------------------------------------------------------------------------
do_install() {
    local from=$1  dest=$2  old_ext=${3:-$KOLD_EXT}  no_warn=$4
    test -f "$from" || fatal "Installation file %s does not exist" "$from"

    # install <filename>
    msg $"install %s" "$(pq $(basename $dest))"
    #msg "install %s --> %s" "$from" "$dest"

    if test -e "$dest"; then
        if [ -n "${dest%%*$old_ext}" ]; then
            local backup="$dest$old_ext"
            # msg "backing up %s to %s" "$(pq $(basename "$dest"))" "$(pq $(basename "$backup"))"
            cmd mv "$dest" "$backup"
        else
            warn $"Overwriting backup"
        fi
    else
        [ "$no_warn" ] || warn $"Target file %s does not exist" "$dest"
    fi

    cmd cp "$from" "$dest"

    local md5_file=$dest$MD5_EXT
    #test -e $md5_file && cmd mv $md5_file $dest$old_ext$MD5_EXT
    local md5_sum=$(cd $(dirname $dest) && md5sum $(basename $dest))
    cmd write_file "$md5_file" "$md5_sum"
}

#------------------------------------------------------------------------------
# Like do_install but overwrites directly without creating a backup.
# Used in multi-kernel mode: each slot is independent, boot another to recover.
#------------------------------------------------------------------------------
do_install_direct() {
    local from=$1  dest=$2
    test -f "$from" || fatal "Installation file %s does not exist" "$from"
    msg $"install %s" "$(pq $(basename $dest))"
    cmd cp "$from" "$dest"
    local md5_file=$dest$MD5_EXT
    local md5_sum=$(cd $(dirname $dest) && md5sum $(basename $dest))
    cmd write_file "$md5_file" "$md5_sum"
}

#------------------------------------------------------------------------------
# Perform the rollback action by moving
#       X      -->  X.kbad
#       X.kold -->  X
# Then make new md5 files.
#------------------------------------------------------------------------------
do_rollback() {
    local dir=$1  vmlinuz=$2  initrd=$3

    local the_initrd="$dir/$initrd"
    local old_initrd="$the_initrd$KOLD_EXT"
    local bad_initrd="$the_initrd$KBAD_EXT"

    local the_vmlinuz="$dir/$vmlinuz"
    local old_vmlinuz="$the_vmlinuz$KOLD_EXT"
    local bad_vmlinuz="$the_vmlinuz$KBAD_EXT"


    test -e "$old_vmlinuz" || fatal $"Could not find old %s file to roll back" "$vmlinuz$K_OLD"
    test -e "$old_initrd"  || fatal $"Could not find old %s file to roll back" "$initrd$K_OLD"

    # Roll back <file-1> to <file-2>
    msg $"Roll back %s to %s" "$(pq $(basename "$old_initrd"))" "$(pq $initrd)"
    cmd mv "$the_initrd" "$bad_initrd"
    cmd mv "$old_initrd" "$the_initrd"

    msg $"Roll back %s to %s" "$(pq $(basename "$old_vmlinuz"))" "$(pq $vmlinuz)"
    cmd mv "$the_vmlinuz" "$bad_vmlinuz"
    cmd mv "$old_vmlinuz" "$the_vmlinuz"

    local dest md5_file md5_sum
    for dest in "$the_initrd" "$the_vmlinuz"; do
        md5_file=$dest$MD5_EXT
        #test -e $md5_file && cmd mv $md5_file $dest$KOLD_EXT$MD5_EXT
        md5_sum=$(cd $(dirname $dest) && md5sum $(basename $dest))
        cmd write_file "$md5_file" "$md5_sum"
    done
}

#------------------------------------------------------------------------------
# Currently not used.  Filter out kernels that don't have a module directory.
#------------------------------------------------------------------------------
find_valid_kernel() {
    local var=$1  list=$2  dir=$2  out

    local line version
    while read line; do
        [ ${#line} -eq 0 ] && continue
        version=$(get_kernel_version "$line")
        if -d "$dir/lib/modules/$version"; then
            out="$out$line\n"
        else
            warn "No module directory found for kernel %s" "$version"
        fi
    done<<List
$(echo -n "$list")
List
    out=$(echo -e "$out")
    eval $var=\$out
}

#------------------------------------------------------------------------------
# Format some simple counts of kernels in lists.  It allows us to use plural
# or singular forms that can be easily translated.
#------------------------------------------------------------------------------
msg_kernel() {
    local list=$1 lab1=$2 lab2=$3
    local cnt=$(count_lines "$list")

    if [ $cnt -ne 1 ]; then
        msg "  %2s %s %s" $(nq $cnt) "$lab2"
    else
        local version=$(get_kernel_version "$list")
        msg "  %2s %-24s (%s)" $(nq $cnt) "$lab1" "$(vq $version)"
        return
    fi
}

#------------------------------------------------------------------------------
# Say a few extra things to the user before we exit
#------------------------------------------------------------------------------
my_done() {
    local action=$1
    echo
    if [ "$PRETEND_MODE" ]; then
        # "pretend mode" goes through the steps withoute altering your system
        local in_pretend=$"in pretend mode"
        Shout "%s %s %s (%s)." "$ME" "$(pqb $action)" $"done" "$(pqb "$in_pretend")"
    else
        Shout "%s %s %s." "$ME" "$(pqb $action)" $"done"
    fi
    offer_restart
}

#------------------------------------------------------------------------------
# Ask the user if they want to restart from the beginning; exit if not.
# Used when the user quits the menu or cancels a pending action.
#------------------------------------------------------------------------------
offer_restart() {
    if YES_no $"Perform another operation?"; then
        clean_up
        # Replay original args minus any --device/-d, then add confirmed device.
        # This preserves --shared-initrd, --multi-kernel, -VV, etc. across restarts.
        local args=() arg skip_next=false
        for arg in "${ORIG_ARGS[@]}"; do
            if $skip_next; then skip_next=false; continue; fi
            case "$arg" in
                --device=*|-d?*) continue ;;
                -d) skip_next=true; continue ;;
            esac
            args+=("$arg")
        done
        exec "$0" "${args[@]}" --device="$DEVICE"
    fi
    my_exit 0
}

#------------------------------------------------------------------------------
# Rule out kernels that cannot handle the squashfs compression method
#------------------------------------------------------------------------------
filter_on_compression() {
    local var=$1  dir=$2  compress=$3
    case $compress in gzip) return ;; esac
    eval "local val=\$$var"
    local version changed
    for version in $(get_kernel_version "$val"); do
        local config=$dir/config-$version
        local short_config=${config#$SQFS_DIR}
        if ! test -e "$config"; then
            warn "Could not find file %s" "$(pqh $short_config)"
            continue
        fi
        if ! test -r "$config"; then
            warn "Could not read file %s" "$(pqh $short_config)"
            continue
        fi
        if ! grep -iq "^CONFIG_SQUASHFS_$compress=y" "$config"; then
            warn "Kernel %s cannot handle %s squashfs compression" "$(pqh $version)" "$(pqh $compress)"
            val=$(echo "$val" | grep -v "^$version$K_IFS")
            changed=true
        fi
    done
    [ "$changed" ] || return
    eval $var=\$val
}

#------------------------------------------------------------------------------
# Convenience routine
#------------------------------------------------------------------------------
update_live() { [ "$DEVICE" = 'live' ] ; return $? ; }
live_persist() { update_live && [ -f /etc/live/config/persist-root ]; }

#------------------------------------------------------------------------------
# Count the number of kernels in a directory
#------------------------------------------------------------------------------
count_kernels() {
    local dir=$1
    count_lines "$($VM_VERSION_PROG -nsr /boot |sed /Memtest/Id)"
}


#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
do_remaster() {
    YES_no $"Would you like to do a remaster now?" || return
    live-remaster --cli
    shout $"Ready to restart %s" "$ME"
    press_enter
    clean_up
    exec $0 "$@"
}

#------------------------------------------------------------------------------
# Do a few things before cleaning up.
#------------------------------------------------------------------------------
my_exit() {
    local ret=${1:-0}

    pause exit $"Exit"

    #Msg "=> cleaning up"
    exit $ret
}

#------------------------------------------------------------------------------
# Trapped on exit.  Umount (order is vital!) remove the work_dir and remove
# the lock file.
#------------------------------------------------------------------------------
clean_up() {
    lib_clean_up
    mp_cleanup $SQFS_DIR $USB_DIR $BIOS_DIR $WORK_DIR
    test -d $WORK_DIR && rmdir $WORK_DIR
    luks_close $LUKS_NAME
    unflock
}

#------------------------------------------------------------------------------
# Write a configuration file based on the current settings
#------------------------------------------------------------------------------
write_config() {
    local file=${1:-$CONFIG_FILE}
    local dir=$(dirname "$file")
    mkdir -p "$dir" || fatal "Could not make config file directory %s" "$dir"
    msg "Writing config file %s" "$(pq "$file")"

    cat<<Config_File >"$file"
$(config_header "$file" "$ME" "$VERSION" "$VERSION_DATE")

#          MAX_FILES="$MAX_FILES"
#            LIVE_MP="$LIVE_MP"
#           LIVE_DIR="$LIVE_DIR"
#           BOOT_DIR="$BOOT_DIR"

#    VM_VERSION_PROG="$VM_VERSION_PROG"
#       LINUXFS_NAME="$LINUXFS_NAME"
#       VMLINUZ_NAME="$VMLINUZ_NAME"
#     VMLINUZ_2_NAME="$VMLINUZ_2_NAME"
#        INITRD_NAME="$INITRD_NAME"
#  TEMPLATE_INITRD_1="$TEMPLATE_INITRD_1"
#  TEMPLATE_INITRD_2="$TEMPLATE_INITRD_2"

#   MIN_LINUXFS_SIZE="$MIN_LINUXFS_SIZE"

          # for kernel and initrd.gz files
#           KOLD_EXT="$KOLD_EXT"
#           KBAD_EXT="$KBAD_EXT"
#           IOLD_EXT="$IOLD_EXT"
#           IBAD_EXT="$IBAD_EXT"
#            MD5_EXT="$MD5_EXT"

          # For linuxfs file (right after a remaster)
#            NEW_EXT="$NEW_EXT"

#          SHELL_LIB="$SHELL_LIB"

#          LUKS_NAME="$LUKS_NAME"
#        CRYPT_FNAME="$CRYPT_FNAME"

#       COLOR_SCHEME="$COLOR_SCHEME"
#    GRAPHICAL_MENUS="$GRAPHICAL_MENUS"
$(config_footer)
Config_File

    return 0
}

#------------------------------------------------------------------------------
# Returns true if we are working on a live encrypted live-usb
#------------------------------------------------------------------------------
encrypted() { [ "$IS_ENCRYPTED" ] ; return $? ;}

#------------------------------------------------------------------------------
# Load the lib either from a neighboring repo or from the standard location.
#------------------------------------------------------------------------------
load_lib() {
    local file=$1  path=$2
    unset FOUND_LIB

    local dir lib found IFS=:
    for dir in $path; do
        lib=$dir/$file
        test -r $lib || continue
        if ! . $lib; then
            printf "Error when loading library %s\n" "$lib" >&2
            printf "This is a fatal error\n" >&2
            exit 15
        fi
        FOUND_LIB=$lib
        return 0
    done

    printf "Could not find library %s on path %s\n" "$file" "$path" >&2
    printf "This is a fatal error\n" >&2
    exit 17
}

#===== Start Here =============================================================
load_lib "$SHELL_LIB" "$LIB_PATH"

set_colors

main "$@"

