* feat(bls): parse and render boot counters
* feat(bls): sort entries with no boot tries left last
* feat(boot): strip boot counters from bls entry ids
* feat(boot): consume bls boot counter tries before booting
* fix(boot): never default to a bad boot counter entry while a good one exists
* feat(menu): add a graphical boot menu
Adds the graphical menu style, selected with menu-style = "graphical" or
--menu-style=graphical. It draws to the graphics output with the existing
framebuffer, using a built-in 8x8 font, and takes input from the keyboard
and from relative or absolute pointers. The Sprout logo is drawn in the
bottom right corner. If there is no graphics output, the simple menu is
used, which still falls back to the basic menu.
The logo is converted from assets/logo-small.png to raw pixels by the build
crate, which adds the png crate as a build-time dependency only.
The default menu style is still simple. Pointer input is untested, as
OVMF has no mouse driver.
* fix(build): only build the logo conversion on the host
CI builds the whole workspace for the UEFI target, which also builds the
build crate for UEFI. The png dependency pulls in simd-adler32, which
crashes LLVM on that target. The logo conversion only runs in build
scripts on the host, so png and the conversion are now left out when
targeting UEFI.
Generators can now declare variant axes. Every generated entry is
multiplied by each combination of choices, one per axis. A choice sets
the axis name to the choice name and inserts its own values, and the
entry name is suffixed with the choice names.
Generators can also declare exclude rules, which remove generated
entries whose values match every glob pattern in a rule. This covers
unwanted variant combinations and entries like BLS rescue kernels.
Entry matching for default-entry and the bootloader interface now
accepts `*` anywhere in the pattern instead of only at the end, so
generated entry names can be matched regardless of the kernel version
in the middle of the name.
The BLS generator gains a pin-names option (default true, the existing
behavior). Two BLS generators reading the same directory previously
produced entries with identical names; disabling pin-names prefixes
them with the generator name instead. Sprout now warns when more than
one entry has the same name.
* docs(config): note that variants suffix pinned BLS entry names
Pinned BLS entry names match the BLS entry ids, but variants still
append their choice names, so those entries need a pattern like
<id>-* to be selected.
The simple menu failed, and boot was aborted, when the console couldn't
set the cursor position or colors or clear the screen, which some
serial consoles and minimal console drivers don't support. Use the
basic menu, which only prints text, when the simple menu fails.
The Ubuntu guide repeated the Step 7 heading, and the Windows guide went
from Step 1 to Step 3. The guides also said to download
sprout-<arch>.efi, then to copy the downloaded sprout.efi file, which
doesn't exist under that name.
The openSUSE guide said most Debian systems use ext4 for /boot, copied
from the Debian guide. openSUSE uses Btrfs by default, so say so and
point to the Btrfs driver, keeping ext4 as the example.
The ARM64 step copied the arm64-efi GRUB modules into
/boot/grub2/x86_64-efi, where GRUB on ARM64 doesn't look for them, so
insmod chain and insmod fat failed.
The unsigned setup guides passed the EFI System Partition device to
efibootmgr -d, which expects the disk, and left out -p, which defaults
to partition 1. The command failed, or created a boot entry for the
wrong partition when the ESP isn't the first one. Pass the disk and the
partition number, as the signed guides do.
GitHub doesn't mark a pull request as a draft when its title starts
with WIP:, so contributors following the guide got a normal pull
request. Say to open a draft pull request instead.
The aarch64 dev VM always used -cpu cortex-a57, even with -accel hvf or
-accel kvm. QEMU rejects named CPU models with hvf, and with KVM unless
the host has that CPU, so booting natively on Apple Silicon or an
aarch64 Linux host with KVM failed. Use -cpu host when accelerated.
On macOS, common.sh always set QEMU_ACCEL to hvf when it was available,
replacing any value the user set, like tcg. Only pick an accelerator
when QEMU_ACCEL isn't set, as the KVM check already does.
format.sh ran shfmt on hack/**/*.sh without enabling globstar, so ** only
matched a single directory. Scripts directly in hack and in deeper
directories like hack/dev/kernel were never formatted.
The dev build passed RUST_TARGET_SUBDIR to the Docker build, but not
RUST_PROFILE, so the Dockerfile always built the release profile and
then copied from the directory of the requested profile. Any profile
other than release, like ./hack/dev/boot.sh x86_64 debug, failed. Pass
the profile as well.
The dev Edera config loaded the kernel from \EFI\BOOT\kernel.efi, but
the dev image copies the kernel to \VMLINUZ, so booting with
SPROUT_CONFIG_NAME=edera always failed to read the kernel.
Dispatching the release workflow again for the same tag cancelled the
running release, which could stop it in the middle of uploading assets
and leave the release with only some of them. Queue the new run behind
the running one instead.
The concurrency groups included the commit SHA, so every run was in a
group of its own and cancel-in-progress never cancelled a run that a
newer push had replaced. Group runs by pull request or ref instead.
Only pull request runs are cancelled, so every push to main still gets
fully checked and published.
The publish job ran for pull requests with contents: write, id-token:
write, and attestations: write, while running build code from the pull
request. None of these were needed there: uploading artifacts doesn't
need contents: write, and attestation was already skipped for pull
requests. Build with read-only permissions, and attest in a separate
job that only runs on pushes to main.
The release workflow checked out the ref it was dispatched on, usually
main, instead of the release tag. The published binaries and their
provenance attestation could come from a different commit than the tag
they were attached to. Check out the tag instead.
The comment said symlinked scan locations are skipped, but nothing
checks for symlinks, and that the empty string is the root of the
filesystem, while the root location is "\\".
Keys that aren't part of the configuration were silently ignored, so a
misspelled key like linux_initrd instead of linux-initrd, or
menu_timeout instead of menu-timeout, quietly did nothing. Warn about
each ignored key with its full path while loading the configuration.
Also warn when an action or generator configures more than one kind,
as only the first one found is used.
LoadFile2 compared the boot policy to Boolean::TRUE, which only matches
the value 1. UEFI treats any non-zero BOOLEAN as TRUE, so check it the
same way.
The security hooks returned their own errors for calls the shim can't
verify, instead of passing them to the original firmware hooks. The
Security2 hook rejected a missing device path or file buffer, which are
optional in the PI specification, and a boot policy of TRUE. The
Security hook returned NOT_FOUND for any file it couldn't read, like
one in a firmware volume.
Pass these calls to the original hook, which is what the firmware does
without the hook installed. This matches systemd-boot, and the
firmware still enforces its own verification, so nothing is let
through that the firmware would reject.
Device path nodes were treated as device nodes when their text
contained "(", and as file path nodes otherwise. A file name with
parentheses, like \EFI\Linux\vmlinuz(1).efi, was then treated as a
device node, so it couldn't be resolved or loaded. Check whether each
node is a media file path node instead.
If anything failed between loading the image and starting it, like
reading an initrd, registering the media loader, or setting the load
options, the loaded image was left in memory. Unload it in that case.
Once the image is started, the firmware unloads it when it exits.
An initrd file, or chain of initrd files, with no contents was still
registered with the initrd media loader, which reports NOT_FOUND for
empty data. The Linux EFI stub treats anything other than
BUFFER_TOO_SMALL as a load error and fails to boot, rather than booting
without an initrd. Skip registering an empty initrd, as the edera
action already does.
The edera action checked whether an initrd path was empty before
stamping it, so a path like "$initrd" that stamped to an empty value
was still read, which failed and aborted boot. Stamp the paths first,
and skip paths that stamp to the root of a filesystem, the same as the
chainload action.
Chained initrds were concatenated back to back. Linux only finds a cpio
archive at a 4-byte aligned offset when it unpacks concatenated
initrds, so a compressed initramfs with an odd length followed by
another archive, like a microcode or overlay cpio, failed to unpack.
Pad each initrd with zeros to a 4-byte boundary, as GRUB and
systemd-boot do.
A value that refers to itself, such as a BLS entry with "options $options",
never stops changing when the context is finalized. After the iteration
limit, finalizing failed and every entry was lost, although the BLS
generator already skips broken entries. Skip just that entry with a
warning.
A value that refers to itself twice doubles in length every iteration,
so it could use up all memory before the limit was reached. Fail once a
value grows past 64 KiB.
filesystem-device-match failed as soon as any filesystem couldn't be
checked, such as a removable device without media or a filesystem that
couldn't be opened exclusively. The fallback value was never used and
boot was aborted. Log a warning and check the next filesystem instead.
The filesystem protocol is now only opened when the label or item
criteria need it, and the partition UUIDs are parsed once up front.
Commit 2da1cea skipped filesystems that couldn't be opened, but an error
while scanning an opened filesystem, such as a driver returning
DEVICE_ERROR or ACCESS_DENIED, still aborted autoconfiguration for every
filesystem. Log a warning and move on to the next scan instead, and do
the same for each directory scanned for Linux kernels.
BLS was detected when reading \loader\entries produced any item, but a
FAT directory always lists "." first, so an empty entries directory
counted as having entries. On an ESP with a leftover empty entries
directory, Linux autoconfiguration was skipped and no Linux entries
were shown. Only count the directory when it has a .conf file.
The initramfs candidates were checked with try_exists, which also
accepts a directory, and fails the whole scan on any error other than
the file not being found. Only accept a regular file, and treat a
candidate that can't be checked as missing.
Linux autoconfiguration only looked for an initramfs named with the
kernel suffix, like initrd.img-6.1 or initramfs-lts. Arch Linux
(initramfs-linux.img), and Fedora or Gentoo using dracut without BLS
(initramfs-<version>.img) add .img to the name, so their kernels were
booted without an initramfs and failed to mount the root filesystem.
Also try each candidate with .img added.
A UTF-8 byte order mark isn't whitespace, so it became part of the first
key of the entry, and that line was ignored. Entries written by editors
that add one lost their title or kernel. Strip it before parsing.
The Boot Loader Specification allows the options key to appear more
than once, with all of the options combined in order. Each options line
replaced the previous one instead, so an entry that set root= on one
line and other options on the next booted without its root filesystem.
RootConfiguration derived Default, which set menu-timeout to 0 and the
version to 0, while an empty configuration file gets a 10 second
timeout and the latest version. --autoconfigure uses the default
configuration, so it booted the first entry without showing the menu.
Implement Default with the same values serde uses.
The sort-key of an entry was compared as written, without stamping the
values into it. Autoconfigured Linux entries all use "$kernel" as the
sort key, so they compared equal and kept the order the filesystem
listed them in, which made the default kernel arbitrary instead of the
newest one. Stamp the sort key along with the title.
Every Boot Loader Interface variable that couldn't be read, written, or
removed aborted boot. The interface only exchanges information with the
operating system, so log a warning and continue instead. This includes
failing to remove LoaderEntryOneShot or LoaderConfigTimeoutOneShot,
which made every following boot fail the same way, as the variable was
never removed.
An entry from --boot or LoaderEntryOneShot that didn't match any entry
aborted boot, which happens when the one-shot entry was removed or
misspelled. Log a warning and use the boot menu instead.
When LoaderEntryDefault was set, every entry that didn't match it was
unmarked as the default, including the one set by default-entry. If the
variable named an entry that no longer exists, such as a removed kernel,
the configured default was lost and the first entry booted instead.
Only apply the variable when some entry matches it.
LoaderConfigTimeoutOneShot asks the boot loader to show the menu on the
next boot, and a value of 0 means the menu waits for the user. Sprout
treated it like LoaderConfigTimeout, so 0 hid the menu, the opposite of
what systemctl reboot --boot-loader-menu=0 asks for, and other values
only changed the timeout without forcing the menu.
Force the menu when the one-shot timeout is set, waiting for the user
when it is 0.
--force-menu and LoaderConfigTimeout=menu-force only stopped --boot and
LoaderEntryOneShot from skipping the menu. The timeout was left as is,
so with a zero timeout the menu booted the default entry without being
drawn, and menu-force counted down and booted the default entry, where
the Boot Loader Interface says it should wait for the user.
Menus now take an optional timeout. menu-force waits for the user, and
--force-menu keeps the configured timeout unless it is zero, in which
case it waits for the user too.
The basic menu listed entries as [10], [11] and so on, but only read a
single digit, so those entries could never be selected.
Keep reading digits while another digit could still select a different
entry, and boot once it can't or when enter is pressed. With ten
entries or fewer, a single digit boots the entry right away as before.
Every key that didn't select an entry, like enter or an arrow key, made
the basic menu wait for the full timeout again. Stop the countdown on
any key instead, like the simple menu, so the menu waits for the user
once they start using it.
Some firmware signals the key event without a key being available. The
simple menu then skipped the countdown for that tick, and the basic menu
started waiting for the full timeout again, so firmware that kept
signaling the key event would never boot the default entry.
Read the key inside the wait, and when no key is available keep waiting
on the same timer.