15 Commits

Author SHA1 Message Date
50f7bc11aa sprout: version 0.0.12 2025-10-27 18:41:32 -04:00
2200ba74f6 fix(cargo): force dev profiles to use opt-level = 2 to workaround hardware acceleration 2025-10-27 18:35:18 -04:00
7a3db08e1d fix(cargo): remove transitive dependency on tokio 2025-10-27 18:26:53 -04:00
e7f5be30dd feat(autoconfigure): generate names using a unique hash 2025-10-27 18:21:28 -04:00
3bbe6561ef fix(docs): fedora setup guide should use [options] 2025-10-27 17:57:29 -04:00
3b5e110d1e feat(config): rename [defaults] to [options] 2025-10-27 17:56:38 -04:00
26315fb4c4 fix(options): stamp initrd and combine options safely by ignoring empty strings 2025-10-27 17:44:30 -04:00
a76f9770dc fix(chainload): support an empty initrd path, which will result in no initrd 2025-10-27 16:27:39 -04:00
59edd63a12 fix(doc): list generator is not the matrix generator 2025-10-27 16:17:33 -04:00
8a2e8c8127 fix(sprout): correct rustdoc and clarify safety in some places 2025-10-27 16:16:09 -04:00
6086778dc0 fix(menu): free timer event to avoid leak 2025-10-27 16:03:25 -04:00
e729d6a60b feat(sprout): cleanup default logging 2025-10-27 15:44:29 -04:00
d6e8fe0245 feat(autoconfigure): find vmlinuz and initramfs pairs with linux autoconfigure module 2025-10-27 15:41:29 -04:00
99653b5192 fix(menu): hide the entry name from the menu since it can be long with autoconfigure 2025-10-27 12:25:22 -04:00
2a76e4f798 chore(code): move bls autoconfigure to separate module 2025-10-27 04:28:25 -04:00
26 changed files with 639 additions and 182 deletions

115
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -32,6 +43,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.24.0" version = "1.24.0"
@@ -44,12 +64,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -59,14 +94,35 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.11" version = "0.0.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
"log", "log",
"serde", "serde",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
@@ -97,12 +153,28 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.8" version = "0.25.8"
@@ -126,6 +198,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.28"
@@ -259,6 +337,29 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -314,6 +415,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "ucs2" name = "ucs2"
version = "0.3.3" version = "0.3.3"
@@ -372,6 +479,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.13" version = "0.7.13"

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.11" version = "0.0.12"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
@@ -13,7 +13,7 @@ toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
[dependencies.image] [dependencies.image]
version = "0.25.6" version = "0.25.8"
default-features = false default-features = false
features = ["png"] features = ["png"]
optional = true optional = true
@@ -22,6 +22,10 @@ optional = true
version = "1.0.228" version = "1.0.228"
features = ["derive"] features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi] [dependencies.uefi]
version = "0.36.0" version = "0.36.0"
features = ["alloc", "logger"] features = ["alloc", "logger"]
@@ -33,6 +37,11 @@ version = "0.12.0"
default = ["splash"] default = ["splash"]
splash = ["dep:image"] splash = ["dep:image"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly.
opt-level = 2
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "symbols" strip = "symbols"
@@ -46,6 +55,7 @@ debug = 1
inherits = "dev" inherits = "dev"
strip = "debuginfo" strip = "debuginfo"
debug = 0 debug = 0
opt-level = 2
[patch.crates-io.simd-adler32] [patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git" git = "https://github.com/edera-dev/sprout-patched-deps.git"

View File

@@ -138,7 +138,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# global options. # global options.
[defaults] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them. # filesystems and generating boot entries for them.
autoconfigure = true autoconfigure = true

View File

@@ -40,7 +40,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# global options. # global options.
[defaults] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them. # filesystems and generating boot entries for them.
autoconfigure = true autoconfigure = true

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "kernel" default-entry = "kernel"
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi" has-item = "\\EFI\\BOOT\\kernel.efi"

View File

@@ -1,4 +1,4 @@
version = 1 version = 1
[defaults] [options]
autoconfigure = true autoconfigure = true

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "edera" default-entry = "edera"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "kernel" default-entry = "kernel"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "shell" default-entry = "shell"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "xen" default-entry = "xen"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -53,18 +53,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?; .context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = configuration let options =
.options utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
// Pass the options to the image, if any are provided. // Pass the options to the image, if any are provided.
// The holder must drop at the end of this function to ensure the options are not leaked, // The holder must drop at the end of this function to ensure the options are not leaked,
// and the holder here ensures it outlives the if block here, as a pointer has to be // and the holder here ensures it outlives the if block here, as a pointer has to be
// passed to the image. This has been hand-validated to be safe. // passed to the image.
// SAFETY: The options outlive the usage of the image, and the image is not used after this.
let mut options_holder: Option<Box<CString16>> = None; let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() { if !options.is_empty() {
let options = Box::new( let options = Box::new(
@@ -88,10 +85,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
options_holder = Some(options); options_holder = Some(options);
} }
// The initrd can be None or empty, so we need to collapse that into a single Option.
let initrd = utils::empty_is_none(configuration.linux_initrd.as_ref());
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(ref linux_initrd) = configuration.linux_initrd { if let Some(linux_initrd) = initrd {
let initrd_path = context.stamp(linux_initrd); // Stamp the path to the initrd.
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path) let linux_initrd = context.stamp(linux_initrd);
let content = utils::read_file_contents(context.root().loaded_image_path()?, &linux_initrd)
.context("unable to read linux initrd")?; .context("unable to read linux initrd")?;
let handle = let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
@@ -103,7 +105,7 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// This call might return, or it may pass full control to another image that will never return. // This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only // Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered. // after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image).context("unable to start image"); let result = uefi::boot::start_image(image);
// Unregister the initrd if it was registered. // Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle

View File

@@ -40,7 +40,23 @@ pub struct EderaConfiguration {
} }
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. /// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
fn build_xen_config(configuration: &EderaConfiguration) -> String { fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String {
// Stamp xen options and combine them.
let xen_options = utils::combine_options(
configuration
.xen_options
.iter()
.map(|item| context.stamp(item)),
);
// Stamp kernel options and combine them.
let kernel_options = utils::combine_options(
configuration
.kernel_options
.iter()
.map(|item| context.stamp(item)),
);
// xen config file format is ini-like // xen config file format is ini-like
[ [
// global section // global section
@@ -50,10 +66,10 @@ fn build_xen_config(configuration: &EderaConfiguration) -> String {
// configuration section for sprout // configuration section for sprout
"[sprout]".to_string(), "[sprout]".to_string(),
// xen options // xen options
format!("options={}", configuration.xen_options.join(" ")), format!("options={}", xen_options),
// kernel options, stub replaces the kernel path // kernel options, stub replaces the kernel path
// the kernel is provided via media loader // the kernel is provided via media loader
format!("kernel=stub {}", configuration.kernel_options.join(" ")), format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored // required or else the last line will be ignored
"".to_string(), "".to_string(),
] ]
@@ -94,7 +110,7 @@ fn register_media_loader_file(
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. /// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> { pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Build the Xen config file content for this configuration. // Build the Xen config file content for this configuration.
let config = build_xen_config(configuration); let config = build_xen_config(context.clone(), configuration);
// Register the media loader for the config. // Register the media loader for the config.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
@@ -113,7 +129,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
let mut media_loaders = vec![config, kernel]; let mut media_loaders = vec![config, kernel];
// Register the initrd if it is provided. // Register the initrd if it is provided.
if let Some(ref initrd) = configuration.initrd { if let Some(initrd) = utils::empty_is_none(configuration.initrd.as_ref()) {
let initrd = let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?; .context("unable to register initrd media loader")?;
@@ -131,7 +147,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders on error. // Unregister the media loaders when an error happens.
for media_loader in media_loaders { for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() { if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error); error!("unable to unregister media loader: {}", error);

View File

@@ -1,101 +1,16 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration; use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use uefi::cstr16; use uefi::fs::FileSystem;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
/// The name prefix of the BLS chainload action that will be used /// bls: autodetect and configure BLS-enabled filesystems.
/// by the BLS generator to chainload entries. pub mod bls;
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations. /// linux: autodetect and configure Linux kernels.
fn scan_for_bls( /// This autoconfiguration module should not be activated
filesystem: &mut FileSystem, /// on BLS-enabled filesystems as it may make duplicate entries.
root: &DevicePath, pub mod linux;
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}
/// Generate a [RootConfiguration] based on the environment. /// Generate a [RootConfiguration] based on the environment.
/// Intakes a `config` to use as the basis of the autoconfiguration. /// Intakes a `config` to use as the basis of the autoconfiguration.
@@ -121,8 +36,14 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
let mut filesystem = FileSystem::new(filesystem); let mut filesystem = FileSystem::new(filesystem);
// Scan the filesystem for BLS supported configurations. // Scan the filesystem for BLS supported configurations.
// If we find any, we will add a BLS generator to the configuration. let bls_found = bls::scan(&mut filesystem, &root, config)
scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; .context("unable to scan for bls configurations")?;
// If BLS was not found, scan for Linux configurations.
if !bls_found {
linux::scan(&mut filesystem, &root, config)
.context("unable to scan for linux configurations")?;
}
} }
Ok(()) Ok(())

101
src/autoconfigure/bls.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the BLS chainload action that will be used
/// by the BLS generator to chainload entries.
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}

213
src/autoconfigure/linux.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Linux chainload action that will be used to boot Linux.
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// We will check for symlinks and if this directory is a symlink, we will skip it.
const SCAN_LOCATIONS: &[&str] = &["/boot", "/"];
/// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd"];
/// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find.
struct KernelPair {
/// The path to a kernel.
kernel: String,
/// The path to an initramfs, if any.
initramfs: Option<String>,
}
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
// All the discovered kernel pairs.
let mut pairs = Vec::new();
// Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
let path = path.to_path_buf();
// Check if the path exists and is a directory.
let exists = filesystem
.metadata(&path)
.ok()
.map(|metadata| metadata.is_directory())
.unwrap_or(false);
// If the path does not exist, return an empty list.
if !exists {
return Ok(pairs);
}
// Open a directory iterator on the path to scan.
// Ignore errors here as in some scenarios this might fail due to symlinks.
let Some(directory) = filesystem.read_dir(&path).ok() else {
return Ok(pairs);
};
// For each item in the directory, find a kernel.
for item in directory {
let item = item.context("unable to read directory item")?;
// Skip over any items that are not regular files.
if !item.is_regular_file() {
continue;
}
// Convert the name from a CString16 to a String.
let name = item.file_name().to_string();
// Find a kernel prefix that matches, if any.
let Some(prefix) = KERNEL_PREFIXES
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let initramfs = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
break None;
};
// Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path.clone();
initramfs_path.push(Path::new(&initramfs));
// Check if the initramfs path exists, if it does, break out of the loop.
if filesystem
.try_exists(&initramfs_path)
.context("unable to check if initramfs path exists")?
{
break Some(initramfs_path);
}
};
// Construct a kernel path from the kernel name.
let mut kernel = path.clone();
kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string();
let initramfs = initramfs.map(|initramfs| initramfs.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
pairs.push(pair);
}
Ok(pairs)
}
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
let mut pairs = Vec::new();
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS {
let scanned = scan_directory(filesystem, location)
.with_context(|| format!("unable to scan directory {}", location))?;
pairs.extend(scanned);
}
// If no kernel pairs were found, return false.
if pairs.is_empty() {
return Ok(false);
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $kernel".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
values: pairs
.into_iter()
.map(|pair| {
BTreeMap::from_iter(vec![
("kernel".to_string(), pair.kernel),
("initrd".to_string(), pair.initramfs.unwrap_or_default()),
])
})
.collect(),
};
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
},
);
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config
.values
.insert("linux-options".to_string(), "".to_string());
}
// Generate a chainload configuration for the list generator.
// The list will provide these values to us.
// Note that we don't need an extra \\ in the paths here.
// The root already contains a trailing slash.
let chainload = ChainloadConfiguration {
path: format!("{}$kernel", root),
options: vec!["$linux-options".to_string()],
linux_initrd: Some(format!("{}$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}

View File

@@ -27,7 +27,7 @@ pub struct RootConfiguration {
pub version: u32, pub version: u32,
/// Default options for Sprout. /// Default options for Sprout.
#[serde(default)] #[serde(default)]
pub defaults: DefaultsConfiguration, pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context. /// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
@@ -65,16 +65,18 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
/// Default configuration for Sprout, used when the corresponding options are not specified. /// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DefaultsConfiguration { pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown. /// If not specified, a boot menu is shown.
pub entry: Option<String>, #[serde(rename = "default-entry", default)]
pub default_entry: Option<String>,
/// The timeout of the boot menu. /// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")] #[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64, pub menu_timeout: u64,
/// Enables autoconfiguration of Sprout based on the environment. /// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool, pub autoconfigure: bool,
} }

View File

@@ -118,7 +118,10 @@ impl SproutContext {
pub fn all_values(&self) -> BTreeMap<String, String> { pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for key in self.all_keys() { for key in self.all_keys() {
values.insert(key.clone(), self.get(key).cloned().unwrap_or_default()); // Acquire the value from the context. Since retrieving all the keys will give us
// a full view of the context, we can be sure that the key exists.
let value = self.get(&key).cloned().unwrap_or_default();
values.insert(key.clone(), value);
} }
values values
} }

View File

@@ -33,8 +33,6 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
// Push the path of the driver from the root. // Push the path of the driver from the root.
full_path.push_str(&context.stamp(&driver.path)); full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path);
// Convert the path to a device path. // Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?; let device_path = utils::text_to_device_path(&full_path)?;

View File

@@ -18,7 +18,7 @@ use uefi_raw::Status;
/// the device root path that can concatenated with subpaths to access files /// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem. /// on a particular filesystem.
/// ///
/// This function only requires one of the criteria to match. /// This function only requires all the criteria to match.
/// The fallback value can be used to provide a value if none is found. /// The fallback value can be used to provide a value if none is found.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor { pub struct FilesystemDeviceMatchExtractor {

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration; use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration; use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result; use anyhow::Result;
use anyhow::bail; use anyhow::bail;
@@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
pub mod bls; pub mod bls;
pub mod list;
pub mod matrix; pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
@@ -32,6 +34,9 @@ pub struct GeneratorDeclaration {
/// It will generate a sprout entry for every supported BLS entry. /// It will generate a sprout entry for every supported BLS entry.
#[serde(default)] #[serde(default)]
pub bls: Option<BlsConfiguration>, pub bls: Option<BlsConfiguration>,
/// List generator configuration.
/// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>,
} }
/// Runs the generator specified by the `generator` option. /// Runs the generator specified by the `generator` option.
@@ -45,6 +50,8 @@ pub fn generate(
matrix::generate(context, matrix) matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls { } else if let Some(bls) = &generator.bls {
bls::generate(context, bls) bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else { } else {
bail!("unknown generator configuration"); bail!("unknown generator configuration");
} }

52
src/generators/list.rs Normal file
View File

@@ -0,0 +1,52 @@
use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
list: &ListConfiguration,
) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in list.values.iter().enumerate() {
let mut context = context.fork();
// Insert the combination into the context.
context.insert(combination);
let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = list.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
}

View File

@@ -1,5 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::list;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -57,30 +58,12 @@ pub fn generate(
) -> Result<Vec<BootableEntry>> { ) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values. // Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values); let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new(); // Use the list generator to generate entries for each combination.
list::generate(
// For each combination, create a new context and entry. context,
for (index, combination) in combinations.into_iter().enumerate() { &list::ListConfiguration {
let mut context = context.fork(); entry: matrix.entry.clone(),
// Insert the combination into the context. values: combinations,
context.insert(&combination); },
let context = context.freeze(); )
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
} }

View File

@@ -8,12 +8,11 @@ use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info; use log::{error, info};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::time::Duration; use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// actions: Code that can be configured and executed by Sprout. /// actions: Code that can be configured and executed by Sprout.
pub mod actions; pub mod actions;
@@ -54,13 +53,8 @@ pub mod options;
/// utils: Utility functions that are used by other parts of Sprout. /// utils: Utility functions that are used by other parts of Sprout.
pub mod utils; pub mod utils;
/// The main entrypoint of sprout. /// Run Sprout, returning an error if one occurs.
/// It is possible this function will not return if actions that are executed fn run() -> Result<()> {
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Parse the options to the sprout executable. // Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?; let options = SproutOptions::parse().context("unable to parse options")?;
@@ -83,10 +77,6 @@ fn main() -> Result<()> {
>(uefi::boot::image_handle()) >(uefi::boot::image_handle())
.context("unable to get loaded image device path")?; .context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed(); let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path, options) RootContext::new(loaded_image_path, options)
}; };
@@ -110,7 +100,7 @@ fn main() -> Result<()> {
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled, // If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism. // trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.defaults.autoconfigure { if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?; autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
} }
@@ -192,7 +182,7 @@ fn main() -> Result<()> {
entry.restamp_title(); entry.restamp_title();
// Mark this entry as the default entry if it is declared as such. // Mark this entry as the default entry if it is declared as such.
if let Some(ref default_entry) = config.defaults.entry { if let Some(ref default_entry) = config.options.default_entry {
// If the entry matches the default entry, mark it as the default entry. // If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) { if entry.is_match(default_entry) {
entry.mark_default(); entry.mark_default();
@@ -221,7 +211,7 @@ fn main() -> Result<()> {
.root() .root()
.options() .options()
.menu_timeout .menu_timeout
.unwrap_or(config.defaults.menu_timeout); .unwrap_or(config.options.menu_timeout);
let menu_timeout = Duration::from_secs(menu_timeout); let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu. // Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
@@ -240,6 +230,26 @@ fn main() -> Result<()> {
.context(format!("unable to execute action '{}'", action))?; .context(format!("unable to execute action '{}'", action))?;
} }
Ok(())
}
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
}
// Sprout doesn't necessarily guarantee anything was booted. // Sprout doesn't necessarily guarantee anything was booted.
// If we reach here, we will exit back to whoever called us. // If we reach here, we will exit back to whoever called us.
Ok(()) Ok(())

View File

@@ -44,11 +44,16 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100); let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?; uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = [timer_event, key_event]; let mut events = vec![timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events) let event = uefi::boot::wait_for_event(&mut events)
.discard_errdata() .discard_errdata()
.context("unable to wait for event")?; .context("unable to wait for event")?;
// Close the timer event that we acquired.
if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?;
}
// The first event is the timer event. // The first event is the timer event.
// If it has triggered, the user did not select a numbered entry. // If it has triggered, the user did not select a numbered entry.
if event == 0 { if event == 0 {
@@ -121,7 +126,7 @@ fn select_with_input<'a>(
// Entry was selected by number. If the number is invalid, we continue. // Entry was selected by number. If the number is invalid, we continue.
MenuOperation::Number(index) => { MenuOperation::Number(index) => {
let Some(entry) = entries.get(index) else { let Some(entry) = entries.get(index) else {
println!("invalid entry number"); info!("invalid entry number");
continue; continue;
}; };
return Ok(entry); return Ok(entry);

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior /// The type of option. This disambiguates different behavior
@@ -113,9 +114,9 @@ pub trait OptionsRepresentable {
// Handle the --help flag case. // Handle the --help flag case.
if description.form == OptionForm::Help { if description.form == OptionForm::Help {
// Generic configured options output. // Generic configured options output.
println!("Configured Options:"); info!("Configured Options:");
for (name, description) in &configured { for (name, description) in &configured {
println!( info!(
" --{}{}: {}", " --{}{}: {}",
name, name,
if description.form == OptionForm::Value { if description.form == OptionForm::Value {

View File

@@ -161,3 +161,23 @@ pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result
let content = fs.read(Path::new(&path)); let content = fs.read(Path::new(&path));
content.context("unable to read file contents") content.context("unable to read file contents")
} }
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}