Compare commits

..
4 Commits
Author SHA1 Message Date
alex a4c31cc402 update sprout logo 2025-10-24 16:53:13 -07:00
alex 226e284edc add favicon 2025-10-19 22:23:27 -07:00
alex 13b8606d9f fix typo 2025-10-19 22:15:56 -07:00
alex bece37cf3a initial sprout website 2025-10-19 22:14:35 -07:00
65 changed files with 76 additions and 3207 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
/target
/.idea
/.vscode
-1
View File
@@ -1 +0,0 @@
* @azenla
-28
View File
@@ -1,28 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
actions-updates:
dependency-type: production
applies-to: version-updates
actions-dev-updates:
dependency-type: development
applies-to: version-updates
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
cargo-updates:
dependency-type: production
applies-to: version-updates
cargo-dev-updates:
dependency-type: development
applies-to: version-updates
-43
View File
@@ -1,43 +0,0 @@
name: zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions:
contents: read # Needed to checkout the repository.
jobs:
zizmor:
name: zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Needed to upload code scanning results.
contents: read # Needed to checkout the repository.
actions: read # Needed to analyze action metadata.
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: setup uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7
- name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif
env:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4
with:
sarif_file: results.sarif
category: zizmor
-97
View File
@@ -1,97 +0,0 @@
name: check
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read # Needed to checkout the repository.
jobs:
rustfmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain with rustfmt'
run: |
rustup update --no-self-update nightly
rustup default nightly
rustup component add rustfmt
- name: 'cargo fmt'
run: cargo fmt --all -- --check
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: 'build ${{ matrix.arch }}'
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
- name: cargo build
run: cargo build --target "${TARGET_ARCH}-unknown-uefi"
clippy:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- x86_64
- aarch64
env:
TARGET_ARCH: "${{ matrix.arch }}"
name: 'clippy ${{ matrix.arch }}'
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain with clippy'
run: |
rustup update --no-self-update nightly
rustup default stable
rustup component add clippy
- name: cargo clippy
run: cargo clippy --target "${TARGET_ARCH}-unknown-uefi"
-41
View File
@@ -1,41 +0,0 @@
name: release assets
on:
release:
types:
- created
permissions:
contents: read # Needed to checkout the repository.
jobs:
assets:
name: assets
permissions:
contents: write # Needed to upload release assets.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
persist-credentials: false
- name: 'install nightly rust toolchain'
run: |
rustup update --no-self-update nightly
rustup default nightly
- name: 'assemble release artifacts'
run: ./hack/assemble.sh
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
RELEASE_TAG: "${{ github.event.release.tag_name }}"
if: ${{ github.event_name == 'release' }}
-33
View File
@@ -1,36 +1,3 @@
/target /target
# Infrastructure
.env
.terraform
terraform.tfstate
terraform.tfstate.backup
aws-packer-config.json
override.install.sh
# Ignore the google cloud auth creds created in Actions workflows
gha-creds-*.json
crane
tok
# IDE
/.idea /.idea
/.vscode /.vscode
# Benchmarks
/results
# cli markdown output
cli.md
# precommit hooks
.editorconfig
.markdownlint-cli2.jsonc
.pre-commit-config.yaml
.prettierrc
.shellcheckrc
# example outputs
/image-cache
/out
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
sprout.edera.dev
-132
View File
@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
conduct@edera.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
-32
View File
@@ -1,32 +0,0 @@
# 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.
## 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.
2. Before opening an issue or PR, please try a few searches to see if there is overlap with existing conversations or WIP contributions.
3. For security or otherwise sensitive topics, please read our [Security Policy].
4. Ask questions! If you want to ask something, chances are someone else wants to ask it as well.
## 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].
## Reporting bugs and other issues
We encourage opening an issue on GitHub to report bugs.
## Pull Requests
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.
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.
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.
[Code of Conduct]: ./CODE_OF_CONDUCT.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
Generated
-379
View File
@@ -1,379 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "image"
version = "0.25.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.7.6"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pxfm"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
dependencies = [
"num-traits",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
"serde_core",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b50d40c28f540d4271109ea0ca7e1268#2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[[package]]
name = "sprout"
version = "0.0.1"
dependencies = [
"anyhow",
"image",
"log",
"serde",
"toml",
"uefi",
"uefi-raw",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "ucs2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79298e11f316400c57ec268f3c2c29ac3c4d4777687955cd3d4f3a35ce7eba"
dependencies = [
"bit_field",
]
[[package]]
name = "uefi"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7569ceafb898907ff764629bac90ac24ba4203c38c33ef79ee88c74aa35b11"
dependencies = [
"bitflags",
"cfg-if",
"log",
"ptr_meta",
"ucs2",
"uefi-macros",
"uefi-raw",
"uguid",
]
[[package]]
name = "uefi-macros"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dad47b3af8f99116c0f6d4d669c439487d9aaf1c8d9480d686cda6f3a8aa23"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "uefi-raw"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cad96b8baaf1615d3fdd0f03d04a0b487d857c1b51b19dcbfe05e2e3c447b78"
dependencies = [
"bitflags",
"uguid",
]
[[package]]
name = "uguid"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
-52
View File
@@ -1,52 +0,0 @@
[package]
name = "sprout"
version = "0.0.2"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
toml = "0.9.7"
log = "0.4.28"
[dependencies.image]
version = "0.25.6"
default-features = false
features = ["png"]
optional = true
[dependencies.serde]
version = "1.0.228"
features = ["derive"]
[dependencies.uefi]
version = "0.35.0"
features = ["alloc", "logger"]
[dependencies.uefi-raw]
version = "0.11.0"
[profile.release]
lto = "thin"
strip = "symbols"
[features]
default = ["splash"]
splash = ["dep:image"]
[profile.release-debuginfo]
inherits = "release"
strip = "none"
debug = 1
[profile.dev-fast]
inherits = "dev"
strip = "debuginfo"
debug = 0
[patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
[patch.crates-io.moxcms]
git = "https://github.com/edera-dev/sprout-patched-deps.git"
rev = "2c4fcc84b50d40c28f540d4271109ea0ca7e1268"
-25
View File
@@ -1,25 +0,0 @@
# syntax=docker/dockerfile:1.7-labs
ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:b8107fa66d3e5ad7f729d3347c7feedbd3f4b60b01006edce39eb6b994ff00bd AS build
RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build
COPY \
--exclude=rust-toolchain.toml \
--exclude=hack \
--chown=build:build \
. /build
WORKDIR /build
ARG TARGETPLATFORM
ARG RUST_TARGET_SUBDIR
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/x86_64" ]; then \
rustup target add x86_64-unknown-uefi; cargo build --bin sprout --profile "${RUST_PROFILE}" --target x86_64-unknown-uefi && \
cp "target/x86_64-unknown-uefi/${RUST_TARGET_SUBDIR}/sprout.efi" /sprout.efi; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ] || [ "${TARGETPLATFORM}" = "linux/aarch64" ]; then \
rustup target add aarch64-unknown-uefi; cargo build --bin sprout --profile "${RUST_PROFILE}" --target aarch64-unknown-uefi && \
cp "target/aarch64-unknown-uefi/${RUST_TARGET_SUBDIR}/sprout.efi" /sprout.efi; fi
FROM scratch AS final
COPY --from=build /sprout.efi /sprout.efi
+2 -114
View File
@@ -1,115 +1,3 @@
<p align="center"> # Sprout Website
<img src="assets/logo.png" alt="sprout logo" width="258" height="200" /> Sprout website at https://sprout.edera.dev
<h1 align="center">Sprout</h1>
Sprout is an **EXPERIMENTAL** programmable UEFI bootloader written in Rust.
Sprout is in use at Edera today in development environments and is intended to ship to production soon.
The name "sprout" is derived from our company name "Edera" which means "ivy."
Given that Sprout is the first thing intended to start on an Edera system, the name was apt.
It supports `x86_64` and `ARM64` EFI-capable systems. It is designed to require UEFI and can be chainloaded from an
existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently
have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or panics.
### Current
- [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support
- [x] Linux boot support via EFI stub
- [x] Load Linux initrd from disk
- [x] Boot first configured entry
### Roadmap
- [ ] Boot menu
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] Windows boot support (untested via chainload)
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system.
- actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment.
- values: key-value pairs that can be specified in the configuration or provided by extractors or generators.
- entries: boot entries that will be displayed to the user.
- phases: stages of the bootloader that can be hooked to run actions at specific points.
## Usage
Sprout is provided as a single EFI binary called `sprout.efi`.
It can be chainloaded from GRUB or other UEFI bootloaders or booted into directly.
Sprout will look for \sprout.toml in the root of the EFI partition it was loaded from.
See [Configuration](#configuration) for how to configure sprout.
## Configuration
Sprout is configured using a TOML file at `\sprout.toml` on the root of the EFI partition sprout was booted from.
### Boot Linux from ESP
```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"
chainload.options = ["root=/dev/sda1"]
chainload.linux-initrd = "\\initrd"
```
### Bootloader Specification (BLS) Support
```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"
```
-11
View File
@@ -1,11 +0,0 @@
# Security Policy
## Reporting Security Issues
The sprout team and community take security bugs in sprout seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
To report a security issue, you can use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/edera-dev/sprout/security/advisories/new) tab.
You may also report security issues to security@edera.dev if that is the preferred method of reporting.
The sprout team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

-17
View File
@@ -1,17 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/.." || exit 1
. "hack/common.sh"
mkdir -p "${ASSEMBLE_DIR}"
build_arch() {
ARCHITECTURE="${1}"
TARGET_ARCH="${ARCHITECTURE}" ./hack/build.sh
cp "target/final/${ARCHITECTURE}/sprout.efi" "${ASSEMBLE_DIR}/sprout-${ARCHITECTURE}.efi"
}
build_arch x86_64
build_arch aarch64
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/.." || exit 1
. "hack/common.sh"
cargo clippy --workspace --fix --allow-dirty --allow-staged --target "${HOST_ARCH}-unknown-uefi"
./hack/format.sh
-11
View File
@@ -1,11 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/.." || exit 1
. "hack/common.sh"
mkdir -p "${FINAL_DIR}"
cargo build --target "${RUST_TARGET}" --profile "${RUST_PROFILE}" --bin sprout
cp "target/${RUST_TARGET}/${RUST_TARGET_SUBDIR}/sprout.efi" "${FINAL_DIR}/sprout.efi"
-21
View File
@@ -1,21 +0,0 @@
#!/bin/sh
set -e
retry() {
for i in $(seq 1 10); do
if "${@}"; then
return 0
else
sleep "${i}"
fi
done
"${@}"
}
if [ -z "${RELEASE_TAG}" ]; then
exit 1
fi
cd target/assemble
retry gh release upload "${RELEASE_TAG}" --clobber ./*
-30
View File
@@ -1,30 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/.." || exit 1
. "hack/common.sh"
delete_image() {
IMAGE="${1}"
docker image ls -q --no-trunc --filter "reference=${DOCKER_PREFIX}/${IMAGE}" | xargs -rn1 docker image rm
}
cargo clean || true
if command -v docker >/dev/null 2>&1; then
delete_image sprout-x86_64 || true
delete_image sprout-aarch64 || true
delete_image sprout-utils-copy-direct || true
delete_image sprout-utils-copy-polyfill || true
delete_image sprout-ovmf-x86_64 || true
delete_image sprout-ovmf-aarch64 || true
delete_image sprout-initramfs-x86_64 || true
delete_image sprout-initramfs-aarch64 || true
delete_image sprout-kernel-x86_64 || true
delete_image sprout-kernel-aarch64 || true
delete_image sprout-kernel-build-x86_64 || true
delete_image sprout-kernel-build-aarch64 || true
delete_image sprout-boot-x86_64 || true
delete_image sprout-boot-aarch64 || true
fi
-43
View File
@@ -1,43 +0,0 @@
#!/bin/sh
# shellcheck disable=SC2034
set -e
DOCKER_PREFIX="ghcr.io/edera-dev/sprout"
DEFAULT_RUST_PROFILE="release"
DEFAULT_DOCKER_TAG="latest"
HOST_ARCH="$(uname -m)"
[ "${HOST_ARCH}" = "arm64" ] && HOST_ARCH="aarch64"
[ "${HOST_ARCH}" = "amd64" ] && HOST_ARCH="x86_64"
[ -z "${TARGET_ARCH}" ] && TARGET_ARCH="${1}"
{ [ -z "${TARGET_ARCH}" ] || [ "${TARGET_ARCH}" = "native" ]; } && TARGET_ARCH="${HOST_ARCH}"
[ -z "${RUST_PROFILE}" ] && RUST_PROFILE="${2}"
[ -z "${RUST_PROFILE}" ] && RUST_PROFILE="${DEFAULT_RUST_PROFILE}"
[ "${TARGET_ARCH}" = "arm64" ] && TARGET_ARCH="aarch64"
[ "${TARGET_ARCH}" = "amd64" ] && TARGET_ARCH="x86_64"
if [ "${TARGET_ARCH}" != "x86_64" ] && [ "${TARGET_ARCH}" != "aarch64" ]; then
echo "Unsupported architecture: ${TARGET_ARCH}" >/dev/stderr
exit 1
fi
[ "${RUST_PROFILE}" = "debug" ] && RUST_PROFILE="dev"
RUST_TARGET_SUBDIR="${RUST_PROFILE}"
[ "${RUST_PROFILE}" = "dev" ] && RUST_TARGET_SUBDIR="debug"
RUST_TARGET="${TARGET_ARCH}-unknown-uefi"
[ -z "${DOCKER_TAG}" ] && DOCKER_TAG="${DEFAULT_DOCKER_TAG}"
DOCKER_TARGET="linux/${TARGET_ARCH}"
FINAL_DIR="target/final/${TARGET_ARCH}"
ASSEMBLE_DIR="target/assemble"
if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
[ -f "/proc/cpuinfo" ] &&
grep -E '^flags.*:.+ vmx .*' /proc/cpuinfo >/dev/null; then
QEMU_ACCEL="kvm"
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

-76
View File
@@ -1,76 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/../.." || exit 1
. "hack/common.sh"
if [ "${SKIP_BUILD}" != "1" ]; then
./hack/dev/build.sh "${TARGET_ARCH}" "${RUST_PROFILE}"
fi
clear
set --
if [ "${TARGET_ARCH}" = "x86_64" ]; then
set -- "${@}" qemu-system-x86_64 -M q35 -cpu SandyBridge,vmx=on
elif [ "${TARGET_ARCH}" = "aarch64" ]; then
set -- "${@}" qemu-system-aarch64 -M virt -cpu cortex-a57
fi
if [ -n "${QEMU_ACCEL}" ]; then
set -- "${@}" "-accel" "kvm"
fi
if [ "${QEMU_GDB}" = "1" ]; then
set -- "${@}" "-s"
fi
if [ "${QEMU_GDB_WAIT}" = "1" ]; then
set -- "${@}" "-S"
fi
set -- "${@}" -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then
set -- "${@}" -nographic
else
if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then
set -- "${@}" -serial stdio
else
set -- "${@}" \
-device virtio-serial-pci,id=vs0 \
-chardev stdio,id=stdio0 \
-device virtconsole,chardev=stdio0,id=console0
fi
if [ "${QEMU_LEGACY_VGA}" = "1" ]; then
set -- "${@}" -vga std
else
set -- "${@}" \
-vga none \
-device "virtio-gpu,edid=on,xres=1024,yres=768"
fi
fi
rm -f "${FINAL_DIR}/ovmf-boot.fd"
cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd"
if [ "${TARGET_ARCH}" = "aarch64" ]; then
dd if=/dev/zero of="${FINAL_DIR}/ovmf-boot.fd" bs=1 count=1 seek=67108863 >/dev/null 2>&1
fi
# shellcheck disable=SC2086
set -- "${@}" \
-drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \
-device nvme,drive=disk1,serial=cafebabe
if [ "${DISK_BOOT}" = "1" ]; then
set -- "${@}" \
-drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"
else
set -- "${@}" \
-drive "if=none,file=fat:rw:${FINAL_DIR}/efi,format=raw,id=disk1"
fi
set -- "${@}" -name "sprout ${TARGET_ARCH}"
exec "${@}"
-34
View File
@@ -1,34 +0,0 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS build
ARG BUILDPLATFORM
ARG EFI_NAME
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
parted dosfstools mtools && \
rm -rf /var/lib/apt/lists/*
WORKDIR /work
COPY sprout.efi /work/${EFI_NAME}.EFI
COPY sprout.toml /work/SPROUT.TOML
COPY kernel.efi /work/KERNEL.EFI
COPY shell.efi /work/SHELL.EFI
COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG
COPY initramfs /work/INITRAMFS
COPY edera-splash.png /work/EDERA-SPLASH.PNG
RUN truncate -s128MiB sprout.img && \
parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \
parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \
parted --script sprout.img set 1 esp on > /dev/null 2>&1 && \
mkfs.vfat -F32 -n EFI sprout.img && \
mmd -i sprout.img ::/EFI && \
mmd -i sprout.img ::/EFI/BOOT && \
mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img XEN.CFG ::/EFI/BOOT/ && \
mcopy -i sprout.img SPROUT.TOML ::/ && \
mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \
mcopy -i sprout.img INITRAMFS ::/ && \
mv sprout.img /sprout.img
FROM scratch AS final
COPY --from=build /sprout.img /sprout.img
-137
View File
@@ -1,137 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/../.." || exit 1
. "hack/common.sh"
EFI_NAME="BOOTX64"
if [ "${TARGET_ARCH}" = "aarch64" ]; then
EFI_NAME="BOOTAA64"
fi
if [ -z "${SPROUT_CONFIG_NAME}" ]; then
SPROUT_CONFIG_NAME="kernel"
fi
echo "[build] ${TARGET_ARCH} ${RUST_PROFILE}"
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: docker is required to build sprout." >/dev/stderr
exit 1
fi
export DOCKER_CLI_HINTS="0"
if [ "${SKIP_CLEANUP}" != 1 ]; then
rm -rf "${FINAL_DIR}"
fi
mkdir -p "${FINAL_DIR}"
if [ "${SKIP_KERNEL_BUILD}" != "1" ] || [ "${SKIP_VM_BUILD}" != "1" ] || [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
docker build -t "${DOCKER_PREFIX}/sprout-utils-copy-direct:${DOCKER_TAG}" \
-f hack/dev/utils/Dockerfile.copy-direct hack
fi
copy_from_image_direct() {
IMAGE="${1}"
SOURCE="${2}"
TARGET="${3}"
docker run --rm -i \
--mount="type=image,source=${IMAGE},target=/image" \
"${DOCKER_PREFIX}/sprout-utils-copy-direct:${DOCKER_TAG}" cat "/image/${SOURCE}" >"${TARGET}" 2>/dev/null
}
copy_from_image_polyfill() {
IMAGE="${1}"
SOURCE="${2}"
TARGET="${3}"
docker build -t "${IMAGE}-copy-polyfill:${DOCKER_TAG}" --build-arg "TARGET_IMAGE=${IMAGE}:${DOCKER_TAG}" \
-f hack/dev/utils/Dockerfile.copy-polyfill hack
# note: the -w '//' is a workaround for Git Bash where / is magically rewritten.
docker run --rm -i -w '//' "${IMAGE}-copy-polyfill:${DOCKER_TAG}" cat "image/${SOURCE}" >"${TARGET}"
}
copy_from_image() {
if ! copy_from_image_direct "${@}" 2>/dev/null; then
echo "[warn] image mounts not supported, falling back to polyfill"
copy_from_image_polyfill "${@}"
fi
}
if [ "${SKIP_KERNEL_BUILD}" != "1" ]; then
echo "[kernel build] ${TARGET_ARCH} ${RUST_PROFILE}"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/kernel/Dockerfile hack/dev/kernel
if [ "${KERNEL_BUILD_TAG}" = "1" ]; then
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-kernel-build-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/kernel/Dockerfile --target build hack/dev/kernel
fi
copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.efi" "${FINAL_DIR}/kernel.efi"
fi
if [ "${SKIP_VM_BUILD}" != "1" ]; then
echo "[vm build] ${TARGET_ARCH} ${RUST_PROFILE}"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/vm/Dockerfile.ovmf "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "ovmf.fd" "${FINAL_DIR}/ovmf.fd"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "shell.efi" "${FINAL_DIR}/shell.efi"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/vm/Dockerfile.initramfs "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}" "initramfs" "${FINAL_DIR}/initramfs"
if [ -n "${SPROUT_XEN_EFI_OVERRIDE}" ]; then
cp "${SPROUT_XEN_EFI_OVERRIDE}" "${FINAL_DIR}/xen.efi"
else
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-xen-${TARGET_ARCH}:${DOCKER_TAG}" -f hack/dev/vm/Dockerfile.xen "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-xen-${TARGET_ARCH}" "xen.efi" "${FINAL_DIR}/xen.efi"
fi
fi
if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
echo "[sprout build] ${TARGET_ARCH} ${RUST_PROFILE}"
if [ "${SPROUT_BUILD_LOCAL}" = "1" ]; then
cargo build --target "${RUST_TARGET}" --profile "${RUST_PROFILE}" --bin sprout
cp "target/${RUST_TARGET}/${RUST_TARGET_SUBDIR}/sprout.efi" "${FINAL_DIR}/sprout.efi"
else
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}:${DOCKER_TAG}" \
--build-arg="RUST_TARGET_SUBDIR=${RUST_TARGET_SUBDIR}" \
-f Dockerfile .
copy_from_image "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}" "sprout.efi" "${FINAL_DIR}/sprout.efi"
fi
cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml"
cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg"
cp "hack/dev/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png"
mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI"
if [ -f "${FINAL_DIR}/kernel.efi" ]; then
cp "${FINAL_DIR}/kernel.efi" "${FINAL_DIR}/efi/EFI/BOOT/KERNEL.EFI"
fi
if [ -f "${FINAL_DIR}/shell.efi" ]; then
cp "${FINAL_DIR}/shell.efi" "${FINAL_DIR}/efi/EFI/BOOT/SHELL.EFI"
fi
if [ -f "${FINAL_DIR}/xen.efi" ]; then
cp "${FINAL_DIR}/xen.efi" "${FINAL_DIR}/efi/EFI/BOOT/XEN.EFI"
fi
if [ -f "${FINAL_DIR}/xen.cfg" ]; then
cp "${FINAL_DIR}/xen.cfg" "${FINAL_DIR}/efi/EFI/BOOT/XEN.CFG"
fi
cp "${FINAL_DIR}/sprout.toml" "${FINAL_DIR}/efi/SPROUT.TOML"
cp "${FINAL_DIR}/edera-splash.png" "${FINAL_DIR}/efi/EDERA-SPLASH.PNG"
cp "${FINAL_DIR}/initramfs" "${FINAL_DIR}/efi/INITRAMFS"
fi
if [ "${SKIP_BOOT_BUILD}" != "1" ]; then
echo "[boot build] ${TARGET_ARCH} ${RUST_PROFILE}"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-boot-${TARGET_ARCH}:${DOCKER_TAG}" \
--build-arg "EFI_NAME=${EFI_NAME}" \
-f hack/dev/boot/Dockerfile "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-boot-${TARGET_ARCH}" "sprout.img" "${FINAL_DIR}/sprout.img"
fi
-15
View File
@@ -1,15 +0,0 @@
version = 1
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi"
[actions.boot-edera]
edera.xen = "$boot\\EFI\\BOOT\\xen.efi"
edera.xen-options = ["clocksource=tsc", "smp=on", "smt=on", "ioapic_ack=new", "dom0_vcpus_pin=on", "spec-ctrl=gds-mit=no", "noreboot", "console=com1"]
edera.kernel = "$boot\\EFI\\BOOT\\kernel.efi"
edera.kernel-options = ["console=hvc0", "edera-xen=yes"]
edera.initrd = "$boot\\initramfs"
[entries.edera]
title = "Boot Edera"
actions = ["boot-edera"]
-13
View File
@@ -1,13 +0,0 @@
version = 1
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi"
[actions.chainload-kernel]
chainload.path = "$boot\\EFI\\BOOT\\kernel.efi"
chainload.options = ["console=hvc0"]
chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel]
title = "Boot Linux"
actions = ["chainload-kernel"]
-7
View File
@@ -1,7 +0,0 @@
[global]
default=sprout
[sprout]
options=clocksource=tsc smp=on smt=on ioapic_ack=new dom0_vcpus_pin=on spec-ctrl=gds-mit=no noreboot console=com1
kernel=\EFI\BOOT\KERNEL.EFI console=hvc0
ramdisk=\initramfs
-11
View File
@@ -1,11 +0,0 @@
version = 1
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi"
[actions.chainload-xen]
chainload.path = "$boot\\EFI\\BOOT\\xen.efi"
[entries.xen]
title = "Boot Xen"
actions = ["chainload-xen"]
-37
View File
@@ -1,37 +0,0 @@
ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.2.tar.xz
ARG KERNEL_CHECKSUM=sha256:fdebcb065065f5c1b8dc68a6fb59cda50cdddbf9103d207c2196d55ea764f57f
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS buildenv
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
build-essential squashfs-tools python3-yaml \
patch diffutils sed mawk findutils zstd \
python3 python3-packaging curl rsync cpio \
flex bison pahole libssl-dev libelf-dev bc kmod && \
rm -rf /var/lib/apt/lists/*
ARG BUILDPLATFORM
RUN if [ "${BUILDPLATFORM}" = "linux/amd64" ] || [ "${BUILDPLATFORM}" = "linux/x86_64" ]; then \
apt-get update && apt-get install -y linux-headers-amd64 gcc-aarch64-linux-gnu && rm -rf /var/lib/apt/lists/*; fi
RUN if [ "${BUILDPLATFORM}" = "linux/arm64" ] || [ "${BUILDPLATFORM}" = "linux/aarch64" ]; then \
apt-get update && apt-get install -y linux-headers-arm64 gcc-x86-64-linux-gnu && rm -rf /var/lib/apt/lists/*; fi
RUN useradd -ms /bin/sh build
COPY --chown=build:build docker-build.sh /build/docker-build.sh
USER build
WORKDIR /build
FROM scratch AS source
ARG KERNEL_SOURCE_URL
ARG KERNEL_CHECKSUM
ADD --checksum=${KERNEL_CHECKSUM} ${KERNEL_SOURCE_URL} /src.tar.xz
FROM --platform=$BUILDPLATFORM buildenv AS build
COPY --from=source --chown=build:build /src.tar.xz /build/src.tar.xz
RUN mkdir /build/src && tar -C /build/src --strip-components=1 -xf /build/src.tar.xz && rm /build/src.tar.xz
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ENV BUILDPLATFORM=${BUILDPLATFORM}
ENV TARGETPLATFORM=${TARGETPLATFORM}
WORKDIR /build/src
RUN /build/docker-build.sh
FROM scratch AS final
COPY --from=build /build/src/kernel.image /kernel.efi
-49
View File
@@ -1,49 +0,0 @@
#!/bin/sh
set -e
TARGET_KARCH=""
TARGET_SARCH=""
MAYBE_CROSS_COMPILE=""
CURRENT_SARCH="$(uname -m)"
[ "${CURRENT_SARCH}" = "amd64" ] && CURRENT_SARCH="x86_64"
[ "${CURRENT_SARCH}" = "arm64" ] && CURRENT_SARCH="aarch64"
if [ "${TARGETPLATFORM}" = "linux/aarch64" ] || [ "${TARGETPLATFORM}" = "linux/arm64" ]; then
TARGET_KARCH="arm64"
TARGET_SARCH="aarch64"
if [ "${CURRENT_SARCH}" != "${TARGET_SARCH}" ]; then
MAYBE_CROSS_COMPILE="aarch64-linux-gnu-"
fi
elif [ "${TARGETPLATFORM}" = "linux/x86_64" ] || [ "${TARGETPLATFORM}" = "linux/amd64" ]; then
TARGET_KARCH="x86_64"
TARGET_SARCH="x86_64"
if [ "${CURRENT_SARCH}" != "${TARGET_SARCH}" ]; then
MAYBE_CROSS_COMPILE="x86_64-linux-gnu-"
fi
else
echo "Unknown platform: ${TARGETPLATFORM}" >/dev/stderr
exit 1
fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig
if [ "${TARGET_KARCH}" = "x86_64" ]
then
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config
./scripts/config -e XEN_PV
./scripts/config -e XEN_PV_DOM0
fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2yesconfig
./scripts/config -e DRM_VIRTIO_GPU
./scripts/config -e FRAMEBUFFER_CONSOLE
./scripts/config -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY
./scripts/config -e XEN_DOM0
make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}"
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d < "arch/arm64/boot/Image.gz" > kernel.image
exit 0
-1
View File
@@ -1 +0,0 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6
-4
View File
@@ -1,4 +0,0 @@
ARG TARGET_IMAGE=scratch
FROM ${TARGET_IMAGE} AS image
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS final
COPY --from=image / /image
-27
View File
@@ -1,27 +0,0 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS rootfs
RUN apk --no-cache add alpine-base tzdata
RUN rc-update add devfs sysinit && \
rc-update add dmesg sysinit && \
rc-update add mdev sysinit && \
rc-update add cgroups sysinit && \
rc-update add sysctl boot && \
rc-update add hostname boot && \
rc-update add bootmisc boot && \
rc-update add syslog boot && \
rc-update add mount-ro shutdown && \
rc-update add killprocs shutdown && \
rc-update add savecache shutdown && \
ln -s /sbin/init /init && \
echo 'root:root' | chpasswd && \
echo 'sprout' > /etc/hostname && \
echo '' > /etc/motd && \
ln -s /usr/share/zoneinfo/UTC /etc/localtime && \
echo 'hvc0::respawn:/sbin/getty -L hvc0 115200 vt100' >> /etc/inittab
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
COPY --from=rootfs / /rootfs
WORKDIR /rootfs
RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs
FROM scratch AS final
COPY --from=build /initramfs /initramfs
-10
View File
@@ -1,10 +0,0 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
ARG TARGETPLATFORM
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/x86_64" ]; then \
apk --no-cache add ovmf edk2-shell; cp /usr/share/ovmf/bios.bin /ovmf.fd; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ] || [ "${TARGETPLATFORM}" = "linux/aarch64" ]; then \
apk --no-cache add aavmf edk2-shell; cp /usr/share/AAVMF/QEMU_EFI.fd /ovmf.fd; fi
FROM scratch AS final
COPY --from=build /ovmf.fd /ovmf.fd
COPY --from=build /usr/share/edk2-shell/ShellFull.efi /shell.efi
-6
View File
@@ -1,6 +0,0 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build
ARG TARGETPLATFORM
RUN apk add --no-cache xen-hypervisor && cp /usr/lib/efi/xen.efi /xen.efi
FROM scratch AS final
COPY --from=build /xen.efi /xen.efi
-7
View File
@@ -1,7 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")/.." || exit 1
cargo fmt --all
shfmt -w hack/**/*.sh
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Sprout Bootloader</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="shortcut icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div class="content-column">
<div id="logo"></div>
<h1>Sprout</h1>
<div id="logo-subtitle">
Experimental Modern UEFI Bootloader in Rust
</div>
<h3>Links</h3>
<div id="links">
<a href="https://github.com/edera-dev/sprout">GitHub</a>
</div>
</div>
</body>
</html>
-4
View File
@@ -1,4 +0,0 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "rust-std", "clippy"]
targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"]
-49
View File
@@ -1,49 +0,0 @@
use crate::context::SproutContext;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
pub mod chainload;
pub mod edera;
pub mod print;
#[cfg(feature = "splash")]
pub mod splash;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct ActionDeclaration {
#[serde(default)]
pub chainload: Option<chainload::ChainloadConfiguration>,
#[serde(default)]
pub print: Option<print::PrintConfiguration>,
#[serde(default)]
#[cfg(feature = "splash")]
pub splash: Option<splash::SplashConfiguration>,
#[serde(default, rename = "edera")]
pub edera: Option<edera::EderaConfiguration>,
}
pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()> {
let Some(action) = context.root().actions().get(name.as_ref()) else {
bail!("unknown action '{}'", name.as_ref());
};
let context = context.finalize().freeze();
if let Some(chainload) = &action.chainload {
chainload::chainload(context.clone(), chainload)?;
return Ok(());
} else if let Some(print) = &action.print {
print::print(context.clone(), print)?;
return Ok(());
} else if let Some(edera) = &action.edera {
edera::edera(context.clone(), edera)?;
}
#[cfg(feature = "splash")]
if let Some(splash) = &action.splash {
splash::splash(context.clone(), splash)?;
return Ok(());
}
bail!("unknown action configuration");
}
-94
View File
@@ -1,94 +0,0 @@
use crate::context::SproutContext;
use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use anyhow::{Context, Result, bail};
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct ChainloadConfiguration {
pub path: String,
#[serde(default)]
pub options: Vec<String>,
#[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>,
}
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
let sprout_image = uefi::boot::image_handle();
let resolved = utils::resolve_path(
context.root().loaded_image_path()?,
&context.stamp(&configuration.path),
)
.context("unable to resolve chainload path")?;
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &resolved.full_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
)
.context("unable to load image")?;
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?;
let options = configuration
.options
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() {
let options = Box::new(
CString16::try_from(&options[..])
.context("unable to convert chainloader options to CString16")?,
);
info!("options: {}", options);
if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large");
}
// SAFETY: option size is checked to validate it is safe to pass.
// Additionally, the pointer is allocated and retained on heap, which makes
// passing the `options` pointer safe to the next image.
unsafe {
loaded_image_protocol
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32);
}
options_holder = Some(options);
}
let mut initrd_handle = None;
if let Some(ref linux_initrd) = configuration.linux_initrd {
let initrd_path = context.stamp(linux_initrd);
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path)
.context("unable to read linux initrd")?;
let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
.context("unable to register linux initrd")?;
initrd_handle = Some(handle);
}
let (base, size) = loaded_image_protocol.info();
info!("loaded image: base={:#x} size={:#x}", base.addr(), size);
let result = uefi::boot::start_image(image).context("unable to start image");
if let Some(initrd_handle) = initrd_handle
&& let Err(error) = initrd_handle.unregister()
{
error!("unable to unregister linux initrd: {}", error);
}
result.context("unable to start image")?;
drop(options_holder);
Ok(())
}
-103
View File
@@ -1,103 +0,0 @@
use std::rc::Rc;
use anyhow::{Context, Result};
use log::error;
use serde::{Deserialize, Serialize};
use uefi::Guid;
use crate::{
actions::{self, chainload::ChainloadConfiguration},
context::SproutContext,
utils::{
self,
media_loader::{
MediaLoaderHandle,
constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
},
},
},
};
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct EderaConfiguration {
pub xen: String,
pub kernel: String,
#[serde(default)]
pub initrd: Option<String>,
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
#[serde(default, rename = "xen-options")]
pub xen_options: Vec<String>,
}
fn build_xen_config(configuration: &EderaConfiguration) -> String {
[
"[global]".to_string(),
"default=sprout".to_string(),
"[sprout]".to_string(),
format!("options={}", configuration.xen_options.join(" ")),
format!("kernel=stub {}", configuration.kernel_options.join(" ")),
"".to_string(), // required or else the last line will be ignored
]
.join("\n")
}
fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> {
MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice())
.context(format!("unable to register {} media loader", what)) /* */
}
fn register_media_loader_file(
context: &Rc<SproutContext>,
guid: Guid,
what: &str,
path: &str,
) -> Result<MediaLoaderHandle> {
let path = context.stamp(path);
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
.context(format!("unable to read {} file", what))?;
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
.context(format!("unable to register {} media loader", what))?;
Ok(handle)
}
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
let config = build_xen_config(configuration);
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
.context("unable to register config media loader")?;
let kernel = register_media_loader_file(
&context,
XEN_EFI_KERNEL_MEDIA_GUID,
"kernel",
&configuration.kernel,
)
.context("unable to register kernel media loader")?;
let mut media_loaders = vec![config, kernel];
if let Some(ref initrd) = configuration.initrd {
let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?;
media_loaders.push(initrd);
}
let result = actions::chainload::chainload(
context.clone(),
&ChainloadConfiguration {
path: configuration.xen.clone(),
options: vec![],
linux_initrd: None,
},
)
.context("unable to chainload to xen");
for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error);
}
}
result
}
-15
View File
@@ -1,15 +0,0 @@
use crate::context::SproutContext;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PrintConfiguration {
#[serde(default)]
pub text: String,
}
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
println!("{}", context.stamp(&configuration.text));
Ok(())
}
-106
View File
@@ -1,106 +0,0 @@
use crate::context::SproutContext;
use crate::utils::framebuffer::Framebuffer;
use crate::utils::read_file_contents;
use anyhow::{Context, Result};
use image::imageops::{FilterType, resize};
use image::math::Rect;
use image::{DynamicImage, ImageBuffer, ImageFormat, ImageReader, Rgba};
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::rc::Rc;
use std::time::Duration;
use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct SplashConfiguration {
pub image: String,
#[serde(default = "default_splash_time")]
pub time: u32,
}
pub fn default_splash_time() -> u32 {
0
}
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
.context("unable to get graphics output")?;
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
.context("unable to open graphics output")
}
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
let input = Rect {
x: 0,
y: 0,
width: image.width(),
height: image.height(),
};
let input_ratio = input.width as f32 / input.height as f32;
let frame_ratio = frame.width as f32 / frame.height as f32;
let mut output = Rect {
x: 0,
y: 0,
width: frame.width,
height: frame.height,
};
if input_ratio < frame_ratio {
output.width = (frame.height as f32 * input_ratio).floor() as u32;
output.height = frame.height;
output.x = frame.x + (frame.width - output.width) / 2;
output.y = frame.y;
} else {
output.width = frame.width;
output.height = (frame.width as f32 / input_ratio).floor() as u32;
output.x = frame.x;
output.y = frame.y + (frame.height - output.height) / 2;
}
output
}
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let image = image.to_rgba8();
resize(&image, frame.width, frame.height, FilterType::Lanczos3)
}
fn draw(image: DynamicImage) -> Result<()> {
let mut gop = setup_graphics()?;
let (width, height) = gop.current_mode_info().resolution();
let display_frame = Rect {
x: 0,
y: 0,
width: width as _,
height: height as _,
};
let fit = fit_to_frame(&image, display_frame);
let image = resize_to_fit(&image, fit);
let mut framebuffer = Framebuffer::new(width, height);
for (x, y, pixel) in image.enumerate_pixels() {
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
continue;
};
fb.red = pixel[0];
fb.green = pixel[1];
fb.blue = pixel[2];
}
framebuffer.blit(&mut gop)?;
Ok(())
}
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
let image = context.stamp(&configuration.image);
let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode()
.context("unable to decode splash image")?;
draw(image)?;
std::thread::sleep(Duration::from_secs(configuration.time as u64));
Ok(())
}
-50
View File
@@ -1,50 +0,0 @@
use crate::actions::ActionDeclaration;
use crate::drivers::DriverDeclaration;
use crate::entries::EntryDeclaration;
use crate::extractors::ExtractorDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::phases::PhasesConfiguration;
use crate::utils;
use anyhow::Context;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::ops::Deref;
use uefi::proto::device_path::LoadedImageDevicePath;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct RootConfiguration {
#[serde(default = "latest_version")]
pub version: u32,
#[serde(default)]
pub values: BTreeMap<String, String>,
#[serde(default)]
pub drivers: BTreeMap<String, DriverDeclaration>,
#[serde(default)]
pub extractors: BTreeMap<String, ExtractorDeclaration>,
#[serde(default)]
pub actions: BTreeMap<String, ActionDeclaration>,
#[serde(default)]
pub entries: BTreeMap<String, EntryDeclaration>,
#[serde(default)]
pub generators: BTreeMap<String, GeneratorDeclaration>,
#[serde(default)]
pub phases: PhasesConfiguration,
}
pub fn latest_version() -> u32 {
1
}
pub fn load() -> Result<RootConfiguration> {
let current_image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
let path = current_image_device_path_protocol.deref().to_boxed();
let content = utils::read_file_contents(&path, "sprout.toml")
.context("unable to read sprout.toml file")?;
let config: RootConfiguration =
toml::from_slice(&content).context("unable to parse sprout.toml file")?;
Ok(config)
}
-150
View File
@@ -1,150 +0,0 @@
use crate::actions::ActionDeclaration;
use anyhow::Result;
use anyhow::anyhow;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use uefi::proto::device_path::DevicePath;
#[derive(Default)]
pub struct RootContext {
actions: BTreeMap<String, ActionDeclaration>,
loaded_image_path: Option<Box<DevicePath>>,
}
impl RootContext {
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self {
RootContext {
actions: BTreeMap::new(),
loaded_image_path: Some(loaded_image_device_path),
}
}
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
&self.actions
}
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
&mut self.actions
}
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
self.loaded_image_path
.as_deref()
.ok_or_else(|| anyhow!("no loaded image path"))
}
}
pub struct SproutContext {
root: Rc<RootContext>,
parent: Option<Rc<SproutContext>>,
values: BTreeMap<String, String>,
}
impl SproutContext {
pub fn new(root: RootContext) -> Self {
Self {
root: Rc::new(root),
parent: None,
values: BTreeMap::new(),
}
}
pub fn root(&self) -> &RootContext {
self.root.as_ref()
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
self.values.get(key.as_ref()).or_else(|| {
self.parent
.as_ref()
.and_then(|parent| parent.get(key.as_ref()))
})
}
pub fn all_keys(&self) -> Vec<String> {
let mut keys = BTreeSet::new();
for key in self.values.keys() {
keys.insert(key.clone());
}
if let Some(parent) = &self.parent {
keys.extend(parent.all_keys());
}
keys.into_iter().collect()
}
pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new();
for key in self.all_keys() {
values.insert(key.clone(), self.get(key).cloned().unwrap_or_default());
}
values
}
pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) {
self.values
.insert(key.as_ref().to_string(), value.to_string());
}
pub fn insert(&mut self, values: &BTreeMap<String, String>) {
for (key, value) in values {
self.values.insert(key.clone(), value.clone());
}
}
pub fn fork(self: &Rc<SproutContext>) -> Self {
Self {
root: self.root.clone(),
parent: Some(self.clone()),
values: BTreeMap::new(),
}
}
pub fn freeze(self) -> Rc<SproutContext> {
Rc::new(self)
}
pub fn finalize(&self) -> SproutContext {
let mut current_values = self.all_values();
loop {
let mut did_change = false;
let mut values = BTreeMap::new();
for (key, value) in &current_values {
let (changed, result) = Self::stamp_values(&current_values, value);
if changed {
did_change = true;
}
values.insert(key.clone(), result);
}
current_values = values;
if !did_change {
break;
}
}
Self {
root: self.root.clone(),
parent: None,
values: current_values,
}
}
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;
for (key, value) in values {
let next_result = result.replace(&format!("${key}"), value);
if result != next_result {
did_change = true;
}
result = next_result;
}
(did_change, result)
}
pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1
}
}
-72
View File
@@ -1,72 +0,0 @@
use crate::context::SproutContext;
use crate::utils;
use anyhow::{Context, Result};
use log::info;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
use uefi::boot::SearchType;
use uefi::proto::device_path::LoadedImageDevicePath;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct DriverDeclaration {
pub path: String,
}
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
let sprout_image = uefi::boot::image_handle();
let image_device_path_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(sprout_image)
.context("unable to open loaded image device path protocol")?;
let mut full_path = utils::device_path_root(&image_device_path_protocol)?;
full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path);
let device_path = utils::text_to_device_path(&full_path)?;
let image = uefi::boot::load_image(
sprout_image,
uefi::boot::LoadImageSource::FromDevicePath {
device_path: &device_path,
boot_policy: uefi::proto::BootPolicy::ExactMatch,
},
)
.context("unable to load image")?;
uefi::boot::start_image(image).context("unable to start driver image")?;
Ok(())
}
fn reconnect() -> Result<()> {
let handles = uefi::boot::locate_handle_buffer(SearchType::AllHandles)
.context("unable to locate handles buffer")?;
for handle in handles.iter() {
// ignore result as there is nothing we can do if it doesn't work.
let _ = uefi::boot::connect_controller(*handle, None, None, true);
}
Ok(())
}
pub fn load(
context: Rc<SproutContext>,
drivers: &BTreeMap<String, DriverDeclaration>,
) -> Result<()> {
if drivers.is_empty() {
return Ok(());
}
info!("loading drivers");
for (name, driver) in drivers {
load_driver(context.clone(), driver).context(format!("unable to load driver: {}", name))?;
}
reconnect().context("unable to reconnect drivers")?;
info!("loaded drivers");
Ok(())
}
-11
View File
@@ -1,11 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct EntryDeclaration {
pub title: String,
#[serde(default)]
pub actions: Vec<String>,
#[serde(default)]
pub values: BTreeMap<String, String>,
}
-21
View File
@@ -1,21 +0,0 @@
use crate::context::SproutContext;
use crate::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
pub mod filesystem_device_match;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct ExtractorDeclaration {
#[serde(default, rename = "filesystem-device-match")]
pub filesystem_device_match: Option<FilesystemDeviceMatchExtractor>,
}
pub fn extract(context: Rc<SproutContext>, extractor: &ExtractorDeclaration) -> Result<String> {
if let Some(filesystem) = &extractor.filesystem_device_match {
filesystem_device_match::extract(context, filesystem)
} else {
bail!("unknown extractor configuration");
}
}
-137
View File
@@ -1,137 +0,0 @@
use crate::context::SproutContext;
use crate::utils;
use anyhow::{Context, Result, anyhow, bail};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::rc::Rc;
use std::str::FromStr;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid};
use uefi_raw::Status;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor {
#[serde(default, rename = "has-label")]
pub has_label: Option<String>,
#[serde(default, rename = "has-item")]
pub has_item: Option<String>,
#[serde(default, rename = "has-partition-uuid")]
pub has_partition_uuid: Option<String>,
#[serde(default, rename = "has-partition-type-uuid")]
pub has_partition_type_uuid: Option<String>,
#[serde(default)]
pub fallback: Option<String>,
}
pub fn extract(
context: Rc<SproutContext>,
extractor: &FilesystemDeviceMatchExtractor,
) -> Result<String> {
let handles = uefi::boot::find_handles::<SimpleFileSystem>()
.context("unable to find filesystem handles")?;
for handle in handles {
let mut has_match = false;
let partition_info = {
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle);
match partition_info {
Ok(partition_info) => {
if let Some(gpt) = partition_info.gpt_partition_entry() {
let uuid = gpt.unique_partition_guid;
let type_uuid = gpt.partition_type_guid;
Some((uuid, type_uuid.0))
} else {
None
}
}
Err(error) => {
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED
{
None
} else {
Err(error).context("unable to open filesystem partition info")?;
None
}
}
}
};
if let Some((partition_uuid, _partition_type_guid)) = partition_info
&& let Some(ref has_partition_uuid) = extractor.has_partition_uuid
{
let parsed_uuid = Guid::from_str(has_partition_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
if partition_uuid != parsed_uuid {
continue;
}
has_match = true;
}
if let Some((_partition_uuid, partition_type_guid)) = partition_info
&& let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid
{
let parsed_uuid = Guid::from_str(has_partition_type_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
if partition_type_guid != parsed_uuid {
continue;
}
has_match = true;
}
let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem protocol")?;
if let Some(ref label) = extractor.has_label {
let want_label = CString16::try_from(context.stamp(label).as_str())
.context("unable to convert label to CString16")?;
let mut root = filesystem
.open_volume()
.context("unable to open filesystem volume")?;
let label = root
.get_boxed_info::<FileSystemVolumeLabel>()
.context("unable to get filesystem volume label")?;
if label.volume_label() != want_label {
continue;
}
has_match = true;
}
if let Some(ref item) = extractor.has_item {
let want_item = CString16::try_from(context.stamp(item).as_str())
.context("unable to convert item to CString16")?;
let mut filesystem = FileSystem::new(filesystem);
let metadata = filesystem.metadata(Path::new(&want_item));
if metadata.is_err() {
continue;
}
let metadata = metadata?;
if !(metadata.is_directory() || metadata.is_regular_file()) {
continue;
}
has_match = true;
}
if !has_match {
continue;
}
let path = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to open filesystem device path")?;
let path = path.deref();
return utils::device_path_root(path).context("unable to get device path root");
}
if let Some(fallback) = &extractor.fallback {
return Ok(fallback.clone());
}
bail!("unable to find matching filesystem")
}
-32
View File
@@ -1,32 +0,0 @@
use crate::context::SproutContext;
use crate::entries::EntryDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result;
use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
pub mod bls;
pub mod matrix;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct GeneratorDeclaration {
#[serde(default)]
pub matrix: Option<MatrixConfiguration>,
#[serde(default)]
pub bls: Option<BlsConfiguration>,
}
pub fn generate(
context: Rc<SproutContext>,
generator: &GeneratorDeclaration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {
bls::generate(context, bls)
} else {
bail!("unknown generator configuration");
}
}
-82
View File
@@ -1,82 +0,0 @@
use crate::context::SproutContext;
use crate::entries::EntryDeclaration;
use crate::generators::bls::entry::BlsEntry;
use crate::utils;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use std::str::FromStr;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem;
mod entry;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct BlsConfiguration {
pub entry: EntryDeclaration,
#[serde(default = "default_bls_path")]
pub path: String,
}
fn default_bls_path() -> String {
"\\loader\\entries".to_string()
}
pub fn generate(
context: Rc<SproutContext>,
bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
let mut entries = Vec::new();
let path = context.stamp(&bls.path);
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
.context("unable to resolve bls path")?;
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs);
let sub_text_path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert subpath to string")?;
let entries_path = Path::new(&sub_text_path);
let entries_iter = fs
.read_dir(entries_path)
.context("unable to read bls entries")?;
for entry in entries_iter {
let entry = entry?;
if !entry.is_regular_file() {
continue;
}
let name = entry.file_name().to_string();
if !name.ends_with(".conf") {
continue;
}
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str())
.context("unable to construct full entry path")?;
let full_entry_path = Path::new(&full_entry_path);
let content = fs
.read(full_entry_path)
.context("unable to read bls file")?;
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
if !entry.is_valid() {
continue;
}
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());
entries.push((context.freeze(), bls.entry.clone()));
}
Ok(entries)
}
-91
View File
@@ -1,91 +0,0 @@
use anyhow::{Error, Result};
use std::str::FromStr;
#[derive(Default, Debug, Clone)]
pub struct BlsEntry {
pub title: Option<String>,
pub options: Option<String>,
pub linux: Option<String>,
pub initrd: Option<String>,
pub efi: Option<String>,
}
impl FromStr for BlsEntry {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
for line in input.lines() {
let line = line.trim();
let Some((key, value)) = line.split_once(" ") else {
continue;
};
match key {
"title" => {
title = Some(value.trim().to_string());
}
"options" => {
options = Some(value.trim().to_string());
}
"linux" => {
linux = Some(value.trim().to_string());
}
"initrd" => {
initrd = Some(value.trim().to_string());
}
"efi" => {
efi = Some(value.trim().to_string());
}
_ => {
continue;
}
}
}
Ok(BlsEntry {
title,
options,
linux,
initrd,
efi,
})
}
}
impl BlsEntry {
pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some()
}
pub fn chainload_path(&self) -> Option<String> {
self.linux
.clone()
.or(self.efi.clone())
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
}
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
}
pub fn options(&self) -> Option<String> {
self.options.clone()
}
pub fn title(&self) -> Option<String> {
self.title.clone()
}
}
-60
View File
@@ -1,60 +0,0 @@
use crate::context::SproutContext;
use crate::entries::EntryDeclaration;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct MatrixConfiguration {
#[serde(default)]
pub entry: EntryDeclaration,
#[serde(default)]
pub values: BTreeMap<String, Vec<String>>,
}
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
for (key, values) in items {
let mut new_result = Vec::new();
for combination in &result {
for value in &values {
let mut new_combination = combination.clone();
new_combination.insert(key.clone(), value.clone());
new_result.push(new_combination);
}
}
result = new_result;
}
result.into_iter().filter(|item| !item.is_empty()).collect()
}
pub fn generate(
context: Rc<SproutContext>,
matrix: &MatrixConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new();
for combination in combinations {
let mut context = context.fork();
context.insert(&combination);
let context = context.freeze();
let mut entry = matrix.entry.clone();
entry.title = context.stamp(&entry.title);
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
entries.push((context, entry));
}
Ok(entries)
}
-109
View File
@@ -1,109 +0,0 @@
#![feature(uefi_std)]
use crate::context::{RootContext, SproutContext};
use crate::phases::phase;
use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap;
use std::ops::Deref;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
pub mod actions;
pub mod config;
pub mod context;
pub mod drivers;
pub mod entries;
pub mod extractors;
pub mod generators;
pub mod phases;
pub mod setup;
pub mod utils;
fn main() -> Result<()> {
setup::init()?;
let config = config::load()?;
if config.version != config::latest_version() {
bail!("unsupported configuration version: {}", config.version);
}
let mut root = {
let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::<
LoadedImageDevicePath,
>(uefi::boot::image_handle())
.context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path)
};
root.actions_mut().extend(config.actions.clone());
let mut context = SproutContext::new(root);
context.insert(&config.values);
let context = context.freeze();
phase(context.clone(), &config.phases.early).context("unable to execute early phase")?;
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors {
let value = extractors::extract(context.clone(), extractor)
.context(format!("unable to extract value {}", name))?;
info!("extracted value {}: {}", name, value);
extracted.insert(name.clone(), value);
}
let mut context = context.fork();
context.insert(&extracted);
let context = context.freeze();
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
let mut all_entries = Vec::new();
for (_name, entry) in config.entries {
all_entries.push((context.clone(), entry));
}
for (_name, generator) in config.generators {
let context = context.fork().freeze();
for entry in generators::generate(context.clone(), &generator)? {
all_entries.push(entry);
}
}
let mut final_entries = Vec::new();
for (context, entry) in all_entries {
let mut context = context.fork();
context.insert(&entry.values);
let context = context.finalize().freeze();
final_entries.push((context, entry));
}
info!("entries:");
for (index, (context, entry)) in final_entries.iter().enumerate() {
let title = context.stamp(&entry.title);
info!(" entry {}: {}", index + 1, title);
}
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
let index = 1;
let (context, entry) = &final_entries[index - 1];
for action in &entry.actions {
let action = context.stamp(action);
actions::execute(context.clone(), &action)
.context(format!("unable to execute action '{}'", action))?;
}
Ok(())
}
-38
View File
@@ -1,38 +0,0 @@
use crate::actions;
use crate::context::SproutContext;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhasesConfiguration {
#[serde(default)]
pub early: Vec<PhaseConfiguration>,
#[serde(default)]
pub startup: Vec<PhaseConfiguration>,
#[serde(default)]
pub late: Vec<PhaseConfiguration>,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct PhaseConfiguration {
#[serde(default)]
pub actions: Vec<String>,
#[serde(default)]
pub values: BTreeMap<String, String>,
}
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> anyhow::Result<()> {
for item in phase {
let mut context = context.fork();
context.insert(&item.values);
let context = context.freeze();
for action in item.actions.iter() {
actions::execute(context.clone(), action)
.context(format!("unable to execute action '{}'", action))?;
}
}
Ok(())
}
-24
View File
@@ -1,24 +0,0 @@
use anyhow::{Context, Result};
use std::os::uefi as uefi_std;
/// Initializes the UEFI environment.
///
/// This fetches the system table and current image handle from uefi_std and injects
/// them into the uefi crate.
pub fn init() -> Result<()> {
let system_table = uefi_std::env::system_table();
let image_handle = uefi_std::env::image_handle();
// SAFETY: The uefi variables above come from the Rust std.
// These variables are nonnull and calling the uefi crates with these values is validated
// to be corrected by hand.
unsafe {
uefi::table::set_system_table(system_table.as_ptr().cast());
let handle = uefi::Handle::from_ptr(image_handle.as_ptr().cast())
.context("unable to resolve image handle")?;
uefi::boot::set_image_handle(handle);
}
uefi::helpers::init().context("unable to initialize uefi")?;
Ok(())
}
-154
View File
@@ -1,154 +0,0 @@
use anyhow::{Context, Result};
use std::ops::Deref;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::{CString16, Handle};
pub mod framebuffer;
pub mod media_loader;
/// Parses the input [path] as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>(
uefi::boot::get_handle_for_protocol::<DevicePathFromText>()
.context("no device path from text protocol")?,
)
.context("unable to open device path from text protocol")?;
device_path_from_text
.convert_text_to_device_path(&path)
.context("unable to convert text to device path")
}
/// Grabs the root part of the [path].
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
pub fn device_path_root(path: &DevicePath) -> Result<String> {
let mut path = path
.node_iter()
.filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.unwrap_or(false)
{
Some(item.unwrap_or_default())
} else {
None
}
})
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join("/");
path.push('/');
Ok(path)
}
/// Grabs the part of the [path] after the root.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "\EFI\BOOT\BOOTX64.efi"
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let path = path
.node_iter()
.filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.unwrap_or(false)
{
None
} else {
Some(item.unwrap_or_default())
}
})
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join("\\");
Ok(path)
}
/// Represents the components of a resolved path.
pub struct ResolvedPath {
/// The root path of the resolved path. This is the device itself.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
pub root_path: Box<DevicePath>,
/// The subpath of the resolved path. This is the path to the file.
/// For example, "\EFI\BOOT\BOOTX64.efi"
pub sub_path: Box<DevicePath>,
/// The full path of the resolved path. This is the safest path to use.
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
pub full_path: Box<DevicePath>,
/// The handle of the filesystem containing the path.
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
pub filesystem_handle: Handle,
}
/// Resolve a path specified by [input] to its various components.
/// Uses [default_root_path] as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
let path_has_device = path
.node_iter()
.next()
.map(|it| {
it.to_string(DisplayOnly(false), AllowShortcuts(false))
.unwrap_or_default()
})
.map(|it| it.to_string().contains("("))
.unwrap_or(false);
if !path_has_device {
let mut input = input.to_string();
if !input.starts_with("\\") {
input.insert(0, '\\');
}
input.insert_str(
0,
device_path_root(default_root_path)
.context("unable to get loaded image device root")?
.as_str(),
);
path = text_to_device_path(input.as_str()).context("unable to convert text to path")?;
}
let path = path.to_boxed();
let root = device_path_root(path.as_ref()).context("unable to convert root to path")?;
let root_path = text_to_device_path(root.as_str())
.context("unable to convert root to path")?
.to_boxed();
let mut root_path = root_path.as_ref();
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut root_path)
.context("unable to locate filesystem device path")?;
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
Ok(ResolvedPath {
root_path: root_path.to_boxed(),
sub_path: text_to_device_path(subpath.as_str())?.to_boxed(),
full_path: path,
filesystem_handle: handle,
})
}
/// Read the contents of a file at the location specified with the [input] path.
/// Internally, this uses [resolve_path] to resolve the path to its various components.
/// [resolve_path] is passed the [default_root_path] which should specify a base root.
///
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with
/// the filesystem handle protocol acquired.
pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result<Vec<u8>> {
let resolved = resolve_path(default_root_path, input)?;
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open filesystem protocol")?;
let mut fs = FileSystem::new(fs);
let path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
let content = fs.read(Path::new(&path));
content.context("unable to read file contents")
}
-33
View File
@@ -1,33 +0,0 @@
use anyhow::{Context, Result};
use uefi::proto::console::gop::{BltOp, BltPixel, BltRegion, GraphicsOutput};
pub struct Framebuffer {
width: usize,
height: usize,
pixels: Vec<BltPixel>,
}
impl Framebuffer {
pub fn new(width: usize, height: usize) -> Self {
Framebuffer {
width,
height,
pixels: vec![BltPixel::new(0, 0, 0); width * height],
}
}
pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> {
self.pixels.get_mut(y * self.width + x)
}
pub fn blit(&self, gop: &mut GraphicsOutput) -> Result<()> {
gop.blt(BltOp::BufferToVideo {
buffer: &self.pixels,
src: BltRegion::Full,
dest: (0, 0),
dims: (self.width, self.height),
})
.context("unable to blit framebuffer")?;
Ok(())
}
}
-197
View File
@@ -1,197 +0,0 @@
use anyhow::{Context, Result, bail};
use std::ffi::c_void;
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::build::DevicePathBuilder;
use uefi::proto::device_path::build::media::Vendor;
use uefi::proto::media::load_file::LoadFile2;
use uefi::{Guid, Handle};
use uefi_raw::protocol::device_path::DevicePathProtocol;
use uefi_raw::protocol::media::LoadFile2Protocol;
use uefi_raw::{Boolean, Status};
pub mod constants;
#[derive(Debug)]
#[repr(C)]
struct MediaLoaderProtocol {
pub load_file: unsafe extern "efiapi" fn(
this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol,
boot_policy: Boolean,
buffer_size: *mut usize,
buffer: *mut c_void,
) -> Status,
pub address: *mut c_void,
pub length: usize,
}
/// Represents a media loader which has been registered in the UEFI stack.
/// You MUST call [MediaLoaderHandle::unregister] when ready to unregister.
/// [Drop] is not implemented for this type.
pub struct MediaLoaderHandle {
guid: Guid,
handle: Handle,
protocol: *mut MediaLoaderProtocol,
path: *mut DevicePath,
}
impl MediaLoaderHandle {
/// The behavior of this function is derived from how Linux calls it.
///
/// Linux calls this function by first passing a NULL [buffer].
/// We must set the size of the buffer it should allocate in [buffer_size].
/// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on
/// the buffer size.
unsafe extern "efiapi" fn load_file(
this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol,
boot_policy: Boolean,
buffer_size: *mut usize,
buffer: *mut c_void,
) -> Status {
if this.is_null() || buffer_size.is_null() || file_path.is_null() {
return Status::INVALID_PARAMETER;
}
// Boot policy must not be true, as that's special behavior that is irrelevant
// for the media loader concept.
if boot_policy == Boolean::TRUE {
return Status::UNSUPPORTED;
}
// 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].
unsafe {
if (*this).length == 0 || (*this).address.is_null() {
return Status::NOT_FOUND;
}
if buffer.is_null() || *buffer_size < (*this).length {
*buffer_size = (*this).length;
return Status::BUFFER_TOO_SMALL;
}
buffer.copy_from((*this).address, (*this).length);
*buffer_size = (*this).length;
}
Status::SUCCESS
}
fn device_path(guid: Guid) -> Box<DevicePath> {
let mut path = Vec::new();
let path = DevicePathBuilder::with_vec(&mut path)
.push(&Vendor {
vendor_guid: guid,
vendor_defined_data: &[],
})
.unwrap()
.finalize()
.unwrap();
path.to_boxed()
}
fn already_registered(guid: Guid) -> Result<bool> {
let path = Self::device_path(guid);
let mut existing_path = path.as_ref();
let result = uefi::boot::locate_device_path::<LoadFile2>(&mut existing_path);
if result.is_ok() {
return Ok(true);
} else if let Err(error) = result
&& error.status() != Status::NOT_FOUND
{
bail!("unable to locate media loader device path: {}", error);
}
Ok(false)
}
/// Registers the provided [data] with the UEFI stack as media loader.
/// This uses a special device path that other EFI programs will look at
/// to load the data from.
pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> {
let path = Self::device_path(guid);
let path = Box::leak(path);
if Self::already_registered(guid)? {
bail!("media loader already registered");
}
let mut handle = unsafe {
uefi::boot::install_protocol_interface(
None,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
}
.context("unable to install media loader device path handle")?;
let data = Box::leak(data);
let protocol = Box::new(MediaLoaderProtocol {
load_file: Self::load_file,
address: data.as_ptr() as *mut _,
length: data.len(),
});
let protocol = Box::leak(protocol);
handle = unsafe {
uefi::boot::install_protocol_interface(
Some(handle),
&LoadFile2Protocol::GUID,
protocol as *mut _ as *mut c_void,
)
}
.context("unable to install media loader load file handle")?;
if !Self::already_registered(guid)? {
bail!("media loader not registered when expected to be registered");
}
Ok(Self {
guid,
handle,
protocol,
path,
})
}
/// Unregisters a media loader from the UEFI stack.
/// This will free the memory allocated by the passed data.
pub fn unregister(self) -> Result<()> {
if !Self::already_registered(self.guid)? {
return Ok(());
}
unsafe {
uefi::boot::uninstall_protocol_interface(
self.handle,
&DevicePathProtocol::GUID,
self.path as *mut c_void,
)
.context("unable to uninstall media loader device path handle")?;
uefi::boot::uninstall_protocol_interface(
self.handle,
&LoadFile2Protocol::GUID,
self.protocol as *mut _ as *mut c_void,
)
.context("unable to uninstall media loader load file handle")?;
let path = Box::from_raw(self.path);
let protocol = Box::from_raw(self.protocol);
let slice =
std::ptr::slice_from_raw_parts_mut(protocol.address as *mut u8, protocol.length);
let data = Box::from_raw(slice);
drop(path);
drop(protocol);
drop(data);
}
Ok(())
}
}
-19
View File
@@ -1,19 +0,0 @@
/// These GUIDs are specific to Linux itself.
pub mod linux {
use uefi::{Guid, guid};
/// The device path GUID for the Linux EFI initrd.
pub const LINUX_EFI_INITRD_MEDIA_GUID: Guid = guid!("5568e427-68fc-4f3d-ac74-ca555231cc68");
}
/// These GUIDs were created by Edera to support Xen loading data
/// from Sprout and other EFI bootloaders.
pub mod xen {
use uefi::{Guid, guid};
/// The device path GUID for the Xen EFI config.
pub const XEN_EFI_CONFIG_MEDIA_GUID: Guid = guid!("bf61f458-a28e-46cd-93d7-07dac5e8cd66");
/// The device path GUID for the Xen EFI config.
pub const XEN_EFI_KERNEL_MEDIA_GUID: Guid = guid!("4010c8bf-6ced-40f5-a53f-e820aee8f34b");
pub const XEN_EFI_RAMDISK_MEDIA_GUID: Guid = guid!("5db1fd01-c3cb-4812-b2ba-8791e52d4a89");
}
+50
View File
@@ -0,0 +1,50 @@
@import url('https://fonts.googleapis.com/css2?family=Dosis:wght@800&family=Source+Sans+Pro&display=swap');
html {
font-family: 'Source Sans Pro', sans-serif;
font-size: 22px;
}
.content-column {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
details>p {
text-align: left;
}
#logo {
background: url("/assets/logo.png");
width: 300px;
height: 233px;
margin-top: 120px;
background-size: 100% auto;
}
#logo:hover {
transform: rotateZ(1080deg);
}
h1, h3 {
font-family: "Dosis", Helvetica, Arial, sans-serif;
font-weight: bold;
}
h1 {
margin-top: 50px;
margin-bottom: 38.5px;
font-size: 64px;
}
h3 {
margin-top: 40px;
margin-bottom: 0;
font-size: 32px;
}
#links > a {
padding: 0.5em;
}