improve documentation of some functions and more readme work

This commit is contained in:
2025-10-19 20:23:55 -07:00
parent f5f431458c
commit 9b8ba30f56
6 changed files with 128 additions and 57 deletions

View File

@@ -1,9 +1,10 @@
<p align="center"> <div align="center">
<img src="assets/logo.png" alt="sprout logo" width="258" height="200" /> ![Sprout Logo](assets/logo.png)
<h1 align="center">Sprout</h1>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust. # Sprout
</div>
Sprout is in use at Edera today in development environments and is intended to ship to production soon. Sprout is in use at Edera today in development environments and is intended to ship to production soon.

View File

@@ -4,23 +4,35 @@ use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration; use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration; use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration; use crate::phases::PhasesConfiguration;
use crate::utils;
use anyhow::Result;
use anyhow::{Context, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; 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)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct RootConfiguration { 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")] #[serde(default = "latest_version")]
pub version: u32, pub version: u32,
/// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
/// 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)] #[serde(default)]
pub drivers: BTreeMap<String, DriverDeclaration>, pub drivers: BTreeMap<String, DriverDeclaration>,
/// 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)] #[serde(default)]
pub extractors: BTreeMap<String, ExtractorDeclaration>, pub extractors: BTreeMap<String, ExtractorDeclaration>,
#[serde(default)] #[serde(default)]
@@ -33,40 +45,6 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
pub fn latest_version() -> u32 { fn latest_version() -> u32 {
1 LATEST_VERSION
}
fn load_raw_config() -> Result<Vec<u8>> {
let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(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<RootConfiguration> {
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)
} }

40
src/config/loader.rs Normal file
View File

@@ -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<Vec<u8>> {
let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(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<RootConfiguration> {
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)
}

View File

@@ -1,11 +1,18 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; 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)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct EntryDeclaration { pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu.
pub title: String, pub title: String,
/// The actions to run when the entry is selected.
#[serde(default)] #[serde(default)]
pub actions: Vec<String>, pub actions: Vec<String>,
/// The values to insert into the context when the entry is selected.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
} }

View File

@@ -1,3 +1,4 @@
#![doc = include_str!("../README.md")]
#![feature(uefi_std)] #![feature(uefi_std)]
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
@@ -20,15 +21,20 @@ pub mod phases;
pub mod setup; pub mod setup;
pub mod utils; 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<()> { fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?; setup::init()?;
let config = config::load()?; // Load the configuration of sprout.
// At this point, the configuration has been validated and the specified
if config.version != config::latest_version() { // version is checked to ensure compatibility.
bail!("unsupported configuration version: {}", config.version); 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 mut root = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::< let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath, LoadedImageDevicePath,
@@ -42,16 +48,25 @@ fn main() -> Result<()> {
RootContext::new(loaded_image_path) RootContext::new(loaded_image_path)
}; };
// Insert the configuration actions into the root context.
root.actions_mut().extend(config.actions.clone()); root.actions_mut().extend(config.actions.clone());
// Create a new sprout context with the root context.
let mut context = SproutContext::new(root); let mut context = SproutContext::new(root);
// Insert the configuration values into the sprout context.
context.insert(&config.values); context.insert(&config.values);
// Freeze the sprout context so it can be shared and cheaply cloned.
let context = context.freeze(); let context = context.freeze();
// Execute the early phase.
phase(context.clone(), &config.phases.early).context("unable to execute 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")?; drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
// Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new(); let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors { for (name, extractor) in &config.extractors {
let value = extractors::extract(context.clone(), extractor) let value = extractors::extract(context.clone(), extractor)
@@ -60,50 +75,69 @@ fn main() -> Result<()> {
extracted.insert(name.clone(), value); extracted.insert(name.clone(), value);
} }
let mut context = context.fork(); let mut context = context.fork();
// Insert the extracted values into the sprout context.
context.insert(&extracted); context.insert(&extracted);
let context = context.freeze(); let context = context.freeze();
// Execute the late phase.
phase(context.clone(), &config.phases.startup).context("unable to execute startup 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 { 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 { for (_name, generator) in config.generators {
let context = context.fork().freeze(); 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)? { 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(); let mut final_entries = Vec::new();
for (context, entry) in all_entries { for (context, entry) in staged_entries {
let mut context = context.fork(); 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); context.insert(&entry.values);
let context = context.finalize().freeze(); let context = context.finalize().freeze();
// Insert the entry configuration into final boot entries with the extended context.
final_entries.push((context, entry)); final_entries.push((context, entry));
} }
// TODO(azenla): Implement boot menu here.
// For now, we just print all of the entries.
info!("entries:"); info!("entries:");
for (index, (context, entry)) in final_entries.iter().enumerate() { for (index, (context, entry)) in final_entries.iter().enumerate() {
let title = context.stamp(&entry.title); let title = context.stamp(&entry.title);
info!(" entry {}: {}", index + 1, title); info!(" entry {}: {}", index + 1, title);
} }
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute 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 { let Some((context, entry)) = final_entries.first() else {
bail!("no entries found"); bail!("no entries found");
}; };
// Execute all the actions for the selected entry.
for action in &entry.actions { for action in &entry.actions {
let action = context.stamp(action); let action = context.stamp(action);
actions::execute(context.clone(), &action) actions::execute(context.clone(), &action)
.context(format!("unable to execute action '{}'", 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(()) Ok(())
} }

View File

@@ -1,29 +1,40 @@
use crate::actions; use crate::actions;
use crate::context::SproutContext; use crate::context::SproutContext;
use anyhow::Context; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; 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)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhasesConfiguration { pub struct PhasesConfiguration {
/// The early phase is run before drivers are loaded.
#[serde(default)] #[serde(default)]
pub early: Vec<PhaseConfiguration>, pub early: Vec<PhaseConfiguration>,
/// The startup phase is run after drivers are loaded, but before entries are displayed.
#[serde(default)] #[serde(default)]
pub startup: Vec<PhaseConfiguration>, pub startup: Vec<PhaseConfiguration>,
/// The late phase is run after the entry is chosen, but before the actions are executed.
#[serde(default)] #[serde(default)]
pub late: Vec<PhaseConfiguration>, pub late: Vec<PhaseConfiguration>,
} }
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhaseConfiguration { pub struct PhaseConfiguration {
/// The actions to run when the phase is executed.
#[serde(default)] #[serde(default)]
pub actions: Vec<String>, pub actions: Vec<String>,
/// The values to insert into the context when the phase is executed.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
} }
pub fn phase(context: Rc<SproutContext>, 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<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> {
for item in phase { for item in phase {
let mut context = context.fork(); let mut context = context.fork();
context.insert(&item.values); context.insert(&item.values);