29 Commits

Author SHA1 Message Date
1171959a52 sprout: version v0.0.7 2025-10-20 19:23:05 -07:00
c5ec8dc6a6 add readme information about sprout command line arguments 2025-10-20 18:21:41 -07:00
c749c8d38e implement a new sprout command line options mechanism 2025-10-20 18:17:29 -07:00
3d2c31ee1a add a windows setup guide 2025-10-20 13:21:43 -07:00
a02ee88afd add a generic linux setup guide 2025-10-20 12:07:22 -07:00
b59626888e add bls quirk to support fedora out of the box 2025-10-20 11:33:33 -07:00
e3bae1dc63 repair fedora setup guide to include copying grub modules 2025-10-20 11:24:42 -07:00
3cd3491df0 fix bad autocomplete in fedora setup guide 2025-10-20 10:37:28 -07:00
e08f6e629f add fedora setup guide 2025-10-20 10:34:55 -07:00
398be12ac4 fix release tag 2025-10-20 09:26:03 -07:00
7407150bff attempt new release mechanism using draft releases and manual job 2025-10-20 09:24:27 -07:00
c23b11469d attempt at codeql config for the right target 2025-10-20 09:08:29 -07:00
29529ddacd remove broken codeql configs, will figure this out later 2025-10-20 01:51:20 -07:00
d2f47dcad6 attempt to configure rust extractor for the right target 2025-10-20 01:44:27 -07:00
f3b7007432 codeql advanced support 2025-10-20 01:29:03 -07:00
4f30d51bb2 make sure rust toolchain is from rust-toolchain.toml in github workflows 2025-10-20 01:18:28 -07:00
48e3644977 fix pull request actions 2025-10-20 01:14:08 -07:00
345e1c800c Merge pull request #11 from edera-dev/dependabot/cargo/cargo-updates-d94e05c250
Bump toml from 0.9.7 to 0.9.8 in the cargo-updates group
2025-10-20 00:33:30 -07:00
e5ae612398 Merge pull request #10 from edera-dev/dependabot/docker/docker-updates-d0b0844295
Bump rustlang/rust from `b8107fa` to `141e9a7` in the docker-updates group
2025-10-20 00:31:54 -07:00
dependabot[bot]
f0427faab2 Bump toml from 0.9.7 to 0.9.8 in the cargo-updates group
Bumps the cargo-updates group with 1 update: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.9.7 to 0.9.8
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.7...toml-v0.9.8)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 07:29:29 +00:00
dependabot[bot]
6278274288 Bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


Updates `rustlang/rust` from `b8107fa` to `141e9a7`

---
updated-dependencies:
- dependency-name: rustlang/rust
  dependency-version: nightly-alpine
  dependency-type: direct:production
  dependency-group: docker-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 07:29:15 +00:00
4cd27a5010 add docker to dependabot 2025-10-20 00:17:49 -07:00
ca23b93071 sprout: version 0.0.4 2025-10-20 00:09:53 -07:00
3453826e9d document ( by hand :( ) all of the code of sprout 2025-10-20 00:06:46 -07:00
106064d3e7 document by hand much more of the sprout code 2025-10-19 23:03:28 -07:00
8997e417b3 update cargo.toml homepage to https://sprout.edera.dev 2025-10-19 22:16:43 -07:00
111c40534c fix typo in CONTRIBUTING.md 2025-10-19 21:54:44 -07:00
5b1daf256b add basic development guide 2025-10-19 21:53:52 -07:00
08da6dd390 add more documentation to some actions and configurations 2025-10-19 21:44:05 -07:00
38 changed files with 1145 additions and 69 deletions

5
.github/codeql/codeql-config.yaml vendored Normal file
View File

@@ -0,0 +1,5 @@
name: "codeql-config"
extractor-options:
rust:
cargo_target: x86_64-unknown-uefi

View File

@@ -26,3 +26,16 @@ updates:
cargo-dev-updates: cargo-dev-updates:
dependency-type: development dependency-type: development
applies-to: version-updates applies-to: version-updates
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
docker-updates:
dependency-type: production
applies-to: version-updates
docker-dev-updates:
dependency-type: development
applies-to: version-updates

View File

@@ -26,10 +26,9 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: 'install nightly rust toolchain with rustfmt' - name: 'install rust toolchain with rustfmt'
run: | run: |
rustup update --no-self-update nightly cargo version
rustup default nightly
rustup component add rustfmt rustup component add rustfmt
- name: 'cargo fmt' - name: 'cargo fmt'
@@ -57,10 +56,9 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: 'install nightly rust toolchain' - name: 'install rust toolchain'
run: | run: |
rustup update --no-self-update nightly cargo version
rustup default nightly
- name: cargo build - name: cargo build
run: cargo build --target "${TARGET_ARCH}-unknown-uefi" run: cargo build --target "${TARGET_ARCH}-unknown-uefi"
@@ -87,10 +85,9 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: 'install nightly rust toolchain with clippy' - name: 'install rust toolchain with clippy'
run: | run: |
rustup update --no-self-update nightly cargo version
rustup default stable
rustup component add clippy rustup component add clippy
- name: cargo clippy - name: cargo clippy

53
.github/workflows/codeql.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: codeql
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '33 16 * * 0'
permissions:
contents: read # Needed to checkout the repository.
jobs:
analyze:
name: analyze (${{ matrix.language }})
runs-on: 'ubuntu-latest'
permissions:
security-events: write # Needed to upload results.
packages: read # Needed to fetch internal or private CodeQL packs.
actions: read # Needed to read workflows.
contents: read # Needed to checkout the repository.
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: rust
build-mode: none
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: initialize codeql
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,9 +1,12 @@
name: publish name: publish
on: on:
release: workflow_dispatch:
types: inputs:
- created release-tag:
description: 'Release Tag'
required: true
type: string
push: push:
branches: branches:
@@ -39,10 +42,9 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: 'install nightly rust toolchain' - name: 'install rust toolchain'
run: | run: |
rustup update --no-self-update nightly cargo version
rustup default nightly
- name: 'assemble artifacts' - name: 'assemble artifacts'
run: ./hack/assemble.sh run: ./hack/assemble.sh
@@ -65,10 +67,18 @@ jobs:
with: with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}" app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}" private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts' - name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh run: ./hack/ci/upload-release-assets.sh
env: env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}" GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.release.tag_name }}" RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event_name == 'release' }} if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}

View File

@@ -1,8 +1,8 @@
# Contributing to Sprout # Contributing to Sprout
Welcome! We're very glad you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies. Welcome! We're happy you're reading this; Edera is excited for all kinds of contributions! Please read the following to ensure you're aware of our flow and policies.
## Before contributing ## Before Contributing
1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md), which applies to all interactions in/with all Edera projects and venues. 1. Please read our [Code of Conduct](CODE_OF_CONDUCT.md), which applies to all interactions in/with all Edera projects and venues.
2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions. 2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions.
@@ -11,7 +11,7 @@ Welcome! We're very glad you're reading this; Edera is excited for all kinds of
## Contributing Code ## Contributing Code
To get started with technical contributions, please read out [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issue]. To get started with technical contributions, please read our [Development Guide]. If you're looking for something easy to tackle, [look for issues labeled `good first issue`][good-first-issues].
## Reporting bugs and other issues ## Reporting bugs and other issues
@@ -20,13 +20,13 @@ We encourage opening an issue on GitHub to report bugs.
## Pull Requests ## Pull Requests
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes. 1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
- Else skip to step 3.
2. Discuss the change with the teams to ensure we have consensus on the change being welcome. 2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft. 3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft.
4. Please include a detailed list of changes that the PR makes. 4. Please include a detailed list of changes that the PR makes.
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`. 5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
6. After the review cycle concludes and we know you are ready for merging, a team member will submit the PR to the merge queue. 6. After the review cycle concludes, and we know you are ready for merging, a team member will submit the PR to the merge queue.
[Code of Conduct]: ./CODE_OF_CONDUCT.md [Code of Conduct]: ./CODE_OF_CONDUCT.md
[Development Guide]: ./DEVELOPMENT.md
[Security Policy]: ./SECURITY.md [Security Policy]: ./SECURITY.md
[good-first-issues]: https://github.com/edera-dev/sprout/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 [good-first-issues]: https://github.com/edera-dev/sprout/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22

22
Cargo.lock generated
View File

@@ -61,7 +61,7 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.3" version = "0.0.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
@@ -252,9 +252,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@@ -277,9 +277,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.7" version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
@@ -292,27 +292,27 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]] [[package]]
name = "ucs2" name = "ucs2"

View File

@@ -2,14 +2,14 @@
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.3" version = "0.0.7"
homepage = "https://github.com/edera-dev/sprout" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.100" anyhow = "1.0.100"
toml = "0.9.7" toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
[dependencies.image] [dependencies.image]

51
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,51 @@
# Sprout Development Guide
This guide is a work in progress.
## Development Setup
You can use any Rust development environment to develop Sprout.
Rustup is recommended as the Rust toolchain manager to manage Rust versions and targets.
Sprout currently requires Rust nightly to support uefi_std. See [uefi_std](https://doc.rust-lang.org/beta/rustc/platform-support/unknown-uefi.html) for more details.
We currently only support `x86_64-unknown-uefi` and `aarch64-unknown-uefi` targets.
To test your changes in QEMU, please run `./hack/dev/boot.sh`, you can specify `x86_64` or `aarch64`
as an argument to boot.sh to boot the specified architecture.
## Hack Scripts
You can use the `./hack` scripts to run common development tasks:
### ./hack/build.sh
Builds the Sprout binary for the target that would support your current machine.
### ./hack/assemble.sh
Builds both x86_64 and aarch64 binaries into `target/assemble`.
### ./hack/clean.sh
Cleans the target directory and any docker images that were built.
### ./hack/format.sh
Formats the code using `rustfmt` and shell scripts with `shfmt`.
### ./hack/autofix.sh
Applies Clippy and `rustfmt` fixes to the code, and formats shell scripts with `shfmt`.
## Dev Scripts
### ./hack/dev/build.sh
Build Sprout as OCI images using Docker, including a kernel, initramfs, xen, and other supporting files.
### ./hack/dev/boot.sh
Boot Sprout's dev environment using QEMU for testing. This will let you test your changes in a real environment booting
Alpine Linux with an initramfs.

View File

@@ -2,7 +2,7 @@
ARG RUST_PROFILE=release ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:b8107fa66d3e5ad7f729d3347c7feedbd3f4b60b01006edce39eb6b994ff00bd AS build FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:141e9a7f13f77237dd4d462364c3a1b21cb8a6791d8924c409573e77b788af5e AS build
RUN apk --no-cache add musl-dev busybox-static RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build RUN adduser -S -s /bin/sh build

View File

@@ -18,6 +18,17 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions. Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
## Documentation
- [Fedora Setup Guide]
- [Generic Linux Setup Guide]
- [Windows Setup Guide]
- [Development Guide]
- [Contributing Guide]
- [Sprout License]
- [Code of Conduct]
- [Security Policy]
## 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
@@ -62,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
@@ -116,3 +138,12 @@ chainload.path = "$boot\\$chainload"
chainload.options = ["$options"] chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd" chainload.linux-initrd = "$boot\\$initrd"
``` ```
[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
[Contributing Guide]: ./CONTRIBUTING.md
[Sprout License]: ./LICENSE
[Code of Conduct]: ./CODE_OF_CONDUCT.md
[Security Policy]: ./SECURITY.md

134
docs/fedora-setup.md Normal file
View File

@@ -0,0 +1,134 @@
# Setup Sprout on Fedora
## Prerequisites
- Modern Fedora release: tested on Fedora Workstation 42 and 43
- EFI System Partition mounted on `/boot/efi` (the default)
- ext4 or FAT32/exFAT formatted `/boot` partition
## Step 1: Base Installation
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 `/boot/efi/EFI/BOOT/sprout.efi` on your EFI System Partition.
Additionally, you will want to install the `edk2-ext4` package, which provides the ext4 filesystem support for Sprout.
```bash
# Install the edk2-ext4 package which provides ext4 support for Sprout.
$ sudo dnf install edk2-ext4
# Create a directory for sprout drivers.
$ sudo mkdir -p /boot/efi/sprout/drivers
# For x86_64 systems, copy the ext4x64.efi driver to the sprout drivers directory.
$ sudo cp /usr/share/edk2/drivers/ext4x64.efi /boot/efi/sprout/drivers/ext4.efi
# For ARM64 systems, copy the ext4aa64.efi driver to the sprout drivers directory.
$ sudo cp /usr/share/edk2/drivers/ext4aa64.efi /boot/efi/sprout/drivers/ext4.efi
```
## Step 2: Configure Sprout
Since Fedora uses the BLS specification, you can use the `bls` generator to autoconfigure Sprout for Fedora.
Write the following file to `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an EFI driver for ext4.
[drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem
# that contains \loader\entries as a directory
# into the value called "boot"
[extractors.boot.filesystem-device-match]
has-item = "\\loader\\entries"
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
```
## Step 3, Option 1: Configure GRUB to load Sprout (recommended)
You can configure GRUB to add a boot entry for Sprout, so you can continue to use GRUB without interruption.
GRUB needs to be configured with the chainloader module to load Sprout.
### x86_64
```bash
# Install x86_64 GRUB modules.
$ sudo dnf install grub2-efi-x64-modules
# Copy x86_64 GRUB modules to /boot/grub2 for use by GRUB if it isn't installed already.
$ [ ! -d /boot/grub2/x86_64-efi ] && sudo cp -r /usr/lib/grub/x86_64-efi /boot/grub2/x86_64-efi
```
### ARM64
```bash
# Install ARM64 GRUB modules.
$ sudo dnf install grub2-efi-aa64-modules
# Copy ARM64 GRUB modules to /boot/grub2 for use by GRUB if it isn't installed already.
$ [ ! -d /boot/grub2/arm64-efi ] && sudo cp -r /usr/lib/grub/arm64-efi /boot/grub2/x86_64-efi
```
You will need to find the UUID of your EFI System Partition. You can do this by running the following command:
```bash
$ grep "/boot/efi" /etc/fstab | awk '{print $1}' | awk -F '=' '{print $2}'
SAMPLE-VALUE
```
The GRUB configuration for Sprout is as follows, replace `SAMPLE-VALUE` with the UUID of your EFI System Partition:
```grub
menuentry 'Sprout' $menuentry_id_option 'sprout' {
insmod part_gpt
insmod fat
insmod chain
search --no-floppy --fs-uuid --set=root SAMPLE-VALUE
chainloader /EFI/BOOT/sprout.efi
}
```
You can append this to `/etc/grub.d/40_custom` and run the following command to update your configuration:
```bash
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg
```
To update your GRUB configuration.
Make sure to update your GRUB environment to show the menu:
```bash
$ sudo grub2-editenv - set menu_auto_hide=0
```
You may now reboot your system and select Sprout from the GRUB menu.
## Step 3, Option 2: Configure your EFI firmware for Sprout
You can configure your EFI boot menu to show Sprout as an option.
To do so, 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.

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

@@ -3,32 +3,56 @@ use anyhow::{Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// EFI chainloader action.
pub mod chainload; pub mod chainload;
/// Edera hypervisor action.
pub mod edera; pub mod edera;
/// EFI console print action.
pub mod print; pub mod print;
/// Splash screen action.
#[cfg(feature = "splash")] #[cfg(feature = "splash")]
pub mod splash; pub mod splash;
/// Declares an action that sprout can execute.
/// Actions allow configuring sprout's internal runtime mechanisms with values
/// that you can specify via other concepts.
///
/// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct ActionDeclaration { pub struct ActionDeclaration {
/// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system
/// or to perform more EFI actions and return to sprout.
#[serde(default)] #[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>, pub chainload: Option<chainload::ChainloadConfiguration>,
/// Print a string to the EFI console.
#[serde(default)] #[serde(default)]
pub print: Option<print::PrintConfiguration>, pub print: Option<print::PrintConfiguration>,
/// Show an image as a fullscreen splash screen.
#[serde(default)] #[serde(default)]
#[cfg(feature = "splash")] #[cfg(feature = "splash")]
pub splash: Option<splash::SplashConfiguration>, pub splash: Option<splash::SplashConfiguration>,
/// Boot the Edera hypervisor and the root operating system.
/// This action is an extension on top of the Xen EFI stub that
/// is specific to Edera.
#[serde(default, rename = "edera")] #[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>, pub edera: Option<edera::EderaConfiguration>,
} }
/// Execute the action specified by `name` which should be stored in the
/// root context of the provided `context`. This function may not return
/// if the provided action executes an operating system or an EFI application
/// that does not return control to sprout.
pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()> { pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()> {
// Retrieve the action from the root context.
let Some(action) = context.root().actions().get(name.as_ref()) else { let Some(action) = context.root().actions().get(name.as_ref()) else {
bail!("unknown action '{}'", name.as_ref()); bail!("unknown action '{}'", name.as_ref());
}; };
// Finalize the context and freeze it.
let context = context.finalize().freeze(); let context = context.finalize().freeze();
// Execute the action.
if let Some(chainload) = &action.chainload { if let Some(chainload) = &action.chainload {
chainload::chainload(context.clone(), chainload)?; chainload::chainload(context.clone(), chainload)?;
return Ok(()); return Ok(());
@@ -45,5 +69,7 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
return Ok(()); return Ok(());
} }
// If we reach here, we don't know how to execute the action that was configured.
// This is likely unreachable, but we should still return an error just in case.
bail!("unknown action configuration"); bail!("unknown action configuration");
} }

View File

@@ -9,24 +9,37 @@ use std::rc::Rc;
use uefi::CString16; use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
/// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct ChainloadConfiguration { pub struct ChainloadConfiguration {
/// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
pub path: String, pub path: String,
/// The options to pass to the image.
/// The options are concatenated by a space and then passed to the EFI application.
#[serde(default)] #[serde(default)]
pub options: Vec<String>, pub options: Vec<String>,
/// An optional path to a Linux initrd.
/// This uses the [LINUX_EFI_INITRD_MEDIA_GUID] mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")] #[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>, pub linux_initrd: Option<String>,
} }
/// Executes the chainload action using the specified `configuration` inside the provided `context`.
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> { pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
// Retrieve the current image handle of sprout.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
// Resolve the path to the image to chainload.
let resolved = utils::resolve_path( let resolved = utils::resolve_path(
context.root().loaded_image_path()?, context.root().loaded_image_path()?,
&context.stamp(&configuration.path), &context.stamp(&configuration.path),
) )
.context("unable to resolve chainload path")?; .context("unable to resolve chainload path")?;
// Load the image to chainload.
let image = uefi::boot::load_image( let image = uefi::boot::load_image(
sprout_image, sprout_image,
uefi::boot::LoadImageSource::FromDevicePath { uefi::boot::LoadImageSource::FromDevicePath {
@@ -36,9 +49,11 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
) )
.context("unable to load image")?; .context("unable to load image")?;
// Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?; .context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image.
let options = configuration let options = configuration
.options .options
.iter() .iter()
@@ -46,6 +61,10 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
// Pass the options to the image, if any are provided.
// The holder must drop at the end of this function to ensure the options are not leaked,
// and the holder here ensures it outlives the if block here, as a pointer has to be
// passed to the image. This has been hand-validated to be safe.
let mut options_holder: Option<Box<CString16>> = None; let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() { if !options.is_empty() {
let options = Box::new( let options = Box::new(
@@ -80,15 +99,28 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
initrd_handle = Some(handle); initrd_handle = Some(handle);
} }
// Retrieve the base and size of the loaded image to display.
let (base, size) = loaded_image_protocol.info(); let (base, size) = loaded_image_protocol.info();
info!("loaded image: base={:#x} size={:#x}", base.addr(), size); info!("loaded image: base={:#x} size={:#x}", base.addr(), size);
// Start the loaded image.
// This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image).context("unable to start image"); let result = uefi::boot::start_image(image).context("unable to start image");
// Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle
&& let Err(error) = initrd_handle.unregister() && let Err(error) = initrd_handle.unregister()
{ {
error!("unable to unregister linux initrd: {}", error); error!("unable to unregister linux initrd: {}", error);
} }
// Assert there was no error starting the image.
result.context("unable to start image")?; result.context("unable to start image")?;
// Explicitly drop the option holder to clarify the lifetime.
drop(options_holder); drop(options_holder);
// Return control to sprout.
Ok(()) Ok(())
} }

View File

@@ -19,53 +19,88 @@ use crate::{
}, },
}; };
/// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack.
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct EderaConfiguration { pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image.
pub xen: String, pub xen: String,
/// The path to the kernel to boot for dom0.
pub kernel: String, pub kernel: String,
/// The path to the initrd to load for dom0.
#[serde(default)] #[serde(default)]
pub initrd: Option<String>, pub initrd: Option<String>,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")] #[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>, pub kernel_options: Vec<String>,
/// The options to pass to the Xen hypervisor.
#[serde(default, rename = "xen-options")] #[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>, pub xen_options: Vec<String>,
} }
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
fn build_xen_config(configuration: &EderaConfiguration) -> String { fn build_xen_config(configuration: &EderaConfiguration) -> String {
// xen config file format is ini-like
[ [
// global section
"[global]".to_string(), "[global]".to_string(),
// default configuration section
"default=sprout".to_string(), "default=sprout".to_string(),
// configuration section for sprout
"[sprout]".to_string(), "[sprout]".to_string(),
// xen options
format!("options={}", configuration.xen_options.join(" ")), format!("options={}", configuration.xen_options.join(" ")),
// kernel options, stub replaces the kernel path
// the kernel is provided via media loader
format!("kernel=stub {}", configuration.kernel_options.join(" ")), format!("kernel=stub {}", configuration.kernel_options.join(" ")),
"".to_string(), // required or else the last line will be ignored // required or else the last line will be ignored
"".to_string(),
] ]
.join("\n") .join("\n")
} }
/// Register a media loader for some `text` with the vendor `guid`.
/// `what` should indicate some identifying value for error messages
/// like `config` or `kernel`.
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> { fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> {
MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice()) MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice())
.context(format!("unable to register {} media loader", what)) /* */ .context(format!("unable to register {} media loader", what)) /* */
} }
/// Register a media loader for the file `path` with the vendor `guid`.
/// `what` should indicate some identifying value for error messages
/// like `config` or `kernel`.
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
fn register_media_loader_file( fn register_media_loader_file(
context: &Rc<SproutContext>, context: &Rc<SproutContext>,
guid: Guid, guid: Guid,
what: &str, what: &str,
path: &str, path: &str,
) -> Result<MediaLoaderHandle> { ) -> Result<MediaLoaderHandle> {
// Stamp the path to the file.
let path = context.stamp(path); let path = context.stamp(path);
// Read the file contents.
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path) let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
.context(format!("unable to read {} file", what))?; .context(format!("unable to read {} file", what))?;
// Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
.context(format!("unable to register {} media loader", what))?; .context(format!("unable to register {} media loader", what))?;
Ok(handle) Ok(handle)
} }
/// Executes the edera action which will boot the Edera hypervisor with the specified
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> { pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Build the Xen config file content for this configuration.
let config = build_xen_config(configuration); let config = build_xen_config(configuration);
// Register the media loader for the config.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
.context("unable to register config media loader")?; .context("unable to register config media loader")?;
// Register the media loaders for the kernel.
let kernel = register_media_loader_file( let kernel = register_media_loader_file(
&context, &context,
XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID,
@@ -74,8 +109,10 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to register kernel media loader")?; .context("unable to register kernel media loader")?;
// Create a vector of media loaders to unregister on error.
let mut media_loaders = vec![config, kernel]; let mut media_loaders = vec![config, kernel];
// Register the initrd if it is provided.
if let Some(ref initrd) = configuration.initrd { if let Some(ref initrd) = configuration.initrd {
let initrd = let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
@@ -83,6 +120,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
media_loaders.push(initrd); media_loaders.push(initrd);
} }
// Chainload to the Xen EFI stub.
let result = actions::chainload::chainload( let result = actions::chainload::chainload(
context.clone(), context.clone(),
&ChainloadConfiguration { &ChainloadConfiguration {
@@ -93,6 +131,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders on error.
for media_loader in media_loaders { for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() { if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error); error!("unable to unregister media loader: {}", error);

View File

@@ -3,12 +3,15 @@ use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// The configuration of the print action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct PrintConfiguration { pub struct PrintConfiguration {
/// The text to print to the console.
#[serde(default)] #[serde(default)]
pub text: String, pub text: String,
} }
/// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> { pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
println!("{}", context.stamp(&configuration.text)); println!("{}", context.stamp(&configuration.text));
Ok(()) Ok(())

View File

@@ -12,25 +12,39 @@ use std::time::Duration;
use uefi::boot::ScopedProtocol; use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput; use uefi::proto::console::gop::GraphicsOutput;
const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct SplashConfiguration { pub struct SplashConfiguration {
/// The path to the image to display.
/// Currently, only PNG images are supported.
pub image: String, pub image: String,
/// The time to display the splash image without interruption, in seconds.
/// The default value is `0` which will display the image and let everything
/// continue.
#[serde(default = "default_splash_time")] #[serde(default = "default_splash_time")]
pub time: u32, pub time: u32,
} }
pub fn default_splash_time() -> u32 { fn default_splash_time() -> u32 {
0 DEFAULT_SPLASH_TIME
} }
/// Acquire the [GraphicsOutput]. We will find the first graphics output only.
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> { fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
// Grab the handle for the graphics output protocol.
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>() let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
.context("unable to get graphics output")?; .context("unable to get graphics output")?;
// Open the graphics output protocol exclusively.
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle) uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
.context("unable to open graphics output") .context("unable to open graphics output")
} }
/// Produces a [Rect] that fits the `image` inside the specified `frame`.
/// The output [Rect] should be used to resize the image.
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect { fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
// Convert the image dimensions to a [Rect].
let input = Rect { let input = Rect {
x: 0, x: 0,
y: 0, y: 0,
@@ -38,9 +52,13 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
height: image.height(), height: image.height(),
}; };
// Calculate the ratio of the image dimensions.
let input_ratio = input.width as f32 / input.height as f32; let input_ratio = input.width as f32 / input.height as f32;
// Calculate the ratio of the frame dimensions.
let frame_ratio = frame.width as f32 / frame.height as f32; let frame_ratio = frame.width as f32 / frame.height as f32;
// Create [Rect] to store the output dimensions.
let mut output = Rect { let mut output = Rect {
x: 0, x: 0,
y: 0, y: 0,
@@ -63,24 +81,38 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
output output
} }
/// Resize the input `image` to fit the `frame`.
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> { fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let image = image.to_rgba8(); let image = image.to_rgba8();
resize(&image, frame.width, frame.height, FilterType::Lanczos3) resize(&image, frame.width, frame.height, FilterType::Lanczos3)
} }
/// Draw the `image` on the screen using [GraphicsOutput].
fn draw(image: DynamicImage) -> Result<()> { fn draw(image: DynamicImage) -> Result<()> {
// Acquire the [GraphicsOutput] protocol.
let mut gop = setup_graphics()?; let mut gop = setup_graphics()?;
// Acquire the current screen size.
let (width, height) = gop.current_mode_info().resolution(); let (width, height) = gop.current_mode_info().resolution();
// Create a display frame.
let display_frame = Rect { let display_frame = Rect {
x: 0, x: 0,
y: 0, y: 0,
width: width as _, width: width as _,
height: height as _, height: height as _,
}; };
// Fit the image to the display frame.
let fit = fit_to_frame(&image, display_frame); let fit = fit_to_frame(&image, display_frame);
// Resize the image to fit the display frame.
let image = resize_to_fit(&image, fit); let image = resize_to_fit(&image, fit);
// Create a framebuffer to draw the image on.
let mut framebuffer = Framebuffer::new(width, height); let mut framebuffer = Framebuffer::new(width, height);
// Iterate over the pixels in the image and put them on the framebuffer.
for (x, y, pixel) in image.enumerate_pixels() { for (x, y, pixel) in image.enumerate_pixels() {
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else { let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
continue; continue;
@@ -90,17 +122,27 @@ fn draw(image: DynamicImage) -> Result<()> {
fb.blue = pixel[2]; fb.blue = pixel[2];
} }
// Blit the framebuffer to the screen.
framebuffer.blit(&mut gop)?; framebuffer.blit(&mut gop)?;
Ok(()) Ok(())
} }
/// Runs the splash action with the specified `configuration` inside the provided `context`.
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> { pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
// Stamp the image path value.
let image = context.stamp(&configuration.image); let image = context.stamp(&configuration.image);
// Read the image contents.
let image = read_file_contents(context.root().loaded_image_path()?, &image)?; let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
// Decode the image as a PNG.
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png) let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode() .decode()
.context("unable to decode splash image")?; .context("unable to decode splash image")?;
// Draw the image on the screen.
draw(image)?; draw(image)?;
// Sleep for the specified time.
std::thread::sleep(Duration::from_secs(configuration.time as u64)); std::thread::sleep(Duration::from_secs(configuration.time as u64));
// Return control to sprout.
Ok(()) Ok(())
} }

View File

@@ -35,12 +35,26 @@ pub struct RootConfiguration {
/// inside the sprout context. /// inside the sprout context.
#[serde(default)] #[serde(default)]
pub extractors: BTreeMap<String, ExtractorDeclaration>, pub extractors: BTreeMap<String, ExtractorDeclaration>,
/// Declares the actions that can execute operations for sprout.
/// Actions are executable modules in sprout that take in specific structured values.
/// Actions are responsible for ensuring that passed strings are stamped to replace values
/// at runtime.
/// Each action has a name that can be referenced by other base concepts like entries.
#[serde(default)] #[serde(default)]
pub actions: BTreeMap<String, ActionDeclaration>, pub actions: BTreeMap<String, ActionDeclaration>,
/// Declares the entries that are displayed on the boot menu. These entries are static
/// but can still use values from the sprout context.
#[serde(default)] #[serde(default)]
pub entries: BTreeMap<String, EntryDeclaration>, pub entries: BTreeMap<String, EntryDeclaration>,
/// Declares the generators that are used to generate entries at runtime.
/// Each generator has its own logic for generating entries, but generally they intake
/// a template entry and stamp that template entry over some values determined at runtime.
/// Each generator has an associated name used to differentiate it across sprout.
#[serde(default)] #[serde(default)]
pub generators: BTreeMap<String, GeneratorDeclaration>, pub generators: BTreeMap<String, GeneratorDeclaration>,
/// Configures the various phases of sprout. This allows you to hook into specific parts
/// of the boot process to execute actions, for example, you can show a boot splash during
/// the early phase.
#[serde(default)] #[serde(default)]
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }

View File

@@ -1,40 +1,58 @@
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;
fn load_raw_config() -> Result<Vec<u8>> { /// Loads the raw configuration from the sprout config file as data.
fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
// 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.
let path = current_image_device_path_protocol.deref().to_boxed(); let path = current_image_device_path_protocol.deref().to_boxed();
let content = utils::read_file_contents(&path, "sprout.toml") info!("configuration file: {}", options.config);
.context("unable to read 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)
} }
pub fn load() -> Result<RootConfiguration> { /// Loads the [RootConfiguration] for Sprout.
let content = load_raw_config()?; pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?; // Load the raw configuration from the sprout config file.
let content = load_raw_config(options)?;
// 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 config file")?;
// Check the version of the configuration without parsing the full configuration.
let version = value let version = value
.get("version") .get("version")
.cloned() .cloned()
.unwrap_or_else(|| Value::Integer(latest_version() as i64)); .unwrap_or_else(|| Value::Integer(latest_version() as i64));
// Parse the version into an u32.
let version: u32 = version let version: u32 = version
.try_into() .try_into()
.context("unable to get configuration version")?; .context("unable to get configuration version")?;
// Check if the version is supported.
if version != latest_version() { if version != latest_version() {
bail!("unsupported configuration version: {}", version); bail!("unsupported configuration version: {}", version);
} }
// If the version is supported, parse the full configuration.
let config: RootConfiguration = value let config: RootConfiguration = value
.try_into() .try_into()
.context("unable to parse sprout.toml file")?; .context("unable to parse sprout.toml file")?;
// Return the parsed configuration.
Ok(config) Ok(config)
} }

View File

@@ -1,39 +1,63 @@
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};
use std::rc::Rc; use std::rc::Rc;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
/// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout.
#[derive(Default)] #[derive(Default)]
pub struct RootContext { pub struct RootContext {
/// The actions that are available in Sprout.
actions: BTreeMap<String, ActionDeclaration>, actions: BTreeMap<String, ActionDeclaration>,
/// 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 {
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self { /// Creates a new root context with the `loaded_image_device_path` which will be stored
RootContext { /// in the context for easy access.
pub fn new(loaded_image_device_path: Box<DevicePath>, options: SproutOptions) -> 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,
} }
} }
/// Access the actions configured inside Sprout.
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> { pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
&self.actions &self.actions
} }
/// Access the actions configured inside Sprout mutably for modification.
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> { pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
&mut self.actions &mut self.actions
} }
/// Access the device path of the loaded Sprout image.
pub fn loaded_image_path(&self) -> Result<&DevicePath> { pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path self.loaded_image_path
.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 [RootContext] which is data that is shared globally, and [SproutContext] which works
/// sort of like a tree of values. You can cheaply clone a [SproutContext] and modify it with
/// new values, which override the values of contexts above it.
///
/// This is a core part of the value mechanism in Sprout which makes templating possible.
pub struct SproutContext { pub struct SproutContext {
root: Rc<RootContext>, root: Rc<RootContext>,
parent: Option<Rc<SproutContext>>, parent: Option<Rc<SproutContext>>,
@@ -41,6 +65,7 @@ pub struct SproutContext {
} }
impl SproutContext { impl SproutContext {
/// Create a new [SproutContext] using `root` as the root context.
pub fn new(root: RootContext) -> Self { pub fn new(root: RootContext) -> Self {
Self { Self {
root: Rc::new(root), root: Rc::new(root),
@@ -49,10 +74,13 @@ impl SproutContext {
} }
} }
/// Access the root context of this context.
pub fn root(&self) -> &RootContext { pub fn root(&self) -> &RootContext {
self.root.as_ref() self.root.as_ref()
} }
/// Retrieve the value specified by `key` from this context or its parents.
/// Returns `None` if the value is not found.
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> { pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
self.values.get(key.as_ref()).or_else(|| { self.values.get(key.as_ref()).or_else(|| {
self.parent self.parent
@@ -61,6 +89,8 @@ impl SproutContext {
}) })
} }
/// Collects all keys that are present in this context or its parents.
/// This is useful for iterating over all keys in a context.
pub fn all_keys(&self) -> Vec<String> { pub fn all_keys(&self) -> Vec<String> {
let mut keys = BTreeSet::new(); let mut keys = BTreeSet::new();
@@ -74,6 +104,8 @@ impl SproutContext {
keys.into_iter().collect() keys.into_iter().collect()
} }
/// Collects all values that are present in this context or its parents.
/// This is useful for iterating over all values in a context.
pub fn all_values(&self) -> BTreeMap<String, String> { pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for key in self.all_keys() { for key in self.all_keys() {
@@ -82,17 +114,24 @@ impl SproutContext {
values values
} }
/// Sets the value `key` to the value specified by `value` in this context.
/// If the parent context has this key, this will override that key.
pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) { pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) {
self.values self.values
.insert(key.as_ref().to_string(), value.to_string()); .insert(key.as_ref().to_string(), value.to_string());
} }
/// Inserts all the specified `values` into this context.
/// These values will take precedence over its parent context.
pub fn insert(&mut self, values: &BTreeMap<String, String>) { pub fn insert(&mut self, values: &BTreeMap<String, String>) {
for (key, value) in values { for (key, value) in values {
self.values.insert(key.clone(), value.clone()); self.values.insert(key.clone(), value.clone());
} }
} }
/// Forks this context as an owned [SproutContext]. This makes it possible
/// to cheaply modify a context without cloning the parent context map.
/// The parent of the returned context is [self].
pub fn fork(self: &Rc<SproutContext>) -> Self { pub fn fork(self: &Rc<SproutContext>) -> Self {
Self { Self {
root: self.root.clone(), root: self.root.clone(),
@@ -101,11 +140,19 @@ impl SproutContext {
} }
} }
/// Freezes this context into a [Rc] which makes it possible to cheaply clone
/// and makes it less easy to modify a context. This can be used to pass the context
/// to various other parts of Sprout and ensure it won't be modified. Instead, once
/// a context is frozen, it should be [self.fork]'d to be modified.
pub fn freeze(self) -> Rc<SproutContext> { pub fn freeze(self) -> Rc<SproutContext> {
Rc::new(self) Rc::new(self)
} }
/// Finalizes a context by producing a context with no parent that contains all the values
/// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no
/// inheritance with other [SproutContext]s. It will still contain a [RootContext] however.
pub fn finalize(&self) -> SproutContext { pub fn finalize(&self) -> SproutContext {
// Collect all the values from the context and its parents.
let mut current_values = self.all_values(); let mut current_values = self.all_values();
loop { loop {
@@ -114,16 +161,21 @@ impl SproutContext {
for (key, value) in &current_values { for (key, value) in &current_values {
let (changed, result) = Self::stamp_values(&current_values, value); let (changed, result) = Self::stamp_values(&current_values, value);
if changed { if changed {
// If the value changed, we need to re-stamp it.
did_change = true; did_change = true;
} }
// Insert the new value into the value map.
values.insert(key.clone(), result); values.insert(key.clone(), result);
} }
current_values = values; current_values = values;
// If the values did not change, we can stop.
if !did_change { if !did_change {
break; break;
} }
} }
// Produce the final context.
Self { Self {
root: self.root.clone(), root: self.root.clone(),
parent: None, parent: None,
@@ -131,6 +183,8 @@ impl SproutContext {
} }
} }
/// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) { fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string(); let mut result = text.as_ref().to_string();
let mut did_change = false; let mut did_change = false;
@@ -144,6 +198,9 @@ impl SproutContext {
(did_change, result) (did_change, result)
} }
/// Stamps the input `text` with all the values in this [SproutContext] and it's parents.
/// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce
/// "hello\\b" as an output string.
pub fn stamp(&self, text: impl AsRef<str>) -> String { pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1 Self::stamp_values(&self.all_values(), text.as_ref()).1
} }

View File

@@ -8,25 +8,37 @@ use std::rc::Rc;
use uefi::boot::SearchType; use uefi::boot::SearchType;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
/// Declares a driver configuration.
/// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct DriverDeclaration { pub struct DriverDeclaration {
/// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed.
pub path: String, pub path: String,
} }
/// Loads the driver specified by the [driver] declaration.
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> { fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
let image_device_path_protocol = let image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image) uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image)
.context("unable to open loaded image device path protocol")?; .context("unable to open loaded image device path protocol")?;
// Get the device path root of the sprout image.
let mut full_path = utils::device_path_root(&image_device_path_protocol)?; let mut full_path = utils::device_path_root(&image_device_path_protocol)?;
// Push the path of the driver from the root.
full_path.push_str(&context.stamp(&driver.path)); full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path); info!("driver path: {}", full_path);
// Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?; let device_path = utils::text_to_device_path(&full_path)?;
// Load the driver image.
let image = uefi::boot::load_image( let image = uefi::boot::load_image(
sprout_image, sprout_image,
uefi::boot::LoadImageSource::FromDevicePath { uefi::boot::LoadImageSource::FromDevicePath {
@@ -36,37 +48,55 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
) )
.context("unable to load image")?; .context("unable to load image")?;
// Start the driver image, this is expected to return control to sprout.
// There is no guarantee that the driver will actually return control as it is
// just a standard EFI image.
uefi::boot::start_image(image).context("unable to start driver image")?; uefi::boot::start_image(image).context("unable to start driver image")?;
Ok(()) Ok(())
} }
/// Reconnects all handles to their controllers.
/// This is effectively a UEFI stack reload in a sense.
/// After we load all the drivers, we need to reconnect all of their handles
/// so that filesystems are recognized again.
fn reconnect() -> Result<()> { fn reconnect() -> Result<()> {
// Locate all of the handles in the UEFI stack.
let handles = uefi::boot::locate_handle_buffer(SearchType::AllHandles) let handles = uefi::boot::locate_handle_buffer(SearchType::AllHandles)
.context("unable to locate handles buffer")?; .context("unable to locate handles buffer")?;
for handle in handles.iter() { for handle in handles.iter() {
// ignore result as there is nothing we can do if it doesn't work. // Ignore the result as there is nothing we can do if reconnecting a controller fails.
// This is also likely to fail in some cases but should fail safely.
let _ = uefi::boot::connect_controller(*handle, None, None, true); let _ = uefi::boot::connect_controller(*handle, None, None, true);
} }
Ok(()) Ok(())
} }
/// Load all the drivers specified in `drivers`.
/// There is no driver order currently. This will reconnect all the controllers
/// to all handles if at least one driver was loaded.
pub fn load( pub fn load(
context: Rc<SproutContext>, context: Rc<SproutContext>,
drivers: &BTreeMap<String, DriverDeclaration>, drivers: &BTreeMap<String, DriverDeclaration>,
) -> Result<()> { ) -> Result<()> {
// If there are no drivers, we don't need to do anything.
if drivers.is_empty() { if drivers.is_empty() {
return Ok(()); return Ok(());
} }
info!("loading drivers"); info!("loading drivers");
// Load all the drivers in no particular order.
for (name, driver) in drivers { for (name, driver) in drivers {
load_driver(context.clone(), driver).context(format!("unable to load driver: {}", name))?; load_driver(context.clone(), driver).context(format!("unable to load driver: {}", name))?;
} }
// Reconnect all the controllers to all handles.
reconnect().context("unable to reconnect drivers")?; reconnect().context("unable to reconnect drivers")?;
info!("loaded drivers"); info!("loaded drivers");
// We've now loaded all the drivers, so we can return.
Ok(()) Ok(())
} }

View File

@@ -4,14 +4,25 @@ use anyhow::{Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// The filesystem device match extractor.
pub mod filesystem_device_match; pub mod filesystem_device_match;
/// Declares an extractor configuration.
/// Extractors allow calculating values at runtime
/// using built-in sprout modules.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct ExtractorDeclaration { pub struct ExtractorDeclaration {
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
#[serde(default, rename = "filesystem-device-match")] #[serde(default, rename = "filesystem-device-match")]
pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>, pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>,
} }
/// Extracts the value using the specified `extractor` under the provided `context`.
/// The extractor must return a value, and if a value cannot be determined, an error
/// should be returned.
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> { pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match { if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem) filesystem_device_match::extract(context, filesystem)

View File

@@ -13,34 +13,57 @@ use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid}; use uefi::{CString16, Guid};
use uefi_raw::Status; use uefi_raw::Status;
/// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem.
///
/// This function only requires one of the criteria to match.
/// The fallback value can be used to provide a value if none is found.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor { pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")] #[serde(default, rename = "has-label")]
pub has_label: Option<String>, pub has_label: Option<String>,
/// Matches a filesystem that has the specified item.
/// An item is either a directory or file.
#[serde(default, rename = "has-item")] #[serde(default, rename = "has-item")]
pub has_item: Option<String>, pub has_item: Option<String>,
/// Matches a filesystem that has the specified partition UUID.
#[serde(default, rename = "has-partition-uuid")] #[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>, pub has_partition_uuid: Option<String>,
/// Matches a filesystem that has the specified partition type UUID.
#[serde(default, rename = "has-partition-type-uuid")] #[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>, pub has_partition_type_uuid: Option<String>,
/// The fallback value to use if no filesystem matches the criteria.
#[serde(default)] #[serde(default)]
pub fallback: Option<String>, pub fallback: Option<String>,
} }
/// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract( pub fn extract(
context: Rc<SproutContext>, context: Rc<SproutContext>,
extractor: &FilesystemDeviceMatchExtractor, extractor: &FilesystemDeviceMatchExtractor,
) -> Result<String> { ) -> Result<String> {
// Find all the filesystems inside the UEFI stack.
let handles = uefi::boot::find_handles::<SimpleFileSystem>() let handles = uefi::boot::find_handles::<SimpleFileSystem>()
.context("unable to find filesystem handles")?; .context("unable to find filesystem handles")?;
// Iterate over all the filesystems and check if they match the criteria.
for handle in handles { for handle in handles {
// This defines whether a match has been found.
let mut has_match = false; let mut has_match = false;
// Extract the partition info for this filesystem.
// There is no guarantee that the filesystem has a partition.
let partition_info = { let partition_info = {
// Open the partition info protocol for this handle.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle); let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle);
match partition_info { match partition_info {
Ok(partition_info) => { Ok(partition_info) => {
// GPT partitions have a unique partition GUID.
// MBR does not.
if let Some(gpt) = partition_info.gpt_partition_entry() { if let Some(gpt) = partition_info.gpt_partition_entry() {
let uuid = gpt.unique_partition_guid; let uuid = gpt.unique_partition_guid;
let type_uuid = gpt.partition_type_guid; let type_uuid = gpt.partition_type_guid;
@@ -51,10 +74,12 @@ pub fn extract(
} }
Err(error) => { Err(error) => {
// If the filesystem does not have a partition, that is okay.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED
{ {
None None
} else { } else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?; Err(error).context("unable to open filesystem partition info")?;
None None
} }
@@ -62,6 +87,7 @@ pub fn extract(
} }
}; };
// Check if the partition info matches partition uuid criteria.
if let Some((partition_uuid, _partition_type_guid)) = partition_info if let Some((partition_uuid, _partition_type_guid)) = partition_info
&& let Some(ref has_partition_uuid) = extractor.has_partition_uuid && let Some(ref has_partition_uuid) = extractor.has_partition_uuid
{ {
@@ -73,6 +99,7 @@ pub fn extract(
has_match = true; has_match = true;
} }
// Check if the partition info matches partition type uuid criteria.
if let Some((_partition_uuid, partition_type_guid)) = partition_info if let Some((_partition_uuid, partition_type_guid)) = partition_info
&& let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid && let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid
{ {
@@ -84,9 +111,11 @@ pub fn extract(
has_match = true; has_match = true;
} }
// Open the filesystem protocol for this handle.
let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle) let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem protocol")?; .context("unable to open filesystem protocol")?;
// Check if the filesystem matches label criteria.
if let Some(ref label) = extractor.has_label { if let Some(ref label) = extractor.has_label {
let want_label = CString16::try_from(context.stamp(label).as_str()) let want_label = CString16::try_from(context.stamp(label).as_str())
.context("unable to convert label to CString16")?; .context("unable to convert label to CString16")?;
@@ -103,35 +132,46 @@ pub fn extract(
has_match = true; has_match = true;
} }
// Check if the filesystem matches item criteria.
if let Some(ref item) = extractor.has_item { if let Some(ref item) = extractor.has_item {
let want_item = CString16::try_from(context.stamp(item).as_str()) let want_item = CString16::try_from(context.stamp(item).as_str())
.context("unable to convert item to CString16")?; .context("unable to convert item to CString16")?;
let mut filesystem = FileSystem::new(filesystem); let mut filesystem = FileSystem::new(filesystem);
// Check the metadata of the item.
let metadata = filesystem.metadata(Path::new(&want_item)); let metadata = filesystem.metadata(Path::new(&want_item));
// Ignore filesystem errors as we can't do anything useful with the error.
if metadata.is_err() { if metadata.is_err() {
continue; continue;
} }
let metadata = metadata?; let metadata = metadata?;
// Only check directories and files.
if !(metadata.is_directory() || metadata.is_regular_file()) { if !(metadata.is_directory() || metadata.is_regular_file()) {
continue; continue;
} }
has_match = true; has_match = true;
} }
// If there is no match, continue to the next filesystem.
if !has_match { if !has_match {
continue; continue;
} }
// If we have a match, return the device root path.
let path = uefi::boot::open_protocol_exclusive::<DevicePath>(handle) let path = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to open filesystem device path")?; .context("unable to open filesystem device path")?;
let path = path.deref(); let path = path.deref();
// Acquire the device path root as a string.
return utils::device_path_root(path).context("unable to get device path root"); return utils::device_path_root(path).context("unable to get device path root");
} }
// If there is a fallback value, use it at this point.
if let Some(fallback) = &extractor.fallback { if let Some(fallback) = &extractor.fallback {
return Ok(fallback.clone()); return Ok(fallback.clone());
} }
// Without a fallback, we can't continue, so bail.
bail!("unable to find matching filesystem") bail!("unable to find matching filesystem")
} }

View File

@@ -10,14 +10,33 @@ use std::rc::Rc;
pub mod bls; pub mod bls;
pub mod matrix; pub mod matrix;
/// Declares a generator configuration.
/// Generators allow generating entries at runtime based on a set of data.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct GeneratorDeclaration { pub struct GeneratorDeclaration {
/// Matrix generator configuration.
/// Matrix allows you to specify multiple value-key values as arrays.
/// This allows multiplying the number of entries by any number of possible
/// configuration options. For example,
/// data.x = ["a", "b"]
/// data.y = ["c", "d"]
/// would generate an entry for each of these combinations:
/// x = a, y = c
/// x = a, y = d
/// x = b, y = c
/// x = b, y = d
#[serde(default)] #[serde(default)]
pub matrix: Option<MatrixConfiguration>, pub matrix: Option<MatrixConfiguration>,
/// BLS generator configuration.
/// BLS allows you to pass a filesystem path that contains a set of BLS entries.
/// It will generate a sprout entry for every supported BLS entry.
#[serde(default)] #[serde(default)]
pub bls: Option<BlsConfiguration>, pub bls: Option<BlsConfiguration>,
} }
/// Runs the generator specified by the `generator` option.
/// It uses the specified `context` as the parent context for
/// the generated entries, injecting more values if needed.
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
generator: &GeneratorDeclaration, generator: &GeneratorDeclaration,

View File

@@ -11,70 +11,124 @@ use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
/// BLS entry parser.
mod entry; mod entry;
/// The default path to the BLS entries directory.
const BLS_TEMPLATE_PATH: &str = "\\loader\\entries";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
/// entries from an input template.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct BlsConfiguration { pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration, pub entry: EntryDeclaration,
/// The path to the BLS entries directory.
#[serde(default = "default_bls_path")] #[serde(default = "default_bls_path")]
pub path: String, pub path: String,
} }
fn default_bls_path() -> String { fn default_bls_path() -> String {
"\\loader\\entries".to_string() BLS_TEMPLATE_PATH.to_string()
} }
// TODO(azenla): remove this once variable substitution is implemented.
/// This function is used to remove the `tuned_initrd` variable from entry values.
/// Fedora uses tuned which adds an initrd that shouldn't be used.
fn quirk_initrd_remove_tuned(input: String) -> String {
input.replace("$tuned_initrd", "").trim().to_string()
}
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
bls: &BlsConfiguration, bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> { ) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
let mut entries = Vec::new(); let mut entries = Vec::new();
// Stamp the path to the BLS entries directory.
let path = context.stamp(&bls.path); let path = context.stamp(&bls.path);
// Resolve the path to the BLS entries directory.
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path) let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
.context("unable to resolve bls path")?; .context("unable to resolve bls path")?;
// Open exclusive access to the BLS filesystem.
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle) let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open bls filesystem")?; .context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs); let mut fs = FileSystem::new(fs);
// Convert the subpath to the BLS entries directory to a string.
let sub_text_path = resolved let sub_text_path = resolved
.sub_path .sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert subpath to string")?; .context("unable to convert subpath to string")?;
// Produce a path to the BLS entries directory.
let entries_path = Path::new(&sub_text_path); let entries_path = Path::new(&sub_text_path);
// Read the BLS entries directory.
let entries_iter = fs let entries_iter = fs
.read_dir(entries_path) .read_dir(entries_path)
.context("unable to read bls entries")?; .context("unable to read bls entries")?;
// For each entry in the BLS entries directory, parse the entry and add it to the list.
for entry in entries_iter { for entry in entries_iter {
let entry = entry?; // Unwrap the entry file info.
let entry = entry.context("unable to read bls item entry")?;
// Skip items that are not regular files.
if !entry.is_regular_file() { if !entry.is_regular_file() {
continue; continue;
} }
// Get the file name of the filesystem item.
let name = entry.file_name().to_string(); let name = entry.file_name().to_string();
// Ignore files that are not .conf files.
if !name.ends_with(".conf") { if !name.ends_with(".conf") {
continue; continue;
} }
// Produce the full path to the entry file.
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str()) let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str())
.context("unable to construct full entry path")?; .context("unable to construct full entry path")?;
let full_entry_path = Path::new(&full_entry_path); let full_entry_path = Path::new(&full_entry_path);
// Read the entry file.
let content = fs let content = fs
.read(full_entry_path) .read(full_entry_path)
.context("unable to read bls file")?; .context("unable to read bls file")?;
// Parse the entry file as a UTF-8 string.
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?; let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
// Parse the entry file as a BLS entry.
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?; let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
// Ignore entries that are not valid for Sprout.
if !entry.is_valid() { if !entry.is_valid() {
continue; continue;
} }
// Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork(); let mut context = context.fork();
context.set("title", entry.title().unwrap_or(name));
context.set("chainload", entry.chainload_path().unwrap_or_default());
context.set("options", entry.options().unwrap_or_default());
context.set("initrd", entry.initrd_path().unwrap_or_default());
let title = entry.title().unwrap_or(name);
let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default();
// Put the initrd through a quirk modifier to support Fedora.
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
context.set("title", title);
context.set("chainload", chainload);
context.set("options", options);
context.set("initrd", initrd);
// Add the entry to the list with a frozen context.
entries.push((context.freeze(), bls.entry.clone())); entries.push((context.freeze(), bls.entry.clone()));
} }

View File

@@ -1,59 +1,82 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use std::str::FromStr; use std::str::FromStr;
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct BlsEntry { pub struct BlsEntry {
/// The title of the entry.
pub title: Option<String>, pub title: Option<String>,
/// The options to pass to the entry.
pub options: Option<String>, pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>, pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>, pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>, pub efi: Option<String>,
} }
/// Parser for a BLS entry.
impl FromStr for BlsEntry { impl FromStr for BlsEntry {
type Err = Error; type Err = Error;
/// Parses the `input` as a BLS entry file.
fn from_str(input: &str) -> Result<Self> { fn from_str(input: &str) -> Result<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None; let mut title: Option<String> = None;
let mut options: Option<String> = None; let mut options: Option<String> = None;
let mut linux: Option<String> = None; let mut linux: Option<String> = None;
let mut initrd: Option<String> = None; let mut initrd: Option<String> = None;
let mut efi: Option<String> = None; let mut efi: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() { for line in input.lines() {
// Trim the line.
let line = line.trim(); let line = line.trim();
// Split the line once by a space.
let Some((key, value)) = line.split_once(" ") else { let Some((key, value)) = line.split_once(" ") else {
continue; continue;
}; };
// Match the key to a field we understand.
match key { match key {
// The title of the entry.
"title" => { "title" => {
title = Some(value.trim().to_string()); title = Some(value.trim().to_string());
} }
// The options to pass to the entry.
"options" => { "options" => {
options = Some(value.trim().to_string()); options = Some(value.trim().to_string());
} }
// The path to the linux kernel.
"linux" => { "linux" => {
linux = Some(value.trim().to_string()); linux = Some(value.trim().to_string());
} }
// The path to the initrd.
"initrd" => { "initrd" => {
initrd = Some(value.trim().to_string()); initrd = Some(value.trim().to_string());
} }
// The path to an EFI image.
"efi" => { "efi" => {
efi = Some(value.trim().to_string()); efi = Some(value.trim().to_string());
} }
// Ignore any other key.
_ => { _ => {
continue; continue;
} }
} }
} }
Ok(BlsEntry { // Produce a BLS entry from the parsed fields.
Ok(Self {
title, title,
options, options,
linux, linux,
@@ -64,10 +87,14 @@ impl FromStr for BlsEntry {
} }
impl BlsEntry { impl BlsEntry {
/// Checks if this BLS entry is something we can actually boot in Sprout.
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some() self.linux.is_some() || self.efi.is_some()
} }
/// Fetches the path to an EFI bootable image to boot, if any.
/// This prioritizes the linux field over efi.
/// It also converts / to \\ to match EFI path style.
pub fn chainload_path(&self) -> Option<String> { pub fn chainload_path(&self) -> Option<String> {
self.linux self.linux
.clone() .clone()
@@ -75,16 +102,20 @@ impl BlsEntry {
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
} }
/// Fetches the path to an initrd to pass to the kernel, if any.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_path(&self) -> Option<String> { pub fn initrd_path(&self) -> Option<String> {
self.initrd self.initrd
.clone() .clone()
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) .map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
} }
/// Fetches the options to pass to the kernel, if any.
pub fn options(&self) -> Option<String> { pub fn options(&self) -> Option<String> {
self.options.clone() self.options.clone()
} }
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> { pub fn title(&self) -> Option<String> {
self.title.clone() self.title.clone()
} }

View File

@@ -5,21 +5,37 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based
/// on input values multiplicatively.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct MatrixConfiguration { pub struct MatrixConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)] #[serde(default)]
pub entry: EntryDeclaration, pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, Vec<String>>, pub values: BTreeMap<String, Vec<String>>,
} }
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce:
/// x: a, y: c
/// x: a, y: d
/// x: b, y: c
/// x: b, y: d
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> { fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect(); let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()]; let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
for (key, values) in items { for (key, values) in items {
let mut new_result = Vec::new(); let mut new_result = Vec::new();
// Produce all the combinations of the input values.
for combination in &result { for combination in &result {
for value in &values { for value in &values {
let mut new_combination = combination.clone(); let mut new_combination = combination.clone();
@@ -34,18 +50,23 @@ fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, S
result.into_iter().filter(|item| !item.is_empty()).collect() result.into_iter().filter(|item| !item.is_empty()).collect()
} }
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
matrix: &MatrixConfiguration, matrix: &MatrixConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> { ) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
// Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values); let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new(); let mut entries = Vec::new();
// For each combination, create a new context and entry.
for combination in combinations { for combination in combinations {
let mut context = context.fork(); let mut context = context.fork();
// Insert the combination into the context.
context.insert(&combination); context.insert(&combination);
let context = context.freeze(); let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone(); let mut entry = matrix.entry.clone();
entry.title = context.stamp(&entry.title); entry.title = context.stamp(&entry.title);
entry.actions = entry entry.actions = entry
@@ -53,6 +74,7 @@ pub fn generate(
.into_iter() .into_iter()
.map(|action| context.stamp(action)) .map(|action| context.stamp(action))
.collect(); .collect();
// Push the entry into the list with the new context.
entries.push((context, entry)); entries.push((context, entry));
} }

View File

@@ -3,22 +3,44 @@
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;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// actions: Code that can be configured and executed by Sprout.
pub mod actions; pub mod actions;
/// config: Sprout configuration mechanism.
pub mod config; pub mod config;
/// context: Stored values that can be cheaply forked and cloned.
pub mod context; pub mod context;
/// drivers: EFI drivers to load and provide extra functionality.
pub mod drivers; pub mod drivers;
/// entries: Boot menu entries that have a title and can execute actions.
pub mod entries; pub mod entries;
/// extractors: Runtime code that can extract values into the Sprout context.
pub mod extractors; pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators; pub mod generators;
/// phases: Hooks into specific parts of the boot process.
pub mod phases; pub mod phases;
/// 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.
pub mod utils; pub mod utils;
/// The main entrypoint of sprout. /// The main entrypoint of sprout.
@@ -28,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.
@@ -45,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.
@@ -125,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)
}

View File

@@ -20,6 +20,9 @@ pub struct PhasesConfiguration {
pub late: Vec<PhaseConfiguration>, pub late: Vec<PhaseConfiguration>,
} }
/// Configures a single phase of the boot process.
/// There can be multiple phase configurations that are
/// executed sequentially.
#[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. /// The actions to run when the phase is executed.
@@ -37,9 +40,11 @@ pub struct PhaseConfiguration {
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> { 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();
// Insert the values into the context.
context.insert(&item.values); context.insert(&item.values);
let context = context.freeze(); let context = context.freeze();
// Execute all the actions in this phase configuration.
for action in item.actions.iter() { for action in item.actions.iter() {
actions::execute(context.clone(), action) actions::execute(context.clone(), action)
.context(format!("unable to execute action '{}'", action))?; .context(format!("unable to execute action '{}'", action))?;

View File

@@ -6,19 +6,22 @@ use std::os::uefi as uefi_std;
/// This fetches the system table and current image handle from uefi_std and injects /// This fetches the system table and current image handle from uefi_std and injects
/// them into the uefi crate. /// them into the uefi crate.
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
// Acquire the system table and image handle from the uefi_std environment.
let system_table = uefi_std::env::system_table(); let system_table = uefi_std::env::system_table();
let image_handle = uefi_std::env::image_handle(); let image_handle = uefi_std::env::image_handle();
// SAFETY: The uefi variables above come from the Rust std. // SAFETY: The UEFI variables above come from the Rust std.
// These variables are nonnull and calling the uefi crates with these values is validated // These variables are not-null and calling the uefi crates with these values is validated
// to be corrected by hand. // to be corrected by hand.
unsafe { unsafe {
// Set the system table and image handle.
uefi::table::set_system_table(system_table.as_ptr().cast()); uefi::table::set_system_table(system_table.as_ptr().cast());
let handle = uefi::Handle::from_ptr(image_handle.as_ptr().cast()) let handle = uefi::Handle::from_ptr(image_handle.as_ptr().cast())
.context("unable to resolve image handle")?; .context("unable to resolve image handle")?;
uefi::boot::set_image_handle(handle); uefi::boot::set_image_handle(handle);
} }
// Initialize the uefi logger mechanism and other helpers.
uefi::helpers::init().context("unable to initialize uefi")?; uefi::helpers::init().context("unable to initialize uefi")?;
Ok(()) Ok(())
} }

View File

@@ -6,7 +6,10 @@ use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle}; use uefi::{CString16, Handle};
/// Support code for the EFI framebuffer.
pub mod framebuffer; pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader; pub mod media_loader;
/// Parses the input [path] as a [DevicePath]. /// Parses the input [path] as a [DevicePath].

View File

@@ -1,13 +1,18 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput}; use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};
/// Represents the EFI framebuffer.
pub struct Framebuffer { pub struct Framebuffer {
/// The width of the framebuffer in pixels.
width: usize, width: usize,
/// The height of the framebuffer in pixels.
height: usize, height: usize,
/// The pixels of the framebuffer.
pixels: Vec<BltPixel>, pixels: Vec<BltPixel>,
} }
impl Framebuffer { impl Framebuffer {
/// Creates a new framebuffer of the specified `width` and `height`.
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize) -> Self {
Framebuffer { Framebuffer {
width, width,
@@ -16,10 +21,12 @@ impl Framebuffer {
} }
} }
/// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate.
pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> {
self.pixels.get_mut(y * self.width + x) self.pixels.get_mut(y * self.width + x)
} }
/// Blit the framebuffer to the specified `gop` [GraphicsOutput].
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> { pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
gop.blt(BltOp::BufferToVideo { gop.blt(BltOp::BufferToVideo {
buffer: &self.pixels, buffer: &self.pixels,

View File

@@ -11,9 +11,11 @@ use uefi_raw::{Boolean, Status};
pub mod constants; pub mod constants;
/// The media loader protocol.
#[derive(Debug)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
struct MediaLoaderProtocol { struct MediaLoaderProtocol {
/// This is the standard EFI LoadFile2 protocol.
pub load_file: unsafe extern "efiapi" fn( pub load_file: unsafe extern "efiapi" fn(
this: *mut MediaLoaderProtocol, this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol, file_path: *const DevicePathProtocol,
@@ -21,7 +23,9 @@ struct MediaLoaderProtocol {
buffer_size: *mut usize, buffer_size: *mut usize,
buffer: *mut c_void, buffer: *mut c_void,
) -> Status, ) -> Status,
/// A pointer to a Box<[u8]> containing the data to load.
pub address: *mut c_void, pub address: *mut c_void,
/// The length of the data to load.
pub length: usize, pub length: usize,
} }
@@ -29,9 +33,13 @@ struct MediaLoaderProtocol {
/// You MUST call [MediaLoaderHandle::unregister] when ready to unregister. /// You MUST call [MediaLoaderHandle::unregister] when ready to unregister.
/// [Drop] is not implemented for this type. /// [Drop] is not implemented for this type.
pub struct MediaLoaderHandle { pub struct MediaLoaderHandle {
/// The vendor GUID of the media loader.
guid: Guid, guid: Guid,
/// The handle of the media loader in the UEFI stack.
handle: Handle, handle: Handle,
/// The protocol interface pointer.
protocol: *mut MediaLoaderProtocol, protocol: *mut MediaLoaderProtocol,
/// The device path pointer.
path: *mut DevicePath, path: *mut DevicePath,
} }
@@ -50,11 +58,12 @@ impl MediaLoaderHandle {
buffer_size: *mut usize, buffer_size: *mut usize,
buffer: *mut c_void, buffer: *mut c_void,
) -> Status { ) -> Status {
// Check if the pointers are non-null first.
if this.is_null() || buffer_size.is_null() || file_path.is_null() { if this.is_null() || buffer_size.is_null() || file_path.is_null() {
return Status::INVALID_PARAMETER; return Status::INVALID_PARAMETER;
} }
// Boot policy must not be true, as that's special behavior that is irrelevant // Boot policy must not be true, and if it is, that is special behavior that is irrelevant
// for the media loader concept. // for the media loader concept.
if boot_policy == Boolean::TRUE { if boot_policy == Boolean::TRUE {
return Status::UNSUPPORTED; return Status::UNSUPPORTED;
@@ -63,48 +72,68 @@ impl MediaLoaderHandle {
// SAFETY: Validated as safe because this is checked to be non-null. It is the caller's // SAFETY: Validated as safe because this is checked to be non-null. It is the caller's
// responsibility to ensure that the right pointer is passed for [this]. // responsibility to ensure that the right pointer is passed for [this].
unsafe { unsafe {
// Check if the length and address are valid.
if (*this).length == 0 || (*this).address.is_null() { if (*this).length == 0 || (*this).address.is_null() {
return Status::NOT_FOUND; return Status::NOT_FOUND;
} }
// Check if the buffer is large enough.
// If it is not, we need to set the buffer size to the length of the data.
// This is the way that Linux calls this function, to check the size to allocate
// for the buffer that holds the data.
if buffer.is_null() || *buffer_size < (*this).length { if buffer.is_null() || *buffer_size < (*this).length {
*buffer_size = (*this).length; *buffer_size = (*this).length;
return Status::BUFFER_TOO_SMALL; return Status::BUFFER_TOO_SMALL;
} }
// Copy the data into the buffer.
buffer.copy_from((*this).address, (*this).length); buffer.copy_from((*this).address, (*this).length);
// Set the buffer size to the length of the data.
*buffer_size = (*this).length; *buffer_size = (*this).length;
} }
// We've successfully loaded the data.
Status::SUCCESS Status::SUCCESS
} }
/// Creates a new device path for the media loader based on a vendor `guid`.
fn device_path(guid: Guid) -> Box<DevicePath> { fn device_path(guid: Guid) -> Box<DevicePath> {
// The buffer for the device path.
let mut path = Vec::new(); let mut path = Vec::new();
// Build a device path for the media loader with a vendor-specific guid.
let path = DevicePathBuilder::with_vec(&mut path) let path = DevicePathBuilder::with_vec(&mut path)
.push(&Vendor { .push(&Vendor {
vendor_guid: guid, vendor_guid: guid,
vendor_defined_data: &[], vendor_defined_data: &[],
}) })
.unwrap() .unwrap() // We know that the device path is valid, so we can unwrap.
.finalize() .finalize()
.unwrap(); .unwrap(); // We know that the device path is valid, so we can unwrap.
// Convert the device path to a boxed device path.
// This is safer than dealing with a pooled device path.
path.to_boxed() path.to_boxed()
} }
/// Checks if the media loader is already registered with the UEFI stack.
fn already_registered(guid: Guid) -> Result<bool> { fn already_registered(guid: Guid) -> Result<bool> {
// Acquire the device path for the media loader.
let path = Self::device_path(guid); let path = Self::device_path(guid);
let mut existing_path = path.as_ref(); let mut existing_path = path.as_ref();
// Locate the LoadFile2 protocol for the media loader based on the device path.
let result = uefi::boot::locate_device_path::<LoadFile2>(&mut existing_path); let result = uefi::boot::locate_device_path::<LoadFile2>(&mut existing_path);
// If the result is okay, the media loader is already registered.
if result.is_ok() { if result.is_ok() {
return Ok(true); return Ok(true);
} else if let Err(error) = result } else if let Err(error) = result
&& error.status() != Status::NOT_FOUND && error.status() != Status::NOT_FOUND
// If the error is not found, that means it's not registered.
{ {
bail!("unable to locate media loader device path: {}", error); bail!("unable to locate media loader device path: {}", error);
} }
// The media loader is not registered.
Ok(false) Ok(false)
} }
@@ -112,13 +141,20 @@ impl MediaLoaderHandle {
/// This uses a special device path that other EFI programs will look at /// This uses a special device path that other EFI programs will look at
/// to load the data from. /// to load the data from.
pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> { pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> {
// Acquire the vendor device path for the media loader.
let path = Self::device_path(guid); let path = Self::device_path(guid);
let path = Box::leak(path);
// Check if the media loader is already registered.
// If it is, we can't register it again safely.
if Self::already_registered(guid)? { if Self::already_registered(guid)? {
bail!("media loader already registered"); bail!("media loader already registered");
} }
// Leak the device path to pass it to the UEFI stack.
let path = Box::leak(path);
// Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs.
let mut handle = unsafe { let mut handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
None, None,
@@ -128,16 +164,20 @@ impl MediaLoaderHandle {
} }
.context("unable to install media loader device path handle")?; .context("unable to install media loader device path handle")?;
// Leak the data we need to pass to the UEFI stack.
let data = Box::leak(data); let data = Box::leak(data);
// Allocate a new box for the protocol interface.
let protocol = Box::new(MediaLoaderProtocol { let protocol = Box::new(MediaLoaderProtocol {
load_file: Self::load_file, load_file: Self::load_file,
address: data.as_ptr() as *mut _, address: data.as_ptr() as *mut _,
length: data.len(), length: data.len(),
}); });
// Leak the protocol interface to pass it to the UEFI stack.
let protocol = Box::leak(protocol); let protocol = Box::leak(protocol);
// Install a protocol interface for the load file protocol for the media loader protocol.
handle = unsafe { handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
Some(handle), Some(handle),
@@ -147,10 +187,13 @@ impl MediaLoaderHandle {
} }
.context("unable to install media loader load file handle")?; .context("unable to install media loader load file handle")?;
// Check if the media loader is registered.
// If it is not, we can't continue safely because something went wrong.
if !Self::already_registered(guid)? { if !Self::already_registered(guid)? {
bail!("media loader not registered when expected to be registered"); bail!("media loader not registered when expected to be registered");
} }
// Return a handle to the media loader.
Ok(Self { Ok(Self {
guid, guid,
handle, handle,
@@ -162,11 +205,16 @@ impl MediaLoaderHandle {
/// Unregisters a media loader from the UEFI stack. /// Unregisters a media loader from the UEFI stack.
/// This will free the memory allocated by the passed data. /// This will free the memory allocated by the passed data.
pub fn unregister(self) -> Result<()> { pub fn unregister(self) -> Result<()> {
// Check if the media loader is registered.
// If it is not, we don't need to do anything.
if !Self::already_registered(self.guid)? { if !Self::already_registered(self.guid)? {
return Ok(()); return Ok(());
} }
// SAFETY: We know that the media loader is registered, so we can safely uninstall it.
// We should have allocated the pointers involved, so we can safely free them.
unsafe { unsafe {
// Uninstall the protocol interface for the device path protocol.
uefi::boot::uninstall_protocol_interface( uefi::boot::uninstall_protocol_interface(
self.handle, self.handle,
&DevicePathProtocol::GUID, &DevicePathProtocol::GUID,
@@ -174,6 +222,7 @@ impl MediaLoaderHandle {
) )
.context("unable to uninstall media loader device path handle")?; .context("unable to uninstall media loader device path handle")?;
// Uninstall the protocol interface for the load file protocol.
uefi::boot::uninstall_protocol_interface( uefi::boot::uninstall_protocol_interface(
self.handle, self.handle,
&LoadFile2Protocol::GUID, &LoadFile2Protocol::GUID,
@@ -181,12 +230,16 @@ impl MediaLoaderHandle {
) )
.context("unable to uninstall media loader load file handle")?; .context("unable to uninstall media loader load file handle")?;
// Retrieve a box for the device path and protocols.
let path = Box::from_raw(self.path); let path = Box::from_raw(self.path);
let protocol = Box::from_raw(self.protocol); let protocol = Box::from_raw(self.protocol);
// Retrieve a box for the data we passed in.
let slice = let slice =
std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length); std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let data = Box::from_raw(slice); let data = Box::from_raw(slice);
// Drop all the allocations explicitly, as we don't want to leak them.
drop(path); drop(path);
drop(protocol); drop(protocol);
drop(data); drop(data);

View File

@@ -13,7 +13,8 @@ pub mod xen {
/// The device path GUID for the Xen EFI config. /// The device path GUID for the Xen EFI config.
pub const XEN_EFI_CONFIG_MEDIA_GUID: Guid = guid!("bf61f458-a28e-46cd-93d7-07dac5e8cd66"); pub const XEN_EFI_CONFIG_MEDIA_GUID: Guid = guid!("bf61f458-a28e-46cd-93d7-07dac5e8cd66");
/// The device path GUID for the Xen EFI config. /// The device path GUID for the Xen EFI kernel.
pub const XEN_EFI_KERNEL_MEDIA_GUID: Guid = guid!("4010c8bf-6ced-40f5-a53f-e820aee8f34b"); pub const XEN_EFI_KERNEL_MEDIA_GUID: Guid = guid!("4010c8bf-6ced-40f5-a53f-e820aee8f34b");
/// The device path GUID for the Xen EFI ramdisk.
pub const XEN_EFI_RAMDISK_MEDIA_GUID: Guid = guid!("5db1fd01-c3cb-4812-b2ba-8791e52d4a89"); pub const XEN_EFI_RAMDISK_MEDIA_GUID: Guid = guid!("5db1fd01-c3cb-4812-b2ba-8791e52d4a89");
} }