5 Commits

11 changed files with 302 additions and 20 deletions

2
Cargo.lock generated
View File

@@ -61,7 +61,7 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.4" version = "0.0.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.4" version = "0.0.7"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"

View File

@@ -21,6 +21,8 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## Documentation ## Documentation
- [Fedora Setup Guide] - [Fedora Setup Guide]
- [Generic Linux Setup Guide]
- [Windows Setup Guide]
- [Development Guide] - [Development Guide]
- [Contributing Guide] - [Contributing Guide]
- [Sprout License] - [Sprout License]
@@ -71,6 +73,17 @@ See [Configuration](#configuration) for how to configure sprout.
Sprout is configured using a TOML file at `\sprout.toml` on the root of the EFI partition sprout was booted from. Sprout is configured using a TOML file at `\sprout.toml` on the root of the EFI partition sprout was booted from.
### Command Line Options
Sprout supports some command line options that can be combined to modify behavior without the configuration file.
```bash
# Boot Sprout with a specific configuration file.
$ sprout.efi --config=\path\to\config.toml
# Boot a specific entry, bypassing the menu.
$ sprout.efi --boot="Boot Xen"
```
### Boot Linux from ESP ### Boot Linux from ESP
```toml ```toml
@@ -127,6 +140,8 @@ chainload.linux-initrd = "$boot\\$initrd"
``` ```
[Fedora Setup Guide]: ./docs/fedora-setup.md [Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md
[Development Guide]: ./DEVELOPMENT.md [Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md [Contributing Guide]: ./CONTRIBUTING.md
[Sprout License]: ./LICENSE [Sprout License]: ./LICENSE

View File

@@ -1,4 +1,4 @@
# Sprout on Fedora # Setup Sprout on Fedora
## Prerequisites ## Prerequisites

View File

@@ -0,0 +1,62 @@
# Setup Sprout to boot Linux
## Prerequisites
- EFI System Partition mounted on a known path
- Linux kernel installed with an optional initramfs
- Linux kernel must support the EFI stub (most distro kernels)
## Step 1: Base Installation
First, identify the path to your EFI System Partition. On most systems, this is `/boot/efi`.
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `/EFI/BOOT/sprout.efi` on your EFI System Partition.
## Step 2: Copy kernel and optional initramfs
Copy the Linux kernel to `/vmlinuz-sprout` on your EFI System Partition.
If needed, copy the initramfs to `/initramfs-sprout` on your EFI System Partition.
## Step 3: Configure Sprout
Write the following file to `/sprout.toml` on your EFI System Partition,
paying attention to place the correct values:
```toml
# sprout configuration: version 1
version = 1
# add a boot entry for booting linux
# which will run the boot-linux action.
[entries.boot-linux]
title = "Boot Linux"
actions = ["boot-linux"]
# use the chainload action to boot linux via the efi stub.
# the options below are passed to the efi stub as the
# kernel command line. the initrd is loaded using the efi stub
# initrd loader mechanism.
[actions.boot-linux]
chainload.path = "\\vmlinuz-sprout"
chainload.options = ["root=/dev/sda1", "my-kernel-option"]
chainload.linux-initrd = "\\initramfs-sprout"
```
You can specify any kernel command line options you want on the chainload options line.
They will be concatenated by a space and passed to the kernel.
## Step 4: Configure EFI firmware to boot Sprout
Since Sprout is still experimental, the following commands will add a boot entry to your EFI firmware for sprout but
intentionally do not set it as the default boot entry.
To add the entry, please find the partition device of your EFI System Partition and run the following:
```bash
$ sudo efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\BOOT\sprout.efi'
```
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
Now if you boot into your UEFI firmware, you should see Sprout as an option to boot.

52
docs/windows-setup.md Normal file
View File

@@ -0,0 +1,52 @@
# Setup Sprout to boot Windows
## Prerequisites
- Secure Boot disabled
- UEFI Windows installation
## Step 1: Base Installation
First, mount the EFI System Partition on your Windows installation:
In an administrator command prompt, run:
```batch
> mountvol X: /s
```
This will mount the EFI System Partition to the drive letter `X:`.
Please note that Windows Explorer will not let you see the drive letter `X:` where the ESP is mounted.
You will need to use the command prompt or PowerShell to access the ESP.
Standard editors can, however, be used to edit files on the ESP.
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `X:\EFI\BOOT\sprout.efi` on your EFI System Partition.
## Step 3: Configure Sprout
Write the following file to `X:\sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# add a boot entry for booting Windows
# which will run the boot-windows action.
[entries.windows]
title = "Windows"
actions = ["boot-windows"]
# use the chainload action to boot the Windows bootloader.
[actions.boot-windows]
chainload.path = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi"
```
## Step 4: Configure EFI Firmware to boot Sprout
It is not trivial to add an EFI boot entry inside Windows.
However, most firmware lets you load arbitrary EFI files from the firmware settings.
You can boot `\EFI\BOOT\sprout.efi` from firmware to boot Sprout.

View File

@@ -0,0 +1,11 @@
version = 1
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\shell.efi"
[actions.chainload-shell]
chainload.path = "$boot\\EFI\\BOOT\\shell.efi"
[entries.xen]
title = "Boot Shell"
actions = ["chainload-shell"]

View File

@@ -1,31 +1,36 @@
use crate::config::{RootConfiguration, latest_version}; use crate::config::{RootConfiguration, latest_version};
use crate::options::SproutOptions;
use crate::utils; use crate::utils;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info;
use std::ops::Deref; use std::ops::Deref;
use toml::Value; use toml::Value;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
/// Loads the raw configuration from the sprout.toml file as data. /// Loads the raw configuration from the sprout config file as data.
fn load_raw_config() -> Result<Vec<u8>> { fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
// Open the LoadedImageDevicePath protocol to get the path to the current image. // Open the LoadedImageDevicePath protocol to get the path to the current image.
let current_image_device_path_protocol = let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle()) uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?; .context("unable to get loaded image device path")?;
// Acquire the device path as a boxed device path. // Acquire the device path as a boxed device path.
let path = current_image_device_path_protocol.deref().to_boxed(); let path = current_image_device_path_protocol.deref().to_boxed();
// Read the contents of the sprout.toml file.
let content = utils::read_file_contents(&path, "sprout.toml") info!("configuration file: {}", options.config);
.context("unable to read sprout.toml file")?;
// Return the contents of the sprout.toml file. // Read the contents of the sprout config file.
let content = utils::read_file_contents(&path, &options.config)
.context("unable to read sprout config file")?;
// Return the contents of the sprout config file.
Ok(content) Ok(content)
} }
/// Loads the [RootConfiguration] for Sprout. /// Loads the [RootConfiguration] for Sprout.
pub fn load() -> Result<RootConfiguration> { pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
// Load the raw configuration from the sprout.toml file. // Load the raw configuration from the sprout config file.
let content = load_raw_config()?; let content = load_raw_config(options)?;
// Parse the raw configuration into a toml::Value which can represent any TOML file. // Parse the raw configuration into a toml::Value which can represent any TOML file.
let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?; let value: Value = toml::from_slice(&content).context("unable to parse sprout config file")?;
// Check the version of the configuration without parsing the full configuration. // Check the version of the configuration without parsing the full configuration.
let version = value let version = value

View File

@@ -1,4 +1,5 @@
use crate::actions::ActionDeclaration; use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
@@ -13,15 +14,18 @@ pub struct RootContext {
actions: BTreeMap<String, ActionDeclaration>, actions: BTreeMap<String, ActionDeclaration>,
/// The device path of the loaded Sprout image. /// The device path of the loaded Sprout image.
loaded_image_path: Option<Box<DevicePath>>, loaded_image_path: Option<Box<DevicePath>>,
/// The global options of Sprout.
options: SproutOptions,
} }
impl RootContext { impl RootContext {
/// Creates a new root context with the `loaded_image_device_path` which will be stored /// Creates a new root context with the `loaded_image_device_path` which will be stored
/// in the context for easy access. /// in the context for easy access.
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self { pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> Self {
Self { Self {
actions: BTreeMap::new(), actions: BTreeMap::new(),
loaded_image_path: Some(loaded_image_device_path), loaded_image_path: Some(loaded_image_device_path),
options,
} }
} }
@@ -41,6 +45,11 @@ impl RootContext {
.as_deref() .as_deref()
.ok_or_else(|| anyhow!("no loaded image path")) .ok_or_else(|| anyhow!("no loaded image path"))
} }
/// Access the global Sprout options.
pub fn options(&self) -> &SproutOptions {
&self.options
}
} }
/// A context of Sprout. This is passed around different parts of Sprout and represents /// A context of Sprout. This is passed around different parts of Sprout and represents

View File

@@ -3,7 +3,7 @@
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result};
use log::info; use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
@@ -37,6 +37,9 @@ pub mod phases;
/// setup: Code that initializes the UEFI environment for Sprout. /// setup: Code that initializes the UEFI environment for Sprout.
pub mod setup; pub mod setup;
/// options: Parse the options of the Sprout executable.
pub mod options;
/// utils: Utility functions that are used by other parts of Sprout. /// utils: Utility functions that are used by other parts of Sprout.
pub mod utils; pub mod utils;
@@ -47,10 +50,13 @@ fn main() -> Result<()> {
// Initialize the basic UEFI environment. // Initialize the basic UEFI environment.
setup::init()?; setup::init()?;
// Parse the options to the sprout executable.
let options = options::parse().context("unable to parse options")?;
// Load the configuration of sprout. // Load the configuration of sprout.
// At this point, the configuration has been validated and the specified // At this point, the configuration has been validated and the specified
// version is checked to ensure compatibility. // version is checked to ensure compatibility.
let config = config::loader::load()?; let config = config::loader::load(&options)?;
// Load the root context. // Load the root context.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
@@ -64,7 +70,7 @@ fn main() -> Result<()> {
"loaded image path: {}", "loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))? loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
); );
RootContext::new(loaded_image_path) RootContext::new(loaded_image_path, options)
}; };
// Insert the configuration actions into the root context. // Insert the configuration actions into the root context.
@@ -144,9 +150,14 @@ fn main() -> Result<()> {
// Execute the late phase. // 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. // Use the boot option if possible, otherwise pick the first entry.
let Some((context, entry)) = final_entries.first() else { let (context, entry) = if let Some(ref boot) = context.root().options().boot {
bail!("no entries found"); final_entries
.iter()
.find(|(_context, entry)| &entry.title == boot)
.context(format!("unable to find entry: {boot}"))?
} else {
final_entries.first().context("no entries found")?
}; };
// Execute all the actions for the selected entry. // Execute all the actions for the selected entry.

117
src/options.rs Normal file
View File

@@ -0,0 +1,117 @@
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout.
#[derive(Debug)]
pub struct SproutOptions {
/// Path to a configuration file to load.
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
}
/// The default Sprout options.
impl Default for SproutOptions {
fn default() -> Self {
Self {
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
}
}
}
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
if value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option: {option}");
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parse the arguments to Sprout as a [SproutOptions] structure.
pub fn parse() -> Result<SproutOptions> {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = SproutOptions::default();
let options = parse_raw().context("unable to parse options")?;
for (key, value) in options {
match key.as_str() {
"config" => {
// The configuration file to load.
result.config = value.context("--config option requires a value")?;
}
"boot" => {
// The entry to boot.
result.boot = Some(value.context("--boot option requires a value")?);
}
_ => bail!("unknown option: --{key}"),
}
}
Ok(result)
}