Hi all i created a script that will allow you to use the Systemd-Boot loader even with SecureBoot enabled.
I have also cross-posted a link to here in @github: systemd/issues/16457.
Explanation of what this script does:
Current latest version is: zz-update-systemd-boot.txt (Remove extension and make executable)
Hope it is useful to the community,
Maybe Kubuntu devs can use this in it's distro
Output from my current running system:
I have also cross-posted a link to here in @github: systemd/issues/16457.
Explanation of what this script does:
- Automatically generate a boot-entry called "Linux Secure-Bootloader" for your UEFI-Bios boot menu.
And Automatically update (as needed) the Shim, MokManager and Systemd-boot used to boot your system in the ESP, see installUEFIbootEntry(). - When ever you install a new kernel, or install/update any software/drivers that causes the ramdisk to be regenarated, the script will automatically update the ones used in the ESP partition to boot your OS.
These files are in a separate 'kubuntu' vendor-dir in the ESP and does not interfere with those that your distro installs. - Automatically generate boot entries (as needed) in Systemd-boot's boot menu that you can choose from:
- The latest kernel and ramdisk.
This entry is generated with a boot-try counter of 3 to detect boot failures, see getLoaderVars(). - The latest kernel and previous ramdisk.
- The old kernel and old ramdisk.
- The latest kernel and ramdisk.
- Automatically adds the kernel commandline options used from current boot, including the current swap partition to resume from.
At moment it also always adds "intel_iommu=on", see getCmdLineOptions() if you don't want or modify this behaviour.
Warning: First boot/After each update of Systemd-boot you need to either:
(Maybe i will automate this step later on)
- Enroll the hash of <ESP>/EFI/systemd/grubx64.efi using the MokManager that will come-up after boot.
- OR automatically sign <ESP>/EFI/systemd/grubx64.efi with an accepted key for your SecureBoot system (eg. in db, KEK or MokList).
You can't use /var/lib/shim-signed/mok/MOK.key to sign it because it is only valid for kernel-drivers...
(Maybe i will automate this step later on)
Current latest version is: zz-update-systemd-boot.txt (Remove extension and make executable)
Code:
#!/usr/bin/env bash
# This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
# https://creativecommons.org/licenses/by-sa/4.0/
#
# @version 2020.07.26
# @author ©TriMoon™ <https://gitlab.com/TriMoon>
# @copyright (C) 2020+ {@link https://gitlab.com/TriMoon|©TriMoon™}
# @license CC-BY-SA-4.0
#
# To install this script execute with 'installme' as argument.
# Manual test after installing can be done by using `update-initramfs -u`
#
# shellcheck disable=SC2046
if test $(id -u) -ne 0; then
printf "%s\n" \
"Need ROOT, aborting !"
exit 1
fi
# Global variables used in this script...
declare -i \
bTestFlags \
BootTries \
versionFound \
latestFound \
prevFound \
oldFound
declare -a \
KERNEL_CMDLINE
declare \
ESP \
MACHINE_ID \
SWAP_PAR_UUID \
ROOT_PAR_UUID \
KERNEL \
KERNEL_LATEST \
VERSION_KERNEL_LATEST \
KERNEL_OLD \
VERSION_KERNEL_OLD \
RAMDISK \
RAMDISK_LATEST \
VERSION_RAMDISK_LATEST \
RAMDISK_OLD \
VERSION_RAMDISK_OLD \
LOADERID \
VERSION \
loader_entries \
loader_conf \
log_name \
log_func_name \
log_header_start \
log_header_end \
log_func_start \
log_func_end \
log_header \
log_func
# shellcheck disable=SC2034
source /etc/os-release
# Install this script
function installScript(){
local -a \
opts \
extradirs
local \
mainDest
log_func_name="Install"
log_func="${log_func_start}${log_func_name}${log_func_end}"
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Installing helper..."
extradirs=(
# One for the kernel's postrm
/etc/kernel/postrm.d
# Another for the initramfs's post-update
/etc/initramfs/post-update.d
)
opts=(
--verbose
--parents
)
# Not always available by default...
mkdir "${opts[@]}" \
/etc/initramfs/post-update.d
# Install a fresh script in main place if needed.
mainDest="/etc/kernel/postinst.d/zz-update-systemd-boot"
if test ! -f "$mainDest" \
|| ! diff "$0" "$mainDest" >/dev/null
then
# copy into place
opts=(
--verbose
--archive
)
cp "${opts[@]}" "$0" "$mainDest"
# Set access permissions
opts=(
--modify
"u::rwx,g::rx,o::r,g:adm:rwx"
)
setfacl "${opts[@]}" "$mainDest"
# Remove the symlinks, if any, to reflect timestamp on new ones.
for d in "${extradirs[@]}"; do
rm --force "$d/zz-update-systemd-boot"
done
fi
# Link the other needed scripts to the main one.
opts=(
--verbose
--symbolic
--force
)
for d in "${extradirs[@]}"; do
if test ! -L "$d/zz-update-systemd-boot"; then
ln "${opts[@]}" \
"$mainDest" \
"$d/zz-update-systemd-boot"
fi
done
# opts=(
# -l
# --all
# "--color=auto"
# )
# for d in /etc/kernel/postinst.d "${extradirs[@]}"; do
# ls "${opts[@]}" "$d/zz-update-systemd-boot"
# done
}
function checkNeededBins(){
local -i missingBins
local -a neededBins
local Bin
log_func_name="check needed bins"
log_func="${log_func_start}${log_func_name}${log_func_end}"
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Check needed bins..."
# Check for needed binaries for functionality.
neededBins=(
# pkg: coreutils
"/bin/cp"
"/bin/mv"
"/bin/rm"
"/usr/bin/head"
"/usr/bin/mktemp"
"/usr/bin/readlink"
"/usr/bin/test"
"/usr/bin/touch"
# pkg: sed
"/bin/sed"
# pkg: grep
"/bin/grep"
# pkg: libc-bin
# To create the BOOTX64.CSV
"/usr/bin/iconv"
# pkg: mount
# To determine swap partition.
"/sbin/swapon"
# pkg: util-linux
# To get UUID of root and swap partitions
"/bin/lsblk"
# pkg: systemd
# To determine root partition
"/bin/systemctl"
# Systemd-boot
"/usr/lib/systemd/boot/efi/systemd-bootx64.efi"
# pkg: shim
# MokManager
"/usr/lib/shim/mmx64.efi"
# Unsigned-Shim loader
"/usr/lib/shim/shimx64.efi"
# pkg: shim-signed
# Signed-Shim loader
# This package is optional because you can sign your own,
# but if you DO sign your own then place it here:
# "/usr/lib/shim/shimx64.efi.signed"
# pkg: efibootmgr
# For EFI-boot entries
"/bin/efibootmgr"
# pkg: systemd
# For determining ESP
"/usr/bin/bootctl"
# pkg: gcc
# To determine machine architecture
"/usr/bin/cc"
# pkg: diffutils
# To determine differences of binary files
"/usr/bin/cmp"
)
missingBins=0
for Bin in "${neededBins[@]}"; do
if test ! -f "$Bin"; then
missingBins=$((missingBins+1))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"'$Bin' missing !"
fi
done
if test $missingBins -gt 0; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Missing $missingBins needed binaries, aborting !"
exit 1
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"All ok"
fi
}
# Grab current Machine ID.
function getMachineID(){
MACHINE_ID=$(</etc/machine-id)
}
# Grab the UUID of the root partition.
function getRootUUID(){
local ROOT_PAR
# ROOT_PAR=$(\
# df -l --output=source / \
# | tail -n +2\
# )
# Ask systemd for the device of the root partition.
ROOT_PAR=$(\
systemctl status -- -.mount \
| sed -En "/What:/ {s|^.*\s(.*)$|\1|; p}"\
)
# get the UUID of the ROOT_PAR
ROOT_PAR_UUID=$(\
lsblk \
--fs \
--noheadings \
--output UUID \
"$ROOT_PAR" \
)
}
# Grab the UUID of the resume partition.
function getResumeUUID(){
local SWAP_PAR
# Ask swapon for the first swap partition.
# FIXME: We should check that its a partition !
SWAP_PAR=$(\
swapon \
--noheadings \
--show=NAME \
| head --lines=1\
)
# Grab the UUID of the SWAP_PAR
SWAP_PAR_UUID=$(\
lsblk \
--fs \
--noheadings \
--output=UUID \
"$SWAP_PAR"\
)
}
# Grab current ESP prtition.
function getESP(){
# Ask bootctl for the esp path.
ESP=$(\
bootctl --print-esp-path\
)
}
# Grab the command line options for the kernel.
function getCmdLineOptions(){
KERNEL_CMDLINE=()
# Iterate over the current command line options provided to the kernel.
for arg in $(</proc/cmdline); do
case $arg in
# Grub uses this, loader entries define it differently...
BOOT_IMAGE=*) ;&
# We set this
resume==*) ;&
# We set this
root=*) ;&
# We override this
intel_iommu=*) ;&
# Just a dummy to be stepped-into by above.
--non-existing-option--)
# We skip
continue ;;
# Add rest
*)
KERNEL_CMDLINE+=( "$arg" )
esac
done
# We override and always add these
# Comment out if not wanted...
KERNEL_CMDLINE+=( "intel_iommu=on" )
# echo "${KERNEL_CMDLINE[@]}"
}
# Grab the subdir to be used for the loader entries
# This function needs these variables defined before calling:
# ESP
function getLoaderVars(){
local d
loader_entries="/loader/entries"
BootTries=3
log_func_name="Loader Vars"
log_func="${log_func_start}${log_func_name}${log_func_end}"
# ID is populated from /etc/os-release
# These tests are just a few ideas at moment.
case "${ID,,}" in
"ubuntu")
# Below only works when already in a GUI,
# thats why we fix it below for now...
if test\
"$DESKTOP_SESSION" = "plasma" \
-a "$XDG_SESSION_DESKTOP" = "KDE" \
-a "$XDG_CURRENT_DESKTOP" = "KDE"
then
LOADERID=kubuntu
fi
LOADERID=kubuntu
;;
esac
if test -z "$LOADERID"; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Could not compose a loaderID, aborting !"
exit 1
fi
# Full path to the loader entry config file(s) without extension,
# so that we can append to it as needed.
loader_conf="${ESP}${loader_entries}/${LOADERID}"
# Create the needed directories when absent.
for d in "${ESP}${loader_entries}" "${ESP}/${LOADERID}"; do
test ! -d "$d" && mkdir --verbose "$d"
done
}
# Fill the kernel and ramdisk variables.
# This function needs these variables defined before calling:
# KERNEL
# RAMDISK
function getKernelAndRamdiskVars(){
### Find the Real filename and versions of:
# Latest kernel
KERNEL_LATEST=$(readlink --canonicalize-existing "/boot/${KERNEL}")
if test -n "$KERNEL_LATEST"; then
VERSION_KERNEL_LATEST="${KERNEL_LATEST#*-}"
VERSION_KERNEL_LATEST="${VERSION_KERNEL_LATEST%-*}"
fi
# Latest ramdisk
RAMDISK_LATEST=$(readlink --canonicalize-existing "/boot/${RAMDISK}")
if test -n "$RAMDISK_LATEST"; then
VERSION_RAMDISK_LATEST="${RAMDISK_LATEST#*-}"
VERSION_RAMDISK_LATEST="${VERSION_RAMDISK_LATEST%-*}"
fi
# Old kernel
KERNEL_OLD=$(readlink --canonicalize-existing "/boot/${KERNEL}.old")
if test -n "$KERNEL_OLD"; then
VERSION_KERNEL_OLD="${KERNEL_OLD#*-}"
VERSION_KERNEL_OLD="${VERSION_KERNEL_OLD%-*}"
fi
# Old ramdisk
RAMDISK_OLD=$(readlink --canonicalize-existing "/boot/${RAMDISK}.old")
if test -n "$RAMDISK_OLD"; then
VERSION_RAMDISK_OLD="${RAMDISK_OLD#*-}"
VERSION_RAMDISK_OLD="${VERSION_RAMDISK_OLD%-*}"
fi
# echo "$VERSION_KERNEL_LATEST"
# echo "$VERSION_RAMDISK_LATEST"
# echo "$VERSION_KERNEL_OLD"
# echo "$VERSION_RAMDISK_OLD"
# exit 123
}
# This function needs these variables defined before calling:
# ESP
function installUEFIbootEntry(){
local -a \
opts \
bootEntries
local -i \
needToCreateEntry
local \
destDir \
entryLabel \
vendor \
shimLib \
bootEntry \
bootEntryLabel
vendor="systemd"
entryLabel="Linux Secure-Bootloader"
destDir="${ESP}/EFI/$vendor"
log_func_name="UEFI bootEntry"
log_func="${log_func_start}${log_func_name}${log_func_end}"
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Preparing..."
# Create the destination dir if non-existant.
test ! -d "${destDir}" && mkdir -v "${destDir}"
opts=(
--verbose
# Does NOT work because of ESP-FS's timestamp precision
# --update
--archive
"--no-preserve=ownership"
)
shimLib="/usr/lib/shim"
# Copy the Signed-Shim loader.
if test -f "${shimLib}/shimx64.efi.signed"; then
# Preventing un-needed writes.
if ! cmp "${shimLib}/shimx64.efi.signed" \
"${destDir}/shimx64.efi" \
>/dev/null
then
cp "${opts[@]}" \
"${shimLib}/shimx64.efi.signed" \
"${destDir}/shimx64.efi"
fi
# Else copy the Unsigned-Shim loader.
elif test -f "${shimLib}/shimx64.efi"; then
# Preventing un-needed writes.
if ! cmp "${shimLib}/shimx64.efi" \
"${destDir}/shimx64.efi" \
>/dev/null
then
cp "${opts[@]}" \
"${shimLib}/shimx64.efi" \
"${destDir}"
fi
else
# This should NEVER happen,
# because we already tested in checkNeededBins()
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Unable to find a Shim binary, aborting !"
exit 1
fi
# Copy the Shim loader's mokManager.
# Preventing un-needed writes.
if ! cmp "${shimLib}/mmx64.efi" \
"${destDir}/mmx64.efi" \
>/dev/null
then
cp "${opts[@]}" \
"${shimLib}/mmx64.efi" \
"${destDir}"
fi
# Copy the Systemd-boot loader.
# Preventing un-needed writes.
if ! cmp "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
"${destDir}/grubx64.efi" \
>/dev/null
then
cp "${opts[@]}" \
"/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
"${destDir}/grubx64.efi"
fi
# Create the 'BOOTX64.CSV' file
printf "%s,%s,%s,%s\n" \
"shimx64.efi" \
"$entryLabel" \
"" \
"This is the SecureBoot entry for Systemd-Boot" \
| iconv -t UCS-2 --output="${destDir}/BOOTX64.CSV"
# Grab EFI-bootloader entries we are interested in.
mapfile -t bootEntries < <(
efibootmgr --verbose \
| grep --ignore-case --regexp="\\\\$vendor\\\\" \
| sed -E 's|^Boot([[:digit:]]+).*|\1|'
)
# Check if we need to create a fresh UEFI bootEntry.
needToCreateEntry=1
if test "${#bootEntries[@]}" -gt 0; then
for bootEntry in "${bootEntries[@]}"; do
bootEntryLabel=$(
efibootmgr \
| sed -En "/^Boot$bootEntry/ {s|Boot$bootEntry(\*)?\s+(.*)|\2|; p}"
)
case "$bootEntryLabel" in
"Linux Bootloader")
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Found default systemd-boot entry"
# Just informational.
;;
"$entryLabel")
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"UEFI SecureBoot entry already exists..."
needToCreateEntry=0
# efibootmgr -B -b "$bootEntry" >/dev/null
;;
*)
# We should normally not end here...
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Found extra entry $bootEntry = '$bootEntryLabel'"
esac
done
fi
if test "${needToCreateEntry}" -eq 1; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Creating UEFI SecureBoot entry..."
efibootmgr \
--create \
--label "$entryLabel" \
--loader "/EFI/systemd/shimx64.efi" \
>/dev/null
fi
}
# Outputs a loader entry config
# $1 = (latest|old|prev)
# $2 = version
# $3 = Kernel filename (without path)
# $4 = Ramdisk filename (without path)
function createLoaderEntry(){
local -i \
fieldWidth
local -a \
contents \
loader_options
local \
txtLine \
EFI_ARCH
fieldWidth=12
contents=()
# Taken from shim's Makefile
EFI_ARCH=$(
cc -dumpmachine \
| cut -f1 -d- \
| sed \
-e 's,aarch64,aa64,' \
-e 's,arm.*,arm,' \
-e 's,i[3456789]86,ia32,' \
-e 's,x86_64,x64,' \
)
# Header
printf -v txtLine "# %s/%s_" \
"${loader_entries}" "${LOADERID}"
# Don't add $1 for latest
test "$1" != "latest" && txtLine+="$1"
contents+=( "${txtLine}.conf" )
# Title (Captialized first char)
printf -v txtLine "%-*s %s" $fieldWidth \
"title" "${LOADERID^} ${VERSION}"
# Add to total output.
contents+=( "${txtLine}" )
# Version
printf -v txtLine "%-*s %s" $fieldWidth \
"version" "$2"
# Not needed anymore because non-prev versions don't need it,
# and prev-version adds it self while transforming...
# # Don't add $1 for old.
# test "$1" != "old" && txtLine+="-$1"
# Add to total output.
contents+=( "${txtLine}" )
# Machine ID
printf -v txtLine "%-*s %s" $fieldWidth \
"machine-id" "$MACHINE_ID"
# Add to total output.
contents+=( "${txtLine}" )
# Architecture
printf -v txtLine "%-*s %s" $fieldWidth \
"architecture" "$EFI_ARCH"
# Add to total output.
contents+=( "${txtLine}" )
# Kernel
printf -v txtLine "%-*s %s" $fieldWidth \
"linux" "/${LOADERID}/$3"
# Add to total output.
contents+=( "${txtLine}" )
# Ramdisk
printf -v txtLine "%-*s %s" $fieldWidth \
"initrd" "/${LOADERID}/$4"
# Add to total output.
contents+=( "${txtLine}" )
# Options
loader_options=()
# TODO: Resume is restricted when in SecureBoot, but how to resume in that case?
# Always add the resume partion.
loader_options+=( "resume=UUID=$SWAP_PAR_UUID" )
# Add the resume partion when SecureBoot is NOT enabled
# # test $(mokutil --sb-state >/dev/null) && \
# # loader_options+=( "resume=UUID=$SWAP_PAR_UUID" )
# test "SecureBoot enabled" != $(mokutil --sb-state) && \
# loader_options+=( "resume=UUID=$SWAP_PAR_UUID" )
# Add root partition to use
test -n "$ROOT_PAR_UUID" && \
loader_options+=( "root=UUID=$ROOT_PAR_UUID" )
# Add the remaining options
loader_options+=( "${KERNEL_CMDLINE[@]}" )
printf -v txtLine "%-*s%s" $fieldWidth \
"options" \
"$(\
printf " %s" \
"${loader_options[@]}" \
)"
# Add to total output.
contents+=( "${txtLine}" )
# Output the total output.
printf "%s\n" "${contents[@]}"
}
# Old-kernel-logic
function oldKernelLogic(){
local -a cp_opts
log_func_name="Old Logic"
log_func="${log_func_start}${log_func_name}${log_func_end}"
bTestFlags=0
# Can't use -nt/-ot because of timestamp precision diffs of FS's...
# The tests: (Binary flags)
# 0001 = Old kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
if test ! -f "${ESP}/${LOADERID}/${KERNEL}.old" \
|| ! cmp "${KERNEL_OLD}" "${ESP}/${LOADERID}/${KERNEL}.old" >/dev/null
then
bTestFlags=$((bTestFlags | 2#0001))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Old kernel is not in ESP or has changed"
fi
# 0010 = Old ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
if test ! -f "${ESP}/${LOADERID}/${RAMDISK}.old" \
|| ! cmp "${RAMDISK_OLD}" "${ESP}/${LOADERID}/${RAMDISK}.old" >/dev/null
then
bTestFlags=$((bTestFlags | 2#0010))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Old ramdisk is not in ESP or has changed"
fi
# 0100 = Prev kernel exists in ESP.
if test -f "${ESP}/${LOADERID}/${RAMDISK}.prev"; then
bTestFlags=$((bTestFlags | 2#0100))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Prev kernel exists in ESP"
fi
# 1000 = Prev ramdisk exists in ESP.
if test -f "${ESP}/${LOADERID}/${RAMDISK}.prev"; then
bTestFlags=$((bTestFlags | 2#1000))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Prev ramdisk exists in ESP"
fi
# We need to create an old entry when: (All true)
# Versions of old kernel/ramdisk are same.
# Versions of old/latest of kernel&ramdisk are DIFFERENT.
# Versions of all kernels&ramdisks are non-empty.
if test \
"$VERSION_KERNEL_OLD" = "$VERSION_RAMDISK_OLD" \
-a "$VERSION_KERNEL_OLD" != "$VERSION_KERNEL_LATEST" \
-a "$VERSION_RAMDISK_OLD" != "$VERSION_RAMDISK_LATEST" \
-a -n "$VERSION_KERNEL_OLD" \
-a -n "$VERSION_RAMDISK_OLD" \
-a -n "$VERSION_KERNEL_LATEST" \
-a -n "$VERSION_RAMDISK_LATEST"
then
# Create an old-entry from a prev-entry when: (All true)
# prevFound set.
# 0001 = Old kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# 0010 = Old ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# 0100 = Prev kernel exists in ESP.
# 1000 = Prev ramdisk exists in ESP.
if test \
$((versionFound & prevFound)) -gt 0 \
-a $((bTestFlags ^ 2#1111)) -eq 0
then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Creating old-entry from prev-entry..."
# Rename the kernel/ramdisk in ESP.
mv \
--verbose \
"${ESP}/${LOADERID}/${KERNEL}" \
"${ESP}/${LOADERID}/${KERNEL}.old"
mv \
--verbose \
"${ESP}/${LOADERID}/${RAMDISK}.prev" \
"${ESP}/${LOADERID}/${RAMDISK}.old"
# Adjust the loader entry-config appropriately into an old version.
# Change the comment at top to reflect new filename.
# Remove our extension from the version (not needed anymore)
# Change the kernel used.
# Change the ramdisk used.
# (Don't use -i so we can preserve timestamp...)
sed -E \
-e "s|(^#.*)_prev.conf|\1_old.conf|" \
-e "s|(^version.*)_prev|\1|" \
-e "s|(^linux.*)${KERNEL}|\1${KERNEL}.old|" \
-e "s|(^initrd.*)${RAMDISK}.prev|\1${RAMDISK}.old|" \
"${loader_conf}_prev.conf" \
>"${loader_conf}_old.conf"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}.old" \
"${loader_conf}_old.conf"
# Remove the loader entry-config for prev,
# it will be re-created as needed...
rm \
--verbose \
--force \
"${loader_conf}_prev.conf"
# Generate a FRESH old-entry when: (Any one true)
# ! prevFound set.
# 0001 = Old kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# 0010 = Old ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# ! 0100 = Prev kernel exists in ESP.
# ! 1000 = Prev ramdisk exists in ESP.
elif test \
$((versionFound & prevFound)) -eq 0 \
-a $((bTestFlags ^ 2#0011)) -eq 0
then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Generating fresh old-entry..."
# Copy the old kernel and it's ramdisk.
cp_opts=(
--verbose
--update
--archive
"--no-preserve=ownership"
)
cp "${cp_opts[@]}" \
"${KERNEL_OLD}" \
"${ESP}/${LOADERID}/${KERNEL}.old"
cp "${cp_opts[@]}" \
"${RAMDISK_OLD}" \
"${ESP}/${LOADERID}/${RAMDISK}.old"
# Create the loader entry-config.
createLoaderEntry \
"old" \
"$VERSION_KERNEL_OLD" \
"${KERNEL}.old" \
"${RAMDISK}.old" \
>"${loader_conf}_old.conf"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}.old" \
"${loader_conf}_old.conf"
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Nothing to be done..."
fi
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Skip..."
fi
}
# Prev-kernel-logic
function prevKernelLogic(){
local -a cp_opts
log_func_name="Previous Logic"
log_func="${log_func_start}${log_func_name}${log_func_end}"
bTestFlags=0
# Can't use -nt/-ot because of timestamp precision diffs of FS's...
# The tests: (Binary flags)
# 00001 = Latest kernel did NOT boot succesfully (yet)
# This will prevent creating a prev loader-entry from latest-entry
# when it has a boot-try extension, either fresh or failed.
if test ! -f "${loader_conf}_.conf"; then
bTestFlags=$((bTestFlags | 2#00001))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Latest kernel did NOT boot succesfully (yet)"
# Only test rest when successully booted at least once.
else
# 00010 = Versions of latest kernel/ramdisk are SAME.
if test "$VERSION_KERNEL_LATEST" = "$VERSION_RAMDISK_LATEST"; then
bTestFlags=$((bTestFlags | 2#00010))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Versions of latest kernel/ramdisk are same"
fi
# 00100 = The Latest ramdisk in ESP does exist.
# AND DIFFERS from the latest ramdisk.
# This only happens when ramdisk is updated...
if test -f "${ESP}/${LOADERID}/${RAMDISK}" \
&& ! cmp "${RAMDISK_LATEST}" "${ESP}/${LOADERID}/${RAMDISK}" >/dev/null
then
bTestFlags=$((bTestFlags | 2#00100))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Latest ramdisk is in ESP and has changed"
fi
# 01000 = Prev ramdisk in ESP does NOT exist.
# OR DIFFERS from the latest ramdisk.
# This will prevent updating the Prev ramdisk in ESP
# with the latest ramdisk in ESP when re-run...
if test ! -f "${ESP}/${LOADERID}/${RAMDISK}.prev" \
|| ! cmp "${RAMDISK_LATEST}" "${ESP}/${LOADERID}/${RAMDISK}.prev" >/dev/null
then
bTestFlags=$((bTestFlags | 2#01000))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Prev ramdisk is not in ESP or has changed"
fi
# 10000 = Versions of latest kernel and old ramdisk are SAME.
# I once noticed this, not sure if it was a glitch...
if test "$VERSION_KERNEL_LATEST" = "$VERSION_RAMDISK_OLD"; then
bTestFlags=$((bTestFlags | 2#10000))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Versions of latest kernel and old ramdisk are same"
fi
fi
# We need to create a prev entry when: (All true)
# ! 00001 = Latest kernel did NOT boot succesfully (yet)
# This will prevent creating a prev loader-entry from latest-entry
# when it has a boot-try extension, either fresh or failed.
# 00010 = Versions of latest kernel/ramdisk are SAME.
# 00100 = The Latest ramdisk in ESP does exist.
# AND DIFFERS from the latest ramdisk.
# This only happens when ramdisk is updated...
# 01000 = Prev ramdisk in ESP does NOT exist.
# OR DIFFERS from the latest ramdisk.
# This will prevent updating the Prev ramdisk in ESP
# with the latest ramdisk in ESP when re-run...
if test $(((bTestFlags & 2#01111) ^ 2#01110)) -eq 0; then
# We need to UPDATE the prev ramdisk only when: (All true)
# latestFound set.
# prevFound set.
if test \
$((versionFound & latestFound)) -gt 0 \
-a $((versionFound & prevFound)) -gt 0
then
# Only update the prev ramdisk.
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Updateing prev-entry ramdisk..."
# Rename the ramdisk in ESP.
mv \
--verbose \
"${ESP}/${LOADERID}/${RAMDISK}" \
"${ESP}/${LOADERID}/${RAMDISK}.prev"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}.prev" \
"${loader_conf}_prev.conf"
# Remove the loader entry-config for latest kernel,
# it will be re-created fresh...
rm \
--verbose \
--force \
"${loader_conf}_.conf"
# We need to create a FRESH prev entry from latest-entry when: (All true)
# latestFound set.
# ! prevFound set.
elif test \
$((versionFound & latestFound)) -gt 0 \
-a $((versionFound & prevFound)) -eq 0
then
# Create a fresh Prev loader entry-config.
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Creating prev-entry from latest-entry..."
# Rename the ramdisk in ESP.
mv \
--verbose \
"${ESP}/${LOADERID}/${RAMDISK}" \
"${ESP}/${LOADERID}/${RAMDISK}.prev"
# Adjust the loader entry-config appropriately into a prev version.
# Change the comment at top to reflect new filename.
# Change the version.
# Change the ramdisk used.
# (Don't use -i so we can preserve timestamp...)
sed -E \
-e "s|(^#.*)_.conf|\1_prev.conf|" \
-e "s|(^version.*)|\1-prev|" \
-e "s|(^initrd.*)${RAMDISK}|\1${RAMDISK}.prev|" \
"${loader_conf}_.conf" \
>"${loader_conf}_prev.conf"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}.prev" \
"${loader_conf}_prev.conf"
# Remove the loader entry-config for latest kernel,
# it will be re-created as needed...
rm \
--verbose \
--force \
"${loader_conf}_.conf"
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Nothing to be done..."
fi
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Skip..."
fi
}
# Latest-kernel-logic
function latestKernelLogic(){
local -a cp_opts
log_func_name="Latest Logic"
log_func="${log_func_start}${log_func_name}${log_func_end}"
bTestFlags=0
# Can't use -nt/-ot because of timestamp precision diffs of FS's...
# The tests: (Binary flags)
# 00001 = Versions of latest kernel/ramdisk are same.
if test "$VERSION_KERNEL_LATEST" = "$VERSION_RAMDISK_LATEST"; then
bTestFlags=$((bTestFlags | 2#00001))
fi
# 00010 = Versions of latest kernel/ramdisk are non-empty.
if test -n "$VERSION_KERNEL_LATEST" -a -n "$VERSION_RAMDISK_LATEST"; then
bTestFlags=$((bTestFlags | 2#00010))
fi
# 00100 = Latest ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
if test ! -f "${ESP}/${LOADERID}/${RAMDISK}" \
|| ! cmp "${RAMDISK_LATEST}" "${ESP}/${LOADERID}/${RAMDISK}" >/dev/null
then
bTestFlags=$((bTestFlags | 2#00100))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Latest ramdisk is not in ESP or has changed"
fi
# 01000 = Latest kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
if test ! -f "${ESP}/${LOADERID}/${KERNEL}" \
|| ! cmp "${KERNEL_LATEST}" "${ESP}/${LOADERID}/${KERNEL}" >/dev/null
then
bTestFlags=$((bTestFlags | 2#01000))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Latest kernel is not in ESP or has changed"
fi
# 10000 = Loader entry-config for latest kernel NOT found.
if test ! -f "${loader_conf}_.conf" -a ! -f "${loader_conf}_+"*.conf; then
bTestFlags=$((bTestFlags | 2#10000))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Loader entry-config for latest kernel NOT found"
fi
# Generate a fresh latest-entry when: (All true)
# 00001 = Versions of latest kernel/ramdisk are same.
# 00010 = Versions of latest kernel/ramdisk are non-empty.
# 00100 = Latest ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# Don't check for kernel here because only ramdisk might have been
# updated, and it will be checked inside while copying.
if test $(((bTestFlags & 2#00111) ^ 2#00111)) -eq 0; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Generating fresh latest-entry..."
# Copy the latest kernel and it's ramdisk.
cp_opts=(
--verbose
--archive
"--no-preserve=ownership"
)
cp "${cp_opts[@]}" \
"${RAMDISK_LATEST}" \
"${ESP}/${LOADERID}/${RAMDISK}"
# Only copy latest kernel when it was different or non-existant in ESP.
# This will prevent un-needed writes.
# 01000 = Latest kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
if test $(((bTestFlags & 2#01000) ^ 2#01000)) -eq 0; then
cp "${cp_opts[@]}" \
"${KERNEL_LATEST}" \
"${ESP}/${LOADERID}/${KERNEL}"
fi
# Create the loader entry-config.
createLoaderEntry \
"latest" \
"$VERSION_KERNEL_LATEST" \
"${KERNEL}" \
"${RAMDISK}" \
>"${loader_conf}_+${BootTries}.conf"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}" \
"${loader_conf}_+${BootTries}.conf"
# Re-generate a fresh latest-entry when: (All true)
# 00001 = Versions of latest kernel/ramdisk are same.
# 00010 = Versions of latest kernel/ramdisk are non-empty.
# ! 00100 = Latest ramdisk does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# ! 01000 = Latest kernel does NOT exist in ESP.
# or DIFFERS from the one in ESP.
# 10000 = Loader entry-config for latest kernel NOT found.
elif test $(((bTestFlags & 2#11111) ^ 2#10011)) -eq 0; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Re-generating fresh latest-entry..."
# Create the loader entry-config.
createLoaderEntry \
"latest" \
"$VERSION_KERNEL_LATEST" \
"${KERNEL}" \
"${RAMDISK}" \
>"${loader_conf}_+${BootTries}.conf"
# Update timestamp of config to that of the ramdisk.
touch \
--reference="${ESP}/${LOADERID}/${RAMDISK}" \
"${loader_conf}_+${BootTries}.conf"
else
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Nothing to be done..."
fi
}
# Loader-configs-logic
function loaderConfigsLogic(){
log_func_name="Loader Configs"
log_func="${log_func_start}${log_func_name}${log_func_end}"
if test ! -d "${ESP}/EFI/systemd"; then
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"systemd-boot is not installed, skipping !"
exit 0
fi
latestFound=$((2#0001))
prevFound=$((2#0010))
oldFound=$((2#0100))
# Don't chain these, they need to be checked individualy for later logic...
# Set oldFound when: (All true)
# Loader entry-config for old exist.
# Old kernel EXIST in ESP already.
# Old ramdisk EXIST in ESP already.
if test \
-f "${loader_conf}_old.conf" \
-a -f "${ESP}/${LOADERID}/${KERNEL}.old" \
-a -f "${ESP}/${LOADERID}/${RAMDISK}.old"
then
versionFound=$((versionFound | oldFound))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Valid entry for OLD found"
# else
# printf "%b %b: %s\n" \
# "${log_header}" \
# "${log_func}" \
# "NO valid entry for OLD found"
fi
# Set prevFound when: (All true)
# Loader entry-config for prev exist.
# Latest kernel EXIST in ESP already.
# Prev ramdisk EXIST in ESP already.
# (Will be result of Prev-kernel-logic below)
if test \
-f "${loader_conf}_prev.conf" \
-a -f "${ESP}/${LOADERID}/${KERNEL}" \
-a -f "${ESP}/${LOADERID}/${RAMDISK}.prev"
then
versionFound=$((versionFound | prevFound))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Valid entry for PREVIOUS found"
# else
# printf "%b %b: %s\n" \
# "${log_header}" \
# "${log_func}" \
# "NO valid entry for PREVIOUS found"
fi
# ONLY check for successfully booted configs.
# eg: no boot-try extensions see: systemd-boot(7)#BOOT COUNTING
# Set latestFound when: (All true)
# Loader entry-config for latest exist.
# Latest kernel/ramdisk EXIST in ESP already.
# (Will be result of Latest-kernel-logic below)
if test \
-f "${loader_conf}_.conf" \
-a -f "${ESP}/${LOADERID}/${KERNEL}" \
-a -f "${ESP}/${LOADERID}/${RAMDISK}"
then
versionFound=$((versionFound | latestFound))
printf "%b %b: %s\n" \
"${log_header}" \
"${log_func}" \
"Valid entry for LATEST found"
# else
# printf "%b %b: %s\n" \
# "${log_header}" \
# "${log_func}" \
# "NO valid entry for LATEST found"
fi
oldKernelLogic
prevKernelLogic
latestKernelLogic
}
KERNEL="vmlinuz"
RAMDISK="initrd.img"
log_name="systemd-boot"
log_header_start="\e[2m"
log_header_end="\e[0m"
log_func_start="(\e[32m"
log_func_end="\e[0m)"
log_header="${log_header_start}${log_name}${log_header_end}"
bTestFlags=0
BootTries=0
versionFound=0
latestFound=0
prevFound=0
oldFound=0
case "$1" in
installme)
printf "%b: %s\n" \
"${log_header}" \
"Requested install of helper"
checkNeededBins
installScript
;;
installBoot)
printf "%b: %s\n" \
"${log_header}" \
"Requested install Boot loader-entry"
checkNeededBins
getESP
installUEFIbootEntry
;;
*)
checkNeededBins
getESP
getMachineID
getRootUUID
getResumeUUID
getCmdLineOptions
getLoaderVars
getKernelAndRamdiskVars
loaderConfigsLogic
installUEFIbootEntry
esac
Maybe Kubuntu devs can use this in it's distro

Output from my current running system:
-
Code:
> bootctl | xsel -ib System: Firmware: UEFI 2.40 (American Megatrends 5.11) Secure Boot: enabled Setup Mode: user Current Boot Loader: Product: systemd-boot 245.4-4ubuntu3.2 Features: ✓ Boot counting ✓ Menu timeout control ✓ One-shot menu timeout control ✓ Default entry control ✓ One-shot entry control ✓ Support for XBOOTLDR partition ✓ Support for passing random seed to OS ✓ Boot loader sets ESP partition information ESP: /dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c File: └─/EFI/SYSTEMD/SHIMX64.EFI Random Seed: Passed to OS: no System Token: set Exists: yes Available Boot Loaders on ESP: ESP: /boot/efi (/dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c) File: └─/EFI/systemd/systemd-bootx64.efi (systemd-boot 245.4-4ubuntu3.1) File: └─/EFI/systemd/shimx64.efi File: └─/EFI/systemd/mmx64.efi File: └─/EFI/systemd/grubx64.efi (systemd-boot 245.4-4ubuntu3.2) File: └─/EFI/BOOT/BOOTX64.EFI Boot Loaders Listed in EFI Variables: Title: Linux Secure-Bootloader ID: 0x0000 Status: active, boot-order Partition: /dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c File: └─/EFI/SYSTEMD/SHIMX64.EFI Title: ubuntu ID: 0x0002 Status: inactive, boot-order Partition: /dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c File: └─/EFI/UBUNTU/SHIMX64.EFI Title: UEFI OS ID: 0x0004 Status: inactive, boot-order Partition: /dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c File: └─/EFI/BOOT/BOOTX64.EFI Title: ubuntu ID: 0x0005 Status: inactive, boot-order Partition: /dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c File: └─/EFI/UBUNTU/GRUBX64.EFI Boot Loader Entries: $BOOT: /boot/efi (/dev/disk/by-partuuid/07aa2f88-2546-4101-95d4-59e76a04f93c) Default Boot Loader Entry: title: Kubuntu 20.04.1 LTS (Focal Fossa) id: kubuntu_.conf source: /boot/efi/loader/entries/kubuntu_.conf version: 5.4.0-42 machine-id: 80479e7d268341ec858db9de810df978 architecture: x64 linux: /kubuntu/vmlinuz initrd: /kubuntu/initrd.img options: resume=UUID=734867ce-811d-4fb3-ac4c-44a61fb1fca9 root=UUID=deb6ca8a-f40e-44c8-aa8d-db16d41219a9 ro quiet splash vt.handoff=7 intel_iommu=on -
Code:
> bootctl list | xsel -ib Boot Loader Entries: title: EFI Tools id: efitools-keytool.conf source: /boot/efi/loader/entries/efitools-keytool.conf version: KeyTool title: Kubuntu Grub-loader id: grub-kubuntu.conf source: /boot/efi/loader/entries/grub-kubuntu.conf version: Grub title: Kubuntu 20.04 LTS (Focal Fossa) (5.4.0-40) id: kubuntu_old.conf source: /boot/efi/loader/entries/kubuntu_old.conf version: 5.4.0-40 machine-id: 80479e7d268341ec858db9de810df978 architecture: x64 linux: /kubuntu/vmlinuz.old initrd: /kubuntu/initrd.img.old options: resume=UUID=734867ce-811d-4fb3-ac4c-44a61fb1fca9 root=UUID=deb6ca8a-f40e-44c8-aa8d-db16d41219a9 ro quiet splash vt.handoff=7 intel_iommu=on title: Kubuntu 20.04 LTS (Focal Fossa) (5.4.0-42-prev) id: kubuntu_prev.conf source: /boot/efi/loader/entries/kubuntu_prev.conf version: 5.4.0-42-prev machine-id: 80479e7d268341ec858db9de810df978 architecture: x64 linux: /kubuntu/vmlinuz initrd: /kubuntu/initrd.img.prev options: resume=UUID=734867ce-811d-4fb3-ac4c-44a61fb1fca9 root=UUID=deb6ca8a-f40e-44c8-aa8d-db16d41219a9 ro quiet splash vt.handoff=7 intel_iommu=on title: Kubuntu 20.04.1 LTS (Focal Fossa) (default) id: kubuntu_.conf source: /boot/efi/loader/entries/kubuntu_.conf version: 5.4.0-42 machine-id: 80479e7d268341ec858db9de810df978 architecture: x64 linux: /kubuntu/vmlinuz initrd: /kubuntu/initrd.img options: resume=UUID=734867ce-811d-4fb3-ac4c-44a61fb1fca9 root=UUID=deb6ca8a-f40e-44c8-aa8d-db16d41219a9 ro quiet splash vt.handoff=7 intel_iommu=on title: EFI Default Loader id: auto-efi-default source: /sys/firmware/efi/efivars/LoaderEntries-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f title: Reboot Into Firmware Interface id: auto-reboot-to-firmware-setup source: /sys/firmware/efi/efivars/LoaderEntries-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f








Comment