From bd37e9058c79638795c9b6b0513af475c3457617 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 4 Oct 2026 10:16:13 -0700 Subject: [PATCH] feat(bls): follow the systemd-boot fallbacks for unified kernel image names --- crates/bls/src/lib.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ crates/bls/src/uki.rs | 30 ++++++++++++++++++------ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/crates/bls/src/lib.rs b/crates/bls/src/lib.rs index bbdd3b5..a1a0727 100644 --- a/crates/bls/src/lib.rs +++ b/crates/bls/src/lib.rs @@ -199,6 +199,16 @@ impl BlsEntry { self.linux.is_some() || self.efi.is_some() || self.uki.is_some() } + /// Whether the entry names more than one of `linux`, `efi` and `uki` to boot. + /// systemd-boot considers such an entry broken and does not show it. + pub fn mixes_boot_targets(&self) -> bool { + [&self.linux, &self.efi, &self.uki] + .iter() + .filter(|target| target.is_some()) + .count() + > 1 + } + /// Whether the entry only boots a unified kernel image, which carries its own initrd. fn is_uki_only(&self) -> bool { self.linux.is_none() && self.efi.is_none() && self.uki.is_some() @@ -806,6 +816,49 @@ mod tests { assert!(entry.is_valid()); } + #[test] + fn uki_title_and_version_use_every_systemd_fallback() { + let sections = uki_sections(&[( + ".osrel", + "IMAGE_ID=img\nNAME=N\nID=i\nVERSION=7.1\nVERSION_ID=7\n", + )]); + let entry = BlsEntry::from_uki(§ions, "/a.efi"); + assert_eq!(entry.title.as_deref(), Some("img")); + assert_eq!(entry.version.as_deref(), Some("7.1")); + + let sections = uki_sections(&[(".osrel", "NAME=N\nID=i\n")]); + assert_eq!( + BlsEntry::from_uki(§ions, "/a.efi").title.as_deref(), + Some("N") + ); + } + + #[test] + fn uki_version_only_falls_back_to_uname_outside_strict_mode() { + let sections = uki_sections(&[(".osrel", "ID=a\n"), (".uname", "6.1.2\n")]); + let loose = BlsEntry::from_uki_with(§ions, "/a.efi", false); + assert_eq!(loose.version.as_deref(), Some("6.1.2")); + let strict = BlsEntry::from_uki_with(§ions, "/a.efi", true); + assert_eq!(strict.version, None); + // The kernel version is still there, it is just not the version of the entry. + assert_eq!(strict.uname.as_deref(), Some("6.1.2")); + } + + #[test] + fn entries_that_mix_boot_targets_are_found() { + for (input, mixed) in [ + ("linux /v\n", false), + ("efi /e.efi\n", false), + ("uki /u.efi\n", false), + ("linux /v\nefi /e.efi\n", true), + ("linux /v\nuki /u.efi\n", true), + ("efi /e.efi\nuki /u.efi\n", true), + ] { + let entry: BlsEntry = input.parse().unwrap(); + assert_eq!(entry.mixes_boot_targets(), mixed, "{input}"); + } + } + #[test] fn uki_entry_falls_back_through_the_fields() { let sections = uki_sections(&[(".osrel", "ID=arch\nBUILD_ID=rolling\n")]); diff --git a/crates/bls/src/uki.rs b/crates/bls/src/uki.rs index acf4b8a..bb2b157 100644 --- a/crates/bls/src/uki.rs +++ b/crates/bls/src/uki.rs @@ -75,7 +75,12 @@ impl BlsEntry { /// Produces an entry for one profile of the unified kernel image at `uki_path`. /// A profile after the first is booted with `@` in its load options. pub fn from_uki_profile(profile: &UkiProfile, uki_path: &str) -> Self { - let mut entry = Self::from_uki(&profile.sections, uki_path); + Self::from_uki_profile_with(profile, uki_path, false) + } + + /// Like [BlsEntry::from_uki_profile], and `strict` follows systemd-boot exactly. + pub fn from_uki_profile_with(profile: &UkiProfile, uki_path: &str, strict: bool) -> Self { + let mut entry = Self::from_uki_with(&profile.sections, uki_path, strict); if let Some(index) = profile.index { entry.profile = (index > 0).then(|| index.to_string()); entry.title = entry @@ -87,11 +92,21 @@ impl BlsEntry { } /// Produces an entry for the unified kernel image at `uki_path`, from its PE `sections`. - /// The title comes from `PRETTY_NAME`, then `ID`. The version comes from `IMAGE_VERSION`, - /// `VERSION_ID`, `BUILD_ID`, then the `.uname` section. The sort key comes from `IMAGE_ID`, - /// then `ID`. The embedded command line is kept in `cmdline` and not in `options`, as the - /// image reads its own command line. pub fn from_uki(sections: &BTreeMap>, uki_path: &str) -> Self { + Self::from_uki_with(sections, uki_path, false) + } + + /// Produces an entry for the unified kernel image at `uki_path`, from its PE `sections`. + /// The title comes from `PRETTY_NAME`, `IMAGE_ID`, `NAME`, then `ID`. The version comes from + /// `IMAGE_VERSION`, `VERSION`, `VERSION_ID`, then `BUILD_ID`, as in systemd-boot, and then + /// from the `.uname` section unless `strict` is set. The sort key comes from `IMAGE_ID`, then + /// `ID`. The embedded command line is kept in `cmdline` and not in `options`, as the image + /// reads its own command line. + pub fn from_uki_with( + sections: &BTreeMap>, + uki_path: &str, + strict: bool, + ) -> Self { let os_release = section_text(sections, ".osrel") .map(|text| OsRelease::parse(&text)) .unwrap_or_default(); @@ -102,10 +117,11 @@ impl BlsEntry { .map(ToString::to_string) }; let uname = section_text(sections, ".uname"); + let version = first(&["IMAGE_VERSION", "VERSION", "VERSION_ID", "BUILD_ID"]); Self { - title: first(&["PRETTY_NAME", "ID"]), - version: first(&["IMAGE_VERSION", "VERSION_ID", "BUILD_ID"]).or_else(|| uname.clone()), + title: first(&["PRETTY_NAME", "IMAGE_ID", "NAME", "ID"]), + version: version.or_else(|| uname.clone().filter(|_| !strict)), sort_key: first(&["IMAGE_ID", "ID"]), uki: Some(uki_path.to_string()), cmdline: section_text(sections, ".cmdline"),