mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 10:10:17 +00:00
improve documentation of some functions and more readme work
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<p align="center">
|
||||
<div align="center">
|
||||
|
||||
<img src="assets/logo.png" alt="sprout logo" width="258" height="200" />
|
||||
<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.
|
||||
|
||||
|
||||
@@ -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<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)]
|
||||
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)]
|
||||
pub extractors: BTreeMap<String, ExtractorDeclaration>,
|
||||
#[serde(default)]
|
||||
@@ -33,40 +45,6 @@ pub struct RootConfiguration {
|
||||
pub phases: PhasesConfiguration,
|
||||
}
|
||||
|
||||
pub fn latest_version() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
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)
|
||||
fn latest_version() -> u32 {
|
||||
LATEST_VERSION
|
||||
}
|
||||
|
||||
40
src/config/loader.rs
Normal file
40
src/config/loader.rs
Normal 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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
/// The values to insert into the context when the entry is selected.
|
||||
#[serde(default)]
|
||||
pub values: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
52
src/main.rs
52
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(())
|
||||
}
|
||||
|
||||
@@ -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<PhaseConfiguration>,
|
||||
/// The startup phase is run after drivers are loaded, but before entries are displayed.
|
||||
#[serde(default)]
|
||||
pub startup: Vec<PhaseConfiguration>,
|
||||
/// The late phase is run after the entry is chosen, but before the actions are executed.
|
||||
#[serde(default)]
|
||||
pub late: Vec<PhaseConfiguration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct PhaseConfiguration {
|
||||
/// The actions to run when the phase is executed.
|
||||
#[serde(default)]
|
||||
pub actions: Vec<String>,
|
||||
/// The values to insert into the context when the phase is executed.
|
||||
#[serde(default)]
|
||||
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 {
|
||||
let mut context = context.fork();
|
||||
context.insert(&item.values);
|
||||
|
||||
Reference in New Issue
Block a user