diff --git a/README.md b/README.md
index 8537ce2..d657720 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,10 @@
-
+
-

-
Sprout
+
-Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust.
+# Sprout
+
+
Sprout is in use at Edera today in development environments and is intended to ship to production soon.
diff --git a/src/config.rs b/src/config.rs
index 84c004f..117e6b6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,23 +4,35 @@ use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration;
-use crate::utils;
-use anyhow::Result;
-use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
-use std::ops::Deref;
-use toml::Value;
-use uefi::proto::device_path::LoadedImageDevicePath;
+/// The configuration loader mechanisms.
+pub mod loader;
+
+/// This is the latest version of the sprout configuration format.
+/// This must be incremented when the configuration breaks compatibility.
+pub const LATEST_VERSION: u32 = 1;
+
+/// The Sprout configuration format.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct RootConfiguration {
+ /// The version of the configuration. This should always be declared
+ /// and be the latest version that is supported. If not specified, it is assumed
+ /// the configuration is the latest version.
#[serde(default = "latest_version")]
pub version: u32,
+ /// Values to be inserted into the root sprout context.
#[serde(default)]
pub values: BTreeMap,
+ /// Drivers to load.
+ /// These drivers provide extra functionality like filesystem support to Sprout.
+ /// Each driver has a name which uniquely identifies it inside Sprout.
#[serde(default)]
pub drivers: BTreeMap,
+ /// Declares the extractors that add values to the sprout context that are calculated
+ /// at runtime. Each extractor has a name which corresponds to the value it will set
+ /// inside the sprout context.
#[serde(default)]
pub extractors: BTreeMap,
#[serde(default)]
@@ -33,40 +45,6 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration,
}
-pub fn latest_version() -> u32 {
- 1
-}
-
-fn load_raw_config() -> Result> {
- let current_image_device_path_protocol =
- uefi::boot::open_protocol_exclusive::(uefi::boot::image_handle())
- .context("unable to get loaded image device path")?;
- let path = current_image_device_path_protocol.deref().to_boxed();
-
- let content = utils::read_file_contents(&path, "sprout.toml")
- .context("unable to read sprout.toml file")?;
- Ok(content)
-}
-
-pub fn load() -> Result {
- let content = load_raw_config()?;
- let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?;
-
- let version = value
- .get("version")
- .cloned()
- .unwrap_or_else(|| Value::Integer(latest_version() as i64));
-
- let version: u32 = version
- .try_into()
- .context("unable to get configuration version")?;
-
- if version != latest_version() {
- bail!("unsupported configuration version: {}", version);
- }
-
- let config: RootConfiguration = value
- .try_into()
- .context("unable to parse sprout.toml file")?;
- Ok(config)
+fn latest_version() -> u32 {
+ LATEST_VERSION
}
diff --git a/src/config/loader.rs b/src/config/loader.rs
new file mode 100644
index 0000000..59710b1
--- /dev/null
+++ b/src/config/loader.rs
@@ -0,0 +1,40 @@
+use std::ops::Deref;
+use anyhow::{bail, Context, Result};
+use toml::Value;
+use uefi::proto::device_path::LoadedImageDevicePath;
+use crate::config::{latest_version, RootConfiguration};
+use crate::utils;
+
+fn load_raw_config() -> Result> {
+ let current_image_device_path_protocol =
+ uefi::boot::open_protocol_exclusive::(uefi::boot::image_handle())
+ .context("unable to get loaded image device path")?;
+ let path = current_image_device_path_protocol.deref().to_boxed();
+
+ let content = utils::read_file_contents(&path, "sprout.toml")
+ .context("unable to read sprout.toml file")?;
+ Ok(content)
+}
+
+pub fn load() -> Result {
+ let content = load_raw_config()?;
+ let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?;
+
+ let version = value
+ .get("version")
+ .cloned()
+ .unwrap_or_else(|| Value::Integer(latest_version() as i64));
+
+ let version: u32 = version
+ .try_into()
+ .context("unable to get configuration version")?;
+
+ if version != latest_version() {
+ bail!("unsupported configuration version: {}", version);
+ }
+
+ let config: RootConfiguration = value
+ .try_into()
+ .context("unable to parse sprout.toml file")?;
+ Ok(config)
+}
diff --git a/src/entries.rs b/src/entries.rs
index 4353c12..84b9f35 100644
--- a/src/entries.rs
+++ b/src/entries.rs
@@ -1,11 +1,18 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
+/// Declares a boot entry to display in the boot menu.
+///
+/// Entries are the user-facing concept of Sprout, making it possible
+/// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct EntryDeclaration {
+ /// The title of the entry which will be display in the boot menu.
pub title: String,
+ /// The actions to run when the entry is selected.
#[serde(default)]
pub actions: Vec,
+ /// The values to insert into the context when the entry is selected.
#[serde(default)]
pub values: BTreeMap,
}
diff --git a/src/main.rs b/src/main.rs
index 5992177..bccb9bd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
+#![doc = include_str!("../README.md")]
#![feature(uefi_std)]
use crate::context::{RootContext, SproutContext};
@@ -20,15 +21,20 @@ pub mod phases;
pub mod setup;
pub mod utils;
+/// The main entrypoint of sprout.
+/// It is possible this function will not return if actions that are executed
+/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
+ // Initialize the basic UEFI environment.
setup::init()?;
- let config = config::load()?;
-
- if config.version != config::latest_version() {
- bail!("unsupported configuration version: {}", config.version);
- }
+ // 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()?;
+ // Load the root context.
+ // This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
let mut root = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath,
@@ -42,16 +48,25 @@ fn main() -> Result<()> {
RootContext::new(loaded_image_path)
};
+ // Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone());
+ // Create a new sprout context with the root context.
let mut context = SproutContext::new(root);
+
+ // Insert the configuration values into the sprout context.
context.insert(&config.values);
+
+ // Freeze the sprout context so it can be shared and cheaply cloned.
let context = context.freeze();
+ // Execute the early phase.
phase(context.clone(), &config.phases.early).context("unable to execute early phase")?;
+ // Load all configured drivers.
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
+ // Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors {
let value = extractors::extract(context.clone(), extractor)
@@ -60,50 +75,69 @@ fn main() -> Result<()> {
extracted.insert(name.clone(), value);
}
let mut context = context.fork();
+ // Insert the extracted values into the sprout context.
context.insert(&extracted);
let context = context.freeze();
+ // Execute the late phase.
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
- let mut all_entries = Vec::new();
+ let mut staged_entries = Vec::new();
+ // Insert all the static entries from the configuration into the entry list.
for (_name, entry) in config.entries {
- all_entries.push((context.clone(), entry));
+ // Associate the main context with the static entry.
+ staged_entries.push((context.clone(), entry));
}
+ // Run all the generators declared in the configuration.
for (_name, generator) in config.generators {
let context = context.fork().freeze();
+ // Add all the entries generated by the generator to the entry list.
+ // The generator specifies the context associated with the entry.
for entry in generators::generate(context.clone(), &generator)? {
- all_entries.push(entry);
+ staged_entries.push(entry);
}
}
+ // Build a list of all the final boot entries.
let mut final_entries = Vec::new();
- for (context, entry) in all_entries {
+ for (context, entry) in staged_entries {
let mut context = context.fork();
+ // Insert the values from the entry configuration into the
+ // sprout context to use with the entry itself.
context.insert(&entry.values);
let context = context.finalize().freeze();
+ // Insert the entry configuration into final boot entries with the extended context.
final_entries.push((context, entry));
}
+ // TODO(azenla): Implement boot menu here.
+ // For now, we just print all of the entries.
info!("entries:");
for (index, (context, entry)) in final_entries.iter().enumerate() {
let title = context.stamp(&entry.title);
info!(" entry {}: {}", index + 1, title);
}
+ // 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");
};
+ // Execute all the actions for the selected entry.
for action in &entry.actions {
let action = context.stamp(action);
actions::execute(context.clone(), &action)
.context(format!("unable to execute action '{}'", action))?;
}
+
+ // Sprout doesn't necessarily guarantee anything was booted.
+ // If we reach here, we will exit back to whoever called us.
Ok(())
}
diff --git a/src/phases.rs b/src/phases.rs
index 4381096..20daa53 100644
--- a/src/phases.rs
+++ b/src/phases.rs
@@ -1,29 +1,40 @@
use crate::actions;
use crate::context::SproutContext;
-use anyhow::Context;
+use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
+/// Configures the various phases of the boot process.
+/// This allows hooking various phases to run actions.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhasesConfiguration {
+ /// The early phase is run before drivers are loaded.
#[serde(default)]
pub early: Vec,
+ /// The startup phase is run after drivers are loaded, but before entries are displayed.
#[serde(default)]
pub startup: Vec,
+ /// The late phase is run after the entry is chosen, but before the actions are executed.
#[serde(default)]
pub late: Vec,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhaseConfiguration {
+ /// The actions to run when the phase is executed.
#[serde(default)]
pub actions: Vec,
+ /// The values to insert into the context when the phase is executed.
#[serde(default)]
pub values: BTreeMap,
}
-pub fn phase(context: Rc, phase: &[PhaseConfiguration]) -> anyhow::Result<()> {
+/// Executes the specified [phase] of the boot process.
+/// The value [phase] should be a reference of a specific phase in the [PhasesConfiguration].
+/// Any error from the actions is propagated into the [Result] and will interrupt further
+/// execution of phase actions.
+pub fn phase(context: Rc, phase: &[PhaseConfiguration]) -> Result<()> {
for item in phase {
let mut context = context.fork();
context.insert(&item.values);