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

+

-sprout logo -

Sprout

+![Sprout Logo](assets/logo.png) -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);