Using systemd-boot on Debian Bullseye

This post describes how to replace Grub with the systemd-bootUEFI boot manager. The setup is very similar to the method described in the article Replace GRUB2 on Ubuntu 10.04 with a few simplifications (no secure boot) and with a slightly improved kernel installer hook script.

Start by creating a boot config file /boot/efi/loader/loader.conf with the following content.

default debian
timeout 1
editor 1

Create the directory that will hold the configuration files for the installed kernel images

mkdir -p /boot/efi/loader/entries

and install the boot loader with the bootctl command

bootctl install --path=/boot/efi

You can then check if the installation was successful with the efibootmgr. This command is also useful to change the boot order and to delete old boot entries.

Now systemd-boot is installed, but it does not know about the installed kernel images in the system.

The following script updates the systemd-boot configuration with the installed kernel images. Save the script to a system location, like /usr/local/bin/update-systemd-boot.sh and call it every time a new kernel is installed or removed from the system.

You will have to change the content of the root_disk variable to use the UUID of the root disk. You can use the blkid command to find the correct id.

Also check the kernel parameters in the flags variable if you need special arguments passed to the kernel.

#!/bin/sh
#
# This is a simple kernel hook to populate the systemd-boot entries
# whenever kernel images are added or removed.
#


# The disk containing the root partition. Find yours with `sudo blkid`
root_disk="UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# The linux kernel arguments
flags="ro quiet"

prog_name=`basename $0`
boot_dir="/boot"
sd_boot_dir="loader/entries"
sd_boot_path="$boot_dir/efi/$sd_boot_dir"
title=
prefix=
force=0
verbose=0


usage() {
    echo "$prog_name [OPTIONS]"
}

help() {
    usage
    echo
    echo "OPTIONS"
    echo "  -h          show this help page"
    echo "  -f          force overwrite"
    echo "  -v          make verbose"
}

while getopts hfv opt; do
    case "$opt" in
        h)  help
            exit 0;;
        f)  force=1;;
        v)  verbose=1;;
        \?)     # unknown flag
            usage >&2
            exit 1;;
    esac
done
shift `expr $OPTIND - 1`

if [ $# -ne 0 ]; then
    echo >&2 "No extra argument allowed"
    exit 1
fi


sd_boot_entry() {
    cat <<-EOF
	title   $title
	version $version
	linux   /$sd_boot_dir/vmlinuz-$version
	initrd  /$sd_boot_dir/initrd.img-$version
	options root=$root_disk $flags
	EOF
}


if [ ! -h "/dev/disk/by-uuid/${root_disk#UUID=}" ]; then
    echo >&2 "error: root disk '$root_disk' not found"
    exit 1
fi

if [ ! -d "$sd_boot_path" ]; then
    echo >&2 "error: directory '$sd_boot_path' not found"
    exit 1
fi

if [ ! -f /etc/machine-id ]; then
    echo >&2 "error: machine id file '/etc/machine-id' not found"
    exit 1
fi
prefix="`cat /etc/machine-id`-v"

if [ -f /etc/os-release ]; then
    title=`sed -ne '/^PRETTY_NAME=/s/.*"\(.*\)".*/\1/p' /etc/os-release`
else
    title=Debian
fi


echo "Updating Systemd boot entries"

# Copy images from the Debian install directory to the EFI partition
find "$boot_dir" -maxdepth 1 -type f -name '*.dpkg-tmp' -prune -o -name 'vmlinuz-*' -exec cp -u {} "$sd_boot_path" \;
find "$boot_dir" -maxdepth 1 -type f -name '*.dpkg-tmp' -prune -o -name 'initrd.img-*' -exec cp -u {} "$sd_boot_path" \;

# Remove files from the EFI if they are missing from the Debian install directory
find "$sd_boot_path" -maxdepth 1 -type f -name 'vmlinuz-*' | while read i; do
    kernel="$boot_dir/`basename $i`"
    if [ ! -f "$kernel" ]; then
        rm -f "$i"
    fi
done
find "$sd_boot_path" -maxdepth 1 -type f -name 'initrd.img-*' | while read i; do
    initrd="$boot_dir/`basename $i`"
    if [ ! -f "$initrd" ]; then
        rm -f "$i"
    fi
done

# Remove the conf file
find "$sd_boot_path" -maxdepth 1 -type f -name "$prefix*.conf" | sort -Vr | while read i; do
    version=`basename $i | sed -e "s/$prefix//; s/.conf$//"`
    kernel="$sd_boot_path/vmlinuz-$version"
    initrd="$sd_boot_path/initrd.img-$version"
    if [ ! -f "$kernel" -o ! -f "$initrd" ]; then
        echo "Removing kernel v$version"
        rm -f "$i" "$kernel" "$initrd"
    fi
done

# Add new kernel entries
find "$boot_dir" -maxdepth 1 -type f -name '*.dpkg-tmp' -prune -o -name 'vmlinuz-*' -print | sort -Vr | while read i; do
    version=`basename $i | sed -e 's/vmlinuz-//'`
    initrd="$boot_dir/initrd.img-$version"
    file="$sd_boot_path/$prefix$version.conf"
    echo "Found kernel `basename $i`"
    if [ ! -f "$initrd" ]; then
        echo "Ignoring $i"
    elif [ $force -eq 1 -o ! -f "$file" ]; then
        echo "Adding $file"
        sd_boot_entry "$version" > "$file"
        cp -v $i "$initrd" "$sd_boot_path"
    fi
done

Calling the update script every time a kernel is installed or removed can be tedious, and luckily this can be automated. Simply create the following two scripts /etc/kernel/postinst.d/zz-update-systemd-boot and /etc/kernel/postrm.d/zz-update-systemd-boot with this content:

#! /bin/sh
set -e

sh /usr/local/bin/update-systemd-boot.sh
exit 0

Now you should see all installed kernel images in the systemd-boot menu.

It is not necessary to remove grub, as long as it appears after systemd-boot in the output of efibootmgr. This also provides a useful fallback in case systemd-boot is having a problem.