#!/bin/bash
# Simple script to download a Grml ISO image to use with grml-rescueboot
# Needs the Debian keyring, sopv + wget
# Licensed under GPL v2+

set -eu -o pipefail

# defaults
grmlflavor=full
force=0
retrieved_iso=0

declare -A bin_to_pkg=( [update-grub]=grub2-common [wget]=wget [sopv]=sqopv )
declare -a missing_packages=()
for binary in "${!bin_to_pkg[@]}" ; do
  if ! command -v "${binary}" &>/dev/null ; then
    echo "ERROR: Binary $binary not found." >&2
    missing_packages+=( "${bin_to_pkg[$binary]}" )
  fi
done

keyringname="/usr/share/keyrings/debian-keyring"
keyring=""
for pgpext in pgp gpg; do
  keyring="${keyringname}.${pgpext}"
  if [ -r "${keyring}" ] ; then
    break
  fi
  keyring=""
done
if [ -z "${keyring}" ] ; then
  echo "ERROR: File ${keyringname}.{pgp,gpg} not found." >&2
  missing_packages+=( debian-keyring )
fi

if [ ${#missing_packages[@]} -ne 0 ] ; then
  echo "TIP: Try running \`apt install ${missing_packages[*]}\` to fix this." >&2
  exit 1
fi

cleanup() {
  umount "${grml_mount}" || true
  rmdir "${grml_mount}" || true
}

trap cleanup EXIT
trap 'trap - EXIT; cleanup; exit 1' HUP INT PIPE TERM

usage() {
  echo "Usage: $(basename "$0") [-f] [-t <small|full>]"
}

while getopts ":t:fh" opt ; do
  case ${opt} in
    t)
      if [ "${OPTARG}" = "full" ] || [ "${OPTARG}" = "small" ] ; then
	grmlflavor="${OPTARG}"
      else
	echo "ERROR: Invalid value '${OPTARG}'. Supported values: small, full" >&2
	usage >&2 ; exit 1
      fi
      ;;
    f)
      force=1
      ;;
    h)
      usage ; exit 0
      ;;
    \?)
      echo "ERROR: Invalid Option: -${OPTARG}" >&2
      usage >&2 ; exit 1
      ;;
    :)
      echo "ERROR: Option -${OPTARG} requires an argument." >&2
      ;;
  esac
done

findesp() {
  local esp

  if ! command -v bootctl &>/dev/null ; then
    # important: this needs to go to stderr, otherwise that would be reported as esp directory
    echo "WARN: EFI support detected, but bootctl not present. Consider installing systemd-boot-tools?" >&2
  else
    esp="$(bootctl --print-esp-path 2> /dev/null || true)"
    if [ "${esp}" ]; then
      echo "${esp}"
      return
    fi
  fi

  for esp in /efi /boot /boot/efi; do
    [ -d "${esp}/EFI/" ] || continue
    echo "${esp}"
    return
  done
}

efi_support() {
  # Absence of an EFI runtime does not prevent loading efivarfs since commit
  # 301de9a2055357375a4e1053d9df0f8f3467ff00 which landed in Linux v6.3.
  # Unconditionally load it, in case nothing else attempted it.
  modprobe efivarfs &>/dev/null || true

  if [[ -d /sys/firmware/efi ]] ; then
    return 0
  fi

  return 1
}

update-ukify() {
  if ! efi_support ; then
    echo "NOTE: NO EFI support, not setting up /efi/EFI/tools/grml.efi"
    return 0
  fi

  if ! command -v ukify &>/dev/null ; then
    echo "WARN: EFI support detected, but ukify not present. Consider installing systemd-ukify?"
    return 0
  fi

  # support setting custom boot options via CUSTOM_BOOTOPTIONS
  if [ -r /etc/default/grml-rescueboot ] ; then
    . /etc/default/grml-rescueboot
  fi

  grml_mount="$(mktemp --directory --tmpdir grml.XXX)"

  mount -o loop,ro "${isofile}" "${grml_mount}"

  local cmdline esp_path initrd_file kernel_file

  if ls "${grml_mount}"/boot/grub/*_default.cfg &>/dev/null ; then
    echo "NOTE: identified GRUB file on ISO, using for kernel command line setup"
    cmdline="findiso=${isofile}"
    cmdline="${cmdline} $(grep -oP 'linux.*vmlinuz\K.*' "${grml_mount}"/boot/grub/*_default.cfg | head -1) ${CUSTOM_BOOTOPTIONS:-}"
    kernel_file="$(awk '/ linux / {print $2}'  "${grml_mount}"/boot/grub/*_default.cfg | head -1)"
    initrd_file="$(awk '/ initrd / {print $2}' "${grml_mount}"/boot/grub/*_default.cfg | head -1)"
  else # we can't get the data from the GRUB default configuration
    echo "WARN: could not find GRUB file on ISO, falling back for kernel command line setup"
    local bootid live_media live_media_path
    bootid="$(cat "${grml_mount}/conf/bootid.txt")"

    live_media="$(find "${grml_mount}" -name '*.squashfs')"
    live_media="${live_media%/*.squashfs}"
    live_media="${live_media##"${grml_mount}"}"
    live_media_path="live-media-path=${live_media}"

    cmdline="boot=live findiso=${isofile} ${live_media_path} bootid=${bootid} ${CUSTOM_BOOTOPTIONS:-}"

    kernel_file="/boot/${bootid%20????}/vmlinuz"
    initrd_file="/boot/${bootid%20????}/initrd.img"
  fi

  esp_path=$(findesp)

  mkdir -p "${esp_path}"/EFI/tools/
  ukify build \
      --linux="${grml_mount}/${kernel_file}" \
      --initrd="${grml_mount}/${initrd_file}" \
      --cmdline="${cmdline}" \
      --output="${esp_path}/EFI/tools/grml.efi"
}

arch="$(uname -m)"
case ${arch} in
  aarch64)
    grmlarch=arm64
    ;;
  x86_64)
    grmlarch=amd64
    ;;
  *)
    echo "ERROR: Unsupported architecture '${arch}'." >&2
    usage >&2
    exit 1
    ;;
esac

echo "Finding out latest ISO image..."
date=$(wget --quiet -O- https://download.grml.org/ | sed --regex -n 's/.*grml-('"$grmlflavor"')-([0-9]{4}\.[0-9]{2})-('"$grmlarch"')\.iso.*/\2/p' | sort | tail -1)

if [ -z "${date}" ] ; then
  echo "ERROR: Could not find out latest ISO." >&2
  exit 1
fi

output_directory="/boot/grml"
mkdir -p "${output_directory}"

diskfree=$(df --output=avail "${output_directory}" | tail -1)
if [ -z "${diskfree}" ] ; then
  echo "ERROR: couldn't calculate free disk space in /boot." >&2
  exit 1
fi

if [ "${grmlflavor}" = "full" ] && [ "${diskfree}" -lt 1048576 ] ; then
  if [ "${force}" = "1" ] ; then
    echo "WARN: There might not be enough free disk space in /boot, continuing anyway as requested via -f."
  else
    echo "ERROR: there doesn't seem to be enough free disk space in /boot." >&2
    echo "Note: >=1GB for grml-full recommended (use -f to force download anyway)."
    exit 1
  fi
elif [ "${grmlflavor}" = "small" ] && [ "${diskfree}" -lt 524288 ] ; then
  if [ "${force}" = "1" ] ; then
    echo "WARN: There might not be enough free disk space in /boot, continuing anyway as requested via -f."
  else
    echo "ERROR: there doesn't seem to be enough free disk space in /boot." >&2
    echo "Note: >=512MB for grml-small recommended (use -f to force download anyway)."
    exit 1
  fi
fi

isoname="grml-${grmlflavor}-${date}-${grmlarch}.iso"
isofile="${output_directory}/${isoname}"
isofiletmp="${isofile}.tmp"

if [ "${force}" = "1" ] || ! [ -f "${isofile}" ] ; then
  echo "Downloading Grml ISO to '${isofile}'."
  wget -O "${isofiletmp}" "https://download.grml.org/${isoname}"
  retrieved_iso=1
elif [ -f "${isofile}" ] ; then
  echo "Found local ${isofile}, skipping download (use -f to force download)."
fi

if [ "${retrieved_iso}" = "1" ] ; then
  sig="$(mktemp)"
  echo "Verifying ISO..."
  wget --quiet -O "${sig}" "https://download.grml.org/${isoname}.asc"

  if ! sopv verify "${sig}" "${keyring}" <"${isofiletmp}" ; then
    echo "ERROR: ISO file will be left in '${isofiletmp}'." >&2
    rm "${sig}"
    exit 1
  fi

  rm "${sig}"
  mv "${isofiletmp}" "${isofile}"

  echo "ISO file is OK."
fi

echo "Invoking 'update-grub' now."
update-grub

update-ukify

echo "Successfully finished grml-rescueboot integration."
