From d6e8fe0245f7f431f028950c06f9aef433d205fd Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Mon, 27 Oct 2025 15:41:29 -0400 Subject: [PATCH] feat(autoconfigure): find vmlinuz and initramfs pairs with linux autoconfigure module --- src/autoconfigure.rs | 15 ++- src/autoconfigure/linux.rs | 209 +++++++++++++++++++++++++++++++++++++ src/generators.rs | 7 ++ src/generators/list.rs | 52 +++++++++ src/generators/matrix.rs | 35 ++----- src/main.rs | 31 ++++-- src/menu.rs | 2 +- src/options/parser.rs | 5 +- 8 files changed, 317 insertions(+), 39 deletions(-) create mode 100644 src/autoconfigure/linux.rs create mode 100644 src/generators/list.rs diff --git a/src/autoconfigure.rs b/src/autoconfigure.rs index ea872b1..d78c683 100644 --- a/src/autoconfigure.rs +++ b/src/autoconfigure.rs @@ -4,9 +4,14 @@ use uefi::fs::FileSystem; use uefi::proto::device_path::DevicePath; use uefi::proto::media::fs::SimpleFileSystem; -/// bls: autodetect and configure BLS-enabled systems. +/// bls: autodetect and configure BLS-enabled filesystems. pub mod bls; +/// linux: autodetect and configure Linux kernels. +/// This autoconfiguration module should not be activated +/// on BLS-enabled filesystems as it may make duplicate entries. +pub mod linux; + /// Generate a [RootConfiguration] based on the environment. /// Intakes a `config` to use as the basis of the autoconfiguration. pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> { @@ -31,8 +36,14 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> { let mut filesystem = FileSystem::new(filesystem); // Scan the filesystem for BLS supported configurations. - bls::scan(&mut filesystem, &root, config) + let bls_found = bls::scan(&mut filesystem, &root, config) .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(()) diff --git a/src/autoconfigure/linux.rs b/src/autoconfigure/linux.rs new file mode 100644 index 0000000..4d1ea64 --- /dev/null +++ b/src/autoconfigure/linux.rs @@ -0,0 +1,209 @@ +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 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, +} + +/// Scan the specified `filesystem` at `path` for [KernelPair] results. +fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result> { + // 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 { + 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('/'); + + // 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); + + // 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), + 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) +} diff --git a/src/generators.rs b/src/generators.rs index 2632e19..13622a9 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1,6 +1,7 @@ use crate::context::SproutContext; use crate::entries::BootableEntry; use crate::generators::bls::BlsConfiguration; +use crate::generators::list::ListConfiguration; use crate::generators::matrix::MatrixConfiguration; use anyhow::Result; use anyhow::bail; @@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize}; use std::rc::Rc; pub mod bls; +pub mod list; pub mod matrix; /// Declares a generator configuration. @@ -32,6 +34,9 @@ pub struct GeneratorDeclaration { /// It will generate a sprout entry for every supported BLS entry. #[serde(default)] pub bls: Option, + /// List generator configuration. + /// Allows you to specify a list of values to generate an entry from. + pub list: Option, } /// Runs the generator specified by the `generator` option. @@ -45,6 +50,8 @@ pub fn generate( matrix::generate(context, matrix) } else if let Some(bls) = &generator.bls { bls::generate(context, bls) + } else if let Some(list) = &generator.list { + list::generate(context, list) } else { bail!("unknown generator configuration"); } diff --git a/src/generators/list.rs b/src/generators/list.rs new file mode 100644 index 0000000..7c5eed3 --- /dev/null +++ b/src/generators/list.rs @@ -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 matrix 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>, +} + +/// Generates a set of entries using the specified `matrix` configuration in the `context`. +pub fn generate( + context: Rc, + list: &ListConfiguration, +) -> Result> { + 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) +} diff --git a/src/generators/matrix.rs b/src/generators/matrix.rs index 09702ee..f6150eb 100644 --- a/src/generators/matrix.rs +++ b/src/generators/matrix.rs @@ -1,5 +1,6 @@ use crate::context::SproutContext; use crate::entries::{BootableEntry, EntryDeclaration}; +use crate::generators::list; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -57,30 +58,12 @@ pub fn generate( ) -> Result> { // Produce all the combinations of the input values. let combinations = build_matrix(&matrix.values); - let mut entries = Vec::new(); - - // For each combination, create a new context and entry. - for (index, combination) in combinations.into_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 = 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) + // Use the list generator to generate entries for each combination. + list::generate( + context, + &list::ListConfiguration { + entry: matrix.entry.clone(), + values: combinations, + }, + ) } diff --git a/src/main.rs b/src/main.rs index bbab397..25b3700 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use crate::options::SproutOptions; use crate::options::parser::OptionsRepresentable; use crate::phases::phase; use anyhow::{Context, Result, bail}; -use log::info; +use log::{error, info}; use std::collections::BTreeMap; use std::ops::Deref; use std::time::Duration; @@ -54,13 +54,8 @@ pub mod options; /// utils: Utility functions that are used by other parts of Sprout. pub mod utils; -/// 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, returning an error if one occurs. +fn run() -> Result<()> { // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; @@ -240,6 +235,26 @@ fn main() -> Result<()> { .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. // If we reach here, we will exit back to whoever called us. Ok(()) diff --git a/src/menu.rs b/src/menu.rs index a22639d..8c48f0e 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -121,7 +121,7 @@ fn select_with_input<'a>( // Entry was selected by number. If the number is invalid, we continue. MenuOperation::Number(index) => { let Some(entry) = entries.get(index) else { - println!("invalid entry number"); + info!("invalid entry number"); continue; }; return Ok(entry); diff --git a/src/options/parser.rs b/src/options/parser.rs index d6cee40..d6f14fa 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result, bail}; +use log::info; use std::collections::BTreeMap; /// The type of option. This disambiguates different behavior @@ -113,9 +114,9 @@ pub trait OptionsRepresentable { // Handle the --help flag case. if description.form == OptionForm::Help { // Generic configured options output. - println!("Configured Options:"); + info!("Configured Options:"); for (name, description) in &configured { - println!( + info!( " --{}{}: {}", name, if description.form == OptionForm::Value {