#!/bin/bash

## Copyright (C) 2025 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## Copyright (C) 2016 - 2020 The Debian Live team
## Copyright (C) 2006 - 2015 Daniel Baumann <mail@daniel-baumann.ch>
## Derived from and inspired by Debian live-build's hybrid-ISO packaging.
## See the file COPYING for copying conditions.

## AI-Assisted

## Convert a bootable raw disk image into a bootable hybrid ISO. Uses the same
## techniques as live-build to allow booting in a wide variety of scenarios.
##
## Note that all command-line arguments and environment variables are trusted
## and therefore many of them are not validated here.

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose
export LC_ALL=C

me="${BASH_SOURCE[0]##*/}"
MYDIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"

# shellcheck source=./misc-helpers.bsh
source "${MYDIR}/misc-helpers.bsh"

usage() {
   cat <<EOF
Usage: ${me} --raw <image> --output <iso> --grub-config-dir <dir> [options]

Required:
  --raw <image>            bootable raw disk image.
  --output <iso>           output ISO path.
  --grub-config-dir <dir>  dm's GRUB config source (iso-build-data/grub-config).

Options:
  --arch <a>               target architecture: amd64|arm64|armhf. Defaults to host
                           architecture.
  --label <volid>          ISO volume label. Defaults to 'DM_LIVE'.
  --dist-pretty <name>     product name shown in the splash.
  --dist-version <ver>     product version shown in the splash.
  --iso-pkg-repo           directory to embed in the ISO to provide a package
                           repository.
  --serial-console         enable a serial console on ttyS0 at speed 115200 and boot
                           the default entry immediately, for headless boot testing.
  --smbios-reader          inline the SMBIOS cmdline reader into the menu (test builds).
  --source-date-epoch <n>  UNIX timestamp for reproducible output. Defaults to env
                           SOURCE_DATE_EPOCH if available, or the raw image's mtime
                           otherwise.
  --keep-workdir           do not delete the temporary work directory on exit.
  --help                   show this help.
EOF
}

error() {
   printf '%s\n' "${me}: ERROR: $*" >&2
   exit 1
}

## Fail loudly on a missing dependency.
require_cmd() {
   local cmd command_v_output
   for cmd in "$@" ; do
      command_v_output="$(command -v "${cmd}" 2>/dev/null)" || true
      if [ -z "${command_v_output}" ] || ! [ -x "${command_v_output}" ]; then
         error "required command not found: '${cmd}'. Install its Debian package."
      fi
   done
}
require_file() {
   local f
   for f in "$@" ; do
      if [ ! -e "${f}" ] ; then
         error "required file not found: '${f}'. Install the Debian package that ships it."
      fi
   done
}

## Utility function for unsettting the last item of an array.
unset_last() {
   local -n target_arr
   target_arr="${1:-}"
   unset "target_arr[$(( "${#target_arr[@]}" - 1 ))]"
}

## Parse arguments.
raw_img=""
output_iso=""
arch=""
volume_label="DM_LIVE"
grub_config_dir=""
dist_pretty=""
dist_version=""
iso_pkg_repo=""
serial_console="false"
smbios_reader="false"
source_date_epoch="${SOURCE_DATE_EPOCH:-}"
keep_workdir="false"

while [ "$#" -gt 0 ] ; do
   case "$1" in
      --raw)
         [ "$#" -ge 2 ] || error "--raw requires a value"
         raw_img="$2"
         shift 2
         ;;
      --output)
         [ "$#" -ge 2 ] || error "--output requires a value"
         output_iso="$2"
         shift 2
         ;;
      --arch)
         [ "$#" -ge 2 ] || error "--arch requires a value"
         arch="$2"
         shift 2
         ;;
      --label)
         [ "$#" -ge 2 ] || error "--label requires a value"
         volume_label="$2"
         shift 2
         ;;
      --grub-config-dir)
         [ "$#" -ge 2 ] || error "--grub-config-dir requires a value"
         grub_config_dir="$2"
         shift 2
         ;;
      --dist-pretty)
         [ "$#" -ge 2 ] || error "--dist-pretty requires a value"
         dist_pretty="$2"
         shift 2
         ;;
      --dist-version)
         [ "$#" -ge 2 ] || error "--dist-version requires a value"
         dist_version="$2"
         shift 2
         ;;
      --iso-pkg-repo)
         [ "$#" -ge 2 ] || error "--iso-pkg-repo requires a value"
         iso_pkg_repo="$2"
         shift 2
         ;;
      --serial-console)
         serial_console="true"
         shift
         ;;
      --smbios-reader)
         smbios_reader="true"
         shift
         ;;
      --source-date-epoch)
         [ "$#" -ge 2 ] || error "--source-date-epoch requires a value"
         source_date_epoch="$2"
         shift 2
         ;;
      --keep-workdir)
         keep_workdir="true"
         shift
         ;;
      --help|-h)
         usage
         exit 0
         ;;
      *)
         error "unknown argument: '$1' (see --help)"
         ;;
   esac
done

[ -n "${raw_img}" ] || error "--raw is required (see --help)"
[ -n "${output_iso}" ] || error "--output is required (see --help)"
[ -r "${raw_img}" ] || error "raw image not readable: '${raw_img}'"

if [ "$(readlink -f -- "${raw_img}")" = "$(readlink -f -- "${output_iso}" 2>/dev/null)" ]; then
   error "--output must not be the same file as --raw"
fi

if [ -z "${arch}" ]; then
   require_cmd dpkg
   arch="$(dpkg --print-architecture)"
fi

if ! [[ "${arch}" =~ ^(amd64|arm64|armhf)$ ]]; then
   error "unsupported --arch '${arch}' (see --help)"
fi

## The contents of grub_config_dir will be used as part of building the ISO's
## /boot/grub.
[ -n "${grub_config_dir}" ] || error "--grub-config-dir is required (see --help)"
[ -d "${grub_config_dir}" ] || error "--grub-config-dir is not a directory: '${grub_config_dir}'"
grub_config_required_file=""
for grub_config_required_file in config.cfg grub.cfg loopback.cfg esp-redirect.cfg theme.cfg smbios-reader.cfg live-theme/theme.txt splash.svg ; do
   [ -r "${grub_config_dir}/${grub_config_required_file}" ] || error "--grub-config-dir is missing required file '${grub_config_required_file}'"
done

## Check for all dependencies we need on the host.
require_cmd xorriso grub-mkimage mkfs.msdos mmd mcopy mksquashfs cp find touch stat chroot mount umount kpartx losetup safe-rm dpkg-query rsvg-convert sed grep

if [ -z "${source_date_epoch}" ]; then
   source_date_epoch="$(stat -c %Y -- "${raw_img}")"
fi
export SOURCE_DATE_EPOCH="${source_date_epoch}"

## Determine which bootloader binaries we need and save info to help locate them
## later. Each entry in efi_platform_pair_list contains "grub_platform:efi_arch".
enable_bios_boot="false"
efi_platform_pair_list=()
case "${arch}" in
   amd64)
      enable_bios_boot="true"
      ## We don't support i386, but some amd64 systems use 32-bit EFI firmware,
      ## which requires the 32-bit bootloader.
      efi_platform_pair_list=( "x86_64-efi:x64" "i386-efi:ia32" )
      ;;
   arm64)
      efi_platform_pair_list=( "arm64-efi:aa64" )
      ;;
   armhf)
      efi_platform_pair_list=( "arm-efi:arm" )
      ;;
esac

## Work directory + teardown. Track loop/kpartx maps and mounts and tear them down
## in reverse on any exit so a failure never leaks a loop device or a bind mount.
## Place the work directory alongside the image to ensure the reimage isn't done
## in a small tmpfs.
raw_dir="$(dirname "${raw_img}")"
workdir="$(mktemp --directory --tmpdir="${raw_dir}" -- dm-raw-to-iso.XXXXXX)"
kpartx_image=""
mount_list=()

cleanup() {
   local rc="${1:-$?}"
   local m
   ## Unmount in reverse order.
   for (( idx=${#mount_list[@]}-1 ; idx>=0 ; idx-- )); do
      m="${mount_list[idx]}"
      ## Never use umount --lazy, it is dangerous unless system reboot is
      ## imminent.
      umount -- "${m}" 2>/dev/null || true
   done
   if [ -n "${kpartx_image}" ] ; then
      kpartx -d -s -v -- "${kpartx_image}" 2>/dev/null || true
   fi
   if [ "${keep_workdir}" = "true" ]; then
      printf '%s\n' "${me}: keeping work directory: '${workdir}'" >&2
   else
      safe-rm --recursive --force -- "${workdir}" 2>/dev/null || true
   fi
   trap - EXIT
   exit "${rc}"
}
on_signal() {
   cleanup 143
}
trap cleanup EXIT
trap on_signal INT TERM

binary_dir="${workdir}/binary"
rootfs_dir="${workdir}/rootfs"
mkdir -p -- "${binary_dir}/boot/grub" "${binary_dir}/live" "${binary_dir}/.disk" "${rootfs_dir}"

## kpartx maps each partition to /dev/mapper/loop<N>p<M>. grml-debootstrap's
## --vmefi layout puts the root filesystem in the last partition.
printf '%s\n' "${me}: mapping partitions of '${raw_img}'" >&2
kpartx_image="${raw_img}"

## -r makes the partition maps read-only.
kpartx_output="$(kpartx -a -r -s -v -- "${raw_img}")"
[ -n "${kpartx_output}" ] || error "kpartx produced no partition maps for '${raw_img}'"

## Pick the last mapped partition and mount it.
mapper_name="$(printf '%s\n' "${kpartx_output}" | sed -n 's/^add map \([^ ]*\) .*/\1/p' | tail -n 1)"
[ -n "${mapper_name}" ] || error "could not parse a partition map from kpartx output"
mapper_dev="/dev/mapper/${mapper_name}"
require_file "${mapper_dev}"
rootfs_mnt="${workdir}/mnt"
mkdir -p -- "${rootfs_mnt}"
mount --read-only -- "${mapper_dev}" "${rootfs_mnt}"
mount_list+=( "${rootfs_mnt}" )

## Make sure the partition looks like a root partition.
expected_root_dir=""
for expected_root_dir in etc usr bin sbin var ; do
   [ -e "${rootfs_mnt}/${expected_root_dir}" ] || error "'${mapper_dev}' does not look like a root filesystem (missing '/${expected_root_dir}')"
done

## Copy the image's contents out.
printf '%s\n' "${me}: copying rootfs out of the image" >&2
cp --archive --one-file-system -- "${rootfs_mnt}/." "${rootfs_dir}/"

## Release the input image immediately, we don't need it anymore.
umount -- "${rootfs_mnt}"
unset_last mount_list
kpartx -d -s -v -- "${raw_img}"
kpartx_image=""

## Ensure mandatory bootloader files are present. Note that some of the EFI
## bootloader files may not be mandatory, so we don't look for those here.
if [ "${enable_bios_boot}" = "true" ] ; then
   require_file "${rootfs_dir}/usr/lib/grub/i386-pc/cdboot.img" \
      "${rootfs_dir}/usr/lib/grub/i386-pc/boot_hybrid.img" \
      "${rootfs_dir}/usr/lib/grub/i386-pc/normal.mod"
fi
require_file "${rootfs_dir}/usr/share/grub/unicode.pf2"

## Bind the host API filesystems into the rootfs copy so the chroot can run.
for api_fs_dir in /dev /dev/pts /proc /sys /run ; do
   bind_path="${rootfs_dir}${api_fs_dir}"
   mkdir -p -- "${bind_path}"
   mount --bind -- "${api_fs_dir}" "${bind_path}"
   mount_list+=( "${bind_path}" )
done

## Create the package manifest.
dpkg-query --admindir="${rootfs_dir}/var/lib/dpkg" --show > "${binary_dir}/live/filesystem.packages"

## Copy the GRUB keyboard layouts where they can be used and generate the
## keyboard layout submenu.
kb_layout_rc=0
kb_layout_submenu="$(chroot "${rootfs_dir}" /etc/grub.d/44_kb_layout)" || kb_layout_rc=$?
if [ "${kb_layout_rc}" -ne 0 ] ; then
   error "Failed to build the keyboard layout submenu (exit '${kb_layout_rc}')"
fi
mkdir -p -- "${binary_dir}/boot/grub/kb_layouts"
if [ -d "${rootfs_dir}/boot/grub/kb_layouts" ] ; then
   cp -a -- "${rootfs_dir}/boot/grub/kb_layouts/." "${binary_dir}/boot/grub/kb_layouts/"
fi

## Extract kernel hardening settings. These are retrieved from grub.cfg rather
## than calculated from grub.d to ensure there is no chance of missing settings
## that may have been generated directly by a grub-mkconfig module.
##
## 'mitigations=auto,smt' is always the first hardening argument. It must be at
## the start of all hardening arguments to ensure it doesn't disable other
## mitigations, so it can be used to signal the beginning of the hardening
## arguments. Non-hardening arguments can be removed from the list on a
## case-by-case basis if needed.
rootfs_kernel_hardening="$(grep '^[[:space:]]*linux[[:space:]]\+\/boot\/vmlinuz' "${rootfs_dir}/boot/grub/grub.cfg" | head -n1 | sed -n 's/.*\(mitigations=auto,nosmt .*\)/\1/p')" || true

## Remove any literal ${dm_smbios_extra}, as we set up the SMBIOS reader for
## the ISO separately when --smbios-reader is passed.
# shellcheck disable=SC2016
rootfs_kernel_hardening="${rootfs_kernel_hardening//'${dm_smbios_extra}'/}"

## Trim and collapse whitespace.
rootfs_kernel_hardening="$(printf '%s' "${rootfs_kernel_hardening}" | tr -s ' ')"
rootfs_kernel_hardening="${rootfs_kernel_hardening#"${rootfs_kernel_hardening%%[![:space:]]*}"}"
rootfs_kernel_hardening="${rootfs_kernel_hardening%"${rootfs_kernel_hardening##*[![:space:]]}"}"

## Fail closed if no hardening vars were found.
if [ -z "${rootfs_kernel_hardening}" ] ; then
   error "no kernel-hardening parameters found in '${rootfs_dir}/boot/grub/grub.cfg'"
fi

## Neutralize /etc/fstab and /etc/crypttab. We will add root mount information
## to the kernel command line later.
if [ -f "${rootfs_dir}/etc/fstab" ] ; then
   printf '%s\n' '# emptied by dm-raw-to-iso' > "${rootfs_dir}/etc/fstab"
fi
if [ -f "${rootfs_dir}/etc/crypttab" ] ; then
   printf '%s\n' '# emptied by dm-raw-to-iso' > "${rootfs_dir}/etc/crypttab"
fi

## Find the latest kernel so we can build an initramfs for it.
kernel_ver="$(newest-kernel-version "$(ls -1 "${rootfs_dir}/boot")")"
[ -n "${kernel_ver}" ] || error "no /boot/vmlinuz-* kernel found in the rootfs"
kernel_img="vmlinuz-${kernel_ver}"
printf '%s\n' "${me}: building dracut-live initramfs for kernel '${kernel_ver}'" >&2

## One of the kernel "images" we find here may be a symlink. Refuse it, as it
## will become dangling after being placed on the ISO.
if [ -L "${rootfs_dir}/boot/${kernel_img}" ] ; then
   error "kernel '/boot/${kernel_img}' is a symlink"
fi

## Make sure dracut and dmsquash-live are available in the rootfs.
chroot "${rootfs_dir}" sh -c 'command -v dracut >/dev/null 2>&1' \
   || error "the rootfs has no 'dracut'"
[ -d "${rootfs_dir}/usr/lib/dracut/modules.d/90dmsquash-live" ] \
   || error "the rootfs lacks the dracut 'dmsquash-live' module"

## Build the initramfs. --no-hostonly enhances portability. dmsquash-live
## handles critical live ISO features like the root overlay and integrity
## checks.
chroot "${rootfs_dir}" env TMPDIR=/tmp dracut --no-hostonly --add dmsquash-live --force --reproducible /boot/initrd.img-live "${kernel_ver}"

## Install the kernel and initramfs into the ISO binary area.
cp -- "${rootfs_dir}/boot/${kernel_img}" "${binary_dir}/live/vmlinuz"
cp -- "${rootfs_dir}/boot/initrd.img-live" "${binary_dir}/live/initrd.img"

safe-rm --force -- "${rootfs_dir}/boot/initrd.img-live"

## Tear down the API binds before squashing so they are not packed.
while [ "${#mount_list[@]}" -gt 0 ]; do
   last_mount="${mount_list[$(( "${#mount_list[@]}" - 1 ))]}"
   umount -- "${last_mount}" 2>/dev/null
   unset_last mount_list
done

## Build the root squashfs. Exclude API / ephemeral filesystems.
##
## mksquashfs honors SOURCE_DATE_EPOCH.
printf '%s\n' "${me}: creating filesystem.squashfs" >&2
mksquashfs "${rootfs_dir}" "${binary_dir}/live/filesystem.squashfs" \
   -reproducible \
   -noappend \
   -comp xz \
   -no-progress \
   -one-file-system \
   -wildcards \
   -e "dev/*" "proc/*" "sys/*" "run/*" "tmp/*"

## Create /.disk/info.
printf '%s\n' "Derivative ISO; label=${volume_label}; arch=${arch}" > "${binary_dir}/.disk/info"

## Install BIOS GRUB components.
if [ "${enable_bios_boot}" = "true" ] ; then
   printf '%s\n' "${me}: installing BIOS GRUB components" >&2
   mkdir -p -- "${binary_dir}/boot/grub/i386-pc"
   ## Include all runtime modules, they all can be useful in some situations.
   cp -a -- "${rootfs_dir}"/usr/lib/grub/i386-pc/*.mod "${binary_dir}/boot/grub/i386-pc/"
   cp -a -- "${rootfs_dir}"/usr/lib/grub/i386-pc/*.lst "${binary_dir}/boot/grub/i386-pc/" 2>/dev/null || true
   core_img="${workdir}/core.img"
   grub-mkimage -d "${rootfs_dir}/usr/lib/grub/i386-pc" -o "${core_img}" -O i386-pc --prefix=/boot/grub biosdisk iso9660
   ## xorriso will use this image as the boot image file for the ISO later.
   cat "${rootfs_dir}/usr/lib/grub/i386-pc/cdboot.img" "${core_img}" > "${binary_dir}/boot/grub/grub_eltorito"
   safe-rm --force -- "${core_img}"
fi

## Prepare to build the EFI System Partition. This is done unconditionally as
## all supported platforms can boot from EFI.
printf '%s\n' "${me}: installing signed EFI loaders and building the ESP" >&2
esp_stage_dir="${workdir}/esp"
mkdir -p -- "${esp_stage_dir}/EFI/boot" "${esp_stage_dir}/boot/grub"

## Locate and install the needed EFI binaries. shim is a first-stage loader
## that is compatible with Secure Boot. grub is of course the primary
## bootloader. mokmanager is used to reconfigure Secure Boot Machine Owner
## Keys; it isn't strictly needed here, but it could be useful for some people.
for efi_platform_pair in "${efi_platform_pair_list[@]}" ; do
   efi_platform="${efi_platform_pair%%:*}"
   efi_arch="${efi_platform_pair#*:}"
   signed_grub_file="${rootfs_dir}/usr/lib/grub/${efi_platform}-signed/gcd${efi_arch}.efi.signed"
   signed_shim_file="${rootfs_dir}/usr/lib/shim/shim${efi_arch}.efi.signed"
   signed_mokmanager_file="${rootfs_dir}/usr/lib/shim/mm${efi_arch}.efi.signed"

   if [ -r "${signed_grub_file}" ] && [ -r "${signed_shim_file}" ] ; then
      cp -a --dereference -- "${signed_shim_file}" "${esp_stage_dir}/EFI/boot/boot${efi_arch}.efi"
      cp -a -- "${signed_grub_file}" "${esp_stage_dir}/EFI/boot/grub${efi_arch}.efi"
      if [ -r "${signed_mokmanager_file}" ] ; then
         cp -a -- "${signed_mokmanager_file}" "${esp_stage_dir}/EFI/boot/mm${efi_arch}.efi"
      fi
   elif [ "${efi_platform_pair}" = "${efi_platform_pair_list[0]}" ] ; then
      error "missing signed EFI dependency for the supported EFI arch '${efi_arch}' in the image (need '${signed_shim_file}' and '${signed_grub_file}')"
   else
      printf '%s\n' "${me}: NOTE: no signed shim+grub for optional EFI arch '${efi_arch}', skipping it." >&2
   fi
done

## Install bootstrap GRUB config that locates the live ISO root.
cp -- "${grub_config_dir}/esp-redirect.cfg" "${esp_stage_dir}/boot/grub/grub.cfg"

## Calculate the approximate size of the ESP before building it.
find "${esp_stage_dir}" -exec touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" -- {} +
esp_byte_count=0
while IFS= read -r -d '' f ; do
   esp_byte_count=$(( esp_byte_count + $(stat -c %s -- "${f}") ))
done < <(find "${esp_stage_dir}" -type f -print0)

## Add some extra clusters' worth for directory slack.
esp_byte_count=$(( esp_byte_count + 4096 * 4 ))

## Add some more slack for filesystem structures, round up to nearest 32 KB
## boundary
esp_block_count=$(( (esp_byte_count / 1024 + 55) / 32 * 32 ))

## Build the ESP.
efi_img="${binary_dir}/boot/grub/efi.img"
## 32-bit hex volume id derived from SOURCE_DATE_EPOCH for reproducibility.
esp_volid="$(printf '%08x' $(( SOURCE_DATE_EPOCH % 4294967296 )) )"
mkfs.msdos -C "${efi_img}" "${esp_block_count}" -i "${esp_volid}" >/dev/null
mmd -i "${efi_img}" ::/EFI ::/EFI/boot ::/boot ::/boot/grub
mcopy -m -o -i "${efi_img}" "${esp_stage_dir}/EFI/boot/"*.efi ::/EFI/boot
mcopy -m -o -i "${efi_img}" "${esp_stage_dir}/boot/grub/grub.cfg" ::/boot/grub

## Copy the loaders to the ISO too, some firmware will use these instead.
cp -a -- "${esp_stage_dir}/EFI" "${binary_dir}/"

## Prepare to write GRUB configuration.
printf '%s\n' "${me}: writing GRUB configuration" >&2
cp -a -- "${rootfs_dir}/usr/share/grub/unicode.pf2" "${binary_dir}/boot/grub/unicode.pf2"

## Prepare to assemble GRUB config.
iso_cmdline="rd.live.overlay.overlayfs=1 splash rd.live.image root=live:CDLABEL=${volume_label} rd.live.dir=live rd.live.squashimg=filesystem.squashfs iso-scan/filename=\${iso_path}"
grub_timeout="30"
if [ "${serial_console}" = "true" ] ; then
   iso_cmdline="${iso_cmdline} console=tty0 console=ttyS0,115200n8"
   ## When the serial console is enabled, boot the default entry immediately.
   grub_timeout="0"
fi
## Add kernel hardening before the SMBIOS reader so SMBIOS can inject console=
## and not be overridden.
iso_cmdline="${iso_cmdline} ${rootfs_kernel_hardening}"
if [ "${smbios_reader}" = "true" ] ; then
   iso_cmdline="${iso_cmdline} \${dm_smbios_extra}"
fi

## Install template GRUB config files.
printf '%s\n' "${me}: assembling GRUB configuration" >&2
cp -- "${grub_config_dir}/grub.cfg" \
  "${grub_config_dir}/config.cfg" \
  "${grub_config_dir}/loopback.cfg" \
  "${grub_config_dir}/theme.cfg" \
  "${binary_dir}/boot/grub/"
mkdir -p -- "${binary_dir}/boot/grub/live-theme"
cp -- "${grub_config_dir}/live-theme/theme.txt" "${binary_dir}/boot/grub/live-theme/theme.txt"

## Append the keyboard layout submenu, sourcing the theme inside it so it
## matches the rest of the menu.
if [ -n "${kb_layout_submenu}" ] ; then
   printf '%s\n' "${kb_layout_submenu}" >> "${binary_dir}/boot/grub/grub.cfg"
   sed -i "s/\(submenu 'Keyboard layout options'.*\)/\1\n  source \/boot\/grub\/theme.cfg/" \
      "${binary_dir}/boot/grub/grub.cfg"
fi

## Tee the GRUB menu to ttyS0 if --serial-console is passed.
if [ "${serial_console}" = "true" ] ; then
  printf '%s\n' "
insmod serial
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_output serial console" >> "${binary_dir}/boot/grub/config.cfg"
fi

## Install the SMBIOS cmdline reader if --smbios-reader is passed.
if [ "${smbios_reader}" = "true" ] ; then
   printf '%s\n' '' >> "${binary_dir}/boot/grub/config.cfg"
   cat -- "${grub_config_dir}/smbios-reader.cfg" >> "${binary_dir}/boot/grub/config.cfg"
fi

## Substitute branding placeholders in the SVG, then rasterize to PNG.
splash_svg="${workdir}/splash.svg"
splash_content="$(cat -- "${grub_config_dir}/splash.svg"; printf '%s\n' 'x')"
splash_content="${splash_content%x}"
splash_content="${splash_content//@DIST_NAME@/${dist_pretty}}"
splash_content="${splash_content//@DIST_VERSION@/${dist_version}}"
printf '%s' "${splash_content}" > "${splash_svg}"
rsvg-convert --format png --width 800 --height 600 \
   --output "${binary_dir}/boot/grub/splash.png" -- "${splash_svg}"

## Substitute setting placeholders in each GRUB config file.
while IFS= read -r -d '' cfg_file ; do
   cfg_content="$(cat -- "${cfg_file}"; printf '%s\n' 'x')"
   cfg_content="${cfg_content%x}"
   cfg_content="${cfg_content//@APPEND_LIVE@/${iso_cmdline}}"
   cfg_content="${cfg_content//@TIMEOUT@/${grub_timeout}}"
   printf '%s' "${cfg_content}" > "${cfg_file}"
done < <(find "${binary_dir}/boot/grub" -type f -name '*.cfg' -print0)

## Assemble command line options to xorriso.
printf '%s\n' "${me}: assembling ISO '${output_iso}'" >&2
xorriso_opts=(
   -R -r -J -joliet-long -l -iso-level 3
   -volid "${volume_label}"
   --modification-date="$(date --utc --date="@${SOURCE_DATE_EPOCH}" +%Y%m%d%H%M%S00)"
)

if [ "${enable_bios_boot}" = "true" ] ; then
   ## Legacy BIOS: hybrid MBR from GRUB (USB) + El Torito no-emulation core (CD).
   xorriso_opts+=(
      --grub2-boot-info
      --grub2-mbr "${rootfs_dir}/usr/lib/grub/i386-pc/boot_hybrid.img"
      -no-emul-boot -boot-load-size 4 -boot-info-table
      -b boot/grub/grub_eltorito
      -eltorito-alt-boot
   )
fi

## For UEFI, expose the ESP FAT image as the El Torito EFI entry and as a
## real GPT partition so USB/HDD firmware ESP-scan finds it.
##
## TODO: `man xorrisofs` states that -efi-boot-part and -isohybrid-gpt-basdat
## cannot be used together, yet they're used together here and in live-build.
## Why does this work / does it work?
xorriso_opts+=(
   -efi-boot-part --efi-boot-image
   -e boot/grub/efi.img -no-emul-boot
   -isohybrid-gpt-basdat
)

## Add memtest86+ to the ISO if applicable.
memtest_bios_file=""
memtest_efi_file=""
if [ "${arch}" = 'amd64' ]; then
   memtest_bios_file="/boot/memtest86+x64.bin"
   memtest_efi_file="/boot/memtest86+x64.efi"
fi
if [ -n "${memtest_bios_file}" ] && [ -e "${memtest_bios_file}" ]; then
   printf '%s\n' "${me}: staging memtest86+ into /live" >&2
   cp -- "${memtest_bios_file}" "${binary_dir}/live/memtest.bin"
   ## The EFI image is best-effort (older memtest86+ ships BIOS only).
   [ -f "${memtest_efi_file}" ] && cp -- "${memtest_efi_file}" "${binary_dir}/live/memtest.efi"
elif [ -n "${memtest_bios_file}" ]; then
   printf '%s\n' "${me}: NOTE: memtest86+ binary '${memtest_bios_file}' not found." >&2
fi

## Add an on-ISO package repo if applicable.
if [ -n "${iso_pkg_repo}" ]; then
   cp -r -- "${iso_pkg_repo}" "${binary_dir}/pkg-repo"
fi

## Pin every staged file's mtime to SOURCE_DATE_EPOCH so the ISO is reproducible.
find "${binary_dir}" -exec touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" -- {} +

safe-rm --force -- "${output_iso}"
xorriso -as mkisofs "${xorriso_opts[@]}" -o "${output_iso}" "${binary_dir}"

## Embed an ISO md5 for GRUB/dracut 'rd.live.check' if isomd5sum is present.
if command -v implantisomd5 >/dev/null 2>&1 ; then
   implantisomd5 "${output_iso}"
fi

## Pin the output mtime for reproducibility.
touch --date="@${SOURCE_DATE_EPOCH}" -- "${output_iso}"

printf '%s\n' "${me}: done: '${output_iso}'" >&2
