From facd2000a58d754d8c318e4d11343d6847f26fc7 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Mon, 27 Oct 2025 02:38:40 -0400 Subject: [PATCH] feat(autoconfigure): initial attempt at bls autoconfiguration --- src/autoconfigure.rs | 137 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 2 + src/main.rs | 24 ++++++-- src/options.rs | 8 +++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/autoconfigure.rs diff --git a/src/autoconfigure.rs b/src/autoconfigure.rs new file mode 100644 index 0000000..3cd5c2a --- /dev/null +++ b/src/autoconfigure.rs @@ -0,0 +1,137 @@ +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 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}; +use uefi::proto::media::fs::SimpleFileSystem; + +/// The name of the BLS chainload action that will be used +/// by the BLS generator to chainload entries. +const BLS_CHAINLOAD_ACTION: &str = "bls-chainload"; + +/// Scan the specified `filesystem` for BLS configurations. +fn scan_for_bls( + filesystem: &mut FileSystem, + root: &DevicePath, + config: &mut RootConfiguration, +) -> Result { + // 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 root = root + .to_string(DisplayOnly(false), AllowShortcuts(false)) + .context("unable to convert device root to string")? + .to_string(); + + // Whether we have a loader.conf file. + let has_loader_conf = filesystem + .try_exists(bls_loader_conf_path) + .context("unable to check for BLS entries directory")?; + + // Whether we have an entries directory. + let has_entries_dir = filesystem + .try_exists(bls_entries_path) + .context("unable to check for BLS loader.conf file")?; + + // 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); + } + + // BLS is now detected, generate a configuration for it. + let generator = BlsConfiguration { + entry: EntryDeclaration { + title: "$title".to_string(), + actions: vec![BLS_CHAINLOAD_ACTION.to_string()], + ..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() + }, + ); + + // We had a BLS supported configuration, so return true. + Ok(true) +} + +/// Add the BLS actions to the configuration. +/// We should only do this once. +fn add_bls_actions(config: &mut RootConfiguration) -> Result<()> { + // Generate a chainload configuration for BLS. + // BLS will provide these values to us. + let chainload = ChainloadConfiguration { + path: "$chainload".to_string(), + options: vec!["$options".to_string()], + linux_initrd: Some("initrd".to_string()), + }; + + // Insert the chainload action into the configuration. + config.actions.insert( + BLS_CHAINLOAD_ACTION.to_string(), + ActionDeclaration { + chainload: Some(chainload), + ..Default::default() + }, + ); + + Ok(()) +} + +/// 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<()> { + // Find all the filesystems that are on the system. + let filesystem_handles = + uefi::boot::find_handles::().context("unable to scan filesystems")?; + + // Whether we have any BLS-supported filesystems. + // We will key off of this to know if we should add BLS actions. + let mut has_any_bls = false; + + // For each filesystem that was detected, scan it for supported autoconfig mechanisms. + for handle in filesystem_handles { + // Acquire the device path root for the filesystem. + let root = { + uefi::boot::open_protocol_exclusive::(handle) + .context("unable to get root for filesystem")? + .to_boxed() + }; + + // Open the filesystem that was detected. + let filesystem = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open filesystem")?; + + // Trade the filesystem protocol for the uefi filesystem helper. + let mut filesystem = FileSystem::new(filesystem); + + // Scan the filesystem for BLS supported configurations. + // If we find any, we will add a BLS generator to the configuration, and then + // at the end we will add the BLS actions to the configuration. + has_any_bls = has_any_bls + || scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; + } + + // If we had any BLS-supported filesystems, add the BLS actions to the configuration. + if has_any_bls { + add_bls_actions(config).context("unable to add BLS actions")?; + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 4ad4225..b6a92ad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -74,6 +74,8 @@ pub struct DefaultsConfiguration { /// The timeout of the boot menu. #[serde(rename = "menu-timeout", default = "default_menu_timeout")] pub menu_timeout: u64, + /// Enables autoconfiguration of Sprout based on the environment. + pub autoconfigure: bool, } fn latest_version() -> u32 { diff --git a/src/main.rs b/src/main.rs index af59c42..ddeb524 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![doc = include_str!("../README.md")] #![feature(uefi_std)] +use crate::config::RootConfiguration; use crate::context::{RootContext, SproutContext}; use crate::entries::BootableEntry; use crate::options::SproutOptions; @@ -17,6 +18,9 @@ use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; /// actions: Code that can be configured and executed by Sprout. pub mod actions; +/// autoconfigure: Autoconfigure Sprout based on the detected environment. +pub mod autoconfigure; + /// config: Sprout configuration mechanism. pub mod config; @@ -60,10 +64,16 @@ fn main() -> Result<()> { // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; - // Load the configuration of sprout. - // At this point, the configuration has been validated and the specified - // version is checked to ensure compatibility. - let config = config::loader::load(&options)?; + // If --autoconfigure is specified, we use a stub configuration. + let mut config = if options.autoconfigure { + info!("autoconfiguration enabled, configuration file will be ignored"); + RootConfiguration::default() + } else { + // Load the configuration of sprout. + // At this point, the configuration has been validated and the specified + // version is checked to ensure compatibility. + config::loader::load(&options)? + }; // Load the root context. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol. @@ -98,6 +108,12 @@ fn main() -> Result<()> { // Load all configured drivers. drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?; + // If --autoconfigure is specified or the loaded configuration has autoconfigure enabled, + // trigger the autoconfiguration mechanism. + if context.root().options().autoconfigure || config.defaults.autoconfigure { + autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?; + } + // Run all the extractors declared in the configuration. let mut extracted = BTreeMap::new(); for (name, extractor) in &config.extractors { diff --git a/src/options.rs b/src/options.rs index 3f73eb2..cf02bba 100644 --- a/src/options.rs +++ b/src/options.rs @@ -11,6 +11,8 @@ const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; /// The parsed options of sprout. #[derive(Debug)] pub struct SproutOptions { + /// Configures Sprout automatically based on the environment. + pub autoconfigure: bool, /// Path to a configuration file to load. pub config: String, /// Entry to boot without showing the boot menu. @@ -25,6 +27,7 @@ pub struct SproutOptions { impl Default for SproutOptions { fn default() -> Self { Self { + autoconfigure: false, config: DEFAULT_CONFIG_PATH.to_string(), boot: None, force_menu: false, @@ -86,6 +89,11 @@ impl OptionsRepresentable for SproutOptions { for (key, value) in options { match key.as_str() { + "autoconfigure" => { + // Enable autoconfiguration. + result.autoconfigure = true; + } + "config" => { // The configuration file to load. result.config = value.context("--config option requires a value")?;