#!/usr/bin/bash
# lamboot-kernel-hook — BLS entry generator for kernel install/remove events.
#
# Called by:
#   /usr/lib/kernel/install.d/90-lamboot.install (kernel-install framework)
#   /etc/kernel/postinst.d/zz-lamboot (Debian/Ubuntu/Proxmox hook)
#   /etc/kernel/postrm.d/zz-lamboot (Debian/Ubuntu/Proxmox hook)
#   lamboot-install --proxmox-host --refresh / --repair-bls (bulk backfill)
#
# Usage:
#   lamboot-kernel-hook add KERNEL_VERSION [KERNEL_IMAGE]
#   lamboot-kernel-hook remove KERNEL_VERSION
#
# Entries are written to the ESP at $ESP/loader/entries/{distro}-{version}.conf.
# The ESP is LamBoot's only reliable BLS source on a host with no separate
# /boot partition (Proxmox LVM/ZFS root): LamBoot scans /loader/entries on
# each mounted volume, and only the ESP volume actually has that directory.
#
# Two correctness points this hook gets right for the no-separate-/boot case
# (where naive kernel-install plugins produce non-bootable entries):
#   * options must carry root= — /etc/kernel/cmdline conventionally omits it,
#     so the kernel would panic with no rootfs. We inject it from the live
#     root device when absent.
#   * linux/initrd paths must keep the /boot/ prefix when /boot lives on the
#     root filesystem (LamBoot resolves the path against the root volume, not
#     a /boot partition). See boot_path_prefix().

set -uo pipefail

# Default: do not overwrite an existing entry (preserve operator edits).
# lamboot-install --repair-bls exports LAMBOOT_BLS_FORCE=1 to force a rebuild.
FORCE="${LAMBOOT_BLS_FORCE:-0}"

ESP=""
BLS_DIR=""
PREFIX=""
CMDLINE_FILE="/etc/kernel/cmdline"

die() { printf "lamboot-kernel-hook: ERROR: %s\n" "$1" >&2; exit 1; }

find_esp() {
    for mp in /boot/efi /efi; do
        if mountpoint -q "$mp" 2>/dev/null; then
            ESP="$mp"
            BLS_DIR="$ESP/loader/entries"
            return 0
        fi
    done
    die "Cannot find ESP mount point (/boot/efi or /efi)"
}

# v0.15.0: when /boot is a separate partition LamBoot reads natively (vfat via
# the FatRo reader; ext2/3/4 + btrfs via native backends), write the BLS entry
# ON /boot so LamBoot sources the kernel in place — matching lamboot-install's
# detect_bls_target. Otherwise keep ESP placement (Proxmox LVM/ZFS root; xfs/
# f2fs/ntfs/zfs separate /boot). boot_path_prefix() already drops the /boot
# prefix for a separate /boot, so the linux/initrd values stay correct.
resolve_bls_dir() {
    mountpoint -q /boot 2>/dev/null || return 0
    local boot_src esp_src boot_fs
    boot_src=$(findmnt -n -o SOURCE /boot 2>/dev/null)
    esp_src=$(findmnt -n -o SOURCE "$ESP" 2>/dev/null)
    boot_fs=$(findmnt -n -o FSTYPE /boot 2>/dev/null)
    [ -n "$boot_src" ] && [ "$boot_src" != "$esp_src" ] || return 0
    case "$boot_fs" in
        vfat|ext2|ext3|ext4|btrfs) BLS_DIR="/boot/loader/entries" ;;
    esac
}

# Path prefix for `linux`/`initrd` BLS fields. LamBoot resolves these against
# the volume it mounts. When /boot is its own partition LamBoot mounts that
# partition (its root IS /boot) → no prefix. When /boot lives on the root
# filesystem (Proxmox LVM/ZFS) LamBoot mounts the root fs → keep /boot/.
boot_path_prefix() {
    if mountpoint -q /boot 2>/dev/null; then
        printf ''
    else
        printf '/boot'
    fi
}

# Map a real /boot/<file> path to the BLS value for the current layout.
bls_path() {
    local real="$1"
    printf '%s/%s' "$PREFIX" "${real#/boot/}"
}

get_distro_id() {
    if [ -f /etc/os-release ]; then
        # shellcheck disable=SC1091
        . /etc/os-release
        echo "${ID:-linux}"
    else
        echo "linux"
    fi
}

get_distro_name() {
    if [ -f /etc/os-release ]; then
        # shellcheck disable=SC1091
        . /etc/os-release
        echo "${PRETTY_NAME:-${NAME:-Linux}}"
    else
        echo "Linux"
    fi
}

get_kernel_cmdline() {
    if [ -f "$CMDLINE_FILE" ]; then
        cat "$CMDLINE_FILE"
        return
    fi
    if [ -f /proc/cmdline ]; then
        sed 's/BOOT_IMAGE=[^ ]* //; s/initrd=[^ ]* //' /proc/cmdline
    fi
}

# Inject root= (and ro) when the configured cmdline lacks it. Without root=
# the kernel cannot mount the rootfs. root= and ro lead, mirroring the layout
# systemd-boot's 90-loaderentry produces, so entries are byte-comparable.
ensure_root_in_cmdline() {
    local cmdline="$1"
    case " $cmdline " in
        *" root="*) printf '%s' "$cmdline"; return ;;
    esac
    local rootdev
    rootdev=$(findmnt -no SOURCE / 2>/dev/null)
    [ -n "$rootdev" ] || { printf '%s' "$cmdline"; return; }
    if [ -n "$cmdline" ]; then
        printf 'root=%s ro %s' "$rootdev" "$cmdline"
    else
        printf 'root=%s ro' "$rootdev"
    fi
}

# Emit one `initrd` line per existing image (microcode first, then the
# initramfs), already mapped to the BLS path for this layout.
find_initrd() {
    local version="$1"
    local kernel_image="${2:-}"
    for ucode in /boot/intel-ucode.img /boot/amd-ucode.img; do
        [ -f "$ucode" ] && bls_path "$ucode"
    done
    # Version-named initramfs first (Debian/Fedora: initramfs-$version.img,
    # initrd.img-$version). Then fall back to the kernel-image FLAVOR: Arch
    # names its image vmlinuz-linux and its initramfs initramfs-linux.img,
    # keyed on the flavor ("linux"), not the kernel version — so the
    # version-based patterns miss it and the entry would ship initrd-less.
    local flavor=""
    case "$kernel_image" in
        */vmlinuz-*) flavor="${kernel_image##*/vmlinuz-}" ;;
    esac
    local pattern
    for pattern in \
        "/boot/initramfs-${version}.img" \
        "/boot/initrd.img-${version}" \
        "/boot/initrd-${version}" \
        "/boot/initramfs-${version}-fallback.img" \
        ${flavor:+"/boot/initramfs-${flavor}.img"} \
        ${flavor:+"/boot/initrd-${flavor}"} \
        ${flavor:+"/boot/initramfs-${flavor}-fallback.img"}; do
        if [ -f "$pattern" ]; then
            bls_path "$pattern"
            break
        fi
    done
}

do_add() {
    local version="$1"
    local kernel_image="${2:-}"

    find_esp
    resolve_bls_dir
    PREFIX="$(boot_path_prefix)"
    mkdir -p "$BLS_DIR"

    local distro_id distro_name
    distro_id=$(get_distro_id)
    distro_name=$(get_distro_name)
    local entry_file="${BLS_DIR}/${distro_id}-${version}.conf"

    if [ -f "$entry_file" ] && [ "$FORCE" != "1" ]; then
        return 0
    fi

    local kernel_real=""
    if [ -n "$kernel_image" ] && [ -f "$kernel_image" ]; then
        kernel_real="$kernel_image"
    elif [ -f "/boot/vmlinuz-${version}" ]; then
        kernel_real="/boot/vmlinuz-${version}"
    else
        die "Cannot find kernel image for version ${version}"
    fi
    local kernel_path
    kernel_path=$(bls_path "$kernel_real")

    local initrd_lines
    initrd_lines=$(find_initrd "$version" "$kernel_real")

    local cmdline
    cmdline=$(ensure_root_in_cmdline "$(get_kernel_cmdline)")

    local machine_id=""
    [ -f /etc/machine-id ] && machine_id=$(cat /etc/machine-id)

    {
        echo "title      ${distro_name} (${version})"
        echo "version    ${version}"
        [ -n "$machine_id" ] && echo "machine-id ${machine_id}"
        echo "sort-key   ${distro_id}"
        echo "linux      ${kernel_path}"
        if [ -n "$initrd_lines" ]; then
            echo "$initrd_lines" | while IFS= read -r line; do
                [ -n "$line" ] && echo "initrd     ${line}"
            done
        fi
        [ -n "$cmdline" ] && echo "options    ${cmdline}"
    } > "$entry_file"

    echo "lamboot-kernel-hook: created ${entry_file}"
}

do_remove() {
    local version="$1"

    find_esp
    resolve_bls_dir

    local distro_id
    distro_id=$(get_distro_id)
    local entry_file="${BLS_DIR}/${distro_id}-${version}.conf"

    if [ -f "$entry_file" ]; then
        rm -f "$entry_file"
        echo "lamboot-kernel-hook: removed ${entry_file}"
    fi
}

action="${1:-}"
version="${2:-}"

case "$action" in
    add)
        [ -n "$version" ] || die "Usage: lamboot-kernel-hook add KERNEL_VERSION [KERNEL_IMAGE]"
        do_add "$version" "${3:-}"
        ;;
    remove)
        [ -n "$version" ] || die "Usage: lamboot-kernel-hook remove KERNEL_VERSION"
        do_remove "$version"
        ;;
    *)
        echo "Usage: lamboot-kernel-hook {add|remove} KERNEL_VERSION [KERNEL_IMAGE]" >&2
        exit 1
        ;;
esac
