diff --git a/src/actions/edera.rs b/src/actions/edera.rs index 1ab837c..a88b9e1 100644 --- a/src/actions/edera.rs +++ b/src/actions/edera.rs @@ -19,53 +19,88 @@ use crate::{ }, }; +/// The configuration of the edera action which boots the Edera hypervisor. +/// Edera is based on Xen but modified significantly with a Rust stack. +/// Sprout is a component of the Edera stack and provides the boot functionality of Xen. #[derive(Serialize, Deserialize, Default, Clone)] pub struct EderaConfiguration { + /// The path to the Xen hypervisor EFI image. pub xen: String, + /// The path to the kernel to boot for dom0. pub kernel: String, + /// The path to the initrd to load for dom0. #[serde(default)] pub initrd: Option, + /// The options to pass to the kernel. #[serde(default, rename = "kernel-options")] pub kernel_options: Vec, + /// The options to pass to the Xen hypervisor. #[serde(default, rename = "xen-options")] pub xen_options: Vec, } +/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. fn build_xen_config(configuration: &EderaConfiguration) -> String { + // xen config file format is ini-like [ + // global section "[global]".to_string(), + // default configuration section "default=sprout".to_string(), + // configuration section for sprout "[sprout]".to_string(), + // xen options format!("options={}", configuration.xen_options.join(" ")), + // kernel options, stub replaces the kernel path + // the kernel is provided via media loader format!("kernel=stub {}", configuration.kernel_options.join(" ")), - "".to_string(), // required or else the last line will be ignored + // required or else the last line will be ignored + "".to_string(), ] .join("\n") } +/// Register a media loader for some `text` with the vendor `guid`. +/// `what` should indicate some identifying value for error messages +/// like `config` or `kernel`. +/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader. fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result { MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice()) .context(format!("unable to register {} media loader", what)) /* */ } +/// Register a media loader for the file `path` with the vendor `guid`. +/// `what` should indicate some identifying value for error messages +/// like `config` or `kernel`. +/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader. fn register_media_loader_file( context: &Rc, guid: Guid, what: &str, path: &str, ) -> Result { + // Stamp the path to the file. let path = context.stamp(path); + // Read the file contents. let content = utils::read_file_contents(context.root().loaded_image_path()?, &path) .context(format!("unable to read {} file", what))?; + // Register the media loader. let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) .context(format!("unable to register {} media loader", what))?; Ok(handle) } +/// Executes the edera action which will boot the Edera hypervisor with the specified +/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. pub fn edera(context: Rc, configuration: &EderaConfiguration) -> Result<()> { + // Build the Xen config file content for this configuration. let config = build_xen_config(configuration); + + // Register the media loader for the config. let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) .context("unable to register config media loader")?; + + // Register the media loaders for the kernel. let kernel = register_media_loader_file( &context, XEN_EFI_KERNEL_MEDIA_GUID, @@ -74,8 +109,10 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> ) .context("unable to register kernel media loader")?; + // Create a vector of media loaders to unregister on error. let mut media_loaders = vec![config, kernel]; + // Register the initrd if it is provided. if let Some(ref initrd) = configuration.initrd { let initrd = register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) @@ -83,6 +120,7 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> media_loaders.push(initrd); } + // Chainload to the Xen EFI stub. let result = actions::chainload::chainload( context.clone(), &ChainloadConfiguration { @@ -93,6 +131,7 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> ) .context("unable to chainload to xen"); + // Unregister the media loaders on error. for media_loader in media_loaders { if let Err(error) = media_loader.unregister() { error!("unable to unregister media loader: {}", error); diff --git a/src/actions/splash.rs b/src/actions/splash.rs index d02d0c8..a51903b 100644 --- a/src/actions/splash.rs +++ b/src/actions/splash.rs @@ -12,25 +12,39 @@ use std::time::Duration; use uefi::boot::ScopedProtocol; use uefi::proto::console::gop::GraphicsOutput; +const DEFAULT_SPLASH_TIME: u32 = 0; + +/// The configuration of the splash action. #[derive(Serialize, Deserialize, Default, Clone)] pub struct SplashConfiguration { + /// The path to the image to display. + /// Currently, only PNG images are supported. pub image: String, + /// The time to display the splash image without interruption, in seconds. + /// The default value is `0` which will display the image and let everything + /// continue. #[serde(default = "default_splash_time")] pub time: u32, } -pub fn default_splash_time() -> u32 { - 0 +fn default_splash_time() -> u32 { + DEFAULT_SPLASH_TIME } +/// Acquire the [GraphicsOutput]. We will find the first graphics output only. fn setup_graphics() -> Result> { + // Grab the handle for the graphics output protocol. let gop_handle = uefi::boot::get_handle_for_protocol::() .context("unable to get graphics output")?; + // Open the graphics output protocol exclusively. uefi::boot::open_protocol_exclusive::(gop_handle) .context("unable to open graphics output") } +/// Produces a [Rect] that fits the `image` inside the specified `frame`. +/// The output [Rect] should be used to resize the image. fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { + // Convert the image dimensions to a [Rect]. let input = Rect { x: 0, y: 0, @@ -38,9 +52,13 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { height: image.height(), }; + // Calculate the ratio of the image dimensions. let input_ratio = input.width as f32 / input.height as f32; + + // Calculate the ratio of the frame dimensions. let frame_ratio = frame.width as f32 / frame.height as f32; + // Create [Rect] to store the output dimensions. let mut output = Rect { x: 0, y: 0, @@ -63,24 +81,38 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { output } +/// Resize the input `image` to fit the `frame`. fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer, Vec> { let image = image.to_rgba8(); resize(&image, frame.width, frame.height, FilterType::Lanczos3) } +/// Draw the `image` on the screen using [GraphicsOutput]. fn draw(image: DynamicImage) -> Result<()> { + // Acquire the [GraphicsOutput] protocol. let mut gop = setup_graphics()?; + + // Acquire the current screen size. let (width, height) = gop.current_mode_info().resolution(); + + // Create a display frame. let display_frame = Rect { x: 0, y: 0, width: width as _, height: height as _, }; + + // Fit the image to the display frame. let fit = fit_to_frame(&image, display_frame); + + // Resize the image to fit the display frame. let image = resize_to_fit(&image, fit); + // Create a framebuffer to draw the image on. let mut framebuffer = Framebuffer::new(width, height); + + // Iterate over the pixels in the image and put them on the framebuffer. for (x, y, pixel) in image.enumerate_pixels() { let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else { continue; @@ -90,17 +122,27 @@ fn draw(image: DynamicImage) -> Result<()> { fb.blue = pixel[2]; } + // Blit the framebuffer to the screen. framebuffer.blit(&mut gop)?; Ok(()) } +/// Runs the splash action with the specified `configuration` inside the provided `context`. pub fn splash(context: Rc, configuration: &SplashConfiguration) -> Result<()> { + // Stamp the image path value. let image = context.stamp(&configuration.image); + // Read the image contents. let image = read_file_contents(context.root().loaded_image_path()?, &image)?; + // Decode the image as a PNG. let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png) .decode() .context("unable to decode splash image")?; + // Draw the image on the screen. draw(image)?; + + // Sleep for the specified time. std::thread::sleep(Duration::from_secs(configuration.time as u64)); + + // Return control to sprout. Ok(()) } diff --git a/src/config/loader.rs b/src/config/loader.rs index b90b81f..5b4a85c 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -5,36 +5,49 @@ 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> { + // Open the LoadedImageDevicePath protocol to get the path to the current image. let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::(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. Ok(content) } +/// Loads the [RootConfiguration] for Sprout. pub fn load() -> Result { + // Load the raw configuration from the sprout.toml file. let content = load_raw_config()?; + // 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")?; + // Check the version of the configuration without parsing the full configuration. let version = value .get("version") .cloned() .unwrap_or_else(|| Value::Integer(latest_version() as i64)); + // Parse the version into an u32. let version: u32 = version .try_into() .context("unable to get configuration version")?; + // Check if the version is supported. if version != latest_version() { bail!("unsupported configuration version: {}", version); } + // If the version is supported, parse the full configuration. let config: RootConfiguration = value .try_into() .context("unable to parse sprout.toml file")?; + + // Return the parsed configuration. Ok(config) } diff --git a/src/context.rs b/src/context.rs index c6964d3..cdd73c2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -5,28 +5,37 @@ use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; use uefi::proto::device_path::DevicePath; +/// Declares a root context for Sprout. +/// This contains data that needs to be shared across Sprout. #[derive(Default)] pub struct RootContext { + /// The actions that are available in Sprout. actions: BTreeMap, + /// The device path of the loaded Sprout image. loaded_image_path: Option>, } 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) -> Self { - RootContext { + Self { actions: BTreeMap::new(), loaded_image_path: Some(loaded_image_device_path), } } + /// Access the actions configured inside Sprout. pub fn actions(&self) -> &BTreeMap { &self.actions } + /// Access the actions configured inside Sprout mutably for modification. pub fn actions_mut(&mut self) -> &mut BTreeMap { &mut self.actions } + /// Access the device path of the loaded Sprout image. pub fn loaded_image_path(&self) -> Result<&DevicePath> { self.loaded_image_path .as_deref() @@ -34,6 +43,12 @@ impl RootContext { } } +/// A context of Sprout. This is passed around different parts of Sprout and represents +/// a [RootContext] which is data that is shared globally, and [SproutContext] which works +/// sort of like a tree of values. You can cheaply clone a [SproutContext] and modify it with +/// new values, which override the values of contexts above it. +/// +/// This is a core part of the value mechanism in Sprout which makes templating possible. pub struct SproutContext { root: Rc, parent: Option>, @@ -41,6 +56,7 @@ pub struct SproutContext { } impl SproutContext { + /// Create a new [SproutContext] using `root` as the root context. pub fn new(root: RootContext) -> Self { Self { root: Rc::new(root), @@ -49,10 +65,13 @@ impl SproutContext { } } + /// Access the root context of this context. pub fn root(&self) -> &RootContext { self.root.as_ref() } + /// Retrieve the value specified by `key` from this context or its parents. + /// Returns `None` if the value is not found. pub fn get(&self, key: impl AsRef) -> Option<&String> { self.values.get(key.as_ref()).or_else(|| { self.parent @@ -61,6 +80,8 @@ impl SproutContext { }) } + /// Collects all keys that are present in this context or its parents. + /// This is useful for iterating over all keys in a context. pub fn all_keys(&self) -> Vec { let mut keys = BTreeSet::new(); @@ -74,6 +95,8 @@ impl SproutContext { keys.into_iter().collect() } + /// Collects all values that are present in this context or its parents. + /// This is useful for iterating over all values in a context. pub fn all_values(&self) -> BTreeMap { let mut values = BTreeMap::new(); for key in self.all_keys() { @@ -82,17 +105,24 @@ impl SproutContext { values } + /// Sets the value `key` to the value specified by `value` in this context. + /// If the parent context has this key, this will override that key. pub fn set(&mut self, key: impl AsRef, value: impl ToString) { self.values .insert(key.as_ref().to_string(), value.to_string()); } + /// Inserts all the specified `values` into this context. + /// These values will take precedence over its parent context. pub fn insert(&mut self, values: &BTreeMap) { for (key, value) in values { self.values.insert(key.clone(), value.clone()); } } + /// Forks this context as an owned [SproutContext]. This makes it possible + /// to cheaply modify a context without cloning the parent context map. + /// The parent of the returned context is [self]. pub fn fork(self: &Rc) -> Self { Self { root: self.root.clone(), @@ -101,11 +131,19 @@ impl SproutContext { } } + /// Freezes this context into a [Rc] which makes it possible to cheaply clone + /// and makes it less easy to modify a context. This can be used to pass the context + /// to various other parts of Sprout and ensure it won't be modified. Instead, once + /// a context is frozen, it should be [self.fork]'d to be modified. pub fn freeze(self) -> Rc { Rc::new(self) } + /// Finalizes a context by producing a context with no parent that contains all the values + /// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no + /// inheritance with other [SproutContext]s. It will still contain a [RootContext] however. pub fn finalize(&self) -> SproutContext { + // Collect all the values from the context and its parents. let mut current_values = self.all_values(); loop { @@ -114,16 +152,21 @@ impl SproutContext { for (key, value) in ¤t_values { let (changed, result) = Self::stamp_values(¤t_values, value); if changed { + // If the value changed, we need to re-stamp it. did_change = true; } + // Insert the new value into the value map. values.insert(key.clone(), result); } current_values = values; + // If the values did not change, we can stop. if !did_change { break; } } + + // Produce the final context. Self { root: self.root.clone(), parent: None, @@ -131,6 +174,8 @@ impl SproutContext { } } + /// Stamps the `text` value with the specified `values` map. The returned value indicates + /// whether the `text` has been changed and the value that was stamped and changed. fn stamp_values(values: &BTreeMap, text: impl AsRef) -> (bool, String) { let mut result = text.as_ref().to_string(); let mut did_change = false; @@ -144,6 +189,9 @@ impl SproutContext { (did_change, result) } + /// Stamps the input `text` with all the values in this [SproutContext] and it's parents. + /// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce + /// "hello\\b" as an output string. pub fn stamp(&self, text: impl AsRef) -> String { Self::stamp_values(&self.all_values(), text.as_ref()).1 } diff --git a/src/generators/bls.rs b/src/generators/bls.rs index 9ea5cbc..b8e1b96 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -11,70 +11,109 @@ use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::media::fs::SimpleFileSystem; +/// BLS entry parser. mod entry; +/// The default path to the BLS entries directory. +const BLS_TEMPLATE_PATH: &str = "\\loader\\entries"; + +/// The configuration of the BLS generator. +/// The BLS uses the Bootloader Specification to produce +/// entries from an input template. #[derive(Serialize, Deserialize, Default, Clone)] pub struct BlsConfiguration { + /// The entry to use for as a template. pub entry: EntryDeclaration, + /// The path to the BLS entries directory. #[serde(default = "default_bls_path")] pub path: String, } fn default_bls_path() -> String { - "\\loader\\entries".to_string() + BLS_TEMPLATE_PATH.to_string() } +/// Generates entries from the BLS entries directory using the specified `bls` configuration and +/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries. pub fn generate( context: Rc, bls: &BlsConfiguration, ) -> Result, EntryDeclaration)>> { let mut entries = Vec::new(); + // Stamp the path to the BLS entries directory. let path = context.stamp(&bls.path); + + // Resolve the path to the BLS entries directory. let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path) .context("unable to resolve bls path")?; + + // Open exclusive access to the BLS filesystem. let fs = uefi::boot::open_protocol_exclusive::(resolved.filesystem_handle) .context("unable to open bls filesystem")?; let mut fs = FileSystem::new(fs); + + // Convert the subpath to the BLS entries directory to a string. let sub_text_path = resolved .sub_path .to_string(DisplayOnly(false), AllowShortcuts(false)) .context("unable to convert subpath to string")?; + + // Produce a path to the BLS entries directory. let entries_path = Path::new(&sub_text_path); + // Read the BLS entries directory. let entries_iter = fs .read_dir(entries_path) .context("unable to read bls entries")?; + // For each entry in the BLS entries directory, parse the entry and add it to the list. for entry in entries_iter { - let entry = entry?; + // Unwrap the entry file info. + let entry = entry.context("unable to read bls item entry")?; + + // Skip items that are not regular files. if !entry.is_regular_file() { continue; } + + // Get the file name of the filesystem item. let name = entry.file_name().to_string(); + + // Ignore files that are not .conf files. if !name.ends_with(".conf") { continue; } + // Produce the full path to the entry file. let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str()) .context("unable to construct full entry path")?; let full_entry_path = Path::new(&full_entry_path); + + // Read the entry file. let content = fs .read(full_entry_path) .context("unable to read bls file")?; + + // Parse the entry file as a UTF-8 string. let content = String::from_utf8(content).context("unable to read bls entry as utf8")?; + + // Parse the entry file as a BLS entry. let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?; + // Ignore entries that are not valid for Sprout. if !entry.is_valid() { continue; } + // Produce a new sprout context for the entry with the extracted values. let mut context = context.fork(); context.set("title", entry.title().unwrap_or(name)); context.set("chainload", entry.chainload_path().unwrap_or_default()); context.set("options", entry.options().unwrap_or_default()); context.set("initrd", entry.initrd_path().unwrap_or_default()); + // Add the entry to the list with a frozen context. entries.push((context.freeze(), bls.entry.clone())); } diff --git a/src/generators/bls/entry.rs b/src/generators/bls/entry.rs index 2cc704a..91c06c8 100644 --- a/src/generators/bls/entry.rs +++ b/src/generators/bls/entry.rs @@ -1,59 +1,82 @@ use anyhow::{Error, Result}; use std::str::FromStr; +/// Represents a parsed BLS entry. +/// Fields unrelated to Sprout are not included. #[derive(Default, Debug, Clone)] pub struct BlsEntry { + /// The title of the entry. pub title: Option, + /// The options to pass to the entry. pub options: Option, + /// The path to the linux kernel. pub linux: Option, + /// The path to the initrd. pub initrd: Option, + /// The path to an EFI image. pub efi: Option, } +/// Parser for a BLS entry. impl FromStr for BlsEntry { type Err = Error; + /// Parses the `input` as a BLS entry file. fn from_str(input: &str) -> Result { + // All the fields in a BLS entry we understand. + // Set all to None initially. let mut title: Option = None; let mut options: Option = None; let mut linux: Option = None; let mut initrd: Option = None; let mut efi: Option = None; + // Iterate over each line in the input and parse it. for line in input.lines() { + // Trim the line. let line = line.trim(); + + // Split the line once by a space. let Some((key, value)) = line.split_once(" ") else { continue; }; + // Match the key to a field we understand. match key { + // The title of the entry. "title" => { title = Some(value.trim().to_string()); } + // The options to pass to the entry. "options" => { options = Some(value.trim().to_string()); } + // The path to the linux kernel. "linux" => { linux = Some(value.trim().to_string()); } + // The path to the initrd. "initrd" => { initrd = Some(value.trim().to_string()); } + // The path to an EFI image. "efi" => { efi = Some(value.trim().to_string()); } + // Ignore any other key. _ => { continue; } } } - Ok(BlsEntry { + // Produce a BLS entry from the parsed fields. + Ok(Self { title, options, linux, @@ -64,10 +87,14 @@ impl FromStr for BlsEntry { } impl BlsEntry { + /// Checks if this BLS entry is something we can actually boot in Sprout. pub fn is_valid(&self) -> bool { self.linux.is_some() || self.efi.is_some() } + /// Fetches the path to an EFI bootable image to boot, if any. + /// This prioritizes the linux field over efi. + /// It also converts / to \\ to match EFI path style. pub fn chainload_path(&self) -> Option { self.linux .clone() @@ -75,16 +102,20 @@ impl BlsEntry { .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) } + /// Fetches the path to an initrd to pass to the kernel, if any. + /// It also converts / to \\ to match EFI path style. pub fn initrd_path(&self) -> Option { self.initrd .clone() .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) } + /// Fetches the options to pass to the kernel, if any. pub fn options(&self) -> Option { self.options.clone() } + /// Fetches the title of the entry, if any. pub fn title(&self) -> Option { self.title.clone() } diff --git a/src/generators/matrix.rs b/src/generators/matrix.rs index e243d62..c4d32f0 100644 --- a/src/generators/matrix.rs +++ b/src/generators/matrix.rs @@ -5,21 +5,37 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::rc::Rc; +/// Matrix generator configuration. +/// The matrix generator produces multiple entries based +/// on input values multiplicatively. #[derive(Serialize, Deserialize, Default, Clone)] pub struct MatrixConfiguration { + /// 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: BTreeMap>, } +/// Builds out multiple generations of `input` based on a matrix style. +/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]} +/// It will produce: +/// x: a, y: c +/// x: a, y: d +/// x: b, y: c +/// x: b, y: d fn build_matrix(input: &BTreeMap>) -> Vec> { + // Convert the input into a vector of tuples. let items: Vec<(String, Vec)> = input.clone().into_iter().collect(); + + // The result is a vector of maps. let mut result: Vec> = vec![BTreeMap::new()]; for (key, values) in items { let mut new_result = Vec::new(); + // Produce all the combinations of the input values. for combination in &result { for value in &values { let mut new_combination = combination.clone(); @@ -34,18 +50,23 @@ fn build_matrix(input: &BTreeMap>) -> Vec, matrix: &MatrixConfiguration, ) -> Result, EntryDeclaration)>> { + // 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 combination in combinations { 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.title = context.stamp(&entry.title); entry.actions = entry @@ -53,6 +74,7 @@ pub fn generate( .into_iter() .map(|action| context.stamp(action)) .collect(); + // Push the entry into the list with the new context. entries.push((context, entry)); } diff --git a/src/main.rs b/src/main.rs index bccb9bd..37b5f17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,34 @@ use std::ops::Deref; use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; +/// actions: Code that can be configured and executed by Sprout. pub mod actions; + +/// config: Sprout configuration mechanism. pub mod config; + +/// context: Stored values that can be cheaply forked and cloned. pub mod context; + +/// drivers: EFI drivers to load and provide extra functionality. pub mod drivers; + +/// entries: Boot menu entries that have a title and can execute actions. pub mod entries; + +/// extractors: Runtime code that can extract values into the Sprout context. pub mod extractors; + +/// generators: Runtime code that can generate entries with specific values. pub mod generators; + +/// phases: Hooks into specific parts of the boot process. pub mod phases; + +/// setup: Code that initializes the UEFI environment for Sprout. pub mod setup; + +/// utils: Utility functions that are used by other parts of Sprout. pub mod utils; /// The main entrypoint of sprout.