diff --git a/src/actions.rs b/src/actions.rs index ea0a0e8..3e0c039 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -3,32 +3,56 @@ use anyhow::{Result, bail}; use serde::{Deserialize, Serialize}; use std::rc::Rc; +/// EFI chainloader action. pub mod chainload; +/// Edera hypervisor action. pub mod edera; +/// EFI console print action. pub mod print; +/// Splash screen action. #[cfg(feature = "splash")] pub mod splash; +/// Declares an action that sprout can execute. +/// Actions allow configuring sprout's internal runtime mechanisms with values +/// that you can specify via other concepts. +/// +/// Actions are the main work that Sprout gets done, like booting Linux. #[derive(Serialize, Deserialize, Default, Clone)] pub struct ActionDeclaration { + /// Chainload to another EFI application. + /// This allows you to load any EFI application, either to boot an operating system + /// or to perform more EFI actions and return to sprout. #[serde(default)] pub chainload: Option, + /// Print a string to the EFI console. #[serde(default)] pub print: Option, + /// Show an image as a fullscreen splash screen. #[serde(default)] #[cfg(feature = "splash")] pub splash: Option, + /// Boot the Edera hypervisor and the root operating system. + /// This action is an extension on top of the Xen EFI stub that + /// is specific to Edera. #[serde(default, rename = "edera")] pub edera: Option, } +/// Execute the action specified by `name` which should be stored in the +/// root context of the provided `context`. This function may not return +/// if the provided action executes an operating system or an EFI application +/// that does not return control to sprout. pub fn execute(context: Rc, name: impl AsRef) -> Result<()> { + // Retrieve the action from the root context. let Some(action) = context.root().actions().get(name.as_ref()) else { bail!("unknown action '{}'", name.as_ref()); }; + // Finalize the context and freeze it. let context = context.finalize().freeze(); + // Execute the action. if let Some(chainload) = &action.chainload { chainload::chainload(context.clone(), chainload)?; return Ok(()); @@ -45,5 +69,7 @@ pub fn execute(context: Rc, name: impl AsRef) -> Result<()> return Ok(()); } + // If we reach here, we don't know how to execute the action that was configured. + // This is likely unreachable, but we should still return an error just in case. bail!("unknown action configuration"); } diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index 15afb1a..891ca27 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -9,24 +9,37 @@ use std::rc::Rc; use uefi::CString16; use uefi::proto::loaded_image::LoadedImage; +/// The configuration of the chainload action. #[derive(Serialize, Deserialize, Default, Clone)] pub struct ChainloadConfiguration { + /// The path to the image to chainload. + /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. pub path: String, + /// The options to pass to the image. + /// The options are concatenated by a space and then passed to the EFI application. #[serde(default)] pub options: Vec, + /// An optional path to a Linux initrd. + /// This uses the [LINUX_EFI_INITRD_MEDIA_GUID] mechanism to load the initrd into the EFI stack. + /// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is + /// generally better and safer as it can support additional load options in the future. #[serde(default, rename = "linux-initrd")] pub linux_initrd: Option, } +/// Executes the chainload action using the specified `configuration` inside the provided `context`. pub fn chainload(context: Rc, configuration: &ChainloadConfiguration) -> Result<()> { + // Retrieve the current image handle of sprout. let sprout_image = uefi::boot::image_handle(); + // Resolve the path to the image to chainload. let resolved = utils::resolve_path( context.root().loaded_image_path()?, &context.stamp(&configuration.path), ) .context("unable to resolve chainload path")?; + // Load the image to chainload. let image = uefi::boot::load_image( sprout_image, uefi::boot::LoadImageSource::FromDevicePath { @@ -36,9 +49,11 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat ) .context("unable to load image")?; + // Open the LoadedImage protocol of the image to chainload. let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::(image) .context("unable to open loaded image protocol")?; + // Stamp and concatenate the options to pass to the image. let options = configuration .options .iter() @@ -46,6 +61,10 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat .collect::>() .join(" "); + // 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, + // 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. let mut options_holder: Option> = None; if !options.is_empty() { let options = Box::new( @@ -80,15 +99,28 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat initrd_handle = Some(handle); } + // Retrieve the base and size of the loaded image to display. let (base, size) = loaded_image_protocol.info(); info!("loaded image: base={:#x} size={:#x}", base.addr(), size); + + // Start the loaded image. + // 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 + // after the optional initrd has been unregistered. let result = uefi::boot::start_image(image).context("unable to start image"); + + // Unregister the initrd if it was registered. if let Some(initrd_handle) = initrd_handle && let Err(error) = initrd_handle.unregister() { error!("unable to unregister linux initrd: {}", error); } + + // Assert there was no error starting the image. result.context("unable to start image")?; + // Explicitly drop the option holder to clarify the lifetime. drop(options_holder); + + // Return control to sprout. Ok(()) } diff --git a/src/actions/print.rs b/src/actions/print.rs index 984404d..a89aa56 100644 --- a/src/actions/print.rs +++ b/src/actions/print.rs @@ -3,12 +3,15 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; use std::rc::Rc; +/// The configuration of the print action. #[derive(Serialize, Deserialize, Default, Clone)] pub struct PrintConfiguration { + /// The text to print to the console. #[serde(default)] pub text: String, } +/// Executes the print action with the specified `configuration` inside the provided `context`. pub fn print(context: Rc, configuration: &PrintConfiguration) -> Result<()> { println!("{}", context.stamp(&configuration.text)); Ok(()) diff --git a/src/config.rs b/src/config.rs index 117e6b6..e322e1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,12 +35,26 @@ pub struct RootConfiguration { /// inside the sprout context. #[serde(default)] pub extractors: BTreeMap, + /// Declares the actions that can execute operations for sprout. + /// Actions are executable modules in sprout that take in specific structured values. + /// Actions are responsible for ensuring that passed strings are stamped to replace values + /// at runtime. + /// Each action has a name that can be referenced by other base concepts like entries. #[serde(default)] pub actions: BTreeMap, + /// Declares the entries that are displayed on the boot menu. These entries are static + /// but can still use values from the sprout context. #[serde(default)] pub entries: BTreeMap, + /// Declares the generators that are used to generate entries at runtime. + /// Each generator has its own logic for generating entries, but generally they intake + /// a template entry and stamp that template entry over some values determined at runtime. + /// Each generator has an associated name used to differentiate it across sprout. #[serde(default)] pub generators: BTreeMap, + /// Configures the various phases of sprout. This allows you to hook into specific parts + /// of the boot process to execute actions, for example, you can show a boot splash during + /// the early phase. #[serde(default)] pub phases: PhasesConfiguration, } diff --git a/src/drivers.rs b/src/drivers.rs index 6358807..908e657 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -8,8 +8,14 @@ use std::rc::Rc; use uefi::boot::SearchType; use uefi::proto::device_path::LoadedImageDevicePath; +/// Declares a driver configuration. +/// Drivers allow extending the functionality of Sprout. +/// Drivers are loaded at runtime and can provide extra functionality like filesystem support. +/// Drivers are loaded by their name, which is used to reference them in other concepts. #[derive(Serialize, Deserialize, Default, Clone)] pub struct DriverDeclaration { + /// The filesystem path to the driver. + /// This file should be an EFI executable that can be located and executed. pub path: String, } diff --git a/src/generators.rs b/src/generators.rs index 8e293d2..037d871 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -10,14 +10,33 @@ use std::rc::Rc; pub mod bls; pub mod matrix; +/// Declares a generator configuration. +/// Generators allow generating entries at runtime based on a set of data. #[derive(Serialize, Deserialize, Default, Clone)] pub struct GeneratorDeclaration { + /// Matrix generator configuration. + /// Matrix allows you to specify multiple value-key values as arrays. + /// This allows multiplying the number of entries by any number of possible + /// configuration options. For example, + /// data.x = ["a", "b"] + /// data.y = ["c", "d"] + /// would generate an entry for each of these combinations: + /// x = a, y = c + /// x = a, y = d + /// x = b, y = c + /// x = b, y = d #[serde(default)] pub matrix: Option, + /// BLS generator configuration. + /// BLS allows you to pass a filesystem path that contains a set of BLS entries. + /// It will generate a sprout entry for every supported BLS entry. #[serde(default)] pub bls: Option, } +/// Runs the generator specified by the `generator` option. +/// It uses the specified `context` as the parent context for +/// the generated entries, injecting more values if needed. pub fn generate( context: Rc, generator: &GeneratorDeclaration, diff --git a/src/phases.rs b/src/phases.rs index 20daa53..d637c38 100644 --- a/src/phases.rs +++ b/src/phases.rs @@ -37,9 +37,11 @@ pub struct PhaseConfiguration { pub fn phase(context: Rc, phase: &[PhaseConfiguration]) -> Result<()> { for item in phase { let mut context = context.fork(); + // Insert the values into the context. context.insert(&item.values); let context = context.freeze(); + // Execute all the actions in this phase configuration. for action in item.actions.iter() { actions::execute(context.clone(), action) .context(format!("unable to execute action '{}'", action))?;