#!/bin/sh
# SPDX-License-Identifier: GPL-3.0+
# Copyright 2025 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>

set -eu

usage() {
  echo "Set up eMMC with a /boot partition which loads the OS from the SSD." >&2
  echo "This tool is usually run from a rescue system on an SD-card to" >&2
  echo "migrate a system from one SoM to another." >&2
  echo "WARNING: this tool will erase the contents of your eMMC." >&2
  echo >&2
  echo "Usage: $0 [--help]" >&2
  echo >&2
  echo "Options:" >&2
  echo "  --help           Display this help and exit." >&2
  echo >&2
  echo "This utility is not able to create a working /boot partition on" >&2
  echo "eMMC for arbitrary installations. It currently understands" >&2
  echo >&2
  echo " - single partition on SSD with the rootfs" >&2
  echo " - two partitions on SSD, one with the rootfs the other swap" >&2
  echo " - the luks/lvm setup created by reform-setup-encrypted-disk" >&2
}

is_current_root() {
  FSTAB_ROOT="$(findmnt --fstab --noheadings --evaluate --mountpoint / --output SOURCE)"
  [ "${1}" = "${FSTAB_ROOT}" ]
}

if [ "$#" -gt 0 ]; then
  case $1 in --help)
    usage
    exit 0
    ;;
  *)
    echo "E: unknown option: $1" >&2
    ;;
  esac
fi

# shellcheck source=/dev/null
if [ -e "./machines/$(cat /proc/device-tree/model).conf" ]; then
  . "./machines/$(cat /proc/device-tree/model).conf"
elif [ -e "/usr/share/reform-tools/machines/$(cat /proc/device-tree/model).conf" ]; then
  . "/usr/share/reform-tools/machines/$(cat /proc/device-tree/model).conf"
else
  echo "E: unable to find config for $(cat /proc/device-tree/model)" >&2
  exit 1
fi

cat <<END

WARNING: This script will destroy the contents of your eMMC drive and replace
it with a /boot partition which is able to boot your system on SSD.

WARNING: This operation needs internet access to your Debian apt mirror. Make
sure that your system is online before proceeding.

END
printf "Are you sure you want to proceed? [y/N] "
read -r response
if [ "$response" != "y" ]; then
  echo "Exiting."
  exit
fi

# This script has two modes: either it is executed from a system on SD-card
# or from a system on SSD. In the former case, it will run reform-rescue-shell
# and run the setup code from within a chroot on the SSD. In the latter case,
# it latter case the setup code will run directly.
case $(findmnt --noheadings --evaluate --mountpoint / --output SOURCE) in
  "/dev/${DEV_SD}"*) : ;;
  *)
    echo "I: Running without chroot isolation of reform-rescue-shell" >&2

    case $(findmnt --fstab --noheadings --mountpoint /boot --output SOURCE) in
      "/dev/${DEV_MMC}p1") : ;;
      *)
        echo "E: The current system does not mount the first partition on eMMC as /boot according to /etc/fstab" >&2
        exit 1
        ;;
    esac
    if ! mountpoint --quiet "/boot"; then
      echo "E: /boot has nothing mounted on it" >&2
      exit 1
    fi
    case $(findmnt --noheadings --evaluate --mountpoint /boot --output SOURCE) in
      "/dev/${DEV_MMC}p1") : ;;
      *)
        echo "E: The current system does not have the first partition on eMMC mounted as /boot" >&2
        exit 1
        ;;
    esac
    umount /boot
    ;;
esac

# eMMC device is being used (case 1): mount points are known, show them. Includes swap.
if [ -n "$(lsblk --noheadings --output=MOUNTPOINT "/dev/${DEV_MMC}")" ]; then
  echo "E: eMMC has the following mounted volumes, unmount them before running this tool" >&2
  lsblk --noheadings --output=MOUNTPOINT "/dev/${DEV_MMC}" | xargs --no-run-if-empty -I '{}' echo "E:   {}" >&2
  exit 1
fi

# eMMC device is being used (case 2): there are not file systems directly mounted on the block device
# but it is opened by consumers like device-mapper, raid or luks, to name some examples. In this situation
# it is not trivial to locate the consumer.
# reform-boot-config does the same thing (could share code?)
get_exclusive_write_lock() {
  ret=0
  python3 - "$1" <<EOF || ret=$?
import errno, os, sys

try:
    os.open(sys.argv[1], os.O_WRONLY | os.O_EXCL)
except OSError as e:
    if e.errno == errno.EBUSY:
        sys.exit(1)
    raise
EOF
  return $ret
}

if ! get_exclusive_write_lock "/dev/${DEV_MMC}"; then
  echo "E: device /dev/${DEV_MMC} (eMMC) is still in use" >&2
  exit 1
fi

# reform-boot-config does the same thing (could share code?)
parted --script --machine "/dev/$DEV_MMC" "mklabel msdos"
# create two partitions as expected by reform-check (but we only format /boot)
# make /boot as big as mkimage.sh would
parted --script --machine "/dev/$DEV_MMC" "mkpart primary ext4 16MiB $((16 + 488))MiB"
parted --script --machine "/dev/$DEV_MMC" "mkpart primary ext4 $((16 + 488))MiB 100%"
udevadm settle
partprobe "/dev/$DEV_MMC"
mkfs.ext4 -F "/dev/${DEV_MMC}p1"
# create extlinux directory to make reform-rescue-shell recognize this as a
# /boot partiton
debugfs -w "/dev/${DEV_MMC}p1" -R "mkdir /extlinux"

case $(findmnt --noheadings --evaluate --mountpoint / --output SOURCE) in
  "/dev/${DEV_SD}"*) : ;;
  *)
    # The script is executed without reform-rescue-shell isolation
    # Above we checked that the system's /etc/fstab mounts the first partition
    # from eMMC on /boot
    mount /boot
    # Verify this assumption
    case $(findmnt --noheadings --evaluate --mountpoint /boot --output SOURCE) in
      "/dev/${DEV_MMC}p1") : ;;
      *)
        echo "E: Mounting /boot did not mount the first partition on eMMC -- check your /etc/fstab" >&2
        exit 1
        ;;
    esac
    ;;
esac

set --

# If this is run from an SD-card, run with the isolation from reform-rescue-shell
case $(findmnt --noheadings --evaluate --mountpoint / --output SOURCE) in
  "/dev/${DEV_SD}"*) set -- "reform-rescue-shell" ;;
esac

# Pass script as string argument with -c instead of sending it to standard input
# of sh because we need stdin of sh connected to the current interactive tty
# or otherwise the user will be unable to send keys to interactive prompts.
# shellcheck disable=SC2016
"$@" sh -c '
set -exuo pipefail
apt-get update --error-on=any
if ! dpkg-query --search "/boot/vmlinuz-*-mnt-reform-arm64" >/dev/null && ! dpkg-query --search "/usr/lib/modules/*-mnt-reform-arm64/vmlinuz" >/dev/null; then
  echo "I: No kernel package installed." >&2
  printf "Do you want this script to install linux-image-mnt-reform-arm64 for you? [y/N] "
  read -r response
  if [ "$response" != "y" ]; then
    echo "E: no kernel packages installed, exiting." >&2
    exit 1
  fi
  apt-get install --yes linux-image-mnt-reform-arm64
fi

# upgrade linux-image-mnt-reform-arm64 in case that the installed kernel is
# not downloadable anymore
apt-get install --yes --only-upgrade linux-image-mnt-reform-arm64

#
#  version          | package with vmlinuz                  | location of vmlinuz
# ------------------+---------------------------------------+---------------------------------------------
#  < 6.19           | linux-image-${kver}-mnt-reform-arm64  | /boot/vmlinuz-*-mnt-reform-arm64
#  >= 6.19 && < 7.2 | linux-binary-${kver}-mnt-reform-arm64 | /boot/vmlinuz-*-mnt-reform-arm64
#  >= 7.2           | linux-binary-${kver}-mnt-reform-arm64 | /usr/lib/modules/*-mnt-reform-arm64/vmlinuz
#
# With the contents of /boot lost we need to re-download the package containing
# /boot/vmlinuz-*-mnt-reform-arm64 and to run all the kernel installation
# scripts, see linux-run-hooks(1).
#
# First, re-download and install the packages shipping the kernel in /boot
{
  dpkg-query --search "/boot/vmlinuz-*-mnt-reform-arm64"
} | sed "s/:.*//" | while read -r pkg; do
  # if a package cannot be downloaded, apt will still exit
  # successfully
  apt-get install --yes --reinstall "$pkg"
  # make sure that the respective kernel exists now in /boot
  if [ -n "$(dpkg --verify "$pkg")" ]; then
    echo "E: Failed re-installation of $pkg" >&2
    exit 1
  fi
done
# Then, dpkg-reconfigure the linux-image-${kver}-mnt-reform-arm64 package for
# each linux-binary-${kver}-mnt-reform-arm64 package that was found. Even if
# the latter was re-installed before, that will not trigger the kernel
# installation scripts for that version.
{
  dpkg-query --search "/boot/vmlinuz-*-mnt-reform-arm64"
  dpkg-query --search "/usr/lib/modules/*-mnt-reform-arm64/vmlinuz"
} | sed "s/:.*//" | while read -r pkg; do
  case "$pkg" in
    linux-image-*-mnt-reform-arm64)
      : # Nothing to do because the package already got re-installed above
      ;;
    linux-binary-*-mnt-reform-arm64)
      dpkg-reconfigure "linux-image-${pkg##linux-binary-}"
      ;;
    *)
      echo "E: unexpected kernel package name: $pkg" >&2
      exit 1
      ;;
  esac
done
if [ -z "$(find "/boot" -name "vmlinuz-*")" ]; then
  echo "E: no kernel images were installed into /boot" >&2
  exit 1
fi
reform-boot-config --no-copy-old-boot --force --emmc "$(findmnt --noheadings --evaluate --mountpoint / --output SOURCE)"
case $(findmnt --fstab --noheadings --mountpoint /boot --output SOURCE) in
  "/dev/${DEV_MMC}p1") : ;;
  *)
    echo "E: reform-boot-config did not add the first partition on eMMC as /boot in /etc/fstab" >&2
    exit 1
    ;;
esac
'

if [ "$EMMC_BOOT" != false ]; then
  reform-flash-bootloader emmc
fi
