#!/bin/bash

set -eu
shopt -s globstar nullglob

# A simple tool to grab and extract debian-installer netboot images.
#
# Copyright (C) 2008 Frank Lin PIAT <fpiat@klabs.be>
# latest version is available from:
#     http://wiki.debian.org/DebianInstaller/NetbootAssistant
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St - Suite 330, Boston, MA 02110, USA.


# ------------------ Declare the constants ------------------- #
PACKAGE_NAME=di-netboot-assistant
PACKAGE_VERSION=0.74

# -------------- Initialize the global variables ------------- #
OFFLINE=false
VERBOSE=false
DEBUG=false
IGNOR_SIG=false
DISOURCELIST=/etc/di-netboot-assistant/di-sources.list
SYSLINUX=/usr/lib/syslinux/
DL_CACHE=/var/cache/di-netboot-assistant
STATUS_LIB=/var/lib/di-netboot-assistant
TEMPLATES=/etc/di-netboot-assistant
TFTP_ROOT=/var/lib/tftpboot
N_A_DIR=d-i/n-a  # where di-netboot-assistant images are set up
DI_PKG_DIR=d-i/n-pkg  # where the debian-installer-*-netboot-* image is copied or bind mounted
LIVE_DIR=d-i/n-live  # where the Debian Live ISO images are mounted (sub-dirs)
CLI_ALIAS=""
REWRITEPKGPATH='\(debian\|ubuntu\)-installer'
SQUASHIMG="tftp://\\\${pxe_default_server}/\$LIVE_DIR/\${ISO_NAME}/live/filesystem.squashfs"
CHECKSUM_FILE="SHA256SUMS"
CHECKSUM_BIN="sha256sum"
DEBIAN_KEYRING="/usr/share/keyrings/debian-archive-keyring.gpg"
UBUNTU_KEYRING="/usr/share/keyrings/ubuntu-archive-keyring.gpg"
DI_ARGS=
TARGET_ARGS=
ARCH=
DEFAULT_ARCH=""
HOME="${HOME:-}"
#MIRROR_REGEXPS=# Not defined on purpose, so user can pass the variable
umask "$(umask | sed -e 's/.$/2/')"              # files must be public.

if [ -n "$HOME" ] && [ -f "$HOME/.di-netboot-assistant/di-netboot-assistant.conf" ]; then
    # shellcheck source=/dev/null
    . "$HOME/.di-netboot-assistant/di-netboot-assistant.conf"
else
    if [ -f "/etc/di-netboot-assistant/di-netboot-assistant.conf" ]; then
        . "/etc/di-netboot-assistant/di-netboot-assistant.conf"
    fi
fi


# ------------------- Declare the functions ------------------ #


# ------------------------------------------------------------ #
# usage()
#       Print script usage help.
# Parameters: release
# Returns: (EXIT STATUS) 0=Success
# ------------------------------------------------------------ #
usage() {
        cat <<XXX
Usage: $PACKAGE_NAME [options] install DI-DIST [--offline] [--arch=ARCH] [--alias=NAME]
       $PACKAGE_NAME [options] [purge|uninstall|uncache] DI-DIST [--arch=ARCH]
       $PACKAGE_NAME [options] rebuild-menu
       $PACKAGE_NAME [--help|--version|--rebuild-menu]

A simple tool to grab and extract debian-installer netboot images.

DI-DIST
        The name of a debian-installer repository (as listed in
        the file '$DISOURCELIST')

Commands:
    install      - download and extract a netboot image.
    uninstall    - remove a previously installed netboot image.
    uncache      - remove downloaded files from the cache.
    purge        - equivalent to uninstall plus uncache.
    rebuild-menu - rebuild the top level menu.
    rebuild-grub - rebuild the grub EFI netboot image.
    fw-toggle    - add/remove non-free firmware.

Options:
  -h, --help      Print this message and exit
  -V, --version   Print script version and exit
  -v, --verbose   Verbose messages
  --offline       Don't download the file (simply re-extract and build menu)
  --di-args=      DI arguments to be appended to "install" entry.
  --ignore-sig    Ignore failing image signature checks.
  --target-args=  Target system boot arguments to be appended to "install".
  --tftproot=DIR  Overwrite the TFTP_ROOT variable from the config file.
  --alias=NAME    Rename the downloaded repository (optional).
  --arch=ARCH     A comma separated list of architecture to Install/Purge,
                  or the keyword "all". It use the current machine's
                  architecture by default.

See the $PACKAGE_NAME(1) manual page for more information.
XXX
}


# ------------------------------------------------------------ #
# detect_current_arch()
#       Detect's the system's current architecture
# Parameters: none
# Returns: (STRING) architecture
# ------------------------------------------------------------ #
detect_current_arch() {
    local s
    if command -v dpkg >/dev/null 2>&1; then
        dpkg --print-architecture
    elif  command -v rpm >/dev/null 2>&1; then
        s=$(rpm --eval "%{_arch}")
        s=$(tr -d " " < /usr/lib/rpm/rpmrc | grep "^buildarchtranslate:$a:")
        s=$(echo "$s" | cut -d: -f3)
        s=$(echo "$s" | sed -e 's/^x86_64$/amd64/' -e 's/^sparc[0-9]*$/sparc/' -e 's/ppc[0-9]*$/powerpc/' -e 's/^armv[3456]*$/armel/' -e 's/^armv7hl$/armhf/' -e 's/^m68kmint$/m68k/')
        echo "$s"
    else
        echo "i386"
    fi
}

# ------------------------------------------------------------ #
# check_di_source_list()
#       Check the validity of di-source.list
# Parameters: release
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
check_di_source_list() {
    local valid_regex='^(#.*|[[:blank:]]*|[[:alnum:]_\\.-]+[	][[:alnum:]_\\.-]+[	][^	]+([	][^" ]+)+)$'

    if [ ! -f "$DISOURCELIST" ]; then
        echo "E: Debian Installer source file missing ($DISOURCELIST)" 1>&2
        return 1
    fi

    if grep -qvEl "$valid_regex"  "$DISOURCELIST" ; then
        echo -n "E: Syntax error lines #"  1>&2
        grep -vnE "$valid_regex" "$DISOURCELIST" \
            | cut -d ":" -f 1 | tr "\n" "," 1>&2
        echo " in file '$DISOURCELIST'." 1>&2
        return 1
    fi
    return 0
}


# ------------------------------------------------------------ #
# list_declared_arch_for_repo()
#       List archs declared for the repository in di-sources.list
# Parameters: repository
# Returns: (STRING) List of architectures
# ------------------------------------------------------------ #
list_declared_arch_for_repo() {
    local release=$1

    echo -n "I: Declared architecures for '$1': "
    get_declared_arch_for_repo "$release" | tr '\n' ' '
    echo ""
}


# ------------------------------------------------------------ #
# get_declared_arch_for_repo()
#       List archs declared for the repository in di-sources.list
# Parameters: repository
# Returns: (STRING) List of architectures
# ------------------------------------------------------------ #
get_declared_arch_for_repo() {
    local release=$1

    if [ "$1" ]; then
        grep -E "^$release\>" "$DISOURCELIST" | cut -f 2 | sort -u
    fi
    echo -n ""
}


# ------------------------------------------------------------ #
# print_do_not_edit_header()
#       Print a "Do no edit this file" warning
# Parameters: templatename
# Returns: (STRING) file header comment
# ------------------------------------------------------------ #
print_do_not_edit_header() {
    local templatename=$1               # Template filename

    [[ "$templatename" =~ "ipxe" ]] && echo '#!ipxe'
    echo "##"
    echo "## DO NOT EDIT THIS FILE"
    echo "##"
    echo "## It is automatically generated by '$PACKAGE_NAME'"
    [ -n  "$templatename" ] && echo "## using '$templatename' as template."
    echo "##"
}


# ------------------------------------------------------------ #
# find_file()
#       Return the name of the first file matching criteria.
# Parameters: name dir [dir...]
# Returns: (STRING) file
# ------------------------------------------------------------ #
find_file() {
    if [ "$1" ] && [ "$2" ]; then
        local name=$1; shift
        find "$@" -type f -name "$name" | head -n 1
    else
        echo ""
    fi
}


# ------------------------------------------------------------ #
# version_lte()
#       Compare two "software" version (like 1.2.1 and 1.3)
# Parameters: V1 V2
# Returns: (EXIT STATUS) 0=v1 <= v2, 1= V1 > V2
# ------------------------------------------------------------ #
version_lte() {
    if command -v dpkg > /dev/null 2>&1; then
        dpkg --compare-versions "$1" "<=" "$2"
        return $?
    else
        printf "%s\n%s\n" "$1" "$2"  | sort -V | head -n 1 | grep -q "^$1\$"
        return $?
    fi
}


# ------------------------------------------------------------ #
# prepare_grub()
#       Install grub-EFI.
# Parameters: opt
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
prepare_grub() {
    local v="" opt=$1 VERS V GRUB AR DIR 
    
    $VERBOSE && v="-v"
    [ -z "$opt"  ] && [ -d $TFTP_ROOT/$N_A_DIR/grub ] && return 0

    [ -d "$TFTP_ROOT/$N_A_DIR/grub/x86_64-efi" ] && rm -r "$TFTP_ROOT/$N_A_DIR/grub/x86_64-efi"
    [ -e "$TFTP_ROOT/$N_A_DIR/grub/font.pf2" ] && rm -r "$TFTP_ROOT/$N_A_DIR/grub/font.pf2"
    [ ! -e "$TFTP_ROOT/debian-installer" ] && \
        ln -srv $TFTP_ROOT/$N_A_DIR/ $TFTP_ROOT/debian-installer

    for AR in x64 aa64 ; do 
        ## We link bootnet*.efi and grub*.efi from the latest available image:
        echo "I: Preparing EFI executables for '${AR}'."
        GRUB=""
        VERS=0
        for FILE in $(find "$TFTP_ROOT/$DI_PKG_DIR/" -name grub${AR}.efi) \
                        $(find "$TFTP_ROOT/$N_A_DIR/" -name grub${AR}.efi) ; do
            V=$(strings "$FILE" | grep -A1 'GNU GRUB  version' | tail -1)
            $VERBOSE && echo "I: Found '$FILE' (version $V)."
            if dpkg --compare-versions "$V" gt "$VERS" ; then
                VERS="$V"
                GRUB="$FILE"
            fi
        done
        if [ -n "$GRUB" ] ; then
            DIR=$(dirname "$GRUB")
            echo "I: Using 'bootnet${AR}.efi' and 'grub${AR}.efi' (version $VERS) from '$DIR'."
            ln $v -fsr "$DIR/grub${AR}.efi"    "$TFTP_ROOT/$N_A_DIR/"
            ln $v -fsr "$DIR/bootnet${AR}.efi" "$TFTP_ROOT/$N_A_DIR/"
            if [ "$AR" = "x64" ] ; then
                ln $v -fsr "$DIR/grub/x86_64-efi" "$TFTP_ROOT/$N_A_DIR/grub/"
                ln $v -fsr "$DIR/grub/font.pf2"   "$TFTP_ROOT/$N_A_DIR/grub/"
                [ ! -e "$TFTP_ROOT/$N_A_DIR/amd64" ] && \
                    ln $v -sr "$TFTP_ROOT/$N_A_DIR"  "$TFTP_ROOT/$N_A_DIR/amd64"
            elif [ "$AR" = "aa64" ] ; then
                ln $v -fsr "$DIR/grub/arm64-efi" "$TFTP_ROOT/$N_A_DIR/grub/"
                ln $v -fsr "$DIR/grub/font.pf2"  "$TFTP_ROOT/$N_A_DIR/grub/"
                [ ! -e "$TFTP_ROOT/$N_A_DIR/arm64" ] && \
                    ln $v -sr "$TFTP_ROOT/$N_A_DIR" "$TFTP_ROOT/$N_A_DIR/arm64"
            else
                echo "E: Unknown architecture: $AR."
            fi
        else
            $VERBOSE && echo "I: No 'grub${AR}.efi' available."
        fi
    done
    return 0
}


# ------------------------------------------------------------ #
# copy_syslinux_bin()
#       Install pxelinux binaries in the target folder.
# Parameters: src dst
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
copy_syslinux_bin() {
    local src=$1                        # Source directory
    local dst=$2                        # Target directory
    local c32_dir=$dst/pxelinux.cfg
    local f srcf newbin pxe_new_ver pxe_cur_ver

    [ ! "$src" ] || [ ! "$dst" ] && return 1

    if [ "$SYSLINUX" = "$src" ]; then
        # avoid recent SYSLINUX EFI binaries incompatible with PXELINUX
        [ ! -d "$src/modules/bios" ] || src="$src/modules/bios"
        # recent SYSLINUX ships PXELINUX at separate location
        newbin=$(find_file pxelinux.0 /usr/lib/PXELINUX "$SYSLINUX" 2>/dev/null)
    else
        newbin=$(find_file pxelinux.0 "$src" 2>/dev/null)
    fi
    [ ! -f "$dst/pxelinux.0" ] && [ ! -f "$newbin" ] && return 1

    pxe_new_ver="$(pxelinux_version "$newbin")"
    pxe_cur_ver="$(pxelinux_version "$dst/pxelinux.0")"
    if version_lte "$pxe_new_ver" "$pxe_cur_ver"; then
        return 0
    fi

    if [ -n "$pxe_cur_ver" ] && [ -n "$pxe_new_ver" ] ; then
        echo "I: Upgrading PXE-Linux ($pxe_cur_ver to $pxe_new_ver)"
    else
        echo "I: Installing PXE-Linux ($pxe_new_ver)"
    fi

    for f in pxelinux.0 menu.c32 vesamenu.c32; do
        if [ pxelinux.0 = "$f" ]; then
            srcf="$newbin"
        else
            srcf="$(find_file $f "$src")"
        fi
        [ "${f#*c32}" ] || f="pxelinux.cfg/$f"
        [ -L "$dst/$f" ] && rm "$dst/$f"
        if [ -f "$srcf" ]; then
            cp "$srcf" "$dst/$f"
        else
            [ -f "$dst/$f" ] && rm "$dst/$f"
        fi
    done
    # Smooth transition to vesamenu
    [ ! -f "$c32_dir/menu.c32" ] && ln -s "vesamenu.c32" "$c32_dir/menu.c32"
    # Add core modules at root (see <https://bugs.debian.org/756275#49>)
    if [ "$TFTP_ROOT/$N_A_DIR/" = "$dst" ] ; then
        for f in ldlinux.c32 libcom32.c32 libutil.c32 ; do
            srcf="$(find_file $f "$src")"
            [ -z "$srcf" ] || cp -np "$srcf" "$TFTP_ROOT/$N_A_DIR/$f"
        done
    fi
    return 0
}


# ------------------------------------------------------------ #
# update_menu()
#       Create the bootloader's top menu.
# Parameters: (NONE)
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
update_menu() {
    if [ ! -d "$TFTP_ROOT/$N_A_DIR" ] ; then
        if [ ! -d "$TFTP_ROOT/$DI_PKG_DIR" ] ; then
            return
        else
            mkdir "$TFTP_ROOT/$N_A_DIR"
        fi
    fi
    cd "$TFTP_ROOT/$N_A_DIR"

    update_pxe_grub_menu
    include_packages_live
    prepare_grub ""

    if find "$TFTP_ROOT/$N_A_DIR" -mindepth 1 -type d | grep -q "." || \
            [ -d $TFTP_ROOT/$DI_PKG_DIR ] ; then
        sed -e 's/^\s*//' > "$TFTP_ROOT/$N_A_DIR/README.txt" <<xREADMEx
                !!! Attention !!!
                All files in this folder are managed by 'di-netboot-assistant'.

                Any modification and any added file may be removed
                at any time by 'di-netboot-assistant'.
xREADMEx
    else
        rm "$TFTP_ROOT/$N_A_DIR/README.txt" > /dev/null 2>&1 || true
    fi
    rmdir --ignore-fail-on-non-empty "$TFTP_ROOT/$N_A_DIR"
    return 0
}


# ------------------------------------------------------------ #
# update_pxe_grub_menu()
#       Create PXElinux and grub-EFI bootloader top menu.
# Parameters: (NONE)
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
update_pxe_grub_menu() {
    local x i s e entry tag

    echo "I: Building menu entries for the netboot-images."
    [ ! -d "pxelinux.cfg" ] && mkdir "pxelinux.cfg"
    print_do_not_edit_header "$TEMPLATES/pxelinux.HEAD" > pxelinux.cfg/default
    print_do_not_edit_header "$TEMPLATES/pxeuboot.HEAD" > pxelinux.cfg/default-arm
    [ ! -d "grub" ] && mkdir -p "grub"
    print_do_not_edit_header "$TEMPLATES/grub.cfg.HEAD" > grub/grub.cfg
    print_do_not_edit_header "$TEMPLATES/ipxemenu.HEAD" > menu.ipxe

    if [ -n "$(find "$TFTP_ROOT/$N_A_DIR" -type d -name pxelinux.cfg.serial-9600 2>/dev/null)" ]; then
        echo -e "CONSOLE 0\nSERIAL 0 9600" > pxelinux.cfg/default.serial-9600
        cat pxelinux.cfg/default >> pxelinux.cfg/default.serial-9600
    else
        [ -f "pxelinux.cfg/default.serial-9600" ] && rm pxelinux.cfg/default.serial-9600
    fi
    [ -f $TEMPLATES/pxelinux.HEAD ] && grep -Ev "^##" $TEMPLATES/pxelinux.HEAD >> pxelinux.cfg/default
    [ -f $TEMPLATES/pxeuboot.HEAD ] && grep -Ev "^##" $TEMPLATES/pxeuboot.HEAD >> pxelinux.cfg/default-arm
    [ -f $TEMPLATES/grub.cfg.HEAD ] && grep -Ev "^##" $TEMPLATES/grub.cfg.HEAD >> grub/grub.cfg
    [ -f $TEMPLATES/ipxemenu.HEAD ] && grep -Ev "^##" $TEMPLATES/ipxemenu.HEAD >> menu.ipxe

    i=0
    for x in "$STATUS_LIB"/*.pxelinux.menu.fragment ; do
        i=$((i + 1))
        grep -Ev "^##" "$x" >> pxelinux.cfg/default
        echo -n "I:     • "
        grep -E  "^[[:space:]]*MENU BEGIN" "$x" | sed -e "s/.*MENU BEGIN[[:space:]]\+//"
    done

    if s=$(find . -path "*/stable/*/boot-screens/splash.png" | head -1) && [ -n "$s" ] ; then
        echo -e "MENU BACKGROUND ::/$N_A_DIR${s#.}\n" >> pxelinux.cfg/default
        echo "I:    Using splash screen from 'stable' image."
    elif [ -d $TFTP_ROOT/$DI_PKG_DIR ] && \
             s=$(find $TFTP_ROOT/$DI_PKG_DIR -name "splash.png" | head -1) && [ -n "$s" ] ; then
        echo -e "MENU BACKGROUND ::${s#"$TFTP_ROOT"}\n" >> pxelinux.cfg/default
        echo "I:    Using splash screen from debian-installer-*-netboot-* package."
    else
        echo "I:    Splash screen not found.  Install 'stable' to use its splash screen."
    fi

    [ $i -eq 0 ] && [ ! -d $TFTP_ROOT/$DI_PKG_DIR ] && rm pxelinux.cfg/default
    rm pxelinux.cfg/default.mig-bak 2>/dev/null || true

    echo "I:    Building menu entries for u-boot."
    i=0
    for x in "$STATUS_LIB"/*.uboot.menu.fragment ; do
        i=$((i + 1))
        grep -Ev "^##" "$x" >>  pxelinux.cfg/default-arm
        echo -n "I:     • "
        grep -E  "^[[:space:]]*LABEL" "$x" | sed -e "s/.*LABEL[[:space:]]\+//"
    done
    [ $i -eq 0 ] && [ ! -d $TFTP_ROOT/$DI_PKG_DIR ] && rm  pxelinux.cfg/default-arm

    i=0
    for x in "$STATUS_LIB"/*.grub.menu.fragment ; do
        i=$((i + 1))
        grep -Ev "^##" "$x" >> grub/grub.cfg
    done
    [ $i -eq 0 ] && [ ! -d $TFTP_ROOT/$DI_PKG_DIR ] && rm grub/grub.cfg

    i=0
    for x in "$STATUS_LIB"/*.ipxe.menu.fragment ; do
        i=$((i + 1))
        e="$(grep -Ev "^##" "$x")"
        echo -e "$e\n" >> menu.ipxe
        entry="$(echo "$e" | sed -n -e "s/[[:space:]]\+/ /g" -e "s/[[:space:]]*echo Booting //" -e 2p)"
        tag="$(echo "$e" | sed -n -e "s/://" -e 1p)"
        sed -i "s%\(\# END_INSTALLER_MENU.*\)%item $tag $entry\n\1%" menu.ipxe
    done
    [ $i -eq 0 ] && [ ! -d $TFTP_ROOT/$DI_PKG_DIR ] && rm menu.ipxe

    i=0
    if [ -f "pxelinux.cfg/default.serial-9600" ]; then
        i=$((i + 1))
        [ -f "$TEMPLATES/pxelinux.HEAD" ] && cat "$TEMPLATES/pxelinux.HEAD" >> pxelinux.cfg/default.serial-9600
        for x in "$STATUS_LIB/"*pxelinux.menu.serial-9600.fragment ; do
            grep -Ev "^##" "$x" >> pxelinux.cfg/default.serial-9600
            echo "" >> pxelinux.cfg/default.serial-9600
        done
        [ $i -eq 0 ] && rm pxelinux.cfg/default.serial-9600
    fi
    return 0
}


# ------------------------------------------------------------ #
# include_packages_live()
#       Create PXElinux bootloader menu for installed
#       debian-installer-*-netboot-* packages and debian-live iso images.
# Parameters: (NONE)
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
include_packages_live() {
    local x cfg ncfgdir gcfg ngcfg title relpath ISO_NAME AR
    if [ ! -e "$TFTP_ROOT/$N_A_DIR/pxelinux.0" ] ; then
        copy_syslinux_bin "$SYSLINUX" "$TFTP_ROOT/$N_A_DIR/" || \
            copy_syslinux_bin "${TFTP_ROOT}/${DI_PKG_DIR}" "$TFTP_ROOT/$N_A_DIR/" || \
            echo "E: No PXE binaries found and installed." 1>&2
    fi
    echo "I: Building menu entries for debian-installer-*-netboot-* packages and Debian Live images."

    echo -e "MENU SEPARATOR\n" >> pxelinux.cfg/default
    for x in "${TFTP_ROOT}/${DI_PKG_DIR}"/images/*/*/*/version.info ; do
        relpath="$(dirname "$x" | sed -e "s#${TFTP_ROOT}##" -e "s#^/*##")"
        title="$(tr -d "\n" < "$x" |sed -e "s#Installer##" -e "s# version: ##" \
                 -e "s#build: ##") $(echo "$relpath" | sed -re "s#^.+/(\w+/\w+)\$#\1#")"
        cat >> pxelinux.cfg/default <<EOF
LABEL $title
        MENU LABEL $title
        CONFIG ::${relpath}/pxelinux.cfg/default ::${relpath}/

EOF
        tag="$(echo "$title" | cut -d ' ' -f4,5 | sed "s/ /_/")"
        AR="$(echo "${relpath}" | cut -d / -f5)"
        cat >> menu.ipxe <<EOF
:$tag
    echo Booting $title
    kernel \${210:string}${relpath}/debian-installer/${AR}/linux initrd=initrd.gz
    initrd \${210:string}${relpath}/debian-installer/${AR}/initrd.gz
    boot

EOF
        sed -i "s%\(\# END_PKG_LIVE_MENU.*\)%item $tag $title\n\1%" menu.ipxe
        
        for AR in amd64 arm64 ; do
            gcfg="${TFTP_ROOT}/${relpath}/debian-installer/${AR}/grub/grub.cfg"
            if [ -f "$gcfg" ] ; then
                ## We do not want to modify the packaged installer images, copy grub.cfg instead:
                ngcfg="grub/grub-${relpath//'/'/'_'}.cfg"
                print_do_not_edit_header "$gcfg" > "${TFTP_ROOT}/$N_A_DIR/$ngcfg"
                sed -e "s#$REWRITEPKGPATH#$relpath/debian-installer#" \
                    -e "s#isolinux/splash.png#$relpath/debian-installer/${AR}/boot-screens/splash.png#" \
                    "$gcfg" >> "$ngcfg"
                cat >> grub/grub.cfg <<EOF
menuentry '$title' {
    configfile /$N_A_DIR/$ngcfg
}

EOF
            fi
        done
        echo "I:     • ${title}"
    done
#################
    echo -e "MENU SEPARATOR\n" >> pxelinux.cfg/default
    echo "I:    Building menu entries for Debian Live ISOs."
    for x in "${TFTP_ROOT}/${LIVE_DIR}"/*/.disk/info ; do
        relpath=$(dirname "$x" | sed -e "s#${TFTP_ROOT}##" -e "s#^/*##" -e "s#\/.disk##")
        # shellcheck disable=SC2034
        ISO_NAME=$(basename "$relpath")
        title=$(sed -e "s#Official ##" -e "s#T.*\$##" "$x")
        ## We cannot modify the iso images, copy grub.cfg instead:
        cfg="${TFTP_ROOT}/${relpath}/isolinux/menu.cfg"
        ncfgdir="live/pxe-${relpath//'/'/'_'}"
        mkdir -p "$ncfgdir"
        cp -a "${TFTP_ROOT}/${relpath}/isolinux/" "$ncfgdir"
        print_do_not_edit_header "$cfg" > "${TFTP_ROOT}/$N_A_DIR/$ncfgdir/isolinux/menu.cfg"
        sed -e "s#live#$relpath/live#" -e "s#isolinux#$relpath/isolinux#" \
            -e "s#quiet#quiet fetch=$(eval echo "${SQUASHIMG}")#" \
            -e "s#linux #kernel ::#" -e "s#initrd=#\n  append initrd=::#"  \
            "$cfg" >> "$ncfgdir/isolinux/menu.cfg"
        cat >> pxelinux.cfg/default <<EOF
LABEL $title
        MENU LABEL $title
        CONFIG ::/$N_A_DIR/$ncfgdir/isolinux/isolinux.cfg ::/$N_A_DIR/$ncfgdir/isolinux/

EOF

        tag="$(echo "$title" | cut -d ' ' -f4,5,6 | sed "s/ /_/g")"
        linux="$(find "${TFTP_ROOT}/${relpath}/live/" -name 'vmlinuz*' -exec basename '{}' \; )"
        initrd="$(find "${TFTP_ROOT}/${relpath}/live/" -name 'initrd*' -exec basename '{}' \; )"
        cat >> menu.ipxe <<EOF
:$tag
    echo Booting $title
    kernel \${210:string}${relpath}/live/$linux initrd=$initrd boot=live components fetch=\${210:string}${relpath}/live/filesystem.squashfs
    initrd \${210:string}${relpath}/live/$initrd
    boot

EOF
        sed -i "s%\(\# END_PKG_LIVE_MENU.*\)%item $tag $title\n\1%" menu.ipxe

        gcfg="${TFTP_ROOT}/${relpath}/boot/grub/grub.cfg"
        if [ -f "$gcfg" ] ; then
            ## We cannot modify the iso-images, copy grub.cfg instead:
            ngcfg="grub/grub-${relpath//'/'/'_'}.cfg"
            print_do_not_edit_header "$gcfg" > "${TFTP_ROOT}/$N_A_DIR/$ngcfg"
            sed -e "s#live#$relpath/live#" -e "s#isolinux#$relpath/isolinux#" \
                -e "s#\"\${loopback}\"#fetch=$(eval echo "${SQUASHIMG}")#" \
                "$gcfg" >> "$ngcfg"
            cat >> grub/grub.cfg <<EOF
menuentry '$title' {
    configfile /$N_A_DIR/$ngcfg
}

EOF
        fi
        echo "I:     • ${title}"
    done
#################
    echo "I:    Building menu entries for u-boot."
    for x in "${TFTP_ROOT}/${DI_PKG_DIR}"/images/*/*/*/boot.scr.uimg ; do
        if [ ! -L n-pkg ] ; then
            ln -s "${TFTP_ROOT}/${DI_PKG_DIR}" n-pkg
        fi
        ## u-boot pxe needs paths relative to the location of 'pxelinux.0'
        relpath=n-pkg/$(dirname "$x" | sed -e "s#${TFTP_ROOT}/${DI_PKG_DIR}##" -e "s#^/*##")
        title="Debian $(echo "$relpath" | sed -re "s#^.+/(\w+/\w+/\w+)\$#\1#")"
        cat >> pxelinux.cfg/default-arm <<EOF
LABEL ${title}
        MENU LABEL ${title}
        fdtdir $(find "$relpath" -type d -name dtbs)
        kernel $(find "$relpath" -type f -name vmlinuz)
        append ---
        initrd $(find "$relpath" -type f -name initrd.gz)

EOF
        echo "I:     • ${title}"
    done

    if [ -f pxelinux.cfg/default ]; then
        sed -n -e "s,^\s*KERNEL\s[\s:/]*\(.*menu.c32\).*,\1,p " \
                       -e "s,^\s*UI\s[\s:/]*\(.*menu.c32\).*,\1,p " \
                       pxelinux.cfg/default | sort -u | while read -r x ; do
            if [ ! -f "${TFTP_ROOT}/$x" ] ; then
                echo "W: The binary '${TFTP_ROOT}/$x' mentioned in the PXE boot menu is missing."
            fi
        done
    else
	# shellcheck disable=SC2086
        find pxelinux.cfg/ -print0 -iregex '.*\(\.c32\|\.bak.*\|~\)$' | \
            xargs -r rm $RM_VERBOSITY
        # shellcheck disable=SC2086
        [ -d pxelinux.cfg ] && rmdir $RM_VERBOSITY --ignore-fail-on-non-empty pxelinux.cfg
        # shellcheck disable=SC2086
        [ ! -d pxelinux.cfg ] && [ -e pxelinux.0 ] && rm $RM_VERBOSITY pxelinux.0
    fi
    return 0
}


# ------------------------------------------------------------ #
# check_tftp_root()
#       Check that declared TFTP root directory is valid.
# Parameters: (NONE)
# Returns: (NULL)
# ------------------------------------------------------------ #
check_tftp_root() {
    if [ -z "$TFTP_ROOT" ] || [ "$TFTP_ROOT" = "." ] || [ "$TFTP_ROOT" = "/" ]; then
        echo "E: Invalid TFTP root specified ($TFTP_ROOT)" 1>&2
        exit 1
    fi

    if [[ ! "$TFTP_ROOT" =~ ^/ ]] ; then
        echo "E: Invalid TFTP root specified ($TFTP_ROOT). Path needs to be absolute." 1>&2
        exit 1
    fi

    if [ ! -d "$TFTP_ROOT" ]; then
        echo "E: TFTP root directory doesn't exists ($TFTP_ROOT)" 1>&2
        echo "I: Make sure you installed a tftp server like tftpd-hpa or atftpd."
        exit 1
    fi

    [ ! -d "$TFTP_ROOT/$N_A_DIR" ] && mkdir -p "$TFTP_ROOT/$N_A_DIR"
    if [ ! -w "$TFTP_ROOT/$N_A_DIR" ]; then
        echo "E: Can't write to DI directory ($TFTP_ROOT/$N_A_DIR)" 1>&2
        exit 1
    fi
}


#This function should be kept in sync with function "uninstall_repo" in debian/postrm

# ------------------------------------------------------------ #
# uninstall_repo()
#       Remove the specfied repository.
# Parameters: dist_conf
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
uninstall_repo() {
    dist_conf="$1"                      # Repository's .conf file
    local s metadatabasename tarfile expand_dir dist_dir

    metadatabasename="${dist_conf%.conf}"
    expand_dir="$(grep -E "^[[:blank:]]*expand_dir=" "$dist_conf" | sed -e 's/^[[:blank:]]*expand_dir=//')"
    [ "$expand_dir" != "/" ] && [ -d "$expand_dir" ] && rm -Rf "$expand_dir"

    dist_dir=$(dirname "$expand_dir")
    # shellcheck disable=SC2086
    rmdir $RM_VERBOSITY --ignore-fail-on-non-empty "$dist_dir"

    s="$metadatabasename.pxelinux.menu.fragment"
    # shellcheck disable=SC2086
    [ -f "$s" ] && rm $RM_VERBOSITY "$s"

    s="$metadatabasename.uboot.menu.fragment"
    # shellcheck disable=SC2086
    [ -f "$s" ] && rm $RM_VERBOSITY "$s"

    s="$metadatabasename.grub.menu.fragment"
    # shellcheck disable=SC2086
    [ -f "$s" ] && rm $RM_VERBOSITY "$s"

    s="$metadatabasename.ipxe.menu.fragment"
    # shellcheck disable=SC2086
    [ -f "$s" ] && rm $RM_VERBOSITY "$s"

    s="$metadatabasename.pxelinux.menu.serial-9600.fragment"
    # shellcheck disable=SC2086
    [ -f "$s" ] && rm $RM_VERBOSITY "$s"

    # shellcheck disable=SC2086
    rm $RM_VERBOSITY "$dist_conf"
    return 0
}

# ------------------------------------------------------------ #
# get_installed_repos()
#       List the installed repositories.
# Parameters: none
# Returns: (STRINGS) Installed repos
# ------------------------------------------------------------ #
get_installed_repos() {
    find  "$STATUS_LIB/"  -name \*--\*.conf \
        | sed -e 's,^.*/,,' -e 's/--.*\.conf//' \
        | uniq | tr '\n' ' '
}


# ------------------------------------------------------------ #
# uninstall_repos()
#       Remove the specfied repository for all specified archs.
# Parameters: repo ignore_missing
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
uninstall_repos() {
    local repo="$1"                     # Name of the repository
    local ignore_missing="$2"   # Don't repo.
    local a archs installed_archs installed_repos

    $DEBUG && set -x

    if [ ! -d "$STATUS_LIB" ]; then
        echo "E: Failed to uninstall repository, lib folder not found." 1>&2
        exit 1
    fi

    installed_archs="$(find  "$STATUS_LIB/"  -name \*"$repo"--\*.conf | sed -e 's/^.*--//' -e 's/\.conf//' | tr '\n' ' ')"
    if [ ! "$installed_archs" ] && [ "$ignore_missing" != "ignore_missing" ]; then
        installed_repos="$(get_installed_repos)"
        echo "E: Repository '$repo' not installed." 1>&2
        echo -e "E: Installed repositories are:\n${installed_repos}" 1>&2
        exit 1
    fi

    [ ! "$ARCH" ] && ARCH=$DEFAULT_ARCH

    if echo "$ARCH" | grep -qE "\<all\>" ; then
        archs="$installed_archs"
    else
        archs="$(echo "$ARCH" | tr ',' ' ')"
    fi

    for a in $archs ; do
        if [ -f "$STATUS_LIB/$repo--$a.conf" ]; then
            uninstall_repo "$STATUS_LIB/$repo--$a.conf"
        else
            if [ "$ignore_missing" != "ignore_missing" ]; then
                echo "E: Repository '$repo' for architecture '$a' doesn't exists." 1>&2
                echo -e "E: Installed are:\n$(echo "$installed_archs" | tr "\n" " ")" 1>&2
                return 1
            fi
        fi
    done

    $DEBUG && set +x
    return 0
}


#This function should be kept in sync with function "url2filename" in debian/postrm

# ------------------------------------------------------------ #
# url2filename()
#       Convert an URL into a valid filename.
# Parameters: (PIPE) url
# Returns: (STRING) filename
# ------------------------------------------------------------ #
url2filename() {
    sed -e 's#//\+#/#g' -e 's#[^[:alnum:]@+_~\.-]#_#g'
}


#This function should be kept in sync with function "remove_repocache" in debian/postrm

# ------------------------------------------------------------ #
# remove_repocache()
#       Remove the cached file.
# Parameters: metadatafile
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
remove_repocache() {
    local metadatafile="$1"             # repository to uncache
    local base file

    base="${metadatafile%~~*}"

    sed -n -e 's/^[[:blank:]]*dl_file=[[:blank:]]*//p' "$metadatafile" | while read -r file ; do
        # shellcheck disable=SC2086
        rm $RM_VERBOSITY "${base}_$(echo "$file" | url2filename)"
    done

    #Purge remaing files (MD5SUMs...) if there are no more cached
    #distribution from the same repository.
    if ! echo "${base}~~"*.meta | grep -qv "$metadatafile" ; then
        # shellcheck disable=SC2086
        rm $RM_VERBOSITY "${base}"_*
    fi

    # shellcheck disable=SC2086
    [ -f "$metadatafile" ] && rm $RM_VERBOSITY "$metadatafile"
    return 0
}


# ------------------------------------------------------------ #
# remove_repocaches()
#       Remove the cached file.
# Parameters: metadatafile
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
remove_repocaches() {
    local del_repo="$1"
    local ignore_missing="$2"
    local count cached_archs archs metadatafile a

    $DEBUG && set -x

    if [ ! -d "$DL_CACHE" ]; then
        echo "E: Failed to clean the cache, cache folder not found." 1>&2
        exit 1
    fi

    cached_archs="$(find $DL_CACHE -name "*~~$del_repo--*.meta" | sed -e 's/^.*--//' -e 's/\.meta//' | tr '\n' ' ')"
    if [ ! "$cached_archs" ] && [ "$ignore_missing" != "ignore_missing" ]; then
        cached_repos="$(find $DL_CACHE -name \*~~\*--\*.meta | sed -e 's/^.*~~//' -e 's/--.*//' | tr '\n' ' ')"
        echo "E: Repository '$del_repo' not cached." 1>&2
        echo "I: (cached repositories are: $cached_repos)" 1>&2
        exit 1
    fi

    [ ! "$ARCH" ] && ARCH=$DEFAULT_ARCH

    if echo "$ARCH" | grep -qE "\<all\>" ; then
        archs="$cached_archs"
    else
        archs="$(echo "$ARCH" | tr ',' ' ')"
    fi

    for a in $archs ; do
        count=0
        for metadatafile in "$DL_CACHE"/*~~"${del_repo}--${a}".meta ; do
            remove_repocache "$metadatafile"
            count=$(( count + 1 ))
        done

        if [ $count -eq 0 ] && [ "$ignore_missing" != "ignore_missing" ]; then
            echo "E: Repository '$del_repo' for architecture '$a' doesn't exists." 1>&2
            echo "I: (cached archs are: $cached_archs)" 1>&2
            exit 1
        fi
    done

    $DEBUG && set +x
    return 0
}


# ------------------------------------------------------------ #
# check_sum()
#       Validate a file's checksum.
# Parameters: csum_file fname actual_file
# Returns: (EXIT STATUS) 0=checksum is ok, 1=checksum mismatch
# ------------------------------------------------------------ #
check_sum() {
    local csum_file=$1          # file containing checksums
    local fname=$2              # file to look for in the checksum file
    local actual_file=$3        # file to calulate checksum from
    local sum regex

    if [ ! -f "$actual_file" ] || [ ! -f "$csum_file" ]; then
        return 1
    fi
    sum=$($CHECKSUM_BIN "$actual_file" | cut -d " " -f 1)
    $VERBOSE && echo -e "I: $CHECKSUM_BIN of '$actual_file':\n$sum" 1>&2
    regex="^[[:blank:]]*${sum}[[:blank:]]+([[:digit:]]+[[:blank:]]+)?(\./|)${fname}[[:blank:]]*$"
    if ! grep -qiE "$regex" "$csum_file" ; then
        $VERBOSE && echo "Checksum not found in '$csum_file'." 1>&2
        return 1
    elif $VERBOSE ; then
        echo "I: Checksum found in '$csum_file':"
        grep -iE "$regex" "$csum_file"
    fi
    return 0
}


# ------------------------------------------------------------ #
# check_signature()
#       Validate signature of checksum file.
# Parameters: download URL
# Returns: (EXIT STATUS) 0=signature is ok,
#                        1=signature wrong,
#                        other error codes for fatal errors
# ------------------------------------------------------------ #
check_signature() {
    local file="$1"       # downloaded checksum file
    local getter="$2"     # download program
    local baseurl="$3"    # URL of the checksum file directory
    local s dmp ret=4

    dmp=$(mktemp)

    if [[ "$url" == *"ubuntu"* ]] ; then
        if $getter "$file.gpg" -- "${baseurl}/${CHECKSUM_FILE}.gpg" ; then
            # shellcheck disable=SC2086
            LANG=C gpgv $GPG_VERBOSITY --keyring "$UBUNTU_KEYRING" "$file.gpg" "$file" > "$dmp" 2>&1
            ret=$?
        else
            echo "E: Could not download '${baseurl}/${CHECKSUM_FILE}.gpg'." 1>&2
            ret=3
        fi
    else
        if $getter "$file.Release" -- "${baseurl}/../../../../Release" && \
                $getter "$file.Release.gpg" -- "${baseurl}/../../../../Release.gpg" ; then
            s=$(echo "${baseurl}" | sed -E "s#.+/(.+/.+/.+/.+)\$#\1#" )
            if check_sum "$file.Release" "${s}/${CHECKSUM_FILE}" "$file" ; then
                # shellcheck disable=SC2086
                LANG=C gpgv $GPG_VERBOSITY --keyring $DEBIAN_KEYRING \
                     "$file.Release.gpg" "$file.Release" > "$dmp" 2>&1
                ret=$?
            else
                ret=3
            fi
        else
            if $IGNOR_SIG ; then
                echo "W: Could not download '${baseurl}/../../../../Release' and/or 'Release.gpg'." 2>&1
            else
                echo "E: Could not download '${baseurl}/../../../../Release' and/or 'Release.gpg'." 1>&2
            fi
            ret=3
        fi
    fi

    $VERBOSE && cat "$dmp"
    grep "Good signature\|Can't check" "$dmp" | sed "s/gpgv/I/"

    if grep -q "^gpgv: Good signature" "$dmp" && [ $ret != 0 ] ; then
        echo "W: Not all signatures could be verified:" 2>&1
        sed 's/.*/      &/' "$dmp"
        echo "   Perhaps an outdated public key has been removed from the keyring." 2>&1
        ret=0
    fi

    [ -f "$dmp" ] && rm "$dmp"
    # shellcheck disable=SC2086
    [ -f "$file.gpg" ] && rm $RM_VERBOSITY "$file.gpg"
    # shellcheck disable=SC2086
    [ -f "$file.Release" ] && rm $RM_VERBOSITY "$file.Release"
    # shellcheck disable=SC2086
    [ -f "$file.Release.gpg" ] && rm $RM_VERBOSITY "$file.Release.gpg"

    return $ret
}


# ------------------------------------------------------------ #
# fetch_files()
#       Download netboot image(s) and save them in the cache.
# Parameters: relase arch baseurl repo_loc tarfile
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
fetch_files() {
    local release="$1"          # Release (or variant)
    local arch="$2"                     # Architecture
    local baseurl="$3"          # Download base URL
    local repo_loc="$4"         # Destination dir
    local tarfile="$5"          # File to download
    local getter file givenfile metadatafile success f
    local csum_file url cached fetch_date

    $DEBUG && set -x

    if ! $OFFLINE ; then
        if command -v wget > /dev/null ; then
            getter="wget -c -x $WGET_VERBOSITY -O"
        elif command -v curl > /dev/null ; then
            getter="curl --location --fail $CURL_VERBOSITY -o"
        else
            echo "E: Can't download file. No download program (wget or curl) found." 1>&2
            return 1
        fi
    fi

    metadatafile="$DL_CACHE/$(echo "${repo_loc}~~${release}--${arch}.meta" | url2filename)"

    success=true
    csum_file=/dev/null
    for givenfile in $CHECKSUM_FILE $tarfile ; do
        f=$(echo "$givenfile" | sed -e 's#^\./##' -e 's#^/##' -e 's#//#/#g')
        url="$baseurl/$f"
        file="$DL_CACHE/$(echo "$repo_loc/$f" | url2filename)"
        [ -e "$file.tmp" ] && rm "$file.tmp"

        # Does the checksum of the previous file match the new one?
        cached=false
        if [ "$givenfile" = "$CHECKSUM_FILE" ]; then
            csum_file="$file.tmp"
            $OFFLINE && cp "$file" "$file.tmp"
        elif check_sum "$csum_file" "$givenfile" "$file" ; then
            cached=true
            cp "$file" "$file.tmp"
            echo "I: File $givenfile is already cached."
        else
            [ -f "$file.tmp" ] && rm "$file.tmp"
            $VERBOSE && ! $OFFLINE && echo "I: File '$givenfile' not cached, or obsolete."
            $OFFLINE && success=false
        fi

        # Download the file, if needed.
        if ! $OFFLINE && ! $cached ; then
            echo "I: Downloading '$givenfile'."
            if $getter "$file.tmp" -- "$url" ; then
                if [ "$givenfile" = "$CHECKSUM_FILE" ] ; then
                    $VERBOSE && echo "Verify signature for '$url':"
                    if ! check_signature "$file.tmp" "$getter" "$baseurl" ; then
                        cat <<EOF

  * * * * * * * * WARNING * * * * * * * *
  *                                     *
  *   Could not verify/find signature.  *
  *                                     *
  * * * * * * * * * * * * * * * * * * * *

EOF
                        if $IGNOR_SIG ; then
                            echo "W: Ignoring failed verification."
                            inp="y"
                        else
                            read -r -e -n 1 -p "      Download and install the image/file anyway? [y|N]: " inp
                        fi
                        inp=${inp:-N}
                        if [ "$inp" != "y" ] && [ "$inp" != "Y" ] ; then
                            echo -e "\nI: Installation canceled."
                            success=false
                            break
                        else
                            echo "I: Continuing image/file installation."
                        fi
                    fi
                elif check_sum "$csum_file" "$givenfile" "$file.tmp" ; then
                    echo "I: Checksum verification succeeded for '$url'."
                    fetch_date="$(date -R)"
                else
                    echo "E: Checksum verification failed for '$url'." 1>&2
                    success=false
                    break
                fi
            else
                echo "E: Can't download '$release' for '$arch' ($url)." 1>&2
                if [ -f "$file" ]; then
                    echo "I: You have a previous version in your cache (see --offline option)."
                fi
                success=false
                break
            fi
        else
            if [ ! -f "$file.tmp" ]; then
                success=false
                echo "E: Can't process '$release' in offline mode, the file is missing:" 1>&2
                echo "E: (expecting '$file' from '$url')" 1>&2
                break
            elif [ -f "$metadatafile" ] ; then
                fetch_date="$( grep "^fetch_date=" "$metadatafile" | \
                cut -d "=" -f 2- 2>/dev/null )"
            else
                # Fall back, in case the file is manually added to the cache.
                fetch_date="$(date -R --reference="$file.tmp" )"
            fi
        fi
    done

    $VERBOSE && echo "I: Moving and/or removing temporary file(s):"
    for givenfile in $CHECKSUM_FILE $tarfile ; do
        f=$(echo "$givenfile" | sed -e 's#^\./##' -e 's#^/##' -e 's#//#/#g')
        file="$DL_CACHE/$(echo "$repo_loc"/"$f" | url2filename)"
        if $success ; then
            # shellcheck disable=SC2086
            [ -f "$file.tmp" ] && mv $MV_VERBOSITY "$file.tmp" "$file"
        else
            # shellcheck disable=SC2086
            [ -f "$file.tmp" ] && rm $RM_VERBOSITY "$file.tmp"
        fi
    done

    # Save metadata
    if $success ; then
        if ! $OFFLINE ; then
	    { echo "#$PACKAGE_NAME for '$release' ($arch)" ; echo "format=1.0" ; \
            echo "fetch_date=$fetch_date" ; echo "repo=$baseurl" ; echo "dl_file=$tarfile" ; \
	    echo "dist=$release" ; } > "$metadatafile"
        fi
    else
        return 1
    fi

    $DEBUG && set +x
    return 0
}


# ------------------------------------------------------------ #
# extract_files()
#       Extract (or copy) netboot image.
# Parameters: repo_loc file expand_dir
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
extract_files() {
    local repo_loc=$1           # Repository URL
    local file=$2               # Downloaded (tar) file
    local expand_dir=$3         # Target location fo extracted files
    local arch=$4               # Architecture
    local dl_file tar_opts nstrp

    file=$(echo "$file" | sed -e 's#^\./##' -e 's#^/##' -e 's#//#/#g')
    dl_file="$DL_CACHE/$(echo "$repo_loc/$file" | url2filename)"

    echo "I: Extracting '$dl_file'."

    if [ -d "$expand_dir" ]; then
        rm -Rf "${expand_dir:?}/*"          # get existing metadata from the downloaded repository
    else
        mkdir -p "$expand_dir"
    fi
    :
    tar_opts=("$dl_file" --directory "$expand_dir" --no-same-permissions)
    ## Some older archives have no leading './' directory (arch dependent):
    if tar --list -f "$dl_file" './' >/dev/null 2>&1 ; then
        nstrp=3
    else
        nstrp=2
    fi
    case "$(basename "$dl_file")" in
        *.tar.gz)
            # shellcheck disable=SC2086
            tar $TAR_VERBOSITY -zxf "${tar_opts[@]}" --strip-components "$nstrp" --exclude "./pxelinux.0" --exclude "./pxelinux.cfg"
            # shellcheck disable=SC2086
            tar $TAR_VERBOSITY -zxf "${tar_opts[@]}" ./version.info 2>/dev/null || true
            ;;
        *.tar.bz2)
            # shellcheck disable=SC2086
            tar $TAR_VERBOSITY -jxf "${tar_opts[@]}" --strip-components "$nstrp" --exclude "./pxelinux.0" --exclude "./pxelinux.cfg"
            # shellcheck disable=SC2086
            tar $TAR_VERBOSITY -jxf "${tar_opts[@]}" ./version.info 2>/dev/null || true
            ;;
        *.img)
            # shellcheck disable=SC2086
            cp $CP_VERBOSITY "$dl_file" "$expand_dir/$(basename "$file")"
            ;;
        *)
            echo "E: Don't know how to handle (unpack...) the file: $dl_file" 1>&2
            return 1
            ;;
    esac
    return 0
}


# ------------------------------------------------------------ #
# pxelinux_version()
#       Retrieve PXElinux version.
# Parameters: bin
# Returns: (STRING) PXElinux version
# ------------------------------------------------------------ #
pxelinux_version() {
    local bin="$1"              # pxelinux.0 file

    if [ -f "$bin" ]; then
        tr -c '[:print:] ' '\n' < "$bin" | sed -n -r "/PXELINUX [.0-9]+/ s/^[^ ]* ([0-9^.]+).*/\1/ p" | sort -r | head -n 1
    else
        echo ""
    fi
}


# ------------------------------------------------------------ #
# tweak_syslinux_arguments()
#       Tweak the kernel arguments in pxelinux configuration
#       files.
# Parameters: (PIPE) pristine configuration file
# Returns: (STRING) tweaked configuration file
# ------------------------------------------------------------ #
tweak_syslinux_arguments() {
    sed -e "/^[[:blank:]]*label[[:blank:]]\+install\$/I,/^\([[:blank:]]*label[[:blank:]]^+[^\(install\)]\|[[:blank:]]*\)\$/I{s!append \(.*\)--\(.*\)!append \1 $DI_ARGS -- \2 $TARGET_ARGS!}"
}


# ------------------------------------------------------------ #
# setup_syslinux()
#       Install and configure syslinux menu.
# Parameters: (PIPE) release arch metadatabasename expand_dir
# Returns: (EXIT STATUS) 0
# ------------------------------------------------------------ #
setup_syslinux() {
    local release=$1                    # D-I image release name
    local arch=$2                       # Architecture
    local metadatabasename=$3   # metadata location
    local expand_dir=$4         # Target installation dir.
    local pxelinuxbin pxeuboot pxelinuxcfg ver f fd dist title
    local menufragment menufragment_grub menufragment_ipxe menufragment_uboot menufragment_serial9600

    ## generate title for menu entries:
    if grep "^repo=" "${metadatabasename}.conf" | grep -q ubuntu ; then
        dist="Ubuntu"
    else
        dist="Debian"
    fi
    fd=$(grep fetch_date "${metadatabasename}.conf" | sed "s/fetch_date=//")
    fd=$(date --date="$fd" "+%Y%m%d  %R")
    title=$(printf "%-35s ${fd}\n" "$dist ${REPO_ALIAS} ($arch)")

    ## generate u-boot pxe menu fragment if device tree is available:
    pxeuboot="$(find "$expand_dir" -type d -name "dtbs" 2>/dev/null )"
    if [ "$pxeuboot" ]  ; then
        [ ! -d "$TFTP_ROOT/$N_A_DIR/pxelinux.cfg" ] && mkdir "$TFTP_ROOT/$N_A_DIR/pxelinux.cfg"
        menufragment_uboot="$metadatabasename.uboot.menu.fragment"
        cat > "$menufragment_uboot" <<EOF
## This is a fragment of the pxe u-boot menu file.
##
## DO NOT EDIT THIS FILE
##
## It is automatically generated by $PACKAGE_NAME
##
LABEL ${REPO_ALIAS}-$arch
        MENU LABEL ${title}
        fdtdir $REPO_ALIAS/$arch/dtbs/
        kernel $REPO_ALIAS/$arch/vmlinuz
        append ---
        initrd $REPO_ALIAS/$arch/initrd.gz

EOF
    fi

    ## prepare pxelinux if pxelinux.0 is available:
    pxelinuxbin="$(find "$expand_dir" -type f -name "pxelinux.0" 2>/dev/null )"
    if [ "$pxelinuxbin" ]; then
        pxelinuxcfg="${pxelinuxbin%%.0}.cfg/default"
        ver="$(sed -ne 's/# D-I config version \(.*\)/\1/p' "$pxelinuxcfg" 2>/dev/null)"
        if [ ! -f "$pxelinuxcfg" ] || echo "${ver:-1.0}" | grep -q -v "^[12]\.0" ; then
            echo "W: The format of this image may not be supported." 1>&2
        fi
        [ ! -d "$TFTP_ROOT/$N_A_DIR/pxelinux.cfg" ] && mkdir "$TFTP_ROOT/$N_A_DIR/pxelinux.cfg"

        copy_syslinux_bin "$SYSLINUX" "$TFTP_ROOT/$N_A_DIR/" || \
            copy_syslinux_bin "$expand_dir" "$TFTP_ROOT/$N_A_DIR/" || \
            echo "E: No PXE binaries installed. Please file a bug." 1>&2

        # ensure only a single PXELINUX version is used for all its modules
        for f in "$expand_dir"/**/*.c32 ; do
            case $(basename "$f") in
                vesamenu.c32|menu.c32)
                    cp -pft "$(dirname "$f")" "$TFTP_ROOT/$N_A_DIR/pxelinux.cfg/$(basename "$f")"
                    ;;
                ldlinux.c32|libcom32.c32|libutil.c32)
                    cp -pft "$(dirname "$f")" "$TFTP_ROOT/$N_A_DIR/$(basename "$f")"
                    ;;
                *)
                    echo "W: Unusual PXELINUX module \"$f\" may not work." 1>&2
                    continue
                    ;;
            esac
        done
    fi

    for f in "$expand_dir"/**/default "$expand_dir"/**/boot.txt "$expand_dir"/**/*.cfg ; do
        [ -f "$f" ] || continue
        mv "$f" "$f.ORIG"
        print_do_not_edit_header "" > "$f"
        if [ "$(basename "$f")" = "grub.cfg" ] ; then
            sed -e "s#$REWRITEPKGPATH/$arch/#$N_A_DIR/$REPO_ALIAS/$arch/#" \
                "$f.ORIG" >> "$f"
        else
            sed -e "s#$REWRITEPKGPATH/$arch/#::/$N_A_DIR/$REPO_ALIAS/$arch/#" \
                -e "s/^\([[:space:]]*default .*$\)/\#\1/" \
                -e "s/^\([[:space:]]*menu default[[:space:]]*$\)/\#\1/" \
                "$f.ORIG" | tweak_syslinux_arguments >> "$f"
        fi
    done
    menufragment="$metadatabasename.pxelinux.menu.fragment"
    menufragment_grub="$metadatabasename.grub.menu.fragment"
    menufragment_ipxe="$metadatabasename.ipxe.menu.fragment"
    menufragment_serial9600="$metadatabasename.pxelinux.menu.serial-9600.fragment"
    cat > "$menufragment" <<EOF
## This is a fragment of syslinux/pxelinux/grub/ipxe menu file.
##
## DO NOT EDIT THIS FILE
##
## It is automatically generated by $PACKAGE_NAME
##
EOF
    cp "$menufragment" "$menufragment_grub"
    cp "$menufragment" "$menufragment_ipxe"
    if [ "$(find "$expand_dir" -type d -name pxelinux.cfg.serial-9600 2>/dev/null)" ]; then
        cp "$menufragment" "$menufragment_serial9600"
    else
        [ -f "$menufragment_serial9600" ] && rm "$menufragment_serial9600"
    fi

    # Create top-menu fragment:
    cat >> "$menufragment" <<EOF
MENU BEGIN ${REPO_ALIAS}-$arch
        MENU TITLE $title
        LABEL ${REPO_ALIAS}-$arch
            MENU LABEL ^Back..
            MENU EXIT
        INCLUDE ::/$N_A_DIR/$REPO_ALIAS/$arch/boot-screens/menu.cfg
MENU END

EOF

    cat >> "$menufragment_ipxe" <<EOF
:${REPO_ALIAS}-$arch
    echo Booting $title
    kernel \${210:string}$N_A_DIR/$REPO_ALIAS/amd64/linux initrd=initrd.gz
    initrd \${210:string}$N_A_DIR/$REPO_ALIAS/amd64/initrd.gz
    boot

EOF

    if [ "$dist" != "Ubuntu" ] ; then
        cat >> "$menufragment_grub" <<EOF
menuentry '$title' {
    configfile /$N_A_DIR/$REPO_ALIAS/$arch/grub/grub.cfg
}

EOF
    else
        cat >> "$menufragment_grub" <<EOF
menuentry '${title}' {
    linux /$N_A_DIR/$REPO_ALIAS/amd64/linux
    initrd /$N_A_DIR/$REPO_ALIAS/amd64/initrd.gz
}

EOF
    fi

    if [ -f "$menufragment_serial9600" ]; then
        (
            echo   "LABEL ${REPO_ALIAS}-$arch"
            printf "    MENU LABEL Debian Installer %-26s [SUB-MENU]\n" "($REPO_ALIAS, $arch)"
            echo   "    KERNEL ::/$N_A_DIR/pxelinux.cfg/menu.c32"
            echo   "    APPEND ::/$N_A_DIR/$REPO_ALIAS/$arch/pxelinux.cfg.serial-9600/default"
        ) >> "$menufragment_serial9600"
    fi
    return 0
}


# ------------------------------------------------------------ #
# install_repo_for_arch()
#       Extract/copy the downloaded file for given arch.
# Parameters: arch release
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
install_repo_for_arch() {
    local arch=$1                       # Architecture
    local release=$2            # D-I image release name
    local reg metadatabasename file fetch_date
    local metadatafile repo_orig repo_mirror expand_dir

    echo "I: Processing $release/$arch."
    $DEBUG && set -x

    metadatabasename="$STATUS_LIB/${REPO_ALIAS}--${arch}"
    metadatafile="$metadatabasename.conf"

    repo_orig="$(grep -E "^${release}[[:blank:]]$arch\>" "$DISOURCELIST")"
    repo_mirror="$repo_orig"
    for reg in $MIRROR_REGEXPS "s=/$==" ; do
        repo_mirror="$(echo "$repo_mirror" | sed -e "$reg")"
    done

    repo="$(echo "$repo_orig" | cut -f 3 | sed -e 's#\([^:]/\)/#\1#g' -e 's#/$##' )"
    repo_mirror="$(echo "$repo_mirror" | cut -f 3 | sed -e 's#\([^:]/\)/#\1#g' -e 's#/$##' )"

    if [ -z "$repo" ]; then
        echo "E: There is no entry declared for architecture '$arch' for repository '$release' in $DISOURCELIST" 1>&2
        list_declared_arch_for_repo "$release"
        return 1
    fi

    if [ "$release" != "$REPO_ALIAS" ] ; then
        echo "I: Repository '$release' filed as '$REPO_ALIAS'."
    fi

    repo_loc="${repo##*://}"
    expand_dir="$TFTP_ROOT/$N_A_DIR/$REPO_ALIAS/$arch"

    file=$(grep -E "^${release}[[:blank:]]$arch" "$DISOURCELIST" | cut -f 4- )
    fetch_date=""
    if ! fetch_files "$release" "$arch" "$repo_mirror" "$repo_loc" "$file" ; then
        return 1
    fi
    if ! extract_files "$repo_loc" "$file" "$expand_dir" "$arch" ; then
        return 1
    fi

    # save metadata of this repository
    grep -v -E "^format=.*" \
         "$DL_CACHE/$(echo "${repo_loc}~~${release}--${arch}.meta" | url2filename)" \
        | sed -e "s/^fetch_date=/format=1.0\n\0/" > "$metadatafile"
    { echo "expand_dir=$expand_dir" ; echo "di_args=$DI_ARGS" ; echo "target_args=$TARGET_ARGS" ; } >> "$metadatafile"

    # PXELINUX/GRUB MENUs
    setup_syslinux "$release" "$arch" "$metadatabasename" "$expand_dir"

    $DEBUG && set +x
    return 0
}


# ------------------------------------------------------------ #
# toggle_fw_for_arch()
#       Add/remove non-free firmware for given arch.
# Parameters: arch release
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
toggle_fw_for_arch() {
    local arch=$1                       # Architecture
    local release=$2            # D-I image release name
    local reg file fetch_date repo_orig repo_mirror expdirs expand_dir

    echo "I: Processing non-free firmware for $release/$arch."
    if [ "$release" != "$REPO_ALIAS" ] ; then
        echo "I: Repository '$release' filed as '$REPO_ALIAS'."
    elif [ "$release" = "daily" ] ; then
        release="sid"
    fi

    $DEBUG && set -x

    if [ "$release" = "n-pkg" ] ; then
        expdirs="$(find "$TFTP_ROOT/$DI_PKG_DIR/" -name 'initrd.gz' | \
                     grep "$arch" | rev |  cut -f2- -d '/' | rev)"
        release="stable"
    else
        expdirs="$TFTP_ROOT/$N_A_DIR/$REPO_ALIAS/$arch"
    fi

    repo_mirror="$NONFREE_FW/$release/current"
    repo_loc="${repo_mirror##*://}"

    for expand_dir in $expdirs ; do
        echo "I: Processing '$expand_dir/'."
        if [ -f "$expand_dir/initrd.gz.orig" ] && [ -f "$expand_dir/initrd.gz.fw" ] && \
               cmp --silent "$expand_dir/initrd.gz" "$expand_dir/initrd.gz.fw" ; then
            ## → The initrd is the original with firmware added. (It has not been overwritten.)
            echo "I: Removing non-free firmware."
            # shellcheck disable=SC2086
            cp -p $CP_VERBOSITY "$expand_dir/initrd.gz.orig" "$expand_dir/initrd.gz"
        else
            ## → The initrd has either been overwritten, or it is the (old) original.
            file="firmware.cpio.gz"
            fetch_date=""
            if ! fetch_files "$release" "$arch" "$repo_mirror" "$repo_loc" $file ; then
                return 1
            fi
            ## Ignore the missing signature of the fw file from now on:
            IGNOR_SIG=true
            echo "I: Adding non-free firmware."
            fw_file="$DL_CACHE/$(echo "$repo_loc/$file" | url2filename)"
            # shellcheck disable=SC2086
            cp -p $CP_VERBOSITY "$expand_dir/initrd.gz" "$expand_dir/initrd.gz.orig"
            cat "$expand_dir/initrd.gz.orig" "$fw_file" > "$expand_dir/initrd.gz.fw"
            # shellcheck disable=SC2086
            cp -p $CP_VERBOSITY "$expand_dir/initrd.gz.fw" "$expand_dir/initrd.gz"
        fi
    done
    $DEBUG && set +x
    return 0
}


# ------------------------------------------------------------ #
# install_repo_for_archs()
#       Extract/copy the downloaded file for specified archs.
# Parameters: release
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
install_repo_for_archs() {
    local release="$1"          # release name to install
    local archs ret

    if [ -z "$release" ]; then
        echo "E: No repository specified (valid repositories are: ${releases})." 1>&2
        return 1
    fi

    if ! grep -Eq "^$release\>" "$DISOURCELIST" ; then
        echo "E: Invalid repository name specified ($release)." 1>&2
        echo -e "E: Declared repositories are:\n${releases}" 1>&2
        return 1
    fi

    [ ! "$ARCH" ] && ARCH=$DEFAULT_ARCH

    if echo "$ARCH" | grep -qE "\<all\>" ; then
        archs="$(get_declared_arch_for_repo "$release")"
    else
        archs="$(echo "$ARCH" | tr ',' ' ')"
    fi

    for arch in $archs ; do
        install_repo_for_arch "$arch" "$release"
        ret=$?
        if [ $ret != 0 ] ; then
            return $ret
        fi
    done
    return 0
}

# ------------------------------------------------------------ #
# toggle-firmware()
#       Add/remove non-free firmware to/from the initrd.
# Parameters: release
# Returns: (EXIT STATUS) 0=Success, 1=Error
# ------------------------------------------------------------ #
toggle-firmware() {
    local release="$1"          # release name to install
    local archs ret

    [ ! "$ARCH" ] && ARCH=$DEFAULT_ARCH
    if echo "$ARCH" | grep -qE "\<all\>" ; then
        if [ "$release" != "n-pkg" ] ; then
            archs="$(get_declared_arch_for_repo "$release")"
        else
            archs="$(find "$TFTP_ROOT/$DI_PKG_DIR/" -name 'initrd.gz' | rev |  cut -f2 -d '/' | rev | uniq)"
        fi
    else
        archs="$(echo "$ARCH" | tr ',' ' ')"
    fi

    for arch in $archs ; do
        if [ "$release" != "n-pkg" ] && [ ! -d "$TFTP_ROOT/$N_A_DIR/$REPO_ALIAS/$arch" ] ; then
            echo "E: Repository '$REPO_ALIAS/$arch' is not available." 1>&2
            return 1
        fi
        toggle_fw_for_arch "$arch" "$release"
        ret=$?
        if [ $ret != 0 ] ; then
            return $ret
        fi
    done
    return 0
}

# ---------------------------  Main  ------------------------- #

ACTION=
COUNT=0

for option in "$@"; do
    case "$option" in
        -h | --help)
            usage
            exit 0 ;;
        -V | --version)
            echo "$PACKAGE_NAME $PACKAGE_VERSION"
            exit 0 ;;
        --arch=*)
            ARCH="$(echo "$option" | sed -e 's/--arch[=]\?//' -e 's/,,/,/' -e 's/^,\+//' -e 's/,\+$//' )"
            #Note:
            if echo "$ARCH" | grep -qE '^[[:alnum:]_,]\+$' ; then
                echo "E: Invalid architecture specified ($ARCH)" 1>&2
                exit 1
            elif [ -z "$ARCH" ] ; then
                ARCH=EMPTY
            fi
            ;;
        --alias=*)
            CLI_ALIAS="$(echo $option | sed -e 's/--alias[=]\?//' | grep -E "^[[:alnum:]_-]+$" )"
            #Note:
            if echo "$CLI_ALIAS" | grep -qE '^[[:alnum:]_,]\+$' ; then
                echo "E: Invalid alias name ($option)" 1>&2
                exit 1
            fi ;;
        --di-args=*)
            DI_ARGS="$DI_ARGS ${option#--di-args=}"
            ;;
        --ignore-sig)
            IGNOR_SIG=true ;;
        --target-args=*)
            TARGET_ARGS="$TARGET_ARGS ${option#--target-args=}"
            ;;
        --tftproot=*)
            TFTP_ROOT="${option#--tftproot=}"
            ;;
        --offline)
            OFFLINE=true ;;
        -v | --verbose)
            VERBOSE=true
            ;;
        --debug)
            # This is an undocumented feature...
            DEBUG=true ;;
        --di-args| --target-args)
            echo "E: Option $option requires a value after equal sign." 1>&2
            exit 1
            ;;
        -*)
            echo "E: Unrecognized option ($option)" 1>&2
            exit 1
            ;;
        rebuild-menu|install|uninstall|uncache|purge|fw-toggle)
            #Actions are processed in the loop below
            if [ "$ACTION" ]; then
                echo "E: Unexpected command '$option'. '$ACTION' was already specified." 1>&2
                exit 1
            fi
            ACTION=$option
            ;;
        *)
            COUNT=$(( COUNT + 1 ))
            ;;
    esac
done

if $VERBOSE; then
    WGET_VERBOSITY=""
    CURL_VERBOSITY=""
    RM_VERBOSITY="${RM_VERBOSITY:=-v}"
    MV_VERBOSITY="${MV_VERBOSITY:=-v}"
    CP_VERBOSITY="${CP_VERBOSITY:=-v}"
    TAR_VERBOSITY="${TAR_VERBOSITY:=-v}"
    GPG_VERBOSITY="${GPG_VERBOSITY:=-v}"
else
    WGET_VERBOSITY="--quiet"
    CURL_VERBOSITY="--silent"
    RM_VERBOSITY="${RM_VERBOSITY:=}"
    MV_VERBOSITY="${MV_VERBOSITY:=}"
    CP_VERBOSITY="${CP_VERBOSITY:=}"
    TAR_VERBOSITY="${TAR_VERBOSITY:=}"
    GPG_VERBOSITY="${GPG_VERBOSITY:=-q}"
fi

DEFAULT_ARCH="$(detect_current_arch)"

if ! check_di_source_list; then
    exit $?
fi

releases="$(grep -vE '^#' "$DISOURCELIST" | cut -f 1 | sort -u | tr "\n" " " |\
                  sed -e 's/^[[:blank:]]\+//' -e 's/[[:blank:]]\+$//')"

if [ -n "$CLI_ALIAS" ]; then
    if [ "$ACTION" = "install" ] && [ $COUNT -gt 1 ] ; then
        echo "E: Option --alias can't be used with multiple repositories." 1>&2
        exit 1
    fi
fi

case "$ACTION" in
    '')
    #Skip, if no action specified
        ;;
    rebuild-menu|rebuild-grub)
        if [ $COUNT -ne 0 ]; then
            echo "E: Unexpected argument after command '$ACTION'." 1>&2
            exit 1
        fi
        ;;
    *)
        if [ $COUNT -eq 0 ]; then
            echo "E: No repository name was passed for '$ACTION'." 1>&2
            ! $OFFLINE && [ "$ACTION" = "install" ] && echo "I: Declared repositories are:" &&\
                echo "${releases}"
            cached_repos="$( find $DL_CACHE -name "*~~*--*.meta" | \
                             sed -e 's/^.*~~//' -e 's/--.*\.meta//' | sort -u | tr "\n" " " | \
                             sed -e 's/[[:blank:]]\+$//')"
            installed_repos="$(get_installed_repos)"
            purgabled_repos="$(echo "$cached_repos" "$installed_repos" | tr " " "\n" | sort -u | \
            tr "\n" " " | sed -e 's/[[:blank:]]\+$//')"
            [ "$ACTION" = "uncache" ] && echo -e "I: Cached repositories are:\n${cached_repos}"
            [ "$ACTION" = "uninstall" ] || [ "$ACTION" = "fw-toggle" ] && echo -e "I: Installed repositories are:\n${installed_repos}"
            [ "$ACTION" = "purge" ] && echo -e "I: Purgable repositories are:\n${purgabled_repos}"
            exit 1
        fi
        ;;
esac


ACTION=
COUNT=0
for option in "$@"; do
    case "$option" in
        -*)
        # Ignore options on this pass
        ;;
        install|uninstall|uncache|purge|fw-toggle)
            ACTION=$option
            ;;
        rebuild-menu)
            ACTION=$option
            update_menu
            ;;
        rebuild-grub)
            ACTION=$option
            prepare_grub "$option"
            ;;
        *)
            if [ -n "$CLI_ALIAS" ] ; then
                REPO_ALIAS="$CLI_ALIAS"
            else
                REPO_ALIAS="$option"
            fi
            if [ "$ARCH" = "EMPTY" ] ; then
                if [ "$ACTION" = "install" ] ; then
                    list_declared_arch_for_repo "$option"
                else
                    echo "I: Installed architectures for '$option': $(find  "$STATUS_LIB/"  -name \*$option--\*.conf | sed -e 's/^.*--//' -e 's/\.conf//' | tr '\n' ' ')"
                fi
                exit 0
            fi
            case "$ACTION" in
                install)
                    check_tftp_root
                    cd "$TFTP_ROOT/$N_A_DIR"
                    if install_repo_for_archs "$option" ; then
                        update_menu
                    else
                        rmdir --ignore-fail-on-non-empty "$TFTP_ROOT/$N_A_DIR"
                        exit 1
                    fi
                    ;;
                fw-toggle)
                    toggle-firmware "$option"
                    ;;
                uninstall)
                    uninstall_repos "$option" ""
                    update_menu
                    ;;
                uncache)
                    remove_repocaches "$option" ""
                    ;;
                purge)
                    uninstall_repos "$option" "ignore_missing"
                    update_menu
                    remove_repocaches "$option" "ignore_missing"
                    ;;
                rebuild-menu|rebuild-grub)
                    echo "W: Argument '$option' ignored ($ACTION expects no argument)." 1>&2
                    ;;
                *)
                    echo "E: Unexpected keyword: '$option'. No action was specified." 1>&2
                    exit 1
                    ;;
            esac
            COUNT=$(( COUNT + 1 ))
    esac
done

if [ ! "$ACTION" ]; then
    usage 1>&2
    exit 1
fi
