document ( by hand :( ) all of the code of sprout

This commit is contained in:
2025-10-20 00:06:46 -07:00
parent 106064d3e7
commit 3453826e9d
8 changed files with 261 additions and 8 deletions

View File

@@ -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<String>,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
/// The options to pass to the Xen hypervisor.
#[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>,
}
/// 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> {
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<SproutContext>,
guid: Guid,
what: &str,
path: &str,
) -> Result<MediaLoaderHandle> {
// 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<SproutContext>, 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<SproutContext>, 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<SproutContext>, 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<SproutContext>, 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);

View File

@@ -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<ScopedProtocol<GraphicsOutput>> {
// Grab the handle for the graphics output protocol.
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
.context("unable to get graphics output")?;
// Open the graphics output protocol exclusively.
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(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<Rgba<u8>, Vec<u8>> {
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<SproutContext>, 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(())
}

View File

@@ -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<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.
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()?;
// 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)
}

View File

@@ -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<String, ActionDeclaration>,
/// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>,
}
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 {
RootContext {
Self {
actions: BTreeMap::new(),
loaded_image_path: Some(loaded_image_device_path),
}
}
/// Access the actions configured inside Sprout.
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
&self.actions
}
/// Access the actions configured inside Sprout mutably for modification.
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
&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<RootContext>,
parent: Option<Rc<SproutContext>>,
@@ -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<str>) -> 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<String> {
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<String, String> {
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<str>, 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<String, String>) {
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<SproutContext>) -> 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<SproutContext> {
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 &current_values {
let (changed, result) = Self::stamp_values(&current_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<String, String>, text: impl AsRef<str>) -> (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<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1
}

View File

@@ -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<SproutContext>,
bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, 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::<SimpleFileSystem>(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()));
}

View File

@@ -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<String>,
/// The options to pass to the entry.
pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
}
/// 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<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = 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<String> {
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<String> {
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<String> {
self.options.clone()
}
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> {
self.title.clone()
}

View File

@@ -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<String, Vec<String>>,
}
/// 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<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = 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<String, Vec<String>>) -> Vec<BTreeMap<String, S
result.into_iter().filter(|item| !item.is_empty()).collect()
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
matrix: &MatrixConfiguration,
) -> Result<Vec<(Rc<SproutContext>, 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));
}

View File

@@ -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.