13 Commits

11 changed files with 234 additions and 98 deletions

74
.github/workflows/publish.yaml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: publish
on:
release:
types:
- created
push:
branches:
- main
pull_request:
branches:
- main
paths:
- bin/**
- src/**
- Cargo.*
- rust-toolchain.toml
- .github/workflows/publish.yaml
permissions:
contents: read # Needed to checkout the repository.
jobs:
artifacts:
name: artifacts
permissions:
contents: write # Needed to upload release assets and artifacts.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sprout-x86_64.efi
path: target/assemble/sprout-x86_64.efi
- name: 'upload sprout-aarch64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sprout-aarch64.efi
path: target/assemble/sprout-aarch64.efi
- name: 'generate cultivator token'
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.release.tag_name }}"
if: ${{ github.event_name == 'release' }}

View File

@@ -1,41 +0,0 @@
name: release assets
on:
release:
types:
- created
permissions:
contents: read # Needed to checkout the repository.
jobs:
assets:
name: assets
permissions:
contents: write # Needed to upload release assets.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
- name: 'assemble release artifacts'
run: ./hack/assemble.sh
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
RELEASE_TAG: "${{ github.event.release.tag_name }}"
if: ${{ github.event_name == 'release' }}

26
Cargo.lock generated
View File

@@ -59,6 +59,19 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "edera-sprout"
version = "0.0.3"
dependencies = [
"anyhow",
"image",
"log",
"serde",
"toml",
"uefi",
"uefi-raw",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -251,19 +264,6 @@ name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268" source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[package]]
name = "sprout"
version = "0.0.1"
dependencies = [
"anyhow",
"image",
"log",
"serde",
"toml",
"uefi",
"uefi-raw",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.106" version = "2.0.106"

View File

@@ -1,6 +1,10 @@
[package] [package]
name = "sprout" name = "edera-sprout"
version = "0.0.2" description = "Modern UEFI bootloader"
license = "Apache-2.0"
version = "0.0.3"
homepage = "https://github.com/edera-dev/sprout"
repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -25,14 +29,14 @@ features = ["alloc", "logger"]
[dependencies.uefi-raw] [dependencies.uefi-raw]
version = "0.11.0" version = "0.11.0"
[profile.release]
lto = "thin"
strip = "symbols"
[features] [features]
default = ["splash"] default = ["splash"]
splash = ["dep:image"] splash = ["dep:image"]
[profile.release]
lto = "thin"
strip = "symbols"
[profile.release-debuginfo] [profile.release-debuginfo]
inherits = "release" inherits = "release"
strip = "none" strip = "none"
@@ -50,3 +54,7 @@ rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[patch.crates-io.moxcms] [patch.crates-io.moxcms]
git = "https://github.com/edera-dev/sprout-patched-deps.git" git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268" rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[bin]]
name = "sprout"
path = "src/main.rs"

View File

@@ -1,7 +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
</div>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust. Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust.
@@ -18,7 +21,7 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## Features ## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently
have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or panics. have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or fails.
### Current ### Current

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -4,22 +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::Context;
use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref;
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)]
@@ -32,19 +45,6 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
pub fn latest_version() -> u32 { fn latest_version() -> u32 {
1 LATEST_VERSION
}
pub fn load() -> Result<RootConfiguration> {
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")?;
let config: RootConfiguration =
toml::from_slice(&content).context("unable to parse sprout.toml file")?;
Ok(config)
} }

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

@@ -0,0 +1,40 @@
use crate::config::{RootConfiguration, latest_version};
use crate::utils;
use anyhow::{Context, Result, bail};
use std::ops::Deref;
use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath;
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")?;
let index = 1; // 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 (context, entry) = &final_entries[index - 1]; 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);