#!/bin/bash
set -e
PROGNAME=${0##*/}

########################################################################
#
#  mkdebusbimg - make disk image of Debian installer with preseeding
#                (for an article about this script see
#                https://alexishuxley.codeberg.page/articles/preparing-a-debian-10-netinst-usb-stick-with-preseeding-and-uefi-support/)
# 
#  Copyright (C) 2021-2025 Alexis Huxley
#  
#  This program 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 3 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, see <http://www.gnu.org/licenses/>.
#
########################################################################

#  Start of stuff you uncomment and customise
#DI_NETWORK_HOST_NAME=questaroli
#DI_NETWORK_HOST_DOMAIN=pasta.net
#DI_NETWORK_HOST_IPADDR=192.168.1.12
#DI_NETWORK_NET_MASK=255.255.255.0
#DI_NETWORK_GATEWAY_IPADDR=192.168.1.51
#DI_NETWORK_DNS_IPADDR=192.168.1.21
#DI_USERS_ROOT_PASSWD='mysecret' 
#DI_REGIONAL_LANGUAGE=en
#DI_REGIONAL_COUNTRY=DE
#DI_REGIONAL_LOCALE=en_GB.UTF-8
#DI_REGIONAL_KEYMAP=us

#  End of stuff you uncomment and customise

#  Other globals
DI_PRESEED_ENABLE=true
GRUB_TIMEOUT=10
USB_IMAGE_SIZE_MB=600   #  generous
USB_IMAGE_ROOTFS_MNTPNT=$(mktemp -d /tmp/$PROGNAME.XXXXXX)
USB_IMAGE_FILE=/tmp/$PROGNAME.img
DI_USERS_ROOT_PASSWD_ENCRYPTED="$(mkpasswd "$DI_USERS_ROOT_PASSWD")" 
CHECK_SET_VARS=(DI_PRESEED_ENABLE DI_NETWORK_HOST_NAME DI_NETWORK_HOST_DOMAIN DI_NETWORK_HOST_IPADDR DI_NETWORK_NET_MASK DI_NETWORK_GATEWAY_IPADDR DI_NETWORK_DNS_IPADDR DI_USERS_ROOT_PASSWD)
NOT_SET_VARS=()

main()
{
    #  Sanity checks
    info "doing some sanity checks ..."
    [ -w / ] || error "you're not root (hint: you need to be root to run this script)"
    for VAR in "${CHECK_SET_VARS[@]}"; do
        eval "[ \"X\$$VAR\" != X ]" || NOT_SET_VARS=( "${NOT_SET_VARS[@]}" $VAR )
    done
    [ ${#NOT_SET_VARS[*]} = 0 ] || error "$(echo "${NOT_SET_VARS[@]}" | sed 's/ /, /'): these variables are not set (hint: edit this script and make sure they are all set near the top)"

    info "creating empty disk image ..."
    dd if=/dev/zero of=$USB_IMAGE_FILE bs=1M count=$USB_IMAGE_SIZE_MB 2>/dev/null
    
    info "detemining loop device to use ..."
    USB_IMAGE_LOOPDEV=$(comm -1 -3 <(losetup -a | sed 's/:.*//') <(ls /dev/loop[0-9]) | head -1)

    info "setting up loop device ..."
    losetup $USB_IMAGE_LOOPDEV $USB_IMAGE_FILE 
    
    info "partitioning ..."
    parted -s $USB_IMAGE_LOOPDEV mklabel gpt
    #  200MB partition starts at 0% and ends at 200MB
    parted -s $USB_IMAGE_LOOPDEV mkpart fat32 0% 200M
    parted -s $USB_IMAGE_LOOPDEV set 1 boot on
    #  400MB partition starts at 200MB and ends at 100%
    parted -s $USB_IMAGE_LOOPDEV mkpart ext4 200M 100%

    info "making filesystems ..."
    mkfs -t vfat -F 32 -n EFIBOOT ${USB_IMAGE_LOOPDEV}p1 >/dev/null
    BOOT_UUID=$(uuidgen)
    mkfs -t ext4 -U $BOOT_UUID ${USB_IMAGE_LOOPDEV}p2 >/dev/null 2>&1
    
    info "mounting file systems ..."
    mkdir -p $USB_IMAGE_ROOTFS_MNTPNT/boot
    mount ${USB_IMAGE_LOOPDEV}p2 $USB_IMAGE_ROOTFS_MNTPNT/boot
    mkdir -p $USB_IMAGE_ROOTFS_MNTPNT/boot/efi
    mount ${USB_IMAGE_LOOPDEV}p1 $USB_IMAGE_ROOTFS_MNTPNT/boot/efi
    
    info "installing grub ..."
    grub-install --removable --no-uefi-secure-boot --target=x86_64-efi --efi-directory=$USB_IMAGE_ROOTFS_MNTPNT/boot/efi --boot-directory=$USB_IMAGE_ROOTFS_MNTPNT/boot --bootloader-id=grub --recheck $USB_IMAGE_LOOPDEV 2> /dev/null

    info "downloading Linux kernel and initrd.gz ..."
    (
        cd $USB_IMAGE_ROOTFS_MNTPNT/boot
        wget -q http://deb.debian.org/debian/dists/buster/main/installer-amd64/current/images/netboot/debian-installer/amd64/linux
        wget -q http://deb.debian.org/debian/dists/buster/main/installer-amd64/current/images/netboot/debian-installer/amd64/initrd.gz
    )

    info "preparing preseed file ..."
    PRESEEDCFG_FILE=$USB_IMAGE_ROOTFS_MNTPNT/boot/efi/preseed.cfg
    [ ! -f $PRESEEDCFG_FILE ] || error "$PRESEEDCFG_FILE: exists already; refusing to overwrite (hint: this probably indicates a dangerous bug in this script)"
    {
        echo "#  regional settings"
        echo "#  (see grub.cfg)"
     
        #  Hostname
        echo "# hostname and and domain name"
        echo "d-i netcfg/get_hostname string $DI_NETWORK_HOST_NAME"
        echo "d-i netcfg/get_domain string $DI_NETWORK_HOST_DOMAIN"
        
        #  Network
        echo "#  network configuration"
        echo "d-i netcfg/disable_autoconfig boolean true"
        echo "d-i netcfg/get_ipaddress string $DI_NETWORK_HOST_IPADDR"
        echo "d-i netcfg/get_netmask string $DI_NETWORK_NET_MASK"
        echo "d-i netcfg/get_gateway string $DI_NETWORK_GATEWAY_IPADDR"
        echo "d-i netcfg/get_nameservers string $DI_NETWORK_DNS_IPADDR"
        echo "d-i netcfg/confirm_static boolean true"
  
        #  Repository
        echo "#  access to a package repository"
        echo "d-i mirror/country string manual"
        echo "d-i mirror/http/hostname string deb.debian.org"
        echo "d-i mirror/http/directory string /debian/"
        echo "d-i mirror/http/proxy string"
    
        #  Root and users
        echo "#  users and passwords"
        echo "d-i passwd/root-password-crypted password $DI_USERS_ROOT_PASSWD_ENCRYPTED"
        echo "d-i passwd/make-user boolean false"

        #  Choose disk
        #  Set the install disk and the GRUB disk. Since the
        #  answer is used twice, we push the answer directly
        #  into the two d-i variables.
        echo "#  which disk to install onto"
        echo "d-i partman/early_command string \\"
        echo "        list-devices usb-partition | sed 's/.$//' | sort -u > /tmp/usb-disks; \\"
        echo "        list-devices disk | sort > /tmp/all-disks; \\"
        echo "        INSTALL_DISK=\$(fgrep -vxf /tmp/usb-disks /tmp/all-disks | head -1); \\"
        echo "        debconf-set partman-auto/disk \$INSTALL_DISK; \\"
        echo "        debconf-set grub-installer/bootdev \$INSTALL_DISK"

        echo "#  GPT partitioning"
        #  Force UEFI booting with prerequisite GPT partition table.
        echo "d-i partman-efi/non_efi_system boolean true"
        echo "d-i partman-partitioning/choose_label string gpt"
        echo "d-i partman-partitioning/default_label string gpt"

        #  Define a partitioning recipe.
        echo "#  partitioning"
        #  Partitioning method
        #  Choosing LVM doesn't mean that LVM will be used for the whole disk. It just
        #  means I don't want the guided installation and I *will* be using LVM.
        echo "d-i partman-auto/method string lvm"
        echo "d-i partman-auto/expert_recipe string myscheme :: \\"
        echo "    550 550 550 fat32 \\"
        echo "        \$primary{ } \\"
        echo "        method{ efi } format{ } \\"
        echo "        . \\"
        echo "    1024 1024 1024 ext4 \\"
        echo "        \$primary{ } \\"
        echo "        method{ format } \\"
        echo "        format{ } \\"
        echo "        use_filesystem{ } \\"
        echo "        filesystem{ ext4 } \\"
        echo "        mountpoint{ /boot } \\"
        echo "        . \\"
        echo "    15360 15360 15360 ext4 \\"
        echo "        method{ lvm } \\"
        echo "        \$lvmok{ } \\"
        echo "        lv_name{ root } \\"
        echo "        format{ } \\"
        echo "        use_filesystem{ } \\"
        echo "        filesystem{ ext4 } \\"
        echo "        mountpoint{ / } \\"
        echo "        . \\"
        echo "    1 1000000000 1000000000 affs1 \\"
        echo "        method{ keep } \\"
        echo "        \$lvmok{ } \\"
        echo "        lv_name{ remainder } \\"
        echo "        . \\"
        echo
        #  Choose the above recipe. (The example preseed
        #  files don't mention this but I had erratic 
        #  behaviour without it.)
        echo "d-i partman-auto/choose_recipe select myscheme"

        #  Some things that don't get asked but still need answers.
        #  How much of the remaining space should be given to LVM?
        echo "d-i partman-auto-lvm/guided_size string max"
        #  Any LVs are to be created in this VG.
        echo "d-i partman-auto-lvm/new_vg_name string vg0"

        #  Participate in package usage survey? No.
        echo "#  Participate in package usage survey? No."
        echo "popularity-contest popularity-contest/participate boolean false"

        #  Packages
        echo "#  packages"
        echo "tasksel tasksel/first multiselect standard"

        #  Install grub on disk
        #  Install grub on disk, not on the USB stick itself
        #  'grub-installer/bootdev' was set during the early_command.

        #  Post-install command
        #  Note that within the in-target command's environment, root's $HOME is
        #  /, not /root; you might need to take that into account (e.g. I rely 
        #  on credentials unpacked into /root/.subversion, but without correcting
        #  $HOME they are not referenced).
        echo "d-i preseed/late_command string \\"
        #                Remove the partition we created simply to stop the OS
        #                partition filling all space.
        echo "        lvremove -f /dev/vg0/remainder"

        #  Confirmations
        echo "#  Don't load non-free drivers at install time (has no"
        echo "#  effect if not needed)"
        echo "d-i hw-detect/load_firmware boolean true"
        echo "#  Write the changes and configure LVM? Yes. "
        echo "d-i partman-lvm/confirm boolean true"
        echo "#  Remove existing logical volume data? Yes."
        echo "d-i partman-lvm/device_remove_lvm boolean true"
        echo "#  Finish partitioning and write changes to disk? Yes."
        echo "d-i partman/choose_partition select finish"
        echo "#  No file system is specified for partition #1 of"
        echo "#  LVM VG vg0, LV remainder. Go back? No."
        echo "d-i partman-basicmethods/method_only boolean false"
        echo "#  You have not selected any partitions for use as"
        echo "#  swap space. Return to partitioning menu? No."
        echo "d-i partman-basicfilesystems/no_swap boolean false"
        echo "#  Write the changes to disk? Yes."
        echo "d-i partman/confirm boolean true"
        echo "#  The installation is complete, ... Continue!"
        echo "d-i finish-install/reboot_in_progress note"
    } > $PRESEEDCFG_FILE

    info "preparing grub configuration ..."
    GRUBCFG_FILE=$USB_IMAGE_ROOTFS_MNTPNT/boot/grub/grub.cfg
    PRESEEDCFG_MD5SUM=$(md5sum $PRESEEDCFG_FILE | awk '{ print $1 }')
    [ ! -f $GRUBCFG_FILE ] || error "$GRUBCFG_FILE: exists already; refusing to overwrite (hint: this probably indicates a dangerous bug in this script)"
    {
        echo "set timeout=$GRUB_TIMEOUT"
        echo "menuentry \"Alexis's Debian Installer (generated $(date +'%d/%m/%y %H:%M:%S'))\" {"
        echo "    search --no-floppy --fs-uuid --set=root $BOOT_UUID"
        echo "    insmod all_video"
        echo -n "    linux /linux"
        echo -n " debian-installer/language=$DI_REGIONAL_LANGUAGE"
        echo -n " debian-installer/country=$DI_REGIONAL_COUNTRY"
        echo -n " debian-installer/locale=$DI_REGIONAL_LOCALE"
        echo -n " keyboard-configuration/xkb-keymap=$DI_REGIONAL_KEYMAP"
        echo -n " ipv6.disable=1"
        if $DI_PRESEED_ENABLE; then
            echo -n " auto preseed/file=/media/preseed.cfg"
            echo -n " preseed/file/checksum=$PRESEEDCFG_MD5SUM"
        fi
        echo
        echo "    initrd /initrd.gz"
        echo "}"
    } > $GRUBCFG_FILE

    info "cleaning up ..."
    umount $USB_IMAGE_ROOTFS_MNTPNT/boot/efi
    umount $USB_IMAGE_ROOTFS_MNTPNT/boot
    losetup -d $USB_IMAGE_LOOPDEV
    rmdir $USB_IMAGE_ROOTFS_MNTPNT/boot
    rmdir $USB_IMAGE_ROOTFS_MNTPNT

    info "USB stick image is $USB_IMAGE_FILE"
}

info() { echo "$PROGNAME: INFO: $1" >&2; }
warning() { echo "$PROGNAME: WARNING: $1" >&2; }
error() { echo "$PROGNAME: ERROR: $1" >&2; exit 1; }
shell() { PS1="$PROGNAME# " bash --norc; }

main "$@"
