implement a new sprout command line options mechanism

This commit is contained in:
2025-10-20 18:17:29 -07:00
parent 3d2c31ee1a
commit c749c8d38e
5 changed files with 170 additions and 17 deletions

View File

@@ -0,0 +1,11 @@
version = 1
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\shell.efi"
[actions.chainload-shell]
chainload.path = "$boot\\EFI\\BOOT\\shell.efi"
[entries.xen]
title = "Boot Shell"
actions = ["chainload-shell"]

View File

@@ -1,31 +1,36 @@
use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions;
use crate::utils;
use anyhow::{Context, Result, bail};
use log::info;
use std::ops::Deref;
use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath;
/// Loads the raw configuration from the sprout.toml file as data.
fn load_raw_config() -> Result<Vec<u8>> {
/// Loads the raw configuration from the sprout config file as data.
fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
// Open the LoadedImageDevicePath protocol to get the path to the current image.
let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
// Acquire the device path as a boxed device path.
let path = current_image_device_path_protocol.deref().to_boxed();
// Read the contents of the sprout.toml file.
let content = utils::read_file_contents(&path, "sprout.toml")
.context("unable to read sprout.toml file")?;
// Return the contents of the sprout.toml file.
info!("configuration file: {}", options.config);
// Read the contents of the sprout config file.
let content = utils::read_file_contents(&path, &options.config)
.context("unable to read sprout config file")?;
// Return the contents of the sprout config file.
Ok(content)
}
/// Loads the [RootConfiguration] for Sprout.
pub fn load() -> Result<RootConfiguration> {
// Load the raw configuration from the sprout.toml file.
let content = load_raw_config()?;
pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
// Load the raw configuration from the sprout config file.
let content = load_raw_config(options)?;
// Parse the raw configuration into a toml::Value which can represent any TOML file.
let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?;
let value: Value = toml::from_slice(&content).context("unable to parse sprout config file")?;
// Check the version of the configuration without parsing the full configuration.
let version = value

View File

@@ -1,4 +1,5 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use anyhow::Result;
use anyhow::anyhow;
use std::collections::{BTreeMap, BTreeSet};
@@ -13,15 +14,18 @@ pub struct RootContext {
actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>,
/// The global options of Sprout.
options: SproutOptions,
}
impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access.
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self {
pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> Self {
Self {
actions: BTreeMap::new(),
loaded_image_path: Some(loaded_image_device_path),
options,
}
}
@@ -41,6 +45,11 @@ impl RootContext {
.as_deref()
.ok_or_else(|| anyhow!("no loaded image path"))
}
/// Access the global Sprout options.
pub fn options(&self) -> &SproutOptions {
&self.options
}
}
/// A context of Sprout. This is passed around different parts of Sprout and represents

View File

@@ -3,7 +3,7 @@
use crate::context::{RootContext, SproutContext};
use crate::phases::phase;
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result};
use log::info;
use std::collections::BTreeMap;
use std::ops::Deref;
@@ -37,6 +37,9 @@ pub mod phases;
/// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;
@@ -47,10 +50,13 @@ fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Parse the options to the sprout executable.
let options = options::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()?;
let config = config::loader::load(&options)?;
// Load the root context.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
@@ -64,7 +70,7 @@ fn main() -> Result<()> {
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path)
RootContext::new(loaded_image_path, options)
};
// Insert the configuration actions into the root context.
@@ -144,9 +150,14 @@ fn main() -> Result<()> {
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Pick the first entry from the list of final entries until a boot menu is implemented.
let Some((context, entry)) = final_entries.first() else {
bail!("no entries found");
// Use the boot option if possible, otherwise pick the first entry.
let (context, entry) = if let Some(ref boot) = context.root().options().boot {
final_entries
.iter()
.find(|(_context, entry)| &entry.title == boot)
.context(format!("unable to find entry: {boot}"))?
} else {
final_entries.first().context("no entries found")?
};
// Execute all the actions for the selected entry.

117
src/options.rs Normal file
View File

@@ -0,0 +1,117 @@
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout.
#[derive(Debug)]
pub struct SproutOptions {
/// Path to a configuration file to load.
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
}
/// The default Sprout options.
impl Default for SproutOptions {
fn default() -> Self {
Self {
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
}
}
}
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
if value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option: {option}");
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parse the arguments to Sprout as a [SproutOptions] structure.
pub fn parse() -> Result<SproutOptions> {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = SproutOptions::default();
let options = parse_raw().context("unable to parse options")?;
for (key, value) in options {
match key.as_str() {
"config" => {
// The configuration file to load.
result.config = value.context("--config option requires a value")?;
}
"boot" => {
// The entry to boot.
result.boot = Some(value.context("--boot option requires a value")?);
}
_ => bail!("unknown option: --{key}"),
}
}
Ok(result)
}