mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 22:42:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd9f67c48 | ||
|
|
b39436cbe2 | ||
|
|
5fd2dbb4a7 | ||
|
|
22dde5d0ac | ||
|
|
660a2df454 | ||
|
|
5427d621c5 | ||
|
|
3f0c124b22 | ||
|
|
ebad8342ee | ||
|
|
f293bc50a8 | ||
|
|
4e08a078d9 | ||
|
|
b67baf5a65 | ||
|
|
c9586ff68d | ||
|
|
592d7b41f6 | ||
|
|
406ad8dede | ||
|
|
2b8113f103 | ||
|
|
358cf92f65 | ||
|
|
fe3de401ff | ||
|
|
2e643d8081 | ||
|
|
cf1ea65414 | ||
|
|
5b10fe9576 | ||
|
|
8f09c94857 | ||
|
|
f6064d8751 | ||
|
|
0d2eaa37c1 | ||
|
|
9595fd53aa | ||
|
|
079bf0c3e8 | ||
|
|
8447861985 | ||
|
|
5dcfbdc8c5 | ||
|
|
a77803dfe8 | ||
|
|
e96a5a64c9 | ||
|
|
cd38f0271d | ||
|
|
aed9903180 | ||
|
|
642236edec | ||
|
|
b4e765279b | ||
|
|
133247c05a | ||
|
|
9438c99c38 | ||
|
|
c54a597917 | ||
|
|
c898a1499c | ||
|
|
85f35a9a3e | ||
|
|
ac69037111 | ||
|
|
9fa57ee197 | ||
|
|
8fce1738f2 | ||
|
|
20ca279fca | ||
|
|
2b5cc4e70f | ||
|
|
e8e3361764 | ||
|
|
df0bda04c0 | ||
|
|
9e595bf8d5 | ||
|
|
78bcaa79ff | ||
|
|
11736ccabb | ||
|
|
98f1a2c8c5 | ||
|
|
3490ec24f2 | ||
|
|
69fd8a0064 | ||
|
|
afbba2c6d3 | ||
|
|
8b94ebe5bd | ||
|
|
dd8ae14da8 | ||
|
|
7b62d414ed |
No files matched your search
@@ -3,8 +3,6 @@ description: File a bug report.
|
|||||||
title: "bug: "
|
title: "bug: "
|
||||||
labels: ["bug", "triage"]
|
labels: ["bug", "triage"]
|
||||||
type: bug
|
type: bug
|
||||||
assignees:
|
|
||||||
- edera-dev/engineering
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ description: Request a feature.
|
|||||||
title: "want: "
|
title: "want: "
|
||||||
labels: ["enhancement", "triage"]
|
labels: ["enhancement", "triage"]
|
||||||
type: feature
|
type: feature
|
||||||
assignees:
|
|
||||||
- edera-dev/engineering
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ permissions:
|
|||||||
contents: read # Needed to checkout the repository.
|
contents: read # Needed to checkout the repository.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
|
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
|
||||||
cancel-in-progress: true
|
# Only cancel superseded pull request runs, so every push to main is fully checked.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ permissions:
|
|||||||
contents: read # Needed to checkout the repository.
|
contents: read # Needed to checkout the repository.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
|
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
|
||||||
cancel-in-progress: true
|
# Only cancel superseded pull request runs, so every push to main is fully checked.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rustfmt:
|
rustfmt:
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ permissions:
|
|||||||
contents: read # Needed to checkout the repository.
|
contents: read # Needed to checkout the repository.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
|
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
|
||||||
cancel-in-progress: true
|
# Only cancel superseded pull request runs, so every push to main is fully checked.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ permissions:
|
|||||||
contents: read # Needed to checkout the repository.
|
contents: read # Needed to checkout the repository.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}"
|
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
|
||||||
cancel-in-progress: true
|
# Only cancel superseded pull request runs, so every push to main is fully checked.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
artifacts:
|
artifacts:
|
||||||
name: artifacts
|
name: artifacts
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # Needed to upload artifacts.
|
contents: read # Needed to checkout the repository.
|
||||||
id-token: write # Needed for attestation.
|
|
||||||
attestations: write # Needed for attestations.
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
artifact-digest: "${{ steps.upload.outputs.artifact-digest }}"
|
||||||
steps:
|
steps:
|
||||||
- name: harden runner
|
- name: harden runner
|
||||||
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
|
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
|
||||||
@@ -48,9 +49,24 @@ jobs:
|
|||||||
name: artifacts
|
name: artifacts
|
||||||
path: target/assemble/*
|
path: target/assemble/*
|
||||||
|
|
||||||
|
# Attestation runs in its own job so that pull requests, which run the build with
|
||||||
|
# code from the pull request, never get the permissions needed to attest.
|
||||||
|
attest:
|
||||||
|
name: attest
|
||||||
|
needs: artifacts
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
permissions:
|
||||||
|
id-token: write # Needed for attestation.
|
||||||
|
attestations: write # Needed for attestations.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: harden runner
|
||||||
|
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'attest artifacts'
|
- name: 'attest artifacts'
|
||||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-name: artifacts.zip
|
subject-name: artifacts.zip
|
||||||
subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}"
|
subject-digest: "sha256:${{ needs.artifacts.outputs.artifact-digest }}"
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
@@ -13,7 +13,8 @@ permissions:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.event.inputs.release-tag }}"
|
group: "${{ github.workflow }}-${{ github.event.inputs.release-tag }}"
|
||||||
cancel-in-progress: true
|
# Let a running release finish, so its assets are never left partially uploaded.
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -32,6 +33,8 @@ jobs:
|
|||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
|
# Build the tagged commit, not whichever ref the workflow was dispatched on.
|
||||||
|
ref: "refs/tags/${{ github.event.inputs.release-tag }}"
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 'install rust toolchain'
|
- name: 'install rust toolchain'
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ We encourage opening an issue on GitHub to report bugs.
|
|||||||
|
|
||||||
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
|
1. For anything more than simple bug/doc fixes, please open a GitHub issue for tracking purposes.
|
||||||
2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
|
2. Discuss the change with the teams to ensure we have consensus on the change being welcome.
|
||||||
3. We encourage opening the PR sooner than later, and prefixing with `WIP:` so GitHub labels it as a Draft.
|
3. We encourage opening the PR sooner than later, as a draft pull request.
|
||||||
4. Please include a detailed list of changes that the PR makes.
|
4. Please include a detailed list of changes that the PR makes.
|
||||||
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
|
5. Once the PR is ready for review, remove the Draft status, and request a review from `edera-dev/engineering`.
|
||||||
6. After the review cycle concludes, and we know you are ready for merging, a team member will submit the PR to the merge queue.
|
6. After the review cycle concludes, and we know you are ready for merging, a team member will submit the PR to the merge queue.
|
||||||
|
|||||||
Generated
+17
-6
@@ -65,14 +65,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-bls"
|
name = "edera-sprout-bls"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-boot"
|
name = "edera-sprout-boot"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"edera-sprout-bls",
|
"edera-sprout-bls",
|
||||||
@@ -83,6 +83,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"jaarg",
|
"jaarg",
|
||||||
"log",
|
"log",
|
||||||
|
"serde_ignored",
|
||||||
"sha2",
|
"sha2",
|
||||||
"toml",
|
"toml",
|
||||||
"uefi",
|
"uefi",
|
||||||
@@ -91,18 +92,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-build"
|
name = "edera-sprout-build"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-config"
|
name = "edera-sprout-config"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-eficore"
|
name = "edera-sprout-eficore"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
@@ -115,7 +116,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edera-sprout-parsing"
|
name = "edera-sprout-parsing"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -237,6 +238,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_ignored"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|||||||
+5
-1
@@ -11,7 +11,7 @@ resolver = "3"
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
homepage = "https://sprout.edera.dev"
|
homepage = "https://sprout.edera.dev"
|
||||||
repository = "https://github.com/edera-dev/sprout"
|
repository = "https://github.com/edera-dev/sprout"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
@@ -41,6 +41,10 @@ version = "1.0.229"
|
|||||||
default-features = false
|
default-features = false
|
||||||
features = ["alloc", "derive"]
|
features = ["alloc", "derive"]
|
||||||
|
|
||||||
|
[workspace.dependencies.serde_ignored]
|
||||||
|
version = "0.1.14"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.sha2]
|
[workspace.dependencies.sha2]
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|||||||
+3
-1
@@ -15,10 +15,12 @@ as an argument to boot.sh to boot the specified architecture.
|
|||||||
|
|
||||||
Sprout is split into multiple crates:
|
Sprout is split into multiple crates:
|
||||||
|
|
||||||
- `edera-sprout-boot` as `crates/boot`: Bootloader entrypoint for Sprout.
|
- `edera-sprout-boot` at `crates/boot`: Bootloader entrypoint for Sprout.
|
||||||
|
- `edera-sprout-bls` at `crates/bls`: Parsing and sorting of Boot Loader Specification entries.
|
||||||
- `edera-sprout-build` at `crates/build`: Build logic for Sprout.
|
- `edera-sprout-build` at `crates/build`: Build logic for Sprout.
|
||||||
- `edera-sprout-config` at `crates/config`: Serialization structures for the Sprout configuration file.
|
- `edera-sprout-config` at `crates/config`: Serialization structures for the Sprout configuration file.
|
||||||
- `edera-sprout-eficore` at `crates/eficore`: Core library for Sprout EFI code.
|
- `edera-sprout-eficore` at `crates/eficore`: Core library for Sprout EFI code.
|
||||||
|
- `edera-sprout-parsing` at `crates/parsing`: Value stamping and other parsing helpers for Sprout.
|
||||||
|
|
||||||
It is intended that overtime Sprout will be split into even more crates.
|
It is intended that overtime Sprout will be split into even more crates.
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ We recommend running Sprout without Secure Boot for development, and with Secure
|
|||||||
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
|
- [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
|
||||||
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
|
- [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
|
||||||
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
|
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
|
||||||
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
|
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/8)
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
@@ -120,6 +120,8 @@ $ sprout.efi --config=\path\to\config.toml
|
|||||||
$ sprout.efi --boot="Boot Xen"
|
$ sprout.efi --boot="Boot Xen"
|
||||||
# Autoconfigure Sprout, without loading a configuration file.
|
# Autoconfigure Sprout, without loading a configuration file.
|
||||||
$ sprout.efi --autoconfigure
|
$ sprout.efi --autoconfigure
|
||||||
|
# Use the basic boot menu instead of the simple one.
|
||||||
|
$ sprout.efi --menu-style=basic
|
||||||
```
|
```
|
||||||
|
|
||||||
### Boot Linux from ESP
|
### Boot Linux from ESP
|
||||||
|
|||||||
+59
-17
@@ -2,6 +2,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
|
use alloc::vec::Vec;
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
|
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ pub struct BlsEntry {
|
|||||||
pub options: Option<String>,
|
pub options: Option<String>,
|
||||||
/// The path to the linux kernel.
|
/// The path to the linux kernel.
|
||||||
pub linux: Option<String>,
|
pub linux: Option<String>,
|
||||||
/// The path to the initrd.
|
/// The paths to the initrds, in the order they appear.
|
||||||
pub initrd: Option<String>,
|
pub initrd: Vec<String>,
|
||||||
/// The path to an EFI image.
|
/// The path to an EFI image.
|
||||||
pub efi: Option<String>,
|
pub efi: Option<String>,
|
||||||
/// The sort key for the entry.
|
/// The sort key for the entry.
|
||||||
@@ -38,12 +39,16 @@ impl FromStr for BlsEntry {
|
|||||||
let mut title: Option<String> = None;
|
let mut title: Option<String> = None;
|
||||||
let mut options: Option<String> = None;
|
let mut options: Option<String> = None;
|
||||||
let mut linux: Option<String> = None;
|
let mut linux: Option<String> = None;
|
||||||
let mut initrd: Option<String> = None;
|
let mut initrd: Vec<String> = Vec::new();
|
||||||
let mut efi: Option<String> = None;
|
let mut efi: Option<String> = None;
|
||||||
let mut sort_key: Option<String> = None;
|
let mut sort_key: Option<String> = None;
|
||||||
let mut version: Option<String> = None;
|
let mut version: Option<String> = None;
|
||||||
let mut machine_id: Option<String> = None;
|
let mut machine_id: Option<String> = None;
|
||||||
|
|
||||||
|
// Some editors write a UTF-8 byte order mark at the start of the file.
|
||||||
|
// It isn't whitespace, so it would otherwise be part of the first key.
|
||||||
|
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
|
||||||
|
|
||||||
// Iterate over each line in the input and parse it.
|
// Iterate over each line in the input and parse it.
|
||||||
for line in input.lines() {
|
for line in input.lines() {
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
@@ -65,9 +70,17 @@ impl FromStr for BlsEntry {
|
|||||||
title = Some(value.trim().to_string());
|
title = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The options to pass to the entry.
|
// The options to pass to the entry. The options key can appear more than once,
|
||||||
|
// and all the options are combined in the order they appear.
|
||||||
"options" => {
|
"options" => {
|
||||||
options = Some(value.trim().to_string());
|
let value = value.trim();
|
||||||
|
match options.as_mut() {
|
||||||
|
Some(options) => {
|
||||||
|
options.push(' ');
|
||||||
|
options.push_str(value);
|
||||||
|
}
|
||||||
|
None => options = Some(value.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The path to the linux kernel.
|
// The path to the linux kernel.
|
||||||
@@ -75,9 +88,10 @@ impl FromStr for BlsEntry {
|
|||||||
linux = Some(value.trim().to_string());
|
linux = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The path to the initrd.
|
// The path to an initrd. An entry can have multiple initrds, either as
|
||||||
|
// repeated initrd lines or as multiple paths separated by whitespace.
|
||||||
"initrd" => {
|
"initrd" => {
|
||||||
initrd = Some(value.trim().to_string());
|
initrd.extend(value.split_whitespace().map(|path| path.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The path to an EFI image.
|
// The path to an EFI image.
|
||||||
@@ -134,12 +148,13 @@ impl BlsEntry {
|
|||||||
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
|
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the path to an initrd to pass to the kernel, if any.
|
/// Fetches the paths to the initrds to pass to the kernel, in order.
|
||||||
/// It also converts / to \\ to match EFI path style.
|
/// It also converts / to \\ to match EFI path style.
|
||||||
pub fn initrd_path(&self) -> Option<String> {
|
pub fn initrd_paths(&self) -> Vec<String> {
|
||||||
self.initrd
|
self.initrd
|
||||||
.clone()
|
.iter()
|
||||||
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
|
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the options to pass to the kernel, if any.
|
/// Fetches the options to pass to the kernel, if any.
|
||||||
@@ -431,7 +446,7 @@ mod tests {
|
|||||||
assert!(entry.title.is_none());
|
assert!(entry.title.is_none());
|
||||||
assert!(entry.linux.is_none());
|
assert!(entry.linux.is_none());
|
||||||
assert!(entry.efi.is_none());
|
assert!(entry.efi.is_none());
|
||||||
assert!(entry.initrd.is_none());
|
assert!(entry.initrd.is_empty());
|
||||||
assert!(entry.options.is_none());
|
assert!(entry.options.is_none());
|
||||||
assert!(entry.sort_key.is_none());
|
assert!(entry.sort_key.is_none());
|
||||||
assert!(entry.version.is_none());
|
assert!(entry.version.is_none());
|
||||||
@@ -455,7 +470,7 @@ efi /EFI/fedora/shimx64.efi
|
|||||||
assert_eq!(entry.version.as_deref(), Some("6.5.6-300.fc39.x86_64"));
|
assert_eq!(entry.version.as_deref(), Some("6.5.6-300.fc39.x86_64"));
|
||||||
assert_eq!(entry.machine_id.as_deref(), Some("abc123def456"));
|
assert_eq!(entry.machine_id.as_deref(), Some("abc123def456"));
|
||||||
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz-6.5.6"));
|
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz-6.5.6"));
|
||||||
assert_eq!(entry.initrd.as_deref(), Some("/boot/initrd-6.5.6.img"));
|
assert_eq!(entry.initrd, ["/boot/initrd-6.5.6.img"]);
|
||||||
assert_eq!(entry.options.as_deref(), Some("root=/dev/sda1 ro quiet"));
|
assert_eq!(entry.options.as_deref(), Some("root=/dev/sda1 ro quiet"));
|
||||||
assert_eq!(entry.sort_key.as_deref(), Some("fedora"));
|
assert_eq!(entry.sort_key.as_deref(), Some("fedora"));
|
||||||
assert_eq!(entry.efi.as_deref(), Some("/EFI/fedora/shimx64.efi"));
|
assert_eq!(entry.efi.as_deref(), Some("/EFI/fedora/shimx64.efi"));
|
||||||
@@ -555,15 +570,42 @@ efi /EFI/fedora/shimx64.efi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initrd_path_normalises_slashes() {
|
fn parse_multiple_initrd_lines_in_order() {
|
||||||
let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap();
|
let input = "linux /vmlinuz\ninitrd /intel-ucode.img\ninitrd /initramfs.img\n";
|
||||||
assert_eq!(entry.initrd_path().as_deref(), Some("boot\\initrd.img"));
|
let entry: BlsEntry = input.parse().unwrap();
|
||||||
|
assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initrd_path_none_when_not_set() {
|
fn parse_multiple_initrds_on_one_line() {
|
||||||
|
let input = "linux /vmlinuz\ninitrd /intel-ucode.img /initramfs.img\n";
|
||||||
|
let entry: BlsEntry = input.parse().unwrap();
|
||||||
|
assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multiple_options_lines_combined_in_order() {
|
||||||
|
let input = "linux /vmlinuz\noptions root=/dev/sda1 ro\noptions quiet\n";
|
||||||
|
let entry: BlsEntry = input.parse().unwrap();
|
||||||
|
assert_eq!(entry.options().as_deref(), Some("root=/dev/sda1 ro quiet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ignores_byte_order_mark() {
|
||||||
|
let entry: BlsEntry = "\u{feff}title Linux\nlinux /vmlinuz\n".parse().unwrap();
|
||||||
|
assert_eq!(entry.title().as_deref(), Some("Linux"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initrd_paths_normalises_slashes() {
|
||||||
|
let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap();
|
||||||
|
assert_eq!(entry.initrd_paths(), ["boot\\initrd.img"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initrd_paths_empty_when_not_set() {
|
||||||
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
|
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
|
||||||
assert!(entry.initrd_path().is_none());
|
assert!(entry.initrd_paths().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ edera-sprout-bls.path = "../bls"
|
|||||||
edera-sprout-parsing.path = "../parsing"
|
edera-sprout-parsing.path = "../parsing"
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
jaarg.workspace = true
|
jaarg.workspace = true
|
||||||
|
serde_ignored.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|||||||
@@ -2,19 +2,59 @@ use crate::context::SproutContext;
|
|||||||
use crate::phases::before_handoff;
|
use crate::phases::before_handoff;
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use alloc::rc::Rc;
|
use alloc::rc::Rc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
|
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
|
||||||
use edera_sprout_parsing::{combine_options, empty_is_none};
|
use edera_sprout_parsing::{append_initrd, combine_options, empty_is_none};
|
||||||
use eficore::bootloader_interface::BootloaderInterface;
|
use eficore::bootloader_interface::BootloaderInterface;
|
||||||
use eficore::loader::source::ImageSource;
|
use eficore::loader::source::ImageSource;
|
||||||
use eficore::loader::{ImageLoadRequest, ImageLoader};
|
use eficore::loader::{ImageLoadRequest, ImageLoader};
|
||||||
use eficore::media_loader::MediaLoaderHandle;
|
use eficore::media_loader::MediaLoaderHandle;
|
||||||
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
|
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
|
||||||
use uefi::CString16;
|
use log::warn;
|
||||||
use uefi::proto::loaded_image::LoadedImage;
|
use uefi::proto::loaded_image::LoadedImage;
|
||||||
|
use uefi::{CString16, Handle};
|
||||||
|
|
||||||
|
/// Read the initrd at the stamped `path` relative to the sprout image.
|
||||||
|
/// Provides [None] if the path refers to the root of a filesystem rather than a file.
|
||||||
|
pub fn read_initrd(context: &Rc<SproutContext>, path: &str) -> Result<Option<Vec<u8>>> {
|
||||||
|
let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), path)
|
||||||
|
.context("unable to resolve initrd path")?;
|
||||||
|
|
||||||
|
// A path without a file component refers to the root of the filesystem, not an initrd.
|
||||||
|
// This happens when a path template like "$root\\$initrd-0" is stamped with an empty
|
||||||
|
// initrd value, such as a BLS entry without an initrd or an unused BLS initrd slot.
|
||||||
|
let subpath = eficore::path::device_path_subpath(&resolved.full_path)
|
||||||
|
.context("unable to get initrd subpath")?;
|
||||||
|
if subpath.trim_matches('\\').is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = resolved.read_file().context("unable to read initrd")?;
|
||||||
|
Ok(Some(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unloads the image with the contained handle when dropped, unless the handle is taken.
|
||||||
|
/// This ensures that an image that is loaded but never started is not left in memory.
|
||||||
|
struct UnloadGuard(Option<Handle>);
|
||||||
|
|
||||||
|
impl Drop for UnloadGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(handle) = self.0.take()
|
||||||
|
&& let Err(error) = uefi::boot::unload_image(handle)
|
||||||
|
{
|
||||||
|
warn!("unable to unload image: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes the chainload action using the specified `configuration` inside the provided `context`.
|
/// Executes the chainload action using the specified `configuration` inside the provided `context`.
|
||||||
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
|
pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfiguration) -> Result<()> {
|
||||||
|
// The initrd can be provided as either a single initrd or a chain, but not both.
|
||||||
|
if configuration.linux_initrd.is_some() && !configuration.linux_initrd_chain.is_empty() {
|
||||||
|
bail!("linux-initrd and linux-initrd-chain cannot be used together");
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the current image handle of sprout.
|
// Retrieve the current image handle of sprout.
|
||||||
let sprout_image = uefi::boot::image_handle();
|
let sprout_image = uefi::boot::image_handle();
|
||||||
|
|
||||||
@@ -32,6 +72,9 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
|
|||||||
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
|
// It will determine if the image needs to be loaded via the shim or can be loaded directly.
|
||||||
let image = ImageLoader::load(request)?;
|
let image = ImageLoader::load(request)?;
|
||||||
|
|
||||||
|
// Unload the image if an error occurs before it is started.
|
||||||
|
let mut unload_guard = UnloadGuard(Some(*image.handle()));
|
||||||
|
|
||||||
// Stamp and combine the options to pass to the image.
|
// Stamp and combine the options to pass to the image.
|
||||||
let options = combine_options(context.stamp_iter(configuration.options.iter()));
|
let options = combine_options(context.stamp_iter(configuration.options.iter()));
|
||||||
|
|
||||||
@@ -66,47 +109,57 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamp the initrd path, if provided.
|
// Stamp the initrd paths. A single linux-initrd is read the same way as a chain of one.
|
||||||
let initrd = configuration
|
let initrd_paths = configuration
|
||||||
.linux_initrd
|
.linux_initrd
|
||||||
.as_ref()
|
.iter()
|
||||||
|
.chain(configuration.linux_initrd_chain.iter())
|
||||||
.map(|item| context.stamp(item));
|
.map(|item| context.stamp(item));
|
||||||
// The initrd can be None or empty, so we need to collapse that into a single Option.
|
|
||||||
let initrd = empty_is_none(initrd);
|
|
||||||
|
|
||||||
// If an initrd is provided, register it with the EFI stack.
|
// Read each initrd and concatenate the contents in order.
|
||||||
let mut initrd_handle = None;
|
// Paths that are empty after stamping are skipped.
|
||||||
if let Some(linux_initrd) = initrd {
|
let mut initrd: Option<Vec<u8>> = None;
|
||||||
let resolved_initrd =
|
for path in initrd_paths {
|
||||||
eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &linux_initrd)
|
let Some(path) = empty_is_none(Some(path)) else {
|
||||||
.context("unable to resolve linux initrd path")?;
|
continue;
|
||||||
|
};
|
||||||
// A path without a file component refers to the root of the filesystem, not an initrd.
|
let Some(content) = read_initrd(&context, &path)? else {
|
||||||
// This happens when a path template like "$root\\$initrd" is stamped with an empty
|
continue;
|
||||||
// initrd value, such as a BLS entry without an initrd.
|
};
|
||||||
let initrd_subpath = eficore::path::device_path_subpath(&resolved_initrd.full_path)
|
match initrd.as_mut() {
|
||||||
.context("unable to get linux initrd subpath")?;
|
Some(initrd) => append_initrd(initrd, &content),
|
||||||
if !initrd_subpath.trim_matches('\\').is_empty() {
|
None => initrd = Some(content),
|
||||||
let content = resolved_initrd
|
|
||||||
.read_file()
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If an initrd was read, register it with the EFI stack.
|
||||||
|
// An empty initrd is not registered, as Linux fails to boot if the initrd it is
|
||||||
|
// given can't be loaded, instead of booting without one.
|
||||||
|
let mut initrd_handle = None;
|
||||||
|
if let Some(content) = initrd.filter(|content| !content.is_empty()) {
|
||||||
|
let handle =
|
||||||
|
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
|
||||||
|
.context("unable to register linux initrd")?;
|
||||||
|
initrd_handle = Some(handle);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark execution of an entry in the bootloader interface.
|
// Mark execution of an entry in the bootloader interface.
|
||||||
BootloaderInterface::mark_exec(context.root().timer())
|
// This is only informational, so it should not prevent booting.
|
||||||
.context("unable to mark execution of boot entry in bootloader interface")?;
|
if let Err(error) = BootloaderInterface::mark_exec(context.root().timer()) {
|
||||||
|
warn!(
|
||||||
|
"unable to mark execution of boot entry in bootloader interface: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Since we are about to hand off control to another image, we need to execute the handoff hook.
|
// Since we are about to hand off control to another image, we need to execute the handoff hook.
|
||||||
// This will perform operations like clearing the screen.
|
// This will perform operations like clearing the screen.
|
||||||
before_handoff(&context).context("unable to execute before handoff hook")?;
|
before_handoff(&context).context("unable to execute before handoff hook")?;
|
||||||
|
|
||||||
|
// The image is about to be started, so it is no longer ours to unload.
|
||||||
|
// Once started, the image is unloaded by the firmware when it exits.
|
||||||
|
unload_guard.0 = None;
|
||||||
|
|
||||||
// Start the loaded image.
|
// Start the loaded image.
|
||||||
// This call might return, or it may pass full control to another image that will never return.
|
// This call might return, or it may pass full control to another image that will never return.
|
||||||
// Capture the result to ensure we can return an error if the image fails to start, but only
|
// Capture the result to ensure we can return an error if the image fails to start, but only
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use crate::{actions, context::SproutContext};
|
|||||||
use alloc::rc::Rc;
|
use alloc::rc::Rc;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use alloc::{format, vec};
|
use alloc::{format, vec};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
|
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
|
||||||
use edera_sprout_config::actions::edera::EderaConfiguration;
|
use edera_sprout_config::actions::edera::EderaConfiguration;
|
||||||
use edera_sprout_parsing::{build_xen_config, combine_options, empty_is_none};
|
use edera_sprout_parsing::{append_initrd, build_xen_config, combine_options, empty_is_none};
|
||||||
use eficore::media_loader::{
|
use eficore::media_loader::{
|
||||||
MediaLoaderHandle,
|
MediaLoaderHandle,
|
||||||
constants::xen::{
|
constants::xen::{
|
||||||
@@ -40,6 +40,11 @@ fn read_loader_payload(context: &Rc<SproutContext>, what: &str, path: &str) -> R
|
|||||||
/// Executes the edera action which will boot the Edera hypervisor with the specified
|
/// Executes the edera action which will boot the Edera hypervisor with the specified
|
||||||
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
|
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
|
||||||
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
|
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
|
||||||
|
// The initrd can be provided as either a single initrd or a chain, but not both.
|
||||||
|
if configuration.initrd.is_some() && !configuration.initrd_chain.is_empty() {
|
||||||
|
bail!("initrd and initrd-chain cannot be used together");
|
||||||
|
}
|
||||||
|
|
||||||
// Only register the initrd media loader if the user actually configured one.
|
// Only register the initrd media loader if the user actually configured one.
|
||||||
let xen_opts = combine_options(context.stamp_iter(configuration.xen_options.iter()));
|
let xen_opts = combine_options(context.stamp_iter(configuration.xen_options.iter()));
|
||||||
let dom0_args = combine_options(context.stamp_iter(configuration.kernel_options.iter()));
|
let dom0_args = combine_options(context.stamp_iter(configuration.kernel_options.iter()));
|
||||||
@@ -80,12 +85,27 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
|||||||
register_media_loader_bytes(XEN_EFI_KERNEL_MEDIA_GUID, "kernel", kernel_bytes)
|
register_media_loader_bytes(XEN_EFI_KERNEL_MEDIA_GUID, "kernel", kernel_bytes)
|
||||||
.context("unable to register kernel media loader")?;
|
.context("unable to register kernel media loader")?;
|
||||||
|
|
||||||
// Extend PCR 9 with the initrd bytes (empty when no initrd is
|
// Read the initrd, or each initrd in the chain concatenated in order, then
|
||||||
// configured).
|
// extend PCR 9 with the combined bytes (empty when no initrd is configured).
|
||||||
let initrd_bytes = match empty_is_none(configuration.initrd.as_ref()) {
|
// Paths are stamped first, and paths that are empty after stamping or that refer
|
||||||
Some(p) => read_loader_payload(&context, "initrd", p)?,
|
// to the root of a filesystem are skipped, the same as the chainload action.
|
||||||
None => Vec::new(),
|
let mut initrd_bytes = Vec::new();
|
||||||
};
|
let initrd_paths = configuration
|
||||||
|
.initrd
|
||||||
|
.iter()
|
||||||
|
.chain(configuration.initrd_chain.iter())
|
||||||
|
.map(|item| context.stamp(item));
|
||||||
|
for path in initrd_paths {
|
||||||
|
let Some(path) = empty_is_none(Some(path)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(content) = actions::chainload::read_initrd(&context, &path)
|
||||||
|
.context("unable to read initrd file")?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
append_initrd(&mut initrd_bytes, &content);
|
||||||
|
}
|
||||||
PlatformTpm::log_event(
|
PlatformTpm::log_event(
|
||||||
PlatformTpm::PCR_INITRD,
|
PlatformTpm::PCR_INITRD,
|
||||||
&initrd_bytes,
|
&initrd_bytes,
|
||||||
@@ -111,6 +131,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
|||||||
path: configuration.xen.clone(),
|
path: configuration.xen.clone(),
|
||||||
options: vec![],
|
options: vec![],
|
||||||
linux_initrd: None,
|
linux_initrd: None,
|
||||||
|
linux_initrd_chain: vec![],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.context("unable to chainload to xen");
|
.context("unable to chainload to xen");
|
||||||
|
|||||||
@@ -48,19 +48,36 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
|
|||||||
// Trade the filesystem protocol for the uefi filesystem helper.
|
// Trade the filesystem protocol for the uefi filesystem helper.
|
||||||
let mut filesystem = FileSystem::new(filesystem);
|
let mut filesystem = FileSystem::new(filesystem);
|
||||||
|
|
||||||
|
// A scan that fails is skipped, as a filesystem that can't be read should not
|
||||||
|
// prevent autoconfiguring the other filesystems.
|
||||||
|
|
||||||
// Scan the filesystem for BLS supported configurations.
|
// Scan the filesystem for BLS supported configurations.
|
||||||
let bls_found = bls::scan(&mut filesystem, &root, config)
|
let bls_found = match bls::scan(&mut filesystem, &root, config) {
|
||||||
.context("unable to scan for bls configurations")?;
|
Ok(found) => found,
|
||||||
|
Err(error) => {
|
||||||
|
warn!(
|
||||||
|
"unable to scan filesystem for bls configurations: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If BLS was not found, scan for Linux configurations.
|
// If BLS was not found, scan for Linux configurations.
|
||||||
if !bls_found {
|
if !bls_found && let Err(error) = linux::scan(&mut filesystem, &root, config) {
|
||||||
linux::scan(&mut filesystem, &root, config)
|
warn!(
|
||||||
.context("unable to scan for linux configurations")?;
|
"unable to scan filesystem for linux configurations: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always look for Windows configurations.
|
// Always look for Windows configurations.
|
||||||
windows::scan(&mut filesystem, &root, config)
|
if let Err(error) = windows::scan(&mut filesystem, &root, config) {
|
||||||
.context("unable to scan for windows configurations")?;
|
warn!(
|
||||||
|
"unable to scan filesystem for windows configurations: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::generators::bls::BLS_INITRD_SLOTS;
|
||||||
use alloc::string::ToString;
|
use alloc::string::ToString;
|
||||||
use alloc::{format, vec};
|
use alloc::{format, vec};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -45,12 +46,22 @@ pub fn scan(
|
|||||||
.context("unable to check for BLS loader.conf file")?;
|
.context("unable to check for BLS loader.conf file")?;
|
||||||
|
|
||||||
// Whether we have an entries directory.
|
// Whether we have an entries directory.
|
||||||
// We actually iterate the entries to see if there are any.
|
// We actually iterate the entries to see if there are any entry files, as the
|
||||||
|
// directory itself always has the "." and ".." items, even when it is empty.
|
||||||
let has_entries_dir = filesystem
|
let has_entries_dir = filesystem
|
||||||
.read_dir(bls_entries_path)
|
.read_dir(bls_entries_path)
|
||||||
.ok()
|
.map(|mut iterator| {
|
||||||
.and_then(|mut iterator| iterator.next())
|
iterator.any(|entry| {
|
||||||
.map(|entry| entry.is_ok())
|
entry.is_ok_and(|entry| {
|
||||||
|
entry.is_regular_file()
|
||||||
|
&& entry
|
||||||
|
.file_name()
|
||||||
|
.to_string()
|
||||||
|
.to_lowercase()
|
||||||
|
.ends_with(".conf")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Detect if a BLS supported configuration is on this filesystem.
|
// Detect if a BLS supported configuration is on this filesystem.
|
||||||
@@ -83,10 +94,15 @@ pub fn scan(
|
|||||||
|
|
||||||
// Generate a chainload configuration for BLS.
|
// Generate a chainload configuration for BLS.
|
||||||
// BLS will provide these values to us.
|
// BLS will provide these values to us.
|
||||||
|
// Every initrd slot is chained in order. Unused slots stamp to the root of the
|
||||||
|
// filesystem, which the chainload action skips.
|
||||||
let chainload = ChainloadConfiguration {
|
let chainload = ChainloadConfiguration {
|
||||||
path: format!("{}\\$chainload", root),
|
path: format!("{}\\$chainload", root),
|
||||||
options: vec!["$options".to_string()],
|
options: vec!["$options".to_string()],
|
||||||
linux_initrd: Some(format!("{}\\$initrd", root)),
|
linux_initrd: None,
|
||||||
|
linux_initrd_chain: (0..BLS_INITRD_SLOTS)
|
||||||
|
.map(|slot| format!("{}\\$initrd-{}", root, slot))
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the chainload action into the configuration.
|
// Insert the chainload action into the configuration.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use edera_sprout_parsing::{
|
|||||||
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
|
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
|
||||||
unique_hash,
|
unique_hash,
|
||||||
};
|
};
|
||||||
|
use log::warn;
|
||||||
use uefi::CString16;
|
use uefi::CString16;
|
||||||
use uefi::fs::{FileSystem, Path, PathBuf};
|
use uefi::fs::{FileSystem, Path, PathBuf};
|
||||||
use uefi::proto::device_path::DevicePath;
|
use uefi::proto::device_path::DevicePath;
|
||||||
@@ -22,8 +23,8 @@ use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
|||||||
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
|
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
|
||||||
|
|
||||||
/// The locations to scan for kernel pairs.
|
/// The locations to scan for kernel pairs.
|
||||||
/// We will check for symlinks and if this directory is a symlink, we will skip it.
|
/// A location that doesn't exist or can't be read is skipped.
|
||||||
/// The empty string represents the root of the filesystem.
|
/// The "\\" location represents the root of the filesystem.
|
||||||
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
|
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
|
||||||
|
|
||||||
/// This is really silly, but if what we are booting is the Canonical stubble stub,
|
/// This is really silly, but if what we are booting is the Canonical stubble stub,
|
||||||
@@ -125,10 +126,11 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
|
|||||||
let mut initramfs_path = path_for_join.clone();
|
let mut initramfs_path = path_for_join.clone();
|
||||||
initramfs_path.push(Path::new(&initramfs));
|
initramfs_path.push(Path::new(&initramfs));
|
||||||
|
|
||||||
// Check if the initramfs path exists, if it does, break out of the loop.
|
// Check if the initramfs path is a file, if it is, break out of the loop.
|
||||||
|
// A path that can't be checked is treated as not existing.
|
||||||
if filesystem
|
if filesystem
|
||||||
.try_exists(&initramfs_path)
|
.metadata(&initramfs_path)
|
||||||
.context("unable to check if initramfs path exists")?
|
.is_ok_and(|metadata| metadata.is_regular_file())
|
||||||
{
|
{
|
||||||
break Some(initramfs_path);
|
break Some(initramfs_path);
|
||||||
}
|
}
|
||||||
@@ -168,10 +170,12 @@ pub fn scan(
|
|||||||
let root_unique_hash = unique_hash(&root);
|
let root_unique_hash = unique_hash(&root);
|
||||||
|
|
||||||
// Scan all locations for kernel pairs, adding them to the list.
|
// Scan all locations for kernel pairs, adding them to the list.
|
||||||
|
// A location that can't be scanned is skipped, so the other locations are still used.
|
||||||
for location in SCAN_LOCATIONS {
|
for location in SCAN_LOCATIONS {
|
||||||
let scanned = scan_directory(filesystem, location)
|
match scan_directory(filesystem, location) {
|
||||||
.with_context(|| format!("unable to scan directory {}", location))?;
|
Ok(scanned) => pairs.extend(scanned),
|
||||||
pairs.extend(scanned);
|
Err(error) => warn!("unable to scan directory {}: {:#}", location, error),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no kernel pairs were found, return false.
|
// If no kernel pairs were found, return false.
|
||||||
@@ -232,6 +236,7 @@ pub fn scan(
|
|||||||
path: "$kernel".to_string(),
|
path: "$kernel".to_string(),
|
||||||
options: vec!["$linux-options".to_string()],
|
options: vec!["$linux-options".to_string()],
|
||||||
linux_initrd: Some("$initrd".to_string()),
|
linux_initrd: Some("$initrd".to_string()),
|
||||||
|
linux_initrd_chain: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the chainload action into the configuration.
|
// Insert the chainload action into the configuration.
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use crate::options::SproutOptions;
|
use crate::options::SproutOptions;
|
||||||
|
use alloc::format;
|
||||||
|
use alloc::string::{String, ToString};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
|
use core::fmt::Display;
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use edera_sprout_config::{RootConfiguration, latest_version};
|
use edera_sprout_config::{RootConfiguration, latest_version};
|
||||||
use eficore::platform::tpm::PlatformTpm;
|
use eficore::platform::tpm::PlatformTpm;
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
|
use serde_ignored::Path;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
use uefi::proto::device_path::LoadedImageDevicePath;
|
use uefi::proto::device_path::LoadedImageDevicePath;
|
||||||
|
|
||||||
@@ -35,6 +39,59 @@ fn load_raw_config(options: &SproutOptions) -> Result<Vec<u8>> {
|
|||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format the configuration key `path` with dots, like "actions.linux.chainload.path".
|
||||||
|
fn key_path(path: &Path) -> String {
|
||||||
|
// Joins the `parent` path and the `key` with a dot, unless the parent is the root.
|
||||||
|
fn join(parent: &Path, key: impl Display) -> String {
|
||||||
|
match parent {
|
||||||
|
Path::Root => key.to_string(),
|
||||||
|
parent => format!("{}.{}", key_path(parent), key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match path {
|
||||||
|
Path::Root => String::new(),
|
||||||
|
Path::Seq { parent, index } => join(parent, index),
|
||||||
|
Path::Map { parent, key } => join(parent, key),
|
||||||
|
// Optional values and wrapper types don't add a key of their own.
|
||||||
|
Path::Some { parent }
|
||||||
|
| Path::NewtypeStruct { parent }
|
||||||
|
| Path::NewtypeVariant { parent } => key_path(parent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Warn about each declaration in `config` that configures more than one kind.
|
||||||
|
/// For example, an action with both chainload and print configured only runs chainload.
|
||||||
|
fn warn_multiple_kinds(config: &RootConfiguration) {
|
||||||
|
for (name, action) in &config.actions {
|
||||||
|
let kinds = [
|
||||||
|
action.chainload.is_some(),
|
||||||
|
action.print.is_some(),
|
||||||
|
action.edera.is_some(),
|
||||||
|
];
|
||||||
|
if kinds.iter().filter(|kind| **kind).count() > 1 {
|
||||||
|
warn!(
|
||||||
|
"action {} configures more than one action, only one is used",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, generator) in &config.generators {
|
||||||
|
let kinds = [
|
||||||
|
generator.matrix.is_some(),
|
||||||
|
generator.bls.is_some(),
|
||||||
|
generator.list.is_some(),
|
||||||
|
];
|
||||||
|
if kinds.iter().filter(|kind| **kind).count() > 1 {
|
||||||
|
warn!(
|
||||||
|
"generator {} configures more than one generator, only one is used",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads the [RootConfiguration] for Sprout.
|
/// Loads the [RootConfiguration] for Sprout.
|
||||||
pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
|
pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
|
||||||
// Load the raw configuration from the sprout config file.
|
// Load the raw configuration from the sprout config file.
|
||||||
@@ -59,9 +116,15 @@ pub fn load(options: &SproutOptions) -> Result<RootConfiguration> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the version is supported, parse the full configuration.
|
// If the version is supported, parse the full configuration.
|
||||||
let config: RootConfiguration = value
|
// Keys that aren't part of the configuration, such as misspelled keys, are ignored.
|
||||||
.try_into()
|
// Warn about each one, as the configuration may not do what was intended.
|
||||||
.context("unable to parse sprout.toml file")?;
|
let config: RootConfiguration = serde_ignored::deserialize(value, |path| {
|
||||||
|
warn!("ignoring unknown configuration key: {}", key_path(&path));
|
||||||
|
})
|
||||||
|
.context("unable to parse sprout.toml file")?;
|
||||||
|
|
||||||
|
// Warn about declarations that configure more than one kind, as only one is used.
|
||||||
|
warn_multiple_kinds(&config);
|
||||||
|
|
||||||
// Return the parsed configuration.
|
// Return the parsed configuration.
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ use uefi::proto::device_path::DevicePath;
|
|||||||
/// The maximum number of iterations that can be performed in [SproutContext::finalize].
|
/// The maximum number of iterations that can be performed in [SproutContext::finalize].
|
||||||
const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
|
const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
|
||||||
|
|
||||||
|
/// The maximum length of a value produced by [SproutContext::finalize].
|
||||||
|
/// A value that refers to itself more than once doubles in length on every iteration,
|
||||||
|
/// so this stops it from using up all memory before the iteration limit is reached.
|
||||||
|
const CONTEXT_FINALIZE_VALUE_LENGTH_LIMIT: usize = 64 * 1024;
|
||||||
|
|
||||||
/// Declares a root context for Sprout.
|
/// Declares a root context for Sprout.
|
||||||
/// This contains data that needs to be shared across Sprout.
|
/// This contains data that needs to be shared across Sprout.
|
||||||
pub struct RootContext {
|
pub struct RootContext {
|
||||||
@@ -201,6 +206,10 @@ impl SproutContext {
|
|||||||
// If the value changed, we need to re-stamp it.
|
// If the value changed, we need to re-stamp it.
|
||||||
did_change = true;
|
did_change = true;
|
||||||
}
|
}
|
||||||
|
// Ensure the value has not grown without bound.
|
||||||
|
if result.len() > CONTEXT_FINALIZE_VALUE_LENGTH_LIMIT {
|
||||||
|
bail!("value {} is too long while finalizing context", key);
|
||||||
|
}
|
||||||
// Insert the new value into the value map.
|
// Insert the new value into the value map.
|
||||||
values.insert(key.clone(), result);
|
values.insert(key.clone(), result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ impl BootableEntry {
|
|||||||
self.title = self.context.stamp(&self.title);
|
self.title = self.context.stamp(&self.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restamp the declared sort key with the current context.
|
||||||
|
/// A sort key that was set on this entry directly is left as is.
|
||||||
|
pub fn restamp_sort_key(&mut self) {
|
||||||
|
if self.sort_key.is_none()
|
||||||
|
&& let Some(ref sort_key) = self.declaration.sort_key
|
||||||
|
{
|
||||||
|
self.sort_key = Some(self.context.stamp(sort_key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark this entry as the default entry.
|
/// Mark this entry as the default entry.
|
||||||
pub fn mark_default(&mut self) {
|
pub fn mark_default(&mut self) {
|
||||||
self.default = true;
|
self.default = true;
|
||||||
|
|||||||
@@ -6,11 +6,106 @@ use core::ops::Deref;
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
|
use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor;
|
||||||
use eficore::partition::PartitionGuidForm;
|
use eficore::partition::PartitionGuidForm;
|
||||||
|
use log::warn;
|
||||||
use uefi::fs::{FileSystem, Path};
|
use uefi::fs::{FileSystem, Path};
|
||||||
use uefi::proto::device_path::DevicePath;
|
use uefi::proto::device_path::DevicePath;
|
||||||
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
|
use uefi::proto::media::file::{File, FileSystemVolumeLabel};
|
||||||
use uefi::proto::media::fs::SimpleFileSystem;
|
use uefi::proto::media::fs::SimpleFileSystem;
|
||||||
use uefi::{CString16, Guid};
|
use uefi::{CString16, Guid, Handle};
|
||||||
|
|
||||||
|
/// Check whether the filesystem at `handle` matches all the criteria of the `extractor`
|
||||||
|
/// configuration inside the `context`. The partition UUIDs are parsed ahead of time into
|
||||||
|
/// `partition_uuid` and `partition_type_uuid`.
|
||||||
|
fn is_match(
|
||||||
|
context: &Rc<SproutContext>,
|
||||||
|
extractor: &FilesystemDeviceMatchExtractor,
|
||||||
|
handle: Handle,
|
||||||
|
partition_uuid: Option<Guid>,
|
||||||
|
partition_type_uuid: Option<Guid>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// Check if the partition info matches partition uuid criteria.
|
||||||
|
if let Some(parsed_uuid) = partition_uuid {
|
||||||
|
// Fetch the root of the device.
|
||||||
|
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
|
||||||
|
.context("unable to fetch the device path of the filesystem")?
|
||||||
|
.deref()
|
||||||
|
.to_boxed();
|
||||||
|
|
||||||
|
// Fetch the partition uuid for this filesystem.
|
||||||
|
let partition_uuid =
|
||||||
|
eficore::partition::partition_guid(&root, PartitionGuidForm::Partition)
|
||||||
|
.context("unable to fetch the partition uuid of the filesystem")?;
|
||||||
|
|
||||||
|
// Compare the partition uuid to the parsed uuid.
|
||||||
|
if partition_uuid != Some(parsed_uuid) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the partition info matches partition type uuid criteria.
|
||||||
|
if let Some(parsed_uuid) = partition_type_uuid {
|
||||||
|
// Fetch the root of the device.
|
||||||
|
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
|
||||||
|
.context("unable to fetch the device path of the filesystem")?
|
||||||
|
.deref()
|
||||||
|
.to_boxed();
|
||||||
|
|
||||||
|
// Fetch the partition type uuid for this filesystem.
|
||||||
|
let partition_type_uuid =
|
||||||
|
eficore::partition::partition_guid(&root, PartitionGuidForm::PartitionType)
|
||||||
|
.context("unable to fetch the partition uuid of the filesystem")?;
|
||||||
|
|
||||||
|
// Compare the partition type uuid to the parsed uuid.
|
||||||
|
if partition_type_uuid != Some(parsed_uuid) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remaining criteria need the filesystem itself.
|
||||||
|
if extractor.has_label.is_none() && extractor.has_item.is_none() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the filesystem protocol for this handle.
|
||||||
|
let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
|
||||||
|
.context("unable to open filesystem protocol")?;
|
||||||
|
|
||||||
|
// Check if the filesystem matches label criteria.
|
||||||
|
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 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the filesystem matches item criteria.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Check the metadata of the item.
|
||||||
|
// Ignore filesystem errors as we can't do anything useful with the error.
|
||||||
|
let Some(metadata) = filesystem.metadata(Path::new(&want_item)).ok() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only check directories and files.
|
||||||
|
if !(metadata.is_directory() || metadata.is_regular_file()) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract a filesystem device path using the specified `context` and `extractor` configuration.
|
/// Extract a filesystem device path using the specified `context` and `extractor` configuration.
|
||||||
pub fn extract(
|
pub fn extract(
|
||||||
@@ -26,107 +121,43 @@ pub fn extract(
|
|||||||
bail!("at least one criteria is required for filesystem-device-match");
|
bail!("at least one criteria is required for filesystem-device-match");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the partition uuid from the extractor.
|
||||||
|
let partition_uuid = extractor
|
||||||
|
.has_partition_uuid
|
||||||
|
.as_deref()
|
||||||
|
.map(Guid::from_str)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
|
||||||
|
|
||||||
|
// Parse the partition type uuid from the extractor.
|
||||||
|
let partition_type_uuid = extractor
|
||||||
|
.has_partition_type_uuid
|
||||||
|
.as_deref()
|
||||||
|
.map(Guid::from_str)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
|
||||||
|
|
||||||
// Find all the filesystems inside the UEFI stack.
|
// Find all the filesystems inside the UEFI stack.
|
||||||
let handles = uefi::boot::find_handles::<SimpleFileSystem>()
|
let handles = uefi::boot::find_handles::<SimpleFileSystem>()
|
||||||
.context("unable to find filesystem handles")?;
|
.context("unable to find filesystem handles")?;
|
||||||
|
|
||||||
// Iterate over all the filesystems and check if they match the criteria.
|
// Iterate over all the filesystems and check if they match the criteria.
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
// This defines whether a match has been found.
|
// A filesystem that can't be checked is skipped, as it should not prevent
|
||||||
let mut has_match = false;
|
// finding a match on the other filesystems or using the fallback value.
|
||||||
|
match is_match(
|
||||||
// Check if the partition info matches partition uuid criteria.
|
&context,
|
||||||
if let Some(ref has_partition_uuid) = extractor.has_partition_uuid {
|
extractor,
|
||||||
// Parse the partition uuid from the extractor.
|
handle,
|
||||||
let parsed_uuid = Guid::from_str(has_partition_uuid)
|
partition_uuid,
|
||||||
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
|
partition_type_uuid,
|
||||||
|
) {
|
||||||
// Fetch the root of the device.
|
Ok(true) => {}
|
||||||
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
|
Ok(false) => continue,
|
||||||
.context("unable to fetch the device path of the filesystem")?
|
Err(error) => {
|
||||||
.deref()
|
warn!("skipping filesystem, unable to check it: {:#}", error);
|
||||||
.to_boxed();
|
|
||||||
|
|
||||||
// Fetch the partition uuid for this filesystem.
|
|
||||||
let partition_uuid =
|
|
||||||
eficore::partition::partition_guid(&root, PartitionGuidForm::Partition)
|
|
||||||
.context("unable to fetch the partition uuid of the filesystem")?;
|
|
||||||
|
|
||||||
// Compare the partition uuid to the parsed uuid.
|
|
||||||
// If it does not match, continue to the next filesystem.
|
|
||||||
if partition_uuid != Some(parsed_uuid) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
has_match = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the partition info matches partition type uuid criteria.
|
|
||||||
if let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid {
|
|
||||||
// Parse the partition type uuid from the extractor.
|
|
||||||
let parsed_uuid = Guid::from_str(has_partition_type_uuid)
|
|
||||||
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
|
|
||||||
|
|
||||||
// Fetch the root of the device.
|
|
||||||
let root = uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
|
|
||||||
.context("unable to fetch the device path of the filesystem")?
|
|
||||||
.deref()
|
|
||||||
.to_boxed();
|
|
||||||
|
|
||||||
// Fetch the partition type uuid for this filesystem.
|
|
||||||
let partition_type_uuid =
|
|
||||||
eficore::partition::partition_guid(&root, PartitionGuidForm::PartitionType)
|
|
||||||
.context("unable to fetch the partition uuid of the filesystem")?;
|
|
||||||
// Compare the partition type uuid to the parsed uuid.
|
|
||||||
// If it does not match, continue to the next filesystem.
|
|
||||||
if partition_type_uuid != Some(parsed_uuid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
has_match = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the filesystem protocol for this handle.
|
|
||||||
let mut filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
|
|
||||||
.context("unable to open filesystem protocol")?;
|
|
||||||
|
|
||||||
// Check if the filesystem matches label criteria.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the filesystem matches item criteria.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Check the metadata of the item.
|
|
||||||
// Ignore filesystem errors as we can't do anything useful with the error.
|
|
||||||
let Some(metadata) = filesystem.metadata(Path::new(&want_item)).ok() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only check directories and files.
|
|
||||||
if !(metadata.is_directory() || metadata.is_regular_file()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
has_match = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no match, continue to the next filesystem.
|
|
||||||
if !has_match {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a match, return the device root path.
|
// If we have a match, return the device root path.
|
||||||
|
|||||||
@@ -18,11 +18,18 @@ use uefi::{
|
|||||||
proto::media::fs::SimpleFileSystem,
|
proto::media::fs::SimpleFileSystem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The number of initrd slots set on each BLS entry.
|
||||||
|
/// Entries set `initrd-0` through `initrd-7`, and slots without an initrd are empty.
|
||||||
|
pub const BLS_INITRD_SLOTS: usize = 8;
|
||||||
|
|
||||||
// TODO(azenla): remove this once variable substitution is implemented.
|
// TODO(azenla): remove this once variable substitution is implemented.
|
||||||
/// This function is used to remove the `tuned_initrd` variable from entry values.
|
/// This function is used to remove the `tuned_initrd` variable from the initrd paths.
|
||||||
/// Fedora uses tuned which adds an initrd that shouldn't be used.
|
/// Fedora uses tuned which adds an initrd that shouldn't be used.
|
||||||
fn quirk_initrd_remove_tuned(input: String) -> String {
|
fn quirk_initrd_remove_tuned(paths: Vec<String>) -> Vec<String> {
|
||||||
input.replace("$tuned_initrd", "").trim().to_string()
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.filter(|path| path != "$tuned_initrd")
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sorts two entries according to the BLS sort system.
|
/// Sorts two entries according to the BLS sort system.
|
||||||
@@ -139,6 +146,18 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put the initrds through a quirk modifier to support Fedora.
|
||||||
|
let initrds = quirk_initrd_remove_tuned(entry.initrd_paths());
|
||||||
|
|
||||||
|
// Skip entries with more initrds than there are slots, as they can't be fully loaded.
|
||||||
|
if initrds.len() > BLS_INITRD_SLOTS {
|
||||||
|
warn!(
|
||||||
|
"bls entry {} has more than {} initrds, skipping",
|
||||||
|
name, BLS_INITRD_SLOTS
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Produce a new sprout context for the entry with the extracted values.
|
// Produce a new sprout context for the entry with the extracted values.
|
||||||
let mut context = context.fork();
|
let mut context = context.fork();
|
||||||
|
|
||||||
@@ -148,9 +167,6 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
|||||||
let version = entry.version().unwrap_or_default();
|
let version = entry.version().unwrap_or_default();
|
||||||
let machine_id = entry.machine_id().unwrap_or_default();
|
let machine_id = entry.machine_id().unwrap_or_default();
|
||||||
|
|
||||||
// Put the initrd through a quirk modifier to support Fedora.
|
|
||||||
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
|
|
||||||
|
|
||||||
// Combine the title with the version if a version is present, except if it already contains it.
|
// Combine the title with the version if a version is present, except if it already contains it.
|
||||||
// Sometimes BLS will have a version in the title already, and this makes it unique.
|
// Sometimes BLS will have a version in the title already, and this makes it unique.
|
||||||
let title_full = if !version.is_empty() && !title_base.contains(&version) {
|
let title_full = if !version.is_empty() && !title_base.contains(&version) {
|
||||||
@@ -163,7 +179,19 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
|||||||
context.set("title", title_full);
|
context.set("title", title_full);
|
||||||
context.set("chainload", chainload);
|
context.set("chainload", chainload);
|
||||||
context.set("options", options);
|
context.set("options", options);
|
||||||
context.set("initrd", initrd);
|
// The initrd value keeps the last initrd, which is what it held before
|
||||||
|
// multiple initrds were supported.
|
||||||
|
context.set("initrd", initrds.last().cloned().unwrap_or_default());
|
||||||
|
|
||||||
|
// Set every initrd slot, even the unused ones. An unset slot would let "$initrd-1"
|
||||||
|
// match "$initrd" instead, producing the initrd value followed by "-1".
|
||||||
|
for slot in 0..BLS_INITRD_SLOTS {
|
||||||
|
context.set(
|
||||||
|
format!("initrd-{}", slot),
|
||||||
|
initrds.get(slot).cloned().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
context.set("version", version);
|
context.set("version", version);
|
||||||
context.set("machine-id", machine_id);
|
context.set("machine-id", machine_id);
|
||||||
|
|
||||||
|
|||||||
+151
-49
@@ -64,6 +64,17 @@ pub mod sbat;
|
|||||||
/// The delay to wait for when an error occurs in Sprout.
|
/// The delay to wait for when an error occurs in Sprout.
|
||||||
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
|
const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// Unwraps the `result` of a bootloader interface operation, logging the error if one occurs.
|
||||||
|
/// The bootloader interface only exchanges information with the operating system, so a
|
||||||
|
/// firmware variable that can't be read or written should not prevent booting.
|
||||||
|
/// If an error occurs, the default value is provided instead.
|
||||||
|
fn advisory<T: Default>(result: Result<T>) -> T {
|
||||||
|
result.unwrap_or_else(|error| {
|
||||||
|
warn!("{:#}", error);
|
||||||
|
T::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Run Sprout, returning an error if one occurs.
|
/// Run Sprout, returning an error if one occurs.
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
|
// For safety reasons, we will note that Secure Boot is in beta on Sprout.
|
||||||
@@ -75,23 +86,36 @@ fn run() -> Result<()> {
|
|||||||
let timer = PlatformTimer::start();
|
let timer = PlatformTimer::start();
|
||||||
|
|
||||||
// Mark the initialization of Sprout in the bootloader interface.
|
// Mark the initialization of Sprout in the bootloader interface.
|
||||||
BootloaderInterface::mark_init(&timer)
|
advisory(
|
||||||
.context("unable to mark initialization in bootloader interface")?;
|
BootloaderInterface::mark_init(&timer)
|
||||||
|
.context("unable to mark initialization in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Tell the bootloader interface what firmware we are running on.
|
// Tell the bootloader interface what firmware we are running on.
|
||||||
BootloaderInterface::set_firmware_info()
|
advisory(
|
||||||
.context("unable to set firmware info in bootloader interface")?;
|
BootloaderInterface::set_firmware_info()
|
||||||
|
.context("unable to set firmware info in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Tell the bootloader interface what loader is being used.
|
// Tell the bootloader interface what loader is being used.
|
||||||
BootloaderInterface::set_loader_info()
|
advisory(
|
||||||
.context("unable to set loader info in bootloader interface")?;
|
BootloaderInterface::set_loader_info()
|
||||||
|
.context("unable to set loader info in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Acquire the number of active PCR banks on the TPM.
|
// Acquire the number of active PCR banks on the TPM.
|
||||||
// If no TPM is available, this will return zero.
|
// If no TPM is available, this will return zero.
|
||||||
let active_pcr_banks = PlatformTpm::active_pcr_banks()?;
|
// This is only reported to the bootloader interface, so a TPM that fails to report its
|
||||||
|
// PCR banks is treated as having none, rather than preventing boot.
|
||||||
|
let active_pcr_banks = PlatformTpm::active_pcr_banks().unwrap_or_else(|error| {
|
||||||
|
warn!("unable to determine the active TPM PCR banks: {}", error);
|
||||||
|
0
|
||||||
|
});
|
||||||
// Tell the bootloader interface what the number of active PCR banks is.
|
// Tell the bootloader interface what the number of active PCR banks is.
|
||||||
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
|
advisory(
|
||||||
.context("unable to set tpm2 active PCR banks in bootloader interface")?;
|
BootloaderInterface::set_tpm2_active_pcr_banks(active_pcr_banks)
|
||||||
|
.context("unable to set tpm2 active PCR banks in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Parse the options to the sprout executable.
|
// Parse the options to the sprout executable.
|
||||||
let options = SproutOptions::parse().context("unable to parse options")?;
|
let options = SproutOptions::parse().context("unable to parse options")?;
|
||||||
@@ -118,20 +142,26 @@ fn run() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Grab the partition GUID of the ESP that sprout was loaded from.
|
// Grab the partition GUID of the ESP that sprout was loaded from.
|
||||||
let loaded_image_partition_guid =
|
// This is only reported to the bootloader interface.
|
||||||
|
let loaded_image_partition_guid = advisory(
|
||||||
eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
|
eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition)
|
||||||
.context("unable to retrieve loaded image partition guid")?;
|
.context("unable to retrieve loaded image partition guid"),
|
||||||
|
);
|
||||||
|
|
||||||
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
|
// Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface.
|
||||||
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
|
if let Some(loaded_image_partition_guid) = loaded_image_partition_guid {
|
||||||
// Tell the system about the partition GUID.
|
// Tell the system about the partition GUID.
|
||||||
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
|
advisory(
|
||||||
.context("unable to set partition guid in bootloader interface")?;
|
BootloaderInterface::set_partition_guid(&loaded_image_partition_guid)
|
||||||
|
.context("unable to set partition guid in bootloader interface"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the bootloader interface what the loaded image path is.
|
// Tell the bootloader interface what the loaded image path is.
|
||||||
BootloaderInterface::set_loader_path(&loaded_image_path)
|
advisory(
|
||||||
.context("unable to set loader path in bootloader interface")?;
|
BootloaderInterface::set_loader_path(&loaded_image_path)
|
||||||
|
.context("unable to set loader path in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Create the root context.
|
// Create the root context.
|
||||||
let mut root = RootContext::new(loaded_image_path, timer, options);
|
let mut root = RootContext::new(loaded_image_path, timer, options);
|
||||||
@@ -230,19 +260,29 @@ fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in &mut entries {
|
// Entries whose context can't be finalized are skipped, as one broken entry
|
||||||
|
// should not prevent booting any of the other entries.
|
||||||
|
entries.retain_mut(|entry| {
|
||||||
let mut context = entry.context().fork();
|
let mut context = entry.context().fork();
|
||||||
// Insert the values from the entry configuration into the
|
// Insert the values from the entry configuration into the
|
||||||
// sprout context to use with the entry itself.
|
// sprout context to use with the entry itself.
|
||||||
context.insert(&entry.declaration().values);
|
context.insert(&entry.declaration().values);
|
||||||
let context = context
|
let context = match context.finalize() {
|
||||||
.finalize()
|
Ok(context) => context.freeze(),
|
||||||
.context("unable to finalize context")?
|
Err(error) => {
|
||||||
.freeze();
|
warn!(
|
||||||
|
"skipping entry {}, unable to finalize context: {:#}",
|
||||||
|
entry.name(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
// Provide the new context to the bootable entry.
|
// Provide the new context to the bootable entry.
|
||||||
entry.swap_context(context);
|
entry.swap_context(context);
|
||||||
// Restamp the title with any values.
|
// Restamp the title and sort key with any values.
|
||||||
entry.restamp_title();
|
entry.restamp_title();
|
||||||
|
entry.restamp_sort_key();
|
||||||
|
|
||||||
// Mark this entry as the default entry if it is declared as such.
|
// Mark this entry as the default entry if it is declared as such.
|
||||||
if let Some(ref default_entry) = config.options.default_entry {
|
if let Some(ref default_entry) = config.options.default_entry {
|
||||||
@@ -251,27 +291,36 @@ fn run() -> Result<()> {
|
|||||||
entry.mark_default();
|
entry.mark_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
// Sort the entries by their sort key, finalizing the order to show entries. This happens
|
// Sort the entries by their sort key, finalizing the order to show entries. This happens
|
||||||
// in reverse order so that entries that would come last show up first in the menu.
|
// in reverse order so that entries that would come last show up first in the menu.
|
||||||
entries.sort_by(|a, b| compare_versions(a.sort_key(), b.sort_key()).reverse());
|
entries.sort_by(|a, b| compare_versions(a.sort_key(), b.sort_key()).reverse());
|
||||||
|
|
||||||
// Tell the bootloader interface what entries are available.
|
// Tell the bootloader interface what entries are available.
|
||||||
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
|
advisory(
|
||||||
.context("unable to set entries in bootloader interface")?;
|
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
|
||||||
|
.context("unable to set entries in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Acquire the timeout setting from the bootloader interface.
|
// Acquire the timeout setting from the bootloader interface.
|
||||||
let bootloader_interface_timeout =
|
let bootloader_interface_timeout = advisory(
|
||||||
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
|
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout"),
|
||||||
|
);
|
||||||
|
|
||||||
// Acquire the default entry from the bootloader interface.
|
// Acquire the default entry from the bootloader interface.
|
||||||
let bootloader_interface_default_entry = BootloaderInterface::get_default_entry()
|
let bootloader_interface_default_entry = advisory(
|
||||||
.context("unable to get bootloader interface default entry")?;
|
BootloaderInterface::get_default_entry()
|
||||||
|
.context("unable to get bootloader interface default entry"),
|
||||||
|
);
|
||||||
|
|
||||||
// Acquire the oneshot entry from the bootloader interface.
|
// Acquire the oneshot entry from the bootloader interface.
|
||||||
let bootloader_interface_oneshot_entry = BootloaderInterface::get_oneshot_entry()
|
let bootloader_interface_oneshot_entry = advisory(
|
||||||
.context("unable to get bootloader interface oneshot entry")?;
|
BootloaderInterface::get_oneshot_entry()
|
||||||
|
.context("unable to get bootloader interface oneshot entry"),
|
||||||
|
);
|
||||||
|
|
||||||
// If --boot is specified, boot that entry immediately.
|
// If --boot is specified, boot that entry immediately.
|
||||||
let mut force_boot_entry = context.root().options().boot.clone();
|
let mut force_boot_entry = context.root().options().boot.clone();
|
||||||
@@ -286,11 +335,21 @@ fn run() -> Result<()> {
|
|||||||
.menu_timeout
|
.menu_timeout
|
||||||
.unwrap_or(config.options.menu_timeout);
|
.unwrap_or(config.options.menu_timeout);
|
||||||
|
|
||||||
|
// Whether the boot menu should wait for the user, instead of counting down.
|
||||||
|
let mut wait_for_user = false;
|
||||||
|
|
||||||
// Apply bootloader interface timeout settings.
|
// Apply bootloader interface timeout settings.
|
||||||
match bootloader_interface_timeout {
|
match bootloader_interface_timeout {
|
||||||
BootloaderInterfaceTimeout::MenuForce => {
|
BootloaderInterfaceTimeout::MenuForce => {
|
||||||
// Force the boot menu.
|
// Force the boot menu, and wait for the user to choose an entry.
|
||||||
force_boot_menu = true;
|
force_boot_menu = true;
|
||||||
|
wait_for_user = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BootloaderInterfaceTimeout::MenuForceTimeout(timeout) => {
|
||||||
|
// Force the boot menu with the specified timeout.
|
||||||
|
force_boot_menu = true;
|
||||||
|
menu_timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
|
BootloaderInterfaceTimeout::MenuHidden | BootloaderInterfaceTimeout::MenuDisabled => {
|
||||||
@@ -310,15 +369,27 @@ fn run() -> Result<()> {
|
|||||||
|
|
||||||
// Apply bootloader interface default entry settings.
|
// Apply bootloader interface default entry settings.
|
||||||
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
|
if let Some(ref bootloader_interface_default_entry) = bootloader_interface_default_entry {
|
||||||
// Iterate over all the entries and mark the default entry as the one specified.
|
if entries
|
||||||
for entry in &mut entries {
|
.iter()
|
||||||
// Mark the entry as the default entry if it matches the specified entry.
|
.any(|entry| entry.is_match(bootloader_interface_default_entry))
|
||||||
// If the entry does not match the specified entry, unmark it as the default entry.
|
{
|
||||||
if entry.is_match(bootloader_interface_default_entry) {
|
// Iterate over all the entries and mark the default entry as the one specified.
|
||||||
entry.mark_default();
|
for entry in &mut entries {
|
||||||
} else {
|
// Mark the entry as the default entry if it matches the specified entry.
|
||||||
entry.unmark_default();
|
// If the entry does not match the specified entry, unmark it as the default entry.
|
||||||
|
if entry.is_match(bootloader_interface_default_entry) {
|
||||||
|
entry.mark_default();
|
||||||
|
} else {
|
||||||
|
entry.unmark_default();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If no entry matches, such as when the entry was removed, the setting is ignored
|
||||||
|
// so that the configured default entry is still used.
|
||||||
|
warn!(
|
||||||
|
"ignoring bootloader interface default entry '{}': no matching entry",
|
||||||
|
bootloader_interface_default_entry
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,21 +407,52 @@ fn run() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert the menu timeout to a duration.
|
// Convert the menu timeout to a duration.
|
||||||
let menu_timeout = Duration::from_secs(menu_timeout);
|
// A zero timeout boots without showing the menu, so a forced menu with a zero timeout
|
||||||
|
// waits for the user instead.
|
||||||
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
|
let menu_timeout = if wait_for_user || (force_boot_menu && menu_timeout == 0) {
|
||||||
let entry = if !force_boot_menu && let Some(ref force_boot_entry) = force_boot_entry {
|
None
|
||||||
BootableEntry::find(force_boot_entry, entries.iter())
|
|
||||||
.context(format!("unable to find entry: {force_boot_entry}"))?
|
|
||||||
} else {
|
} else {
|
||||||
|
Some(Duration::from_secs(menu_timeout))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine the menu style based on the options or configuration.
|
||||||
|
// We prefer the options over the configuration to allow for overriding.
|
||||||
|
let menu_style = context
|
||||||
|
.root()
|
||||||
|
.options()
|
||||||
|
.menu_style
|
||||||
|
.unwrap_or(config.options.menu_style);
|
||||||
|
|
||||||
|
// Find the forced boot entry, unless the boot menu is forced.
|
||||||
|
// If the forced boot entry can't be found, such as when it was removed,
|
||||||
|
// the boot menu is used instead.
|
||||||
|
let forced_entry = force_boot_entry
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| !force_boot_menu)
|
||||||
|
.and_then(|force_boot_entry| {
|
||||||
|
let entry = BootableEntry::find(force_boot_entry, entries.iter());
|
||||||
|
if entry.is_none() {
|
||||||
|
warn!(
|
||||||
|
"unable to find entry '{}', using the boot menu",
|
||||||
|
force_boot_entry
|
||||||
|
);
|
||||||
|
}
|
||||||
|
entry
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the forced boot entry if possible, otherwise pick an entry using a boot menu.
|
||||||
|
let entry = match forced_entry {
|
||||||
|
Some(entry) => entry,
|
||||||
// Delegate to the menu to select an entry to boot.
|
// Delegate to the menu to select an entry to boot.
|
||||||
menu::select(&timer, menu_timeout, &entries)
|
None => menu::select(&timer, menu_timeout, menu_style, &entries)
|
||||||
.context("unable to select entry via boot menu")?
|
.context("unable to select entry via boot menu")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tell the bootloader interface what the selected entry is.
|
// Tell the bootloader interface what the selected entry is.
|
||||||
BootloaderInterface::set_selected_entry(entry.name().to_string())
|
advisory(
|
||||||
.context("unable to set selected entry in bootloader interface")?;
|
BootloaderInterface::set_selected_entry(entry.name().to_string())
|
||||||
|
.context("unable to set selected entry in bootloader interface"),
|
||||||
|
);
|
||||||
|
|
||||||
// Execute the late phase, now that the entry is chosen but before its actions are executed.
|
// Execute the late phase, now that the entry is chosen but before its actions are executed.
|
||||||
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
|
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
|
||||||
|
|||||||
+86
-137
@@ -1,36 +1,44 @@
|
|||||||
use crate::entries::BootableEntry;
|
use crate::entries::BootableEntry;
|
||||||
|
use crate::menu::basic::BasicMenu;
|
||||||
|
use crate::menu::simple::SimpleMenu;
|
||||||
use alloc::vec;
|
use alloc::vec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
use edera_sprout_config::MenuStyle;
|
||||||
use eficore::bootloader_interface::BootloaderInterface;
|
use eficore::bootloader_interface::BootloaderInterface;
|
||||||
use eficore::platform::timer::PlatformTimer;
|
use eficore::platform::timer::PlatformTimer;
|
||||||
use log::{info, warn};
|
use log::warn;
|
||||||
use uefi::ResultExt;
|
use uefi::ResultExt;
|
||||||
use uefi::boot::TimerTrigger;
|
use uefi::boot::TimerTrigger;
|
||||||
use uefi::proto::console::text::{Input, Key, ScanCode};
|
use uefi::proto::console::text::{Input, Key};
|
||||||
use uefi_raw::table::boot::{EventType, Tpl};
|
use uefi_raw::table::boot::{EventType, Tpl};
|
||||||
|
|
||||||
/// The characters that can be used to select an entry from keys.
|
/// basic: A menu that prints the entries and selects them by number.
|
||||||
const ENTRY_NUMBER_TABLE: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
pub mod basic;
|
||||||
|
|
||||||
/// Represents the operation that can be performed by the boot menu.
|
/// simple: A full-screen menu that selects entries with the arrow keys.
|
||||||
#[derive(PartialEq, Eq)]
|
pub mod simple;
|
||||||
enum MenuOperation {
|
|
||||||
/// The user selected a numbered entry.
|
/// The longest duration a single timer is set for. This is well within the range of
|
||||||
Number(usize),
|
/// the 100ns units that timers are set in, and still over a century long.
|
||||||
/// The user selected the escape key to exit the boot menu.
|
const MAX_TIMER_DURATION: Duration = Duration::from_secs(u32::MAX as u64);
|
||||||
Exit,
|
|
||||||
/// The user selected the enter key to display the entries again.
|
/// A boot menu that can be shown to select an entry to boot.
|
||||||
Continue,
|
pub trait BootMenu {
|
||||||
/// Timeout occurred.
|
/// Select an entry from `entries` to boot. If no entry is chosen before `timeout` passes,
|
||||||
Timeout,
|
/// the default entry is selected. If `timeout` is zero, the default entry is selected
|
||||||
/// No operation should be performed.
|
/// without showing the menu. If `timeout` is [None], the menu waits for the user.
|
||||||
Nop,
|
fn select<'a>(
|
||||||
|
&self,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &'a [BootableEntry],
|
||||||
|
) -> Result<&'a BootableEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a key from the input device with a duration, returning the [MenuOperation] that was
|
/// Read a key from `input`, giving up once `timeout` passes.
|
||||||
/// performed.
|
/// If `timeout` is [None], this waits for a key indefinitely.
|
||||||
fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
|
/// Returns the key that was pressed, or [None] if the timeout passed.
|
||||||
|
pub fn read_key(input: &mut Input, timeout: Option<Duration>) -> Result<Option<Key>> {
|
||||||
// The event to wait for a key press.
|
// The event to wait for a key press.
|
||||||
let key_event = input
|
let key_event = input
|
||||||
.wait_for_key_event()
|
.wait_for_key_event()
|
||||||
@@ -44,17 +52,38 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
|
|||||||
.context("unable to create timer event")?
|
.context("unable to create timer event")?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set a timer to trigger after the specified duration.
|
|
||||||
let trigger = TimerTrigger::Relative(*timeout);
|
|
||||||
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
|
|
||||||
|
|
||||||
let events = vec![timer_event, key_event];
|
let events = vec![timer_event, key_event];
|
||||||
|
|
||||||
// Wait for either the timer event or the key event to trigger.
|
// Wait for a key until the timer triggers.
|
||||||
// Store the result so that we can free the timer event.
|
// Store the result so that we can free the timer event.
|
||||||
let event_result = uefi::boot::wait_for_event(&events)
|
let result = (|| {
|
||||||
.discard_errdata()
|
// Set a timer to trigger after the specified duration.
|
||||||
.context("unable to wait for event");
|
// The timer is limited to what the firmware can represent, as a longer timeout
|
||||||
|
// can't be converted into a timer trigger.
|
||||||
|
// Without a timeout, the timer is never set, so it never triggers.
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
let trigger = TimerTrigger::Relative(timeout.min(MAX_TIMER_DURATION));
|
||||||
|
uefi::boot::set_timer(&events[0], trigger).context("unable to set timeout timer")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Wait for either the timer event or the key event to trigger.
|
||||||
|
let event = uefi::boot::wait_for_event(&events)
|
||||||
|
.discard_errdata()
|
||||||
|
.context("unable to wait for event")?;
|
||||||
|
|
||||||
|
// The first event is the timer event, so the timeout has passed.
|
||||||
|
if event == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some firmware signals the key event without a key being available.
|
||||||
|
// In that case, keep waiting on the same timer so the timeout is not extended.
|
||||||
|
if let Some(key) = input.read_key().context("unable to read key")? {
|
||||||
|
return Ok(Some(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Close the timer event that we acquired.
|
// Close the timer event that we acquired.
|
||||||
// We don't need to close the key event because it is owned globally.
|
// We don't need to close the key event because it is owned globally.
|
||||||
@@ -63,131 +92,51 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
|
|||||||
// Store the result of the close event so we can determine if we can safely assert it.
|
// Store the result of the close event so we can determine if we can safely assert it.
|
||||||
let close_event_result =
|
let close_event_result =
|
||||||
uefi::boot::close_event(timer_event).context("unable to close timer event");
|
uefi::boot::close_event(timer_event).context("unable to close timer event");
|
||||||
if event_result.is_err()
|
if result.is_err()
|
||||||
&& let Err(ref close_event_error) = close_event_result
|
&& let Err(ref close_event_error) = close_event_result
|
||||||
{
|
{
|
||||||
// Log a warning if we failed to close the timer event.
|
// Log a warning if we failed to close the timer event.
|
||||||
// This is done to ensure we don't mask the wait_for_event error.
|
// This is done to ensure we don't mask the error from reading the key.
|
||||||
warn!("unable to close timer event: {}", close_event_error);
|
warn!("unable to close timer event: {}", close_event_error);
|
||||||
} else {
|
} else {
|
||||||
// If we reach here, we can safely assert that the close event succeeded without
|
// If we reach here, we can safely assert that the close event succeeded without
|
||||||
// masking the wait_for_event error.
|
// masking the error from reading the key.
|
||||||
close_event_result?;
|
close_event_result?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire the event that triggered.
|
result
|
||||||
let event = event_result?;
|
|
||||||
|
|
||||||
// The first event is the timer event.
|
|
||||||
// If it has triggered, the user did not select a numbered entry.
|
|
||||||
if event == 0 {
|
|
||||||
return Ok(MenuOperation::Timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we reach here, there is a key event.
|
|
||||||
// Some firmware signals the key event without a key being available,
|
|
||||||
// in which case there is nothing to do.
|
|
||||||
let Some(key) = input.read_key().context("unable to read key")? else {
|
|
||||||
return Ok(MenuOperation::Nop);
|
|
||||||
};
|
|
||||||
|
|
||||||
match key {
|
|
||||||
Key::Printable(c) => {
|
|
||||||
// If the key is not ascii, we can't process it.
|
|
||||||
if !c.is_ascii() {
|
|
||||||
return Ok(MenuOperation::Continue);
|
|
||||||
}
|
|
||||||
// Convert the key to a char.
|
|
||||||
let c: char = c.into();
|
|
||||||
// Find the key pressed in the entry number table or continue.
|
|
||||||
Ok(ENTRY_NUMBER_TABLE
|
|
||||||
.iter()
|
|
||||||
.position(|&x| x == c)
|
|
||||||
.map(MenuOperation::Number)
|
|
||||||
.unwrap_or(MenuOperation::Continue))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The escape key is used to exit the boot menu.
|
|
||||||
Key::Special(ScanCode::ESCAPE) => Ok(MenuOperation::Exit),
|
|
||||||
|
|
||||||
// If the special key is unknown, do nothing.
|
|
||||||
Key::Special(_) => Ok(MenuOperation::Nop),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects an entry from the list of entries using the boot menu.
|
/// Shows a boot menu of the specified `style` to select a bootable entry to boot.
|
||||||
fn select_with_input<'a>(
|
/// See [BootMenu::select] for how `timeout` is handled.
|
||||||
input: &mut Input,
|
|
||||||
timeout: Duration,
|
|
||||||
entries: &'a [BootableEntry],
|
|
||||||
) -> Result<&'a BootableEntry> {
|
|
||||||
loop {
|
|
||||||
// If the timeout is not zero, let's display the boot menu.
|
|
||||||
if !timeout.is_zero() {
|
|
||||||
// Until a pretty menu is available, we just print all the entries.
|
|
||||||
info!("Boot Menu:");
|
|
||||||
for (index, entry) in entries.iter().enumerate() {
|
|
||||||
let title = entry.context().stamp(&entry.declaration().title);
|
|
||||||
info!(" [{}] {}", index, title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read from input until a valid operation is selected.
|
|
||||||
let operation = loop {
|
|
||||||
// If the timeout is zero, we can exit immediately because there is nothing to do.
|
|
||||||
if timeout.is_zero() {
|
|
||||||
break MenuOperation::Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Select a boot entry using the number keys.");
|
|
||||||
info!("Press Escape to exit and enter to display the entries again.");
|
|
||||||
|
|
||||||
let operation = read(input, &timeout)?;
|
|
||||||
if operation != MenuOperation::Nop {
|
|
||||||
break operation;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match operation {
|
|
||||||
// Entry was selected by number. If the number is invalid, we continue.
|
|
||||||
MenuOperation::Number(index) => {
|
|
||||||
let Some(entry) = entries.get(index) else {
|
|
||||||
info!("invalid entry number");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
return Ok(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the user exits the boot menu or a timeout occurs, we should
|
|
||||||
// boot the default entry, if any.
|
|
||||||
MenuOperation::Exit | MenuOperation::Timeout => {
|
|
||||||
return entries
|
|
||||||
.iter()
|
|
||||||
.find(|item| item.is_default())
|
|
||||||
.context("no default entry available");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the operation is to continue or nop, we can just run the loop again.
|
|
||||||
MenuOperation::Continue | MenuOperation::Nop => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows a boot menu to select a bootable entry to boot.
|
|
||||||
/// The actual work is done internally in [select_with_input] which is called
|
|
||||||
/// within the context of the standard input device.
|
|
||||||
pub fn select<'live>(
|
pub fn select<'live>(
|
||||||
timer: &'live PlatformTimer,
|
timer: &'live PlatformTimer,
|
||||||
timeout: Duration,
|
timeout: Option<Duration>,
|
||||||
|
style: MenuStyle,
|
||||||
entries: &'live [BootableEntry],
|
entries: &'live [BootableEntry],
|
||||||
) -> Result<&'live BootableEntry> {
|
) -> Result<&'live BootableEntry> {
|
||||||
// Notify the bootloader interface that we are about to display the menu.
|
// Notify the bootloader interface that we are about to display the menu.
|
||||||
BootloaderInterface::mark_menu(timer)
|
// This is only informational, so it should not prevent booting.
|
||||||
.context("unable to mark menu display in bootloader interface")?;
|
if let Err(error) = BootloaderInterface::mark_menu(timer) {
|
||||||
|
warn!(
|
||||||
|
"unable to mark menu display in bootloader interface: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Acquire the standard input device and run the boot menu.
|
// Pick the menu that implements the requested style.
|
||||||
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
|
match style {
|
||||||
|
MenuStyle::Basic => BasicMenu.select(timeout, entries),
|
||||||
|
|
||||||
|
// The simple menu needs a console that can move the cursor and set colors, which
|
||||||
|
// not every console supports. If it fails, the basic menu is used instead.
|
||||||
|
MenuStyle::Simple => SimpleMenu.select(timeout, entries).or_else(|error| {
|
||||||
|
warn!(
|
||||||
|
"unable to show the simple boot menu, using the basic menu: {:#}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
BasicMenu.select(timeout, entries)
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
use crate::entries::BootableEntry;
|
||||||
|
use crate::menu::{BootMenu, read_key};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use core::time::Duration;
|
||||||
|
use log::info;
|
||||||
|
use uefi::proto::console::text::{Input, Key, ScanCode};
|
||||||
|
|
||||||
|
/// The characters that can be used to select an entry from keys.
|
||||||
|
const ENTRY_NUMBER_TABLE: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||||
|
|
||||||
|
/// A boot menu that prints the entries and selects them by number.
|
||||||
|
pub struct BasicMenu;
|
||||||
|
|
||||||
|
/// Represents the operation that can be performed by the boot menu.
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum MenuOperation {
|
||||||
|
/// The user typed a digit of an entry number.
|
||||||
|
Digit(usize),
|
||||||
|
/// The user selected the escape key to exit the boot menu.
|
||||||
|
Exit,
|
||||||
|
/// The user selected the enter key to boot the typed entry number,
|
||||||
|
/// or to display the entries again if no number was typed.
|
||||||
|
Enter,
|
||||||
|
/// The user selected some other key to display the entries again.
|
||||||
|
Continue,
|
||||||
|
/// Timeout occurred.
|
||||||
|
Timeout,
|
||||||
|
/// No operation should be performed.
|
||||||
|
Nop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a key from the input device with an optional timeout, returning the [MenuOperation]
|
||||||
|
/// that was performed. Without a timeout, this waits for a key indefinitely.
|
||||||
|
fn read(input: &mut Input, timeout: Option<Duration>) -> Result<MenuOperation> {
|
||||||
|
// If the timer triggered, the user did not select a numbered entry.
|
||||||
|
let Some(key) = read_key(input, timeout)? else {
|
||||||
|
return Ok(MenuOperation::Timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
match key {
|
||||||
|
Key::Printable(c) => {
|
||||||
|
// If the key is not ascii, we can't process it.
|
||||||
|
if !c.is_ascii() {
|
||||||
|
return Ok(MenuOperation::Continue);
|
||||||
|
}
|
||||||
|
// Convert the key to a char.
|
||||||
|
let c: char = c.into();
|
||||||
|
// Serial consoles may send a line feed instead of a carriage return.
|
||||||
|
if matches!(c, '\r' | '\n') {
|
||||||
|
return Ok(MenuOperation::Enter);
|
||||||
|
}
|
||||||
|
// Find the key pressed in the entry number table or continue.
|
||||||
|
Ok(ENTRY_NUMBER_TABLE
|
||||||
|
.iter()
|
||||||
|
.position(|&x| x == c)
|
||||||
|
.map(MenuOperation::Digit)
|
||||||
|
.unwrap_or(MenuOperation::Continue))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The escape key is used to exit the boot menu.
|
||||||
|
Key::Special(ScanCode::ESCAPE) => Ok(MenuOperation::Exit),
|
||||||
|
|
||||||
|
// If the special key is unknown, do nothing.
|
||||||
|
Key::Special(_) => Ok(MenuOperation::Nop),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the default entry in `entries`.
|
||||||
|
fn default_entry(entries: &[BootableEntry]) -> Result<&BootableEntry> {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.is_default())
|
||||||
|
.context("no default entry available")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects an entry from the list of entries using the boot menu.
|
||||||
|
fn select_with_input<'a>(
|
||||||
|
input: &mut Input,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &'a [BootableEntry],
|
||||||
|
) -> Result<&'a BootableEntry> {
|
||||||
|
// If the timeout is zero, boot the default entry without showing the menu.
|
||||||
|
if timeout.is_some_and(|timeout| timeout.is_zero()) {
|
||||||
|
return default_entry(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The time to wait for a key, or None once a key stops the countdown.
|
||||||
|
let mut countdown = timeout;
|
||||||
|
|
||||||
|
// The entry number typed so far, while more digits could still select another entry.
|
||||||
|
let mut typed: Option<usize> = None;
|
||||||
|
// Whether to print the entries before reading the next key.
|
||||||
|
let mut display = true;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if display {
|
||||||
|
// Print all the entries with the number used to select them.
|
||||||
|
info!("Boot Menu:");
|
||||||
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
|
let title = entry.context().stamp(&entry.declaration().title);
|
||||||
|
info!(" [{}] {}", index, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Select a boot entry using the number keys.");
|
||||||
|
if entries.len() > ENTRY_NUMBER_TABLE.len() {
|
||||||
|
info!("Press enter after the number if the entry does not boot right away.");
|
||||||
|
}
|
||||||
|
info!("Press Escape to exit and enter to display the entries again.");
|
||||||
|
display = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let operation = read(input, countdown)?;
|
||||||
|
|
||||||
|
// Any key stops the countdown, so the menu waits for the user from now on.
|
||||||
|
if operation != MenuOperation::Timeout {
|
||||||
|
countdown = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match operation {
|
||||||
|
// A digit of the entry number was typed.
|
||||||
|
MenuOperation::Digit(digit) => {
|
||||||
|
let number = typed
|
||||||
|
.take()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.saturating_mul(10)
|
||||||
|
.saturating_add(digit);
|
||||||
|
|
||||||
|
// If another digit could select a different entry, wait for it or for enter.
|
||||||
|
if number != 0 && number.saturating_mul(10) < entries.len() {
|
||||||
|
typed = Some(number);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, boot the entry. If the number is invalid, we continue.
|
||||||
|
let Some(entry) = entries.get(number) else {
|
||||||
|
info!("invalid entry number");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
return Ok(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter boots the typed entry number, or displays the entries again.
|
||||||
|
MenuOperation::Enter => {
|
||||||
|
let Some(number) = typed.take() else {
|
||||||
|
display = true;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(entry) = entries.get(number) else {
|
||||||
|
info!("invalid entry number");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
return Ok(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user exits the boot menu or a timeout occurs, we should
|
||||||
|
// boot the default entry, if any.
|
||||||
|
MenuOperation::Exit | MenuOperation::Timeout => {
|
||||||
|
return default_entry(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other key clears the typed number and displays the entries again.
|
||||||
|
MenuOperation::Continue => {
|
||||||
|
typed = None;
|
||||||
|
display = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the operation is nop, there is nothing to do.
|
||||||
|
MenuOperation::Nop => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootMenu for BasicMenu {
|
||||||
|
/// Selects an entry using the basic menu.
|
||||||
|
/// The actual work is done internally in [select_with_input] which is called
|
||||||
|
/// within the context of the standard input device.
|
||||||
|
fn select<'a>(
|
||||||
|
&self,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &'a [BootableEntry],
|
||||||
|
) -> Result<&'a BootableEntry> {
|
||||||
|
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
use crate::entries::BootableEntry;
|
||||||
|
use crate::menu::{BootMenu, read_key};
|
||||||
|
use alloc::format;
|
||||||
|
use alloc::string::String;
|
||||||
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use core::fmt::Write;
|
||||||
|
use core::iter::repeat;
|
||||||
|
use core::time::Duration;
|
||||||
|
use uefi::proto::console::text::{Color, Input, Key, Output, ScanCode};
|
||||||
|
|
||||||
|
/// The number of columns to assume if the console does not report its mode.
|
||||||
|
const FALLBACK_COLUMNS: usize = 80;
|
||||||
|
|
||||||
|
/// The number of rows to assume if the console does not report its mode.
|
||||||
|
const FALLBACK_ROWS: usize = 25;
|
||||||
|
|
||||||
|
/// How often the countdown is updated.
|
||||||
|
const TICK: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
/// The number of spaces on either side of an entry title.
|
||||||
|
const ENTRY_PADDING: usize = 2;
|
||||||
|
|
||||||
|
/// The hint shown once the countdown has been stopped.
|
||||||
|
const HINT: &str = "Use the arrow keys to select an entry and press enter to boot.";
|
||||||
|
|
||||||
|
/// A full-screen boot menu that selects entries with the arrow keys, similar to systemd-boot.
|
||||||
|
pub struct SimpleMenu;
|
||||||
|
|
||||||
|
/// The position and size of the menu on the console.
|
||||||
|
struct Layout {
|
||||||
|
/// The number of rows on the console.
|
||||||
|
rows: usize,
|
||||||
|
/// The number of columns that can be drawn to.
|
||||||
|
columns: usize,
|
||||||
|
/// The column that entries start at.
|
||||||
|
x: usize,
|
||||||
|
/// The row that entries start at.
|
||||||
|
y: usize,
|
||||||
|
/// The width of each entry, including padding.
|
||||||
|
width: usize,
|
||||||
|
/// The number of entries that fit on the console.
|
||||||
|
visible: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
/// Calculate the layout of `entries` on the console `output`.
|
||||||
|
fn new(output: &Output, entries: &[BootableEntry]) -> Self {
|
||||||
|
// Some consoles do not report a mode, so we assume a standard size for them.
|
||||||
|
let (columns, rows) = output
|
||||||
|
.current_mode()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|mode| (mode.columns(), mode.rows()))
|
||||||
|
.unwrap_or((FALLBACK_COLUMNS, FALLBACK_ROWS));
|
||||||
|
|
||||||
|
// Leave the last column empty, as writing to it makes some consoles scroll.
|
||||||
|
let columns = columns.saturating_sub(1);
|
||||||
|
|
||||||
|
// All entries are the width of the longest title so the highlight lines up.
|
||||||
|
let longest = entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.title().chars().count())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let width = (longest + ENTRY_PADDING * 2).min(columns);
|
||||||
|
|
||||||
|
// Keep the last two rows for the status line and a blank row above it.
|
||||||
|
let space = rows.saturating_sub(2).max(1);
|
||||||
|
let visible = entries.len().min(space);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
x: (columns - width) / 2,
|
||||||
|
y: (space - visible) / 2,
|
||||||
|
width,
|
||||||
|
visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw `text` at `column` and `row`, padded with spaces or cut to fit `width`.
|
||||||
|
fn draw(
|
||||||
|
output: &mut Output,
|
||||||
|
column: usize,
|
||||||
|
row: usize,
|
||||||
|
width: usize,
|
||||||
|
text: &str,
|
||||||
|
colors: (Color, Color),
|
||||||
|
) -> Result<()> {
|
||||||
|
// Replace anything that isn't printable ASCII, as not all consoles can draw it.
|
||||||
|
let line: String = text
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c == ' ' || c.is_ascii_graphic() {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
'?'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.chain(repeat(' '))
|
||||||
|
.take(width)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
output
|
||||||
|
.set_color(colors.0, colors.1)
|
||||||
|
.context("unable to set console color")?;
|
||||||
|
output
|
||||||
|
.set_cursor_position(column, row)
|
||||||
|
.context("unable to set console cursor position")?;
|
||||||
|
output
|
||||||
|
.write_str(&line)
|
||||||
|
.map_err(|_| anyhow!("unable to write to console"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the visible `entries` starting at `offset` with `selected` highlighted,
|
||||||
|
/// and the `status` line at the bottom of the console.
|
||||||
|
fn render(
|
||||||
|
output: &mut Output,
|
||||||
|
layout: &Layout,
|
||||||
|
entries: &[BootableEntry],
|
||||||
|
selected: usize,
|
||||||
|
offset: usize,
|
||||||
|
status: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
for (row, (index, entry)) in entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(offset)
|
||||||
|
.take(layout.visible)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
// The selected entry is drawn in inverted colors.
|
||||||
|
let colors = if index == selected {
|
||||||
|
(Color::Black, Color::LightGray)
|
||||||
|
} else {
|
||||||
|
(Color::LightGray, Color::Black)
|
||||||
|
};
|
||||||
|
let text = format!("{:padding$}{}", "", entry.title(), padding = ENTRY_PADDING);
|
||||||
|
draw(
|
||||||
|
output,
|
||||||
|
layout.x,
|
||||||
|
layout.y + row,
|
||||||
|
layout.width,
|
||||||
|
&text,
|
||||||
|
colors,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the status line across the console.
|
||||||
|
let padding = layout.columns.saturating_sub(status.len()) / 2;
|
||||||
|
let text = format!("{:padding$}{}", "", status);
|
||||||
|
draw(
|
||||||
|
output,
|
||||||
|
0,
|
||||||
|
layout.rows.saturating_sub(1),
|
||||||
|
layout.columns,
|
||||||
|
&text,
|
||||||
|
(Color::LightGray, Color::Black),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the menu until an entry is chosen, returning the index of that entry.
|
||||||
|
/// The `selected` entry is booted if no key is pressed before `timeout` passes.
|
||||||
|
/// Without a timeout, the menu waits for the user.
|
||||||
|
fn run(
|
||||||
|
input: &mut Input,
|
||||||
|
output: &mut Output,
|
||||||
|
layout: &Layout,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &[BootableEntry],
|
||||||
|
mut selected: usize,
|
||||||
|
) -> Result<usize> {
|
||||||
|
let last = entries.len() - 1;
|
||||||
|
// The first visible entry, which changes when scrolling through a long list.
|
||||||
|
let mut offset = 0;
|
||||||
|
// The time left before booting, or None once a key stops the countdown.
|
||||||
|
let mut remaining = timeout;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Scroll so that the selected entry is visible.
|
||||||
|
if selected < offset {
|
||||||
|
offset = selected;
|
||||||
|
} else if selected >= offset + layout.visible {
|
||||||
|
offset = selected + 1 - layout.visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = match remaining {
|
||||||
|
Some(remaining) => format!("Boot in {} s.", remaining.as_secs()),
|
||||||
|
None => String::from(HINT),
|
||||||
|
};
|
||||||
|
render(output, layout, entries, selected, offset, &status)?;
|
||||||
|
|
||||||
|
// Wait for a key, waking up every tick to update the countdown.
|
||||||
|
let tick = remaining.map_or(TICK, |remaining| remaining.min(TICK));
|
||||||
|
let Some(key) = read_key(input, Some(tick))? else {
|
||||||
|
// Without a countdown there's nothing to do until a key is pressed.
|
||||||
|
let Some(left) = remaining else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = left.saturating_sub(tick);
|
||||||
|
if left.is_zero() {
|
||||||
|
return Ok(selected);
|
||||||
|
}
|
||||||
|
remaining = Some(left);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Any key stops the countdown, which leaves escape with nothing else to do.
|
||||||
|
remaining = None;
|
||||||
|
|
||||||
|
match key {
|
||||||
|
Key::Special(ScanCode::UP) => selected = selected.saturating_sub(1),
|
||||||
|
Key::Special(ScanCode::DOWN) => selected = (selected + 1).min(last),
|
||||||
|
Key::Special(ScanCode::HOME) => selected = 0,
|
||||||
|
Key::Special(ScanCode::END) => selected = last,
|
||||||
|
Key::Special(ScanCode::PAGE_UP) => selected = selected.saturating_sub(layout.visible),
|
||||||
|
Key::Special(ScanCode::PAGE_DOWN) => selected = (selected + layout.visible).min(last),
|
||||||
|
|
||||||
|
// Serial consoles may send a line feed instead of a carriage return.
|
||||||
|
Key::Printable(c) if matches!(char::from(c), '\r' | '\n') => return Ok(selected),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects an entry from `entries` using the console `input` and `output`.
|
||||||
|
fn select_with_console<'a>(
|
||||||
|
input: &mut Input,
|
||||||
|
output: &mut Output,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &'a [BootableEntry],
|
||||||
|
) -> Result<&'a BootableEntry> {
|
||||||
|
let default = entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.is_default())
|
||||||
|
.context("no default entry available")?;
|
||||||
|
|
||||||
|
// If the timeout is zero, boot the default entry without showing the menu.
|
||||||
|
if timeout.is_some_and(|timeout| timeout.is_zero()) {
|
||||||
|
return Ok(&entries[default]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = Layout::new(output, entries);
|
||||||
|
let cursor_visible = output.cursor_visible();
|
||||||
|
|
||||||
|
output
|
||||||
|
.set_color(Color::LightGray, Color::Black)
|
||||||
|
.context("unable to set console color")?;
|
||||||
|
output.clear().context("unable to clear console")?;
|
||||||
|
// Not all consoles can hide the cursor, so this is allowed to fail.
|
||||||
|
let _ = output.enable_cursor(false);
|
||||||
|
|
||||||
|
let result = run(input, output, &layout, timeout, entries, default);
|
||||||
|
|
||||||
|
// Put the console back so that anything printed after the menu is readable.
|
||||||
|
let _ = output.set_color(Color::LightGray, Color::Black);
|
||||||
|
let _ = output.clear();
|
||||||
|
let _ = output.enable_cursor(cursor_visible);
|
||||||
|
|
||||||
|
Ok(&entries[result?])
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootMenu for SimpleMenu {
|
||||||
|
/// Selects an entry using the simple menu.
|
||||||
|
/// The actual work is done internally in [select_with_console] which is called
|
||||||
|
/// within the context of the standard input and output devices.
|
||||||
|
fn select<'a>(
|
||||||
|
&self,
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
entries: &'a [BootableEntry],
|
||||||
|
) -> Result<&'a BootableEntry> {
|
||||||
|
uefi::system::with_stdin(|input| {
|
||||||
|
uefi::system::with_stdout(|output| select_with_console(input, output, timeout, entries))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
-67
@@ -1,11 +1,12 @@
|
|||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use core::cell::Cell;
|
||||||
use core::ptr::null_mut;
|
use core::ptr::null_mut;
|
||||||
|
use edera_sprout_config::MenuStyle;
|
||||||
use jaarg::{
|
use jaarg::{
|
||||||
ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts,
|
HelpWriter, HelpWriterContext, Opt, Opts, ParseControl, ParseResult, StandardFullHelpWriter,
|
||||||
ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter,
|
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{info, warn};
|
||||||
use uefi_raw::Status;
|
use uefi_raw::Status;
|
||||||
|
|
||||||
/// Default configuration file path.
|
/// Default configuration file path.
|
||||||
@@ -24,6 +25,8 @@ pub struct SproutOptions {
|
|||||||
pub force_menu: bool,
|
pub force_menu: bool,
|
||||||
/// The timeout for the boot menu in seconds.
|
/// The timeout for the boot menu in seconds.
|
||||||
pub menu_timeout: Option<u64>,
|
pub menu_timeout: Option<u64>,
|
||||||
|
/// The style of boot menu to display.
|
||||||
|
pub menu_style: Option<MenuStyle>,
|
||||||
/// Retains the boot console before boot.
|
/// Retains the boot console before boot.
|
||||||
pub retain_boot_console: bool,
|
pub retain_boot_console: bool,
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ impl Default for SproutOptions {
|
|||||||
boot: None,
|
boot: None,
|
||||||
force_menu: false,
|
force_menu: false,
|
||||||
menu_timeout: None,
|
menu_timeout: None,
|
||||||
|
menu_style: None,
|
||||||
retain_boot_console: false,
|
retain_boot_console: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,6 +58,7 @@ impl SproutOptions {
|
|||||||
Boot,
|
Boot,
|
||||||
ForceMenu,
|
ForceMenu,
|
||||||
MenuTimeout,
|
MenuTimeout,
|
||||||
|
MenuStyle,
|
||||||
RetainBootConsole,
|
RetainBootConsole,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,78 +74,112 @@ impl SproutOptions {
|
|||||||
Opt::flag(ArgID::ForceMenu, &["--force-menu"]).help_text("Force showing the boot menu"),
|
Opt::flag(ArgID::ForceMenu, &["--force-menu"]).help_text("Force showing the boot menu"),
|
||||||
Opt::value(ArgID::MenuTimeout, &["--menu-timeout"], "TIMEOUT")
|
Opt::value(ArgID::MenuTimeout, &["--menu-timeout"], "TIMEOUT")
|
||||||
.help_text("Boot menu timeout, in seconds"),
|
.help_text("Boot menu timeout, in seconds"),
|
||||||
|
Opt::value(ArgID::MenuStyle, &["--menu-style"], "STYLE")
|
||||||
|
.help_text("Boot menu style, basic or simple"),
|
||||||
Opt::flag(ArgID::RetainBootConsole, &["--retain-boot-console"])
|
Opt::flag(ArgID::RetainBootConsole, &["--retain-boot-console"])
|
||||||
.help_text("Retain boot console before boot"),
|
.help_text("Retain boot console before boot"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Acquire the arguments as determined by the UEFI core.
|
// Acquire the arguments as determined by the UEFI core.
|
||||||
let args = eficore::env::args()?;
|
let mut args = eficore::env::args()?;
|
||||||
|
|
||||||
// Use the default value of sprout options and have the raw options be parsed into it.
|
// Parse the arguments, dropping any argument that can't be parsed and parsing again.
|
||||||
let mut result = Self::default();
|
// A typo in a boot entry or junk from the firmware should not prevent booting.
|
||||||
|
loop {
|
||||||
|
// Use the default value of sprout options and have the raw options be parsed into it.
|
||||||
|
let mut result = Self::default();
|
||||||
|
|
||||||
// Parse the OPTIONS into a map using jaarg.
|
// The number of arguments handed to the parser, used to find the one it rejected.
|
||||||
match OPTIONS.parse(
|
let consumed = Cell::new(0usize);
|
||||||
"sprout",
|
// The reason the parser rejected an argument, if it did.
|
||||||
args.iter(),
|
let mut rejection = None;
|
||||||
|program_name, id, _opt, _name, value| {
|
|
||||||
match id {
|
// Parse the OPTIONS into a map using jaarg.
|
||||||
ArgID::AutoConfigure => {
|
let parsed = OPTIONS.parse(
|
||||||
// Enable autoconfiguration.
|
"sprout",
|
||||||
result.autoconfigure = true;
|
args.iter().inspect(|_| consumed.set(consumed.get() + 1)),
|
||||||
}
|
|program_name, id, _opt, name, value| {
|
||||||
ArgID::Config => {
|
match id {
|
||||||
// The configuration file to load.
|
ArgID::AutoConfigure => {
|
||||||
result.config = value.into();
|
// Enable autoconfiguration.
|
||||||
}
|
result.autoconfigure = true;
|
||||||
ArgID::Boot => {
|
}
|
||||||
// The entry to boot.
|
ArgID::Config => {
|
||||||
result.boot = Some(value.into());
|
// The configuration file to load.
|
||||||
}
|
result.config = value.into();
|
||||||
ArgID::ForceMenu => {
|
}
|
||||||
// Force showing of the boot menu.
|
ArgID::Boot => {
|
||||||
result.force_menu = true;
|
// The entry to boot.
|
||||||
}
|
result.boot = Some(value.into());
|
||||||
ArgID::MenuTimeout => {
|
}
|
||||||
// The timeout for the boot menu in seconds.
|
ArgID::ForceMenu => {
|
||||||
result.menu_timeout = Some(value.parse::<u64>()?);
|
// Force showing of the boot menu.
|
||||||
}
|
result.force_menu = true;
|
||||||
ArgID::RetainBootConsole => {
|
}
|
||||||
// Retain the boot console before booting.
|
ArgID::MenuTimeout => {
|
||||||
result.retain_boot_console = true;
|
// The timeout for the boot menu in seconds.
|
||||||
}
|
match value.parse::<u64>() {
|
||||||
ArgID::Help => {
|
Ok(timeout) => result.menu_timeout = Some(timeout),
|
||||||
let ctx = HelpWriterContext {
|
Err(error) => {
|
||||||
options: &OPTIONS,
|
warn!("ignoring invalid {} value '{}': {}", name, value, error)
|
||||||
program_name,
|
}
|
||||||
};
|
}
|
||||||
info!("{}", StandardFullHelpWriter::new(ctx));
|
}
|
||||||
return Ok(ParseControl::Quit);
|
ArgID::MenuStyle => {
|
||||||
|
// The style of boot menu to display.
|
||||||
|
match value {
|
||||||
|
"basic" => result.menu_style = Some(MenuStyle::Basic),
|
||||||
|
"simple" => result.menu_style = Some(MenuStyle::Simple),
|
||||||
|
_ => warn!(
|
||||||
|
"ignoring invalid {} value '{}': expected basic or simple",
|
||||||
|
name, value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArgID::RetainBootConsole => {
|
||||||
|
// Retain the boot console before booting.
|
||||||
|
result.retain_boot_console = true;
|
||||||
|
}
|
||||||
|
ArgID::Help => {
|
||||||
|
let ctx = HelpWriterContext {
|
||||||
|
options: &OPTIONS,
|
||||||
|
program_name,
|
||||||
|
};
|
||||||
|
info!("{}", StandardFullHelpWriter::new(ctx));
|
||||||
|
return Ok(ParseControl::Quit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(ParseControl::Continue)
|
||||||
|
},
|
||||||
|
|_program_name, error| {
|
||||||
|
rejection = Some(error.to_string());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
ParseResult::ContinueSuccess => return Ok(result),
|
||||||
|
ParseResult::ExitSuccess => unsafe {
|
||||||
|
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut())
|
||||||
|
.context("unable to exit")?;
|
||||||
|
return Err(anyhow!("unable to exit"));
|
||||||
|
},
|
||||||
|
|
||||||
|
ParseResult::ExitError => {
|
||||||
|
// The parser stops at the argument it rejected, which makes it the last
|
||||||
|
// argument it was handed. This is also true of an option missing its value,
|
||||||
|
// which is only detected once the arguments run out.
|
||||||
|
let Some(index) = consumed.get().checked_sub(1).filter(|i| *i < args.len())
|
||||||
|
else {
|
||||||
|
bail!("unable to parse options: {}", rejection.unwrap_or_default());
|
||||||
|
};
|
||||||
|
let arg = args.remove(index);
|
||||||
|
warn!(
|
||||||
|
"ignoring argument '{}': {}",
|
||||||
|
arg,
|
||||||
|
rejection.unwrap_or_default()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(ParseControl::Continue)
|
}
|
||||||
},
|
|
||||||
|program_name, error| {
|
|
||||||
let ctx = ErrorUsageWriterContext {
|
|
||||||
options: &OPTIONS,
|
|
||||||
program_name,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
error!("{}", StandardErrorUsageWriter::new(ctx));
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
ParseResult::ContinueSuccess => Ok(result),
|
|
||||||
ParseResult::ExitSuccess => unsafe {
|
|
||||||
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut())
|
|
||||||
.context("unable to exit")?;
|
|
||||||
Err(anyhow!("unable to exit"))
|
|
||||||
},
|
|
||||||
|
|
||||||
ParseResult::ExitError => unsafe {
|
|
||||||
uefi::boot::exit(uefi::boot::image_handle(), Status::ABORTED, 0, null_mut())
|
|
||||||
.context("unable to exit")?;
|
|
||||||
Err(anyhow!("unable to exit"))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,4 +18,9 @@ pub struct ChainloadConfiguration {
|
|||||||
/// generally better and safer as it can support additional load options in the future.
|
/// generally better and safer as it can support additional load options in the future.
|
||||||
#[serde(default, rename = "linux-initrd")]
|
#[serde(default, rename = "linux-initrd")]
|
||||||
pub linux_initrd: Option<String>,
|
pub linux_initrd: Option<String>,
|
||||||
|
/// An optional list of paths to Linux initrds to chain together.
|
||||||
|
/// The contents of each initrd are concatenated in order and loaded as a single initrd
|
||||||
|
/// using the same mechanism as `linux-initrd`. This cannot be used with `linux-initrd`.
|
||||||
|
#[serde(default, rename = "linux-initrd-chain")]
|
||||||
|
pub linux_initrd_chain: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@ pub struct EderaConfiguration {
|
|||||||
/// The path to the initrd to load for dom0.
|
/// The path to the initrd to load for dom0.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub initrd: Option<String>,
|
pub initrd: Option<String>,
|
||||||
|
/// The paths to the initrds to chain together for dom0.
|
||||||
|
/// The contents of each initrd are concatenated in order and loaded as a single initrd.
|
||||||
|
/// This cannot be used with `initrd`.
|
||||||
|
#[serde(default, rename = "initrd-chain")]
|
||||||
|
pub initrd_chain: Vec<String>,
|
||||||
/// The options to pass to the kernel.
|
/// The options to pass to the kernel.
|
||||||
#[serde(default, rename = "kernel-options")]
|
#[serde(default, rename = "kernel-options")]
|
||||||
pub kernel_options: Vec<String>,
|
pub kernel_options: Vec<String>,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub const LATEST_VERSION: u32 = 1;
|
|||||||
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
|
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
|
||||||
|
|
||||||
/// The Sprout configuration format.
|
/// The Sprout configuration format.
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct RootConfiguration {
|
pub struct RootConfiguration {
|
||||||
/// The version of the configuration. This should always be declared
|
/// The version of the configuration. This should always be declared
|
||||||
/// and be the latest version that is supported. If not specified, it is assumed
|
/// and be the latest version that is supported. If not specified, it is assumed
|
||||||
@@ -76,7 +76,7 @@ pub struct RootConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Options configuration for Sprout, used when the corresponding options are not specified.
|
/// Options configuration for Sprout, used when the corresponding options are not specified.
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct OptionsConfiguration {
|
pub struct OptionsConfiguration {
|
||||||
/// The entry to mark as the default entry, instead of the first entry.
|
/// The entry to mark as the default entry, instead of the first entry.
|
||||||
#[serde(rename = "default-entry", default)]
|
#[serde(rename = "default-entry", default)]
|
||||||
@@ -84,11 +84,55 @@ pub struct OptionsConfiguration {
|
|||||||
/// The timeout of the boot menu.
|
/// The timeout of the boot menu.
|
||||||
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
|
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
|
||||||
pub menu_timeout: u64,
|
pub menu_timeout: u64,
|
||||||
|
/// The style of boot menu to display.
|
||||||
|
#[serde(rename = "menu-style", default)]
|
||||||
|
pub menu_style: MenuStyle,
|
||||||
/// Enables autoconfiguration of Sprout based on the environment.
|
/// Enables autoconfiguration of Sprout based on the environment.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autoconfigure: bool,
|
pub autoconfigure: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The default configuration, which matches an empty configuration file.
|
||||||
|
impl Default for RootConfiguration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: latest_version(),
|
||||||
|
options: Default::default(),
|
||||||
|
values: Default::default(),
|
||||||
|
drivers: Default::default(),
|
||||||
|
extractors: Default::default(),
|
||||||
|
actions: Default::default(),
|
||||||
|
entries: Default::default(),
|
||||||
|
generators: Default::default(),
|
||||||
|
phases: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default options, which match an empty options section.
|
||||||
|
impl Default for OptionsConfiguration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
default_entry: None,
|
||||||
|
menu_timeout: default_menu_timeout(),
|
||||||
|
menu_style: Default::default(),
|
||||||
|
autoconfigure: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style of boot menu to display.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MenuStyle {
|
||||||
|
/// A basic menu that prints the entries and selects them by number.
|
||||||
|
#[serde(rename = "basic")]
|
||||||
|
Basic,
|
||||||
|
/// A simple full-screen menu that selects entries with the arrow keys.
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "simple")]
|
||||||
|
Simple,
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the latest version of the Sprout configuration format.
|
/// Get the latest version of the Sprout configuration format.
|
||||||
pub fn latest_version() -> u32 {
|
pub fn latest_version() -> u32 {
|
||||||
LATEST_VERSION
|
LATEST_VERSION
|
||||||
@@ -97,3 +141,34 @@ pub fn latest_version() -> u32 {
|
|||||||
fn default_menu_timeout() -> u64 {
|
fn default_menu_timeout() -> u64 {
|
||||||
DEFAULT_MENU_TIMEOUT_SECONDS
|
DEFAULT_MENU_TIMEOUT_SECONDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde::de::value::{Error, MapDeserializer};
|
||||||
|
|
||||||
|
/// Deserialize a configuration from an empty map, like an empty configuration file.
|
||||||
|
fn empty() -> RootConfiguration {
|
||||||
|
let map = MapDeserializer::<_, Error>::new(core::iter::empty::<(&str, &str)>());
|
||||||
|
RootConfiguration::deserialize(map).expect("empty configuration should deserialize")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_matches_empty_configuration() {
|
||||||
|
let default = RootConfiguration::default();
|
||||||
|
let empty = empty();
|
||||||
|
assert_eq!(default.version, empty.version);
|
||||||
|
assert_eq!(default.options.menu_timeout, empty.options.menu_timeout);
|
||||||
|
assert_eq!(default.options.menu_style, empty.options.menu_style);
|
||||||
|
assert_eq!(default.options.default_entry, empty.options.default_entry);
|
||||||
|
assert_eq!(default.options.autoconfigure, empty.options.autoconfigure);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_menu_timeout_is_not_zero() {
|
||||||
|
assert_eq!(
|
||||||
|
RootConfiguration::default().options.menu_timeout,
|
||||||
|
DEFAULT_MENU_TIMEOUT_SECONDS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,12 @@ mod bitflags;
|
|||||||
const LOADER_NAME: &str = "Sprout";
|
const LOADER_NAME: &str = "Sprout";
|
||||||
|
|
||||||
/// Represents the configured timeout for the bootloader interface.
|
/// Represents the configured timeout for the bootloader interface.
|
||||||
|
#[derive(Default)]
|
||||||
pub enum BootloaderInterfaceTimeout {
|
pub enum BootloaderInterfaceTimeout {
|
||||||
/// Force the menu to be shown.
|
/// Force the menu to be shown, and wait for the user.
|
||||||
MenuForce,
|
MenuForce,
|
||||||
|
/// Force the menu to be shown, with a timeout.
|
||||||
|
MenuForceTimeout(u64),
|
||||||
/// Hide the menu.
|
/// Hide the menu.
|
||||||
MenuHidden,
|
MenuHidden,
|
||||||
/// Disable the menu.
|
/// Disable the menu.
|
||||||
@@ -27,6 +30,7 @@ pub enum BootloaderInterfaceTimeout {
|
|||||||
/// Set a timeout for the menu.
|
/// Set a timeout for the menu.
|
||||||
Timeout(u64),
|
Timeout(u64),
|
||||||
/// Timeout is unspecified.
|
/// Timeout is unspecified.
|
||||||
|
#[default]
|
||||||
Unspecified,
|
Unspecified,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +223,9 @@ impl BootloaderInterface {
|
|||||||
|
|
||||||
// If we reach here, we know the value was specified.
|
// If we reach here, we know the value was specified.
|
||||||
// If `remove` is true, remove the variable.
|
// If `remove` is true, remove the variable.
|
||||||
if remove {
|
// If removal fails, the value is still used, as it was meant for this boot.
|
||||||
Self::VENDOR
|
if remove && let Err(error) = Self::VENDOR.remove(key) {
|
||||||
.remove(key)
|
warn!("unable to remove {} variable: {:#}", key, error);
|
||||||
.context("unable to remove timeout variable")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the value is empty, return Unspecified.
|
// If the value is empty, return Unspecified.
|
||||||
@@ -255,12 +258,8 @@ impl BootloaderInterface {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The specification says that a value of 0 means that the menu should be hidden.
|
|
||||||
if value == 0 {
|
|
||||||
return Ok(Some(BootloaderInterfaceTimeout::MenuHidden));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we reach here, we know it must be a real timeout value.
|
// If we reach here, we know it must be a real timeout value.
|
||||||
|
// A value of 0 is left to the caller, as its meaning depends on the variable.
|
||||||
Ok(Some(BootloaderInterfaceTimeout::Timeout(value)))
|
Ok(Some(BootloaderInterfaceTimeout::Timeout(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,8 +273,16 @@ impl BootloaderInterface {
|
|||||||
.context("unable to check for LoaderConfigTimeoutOneShot variable")?;
|
.context("unable to check for LoaderConfigTimeoutOneShot variable")?;
|
||||||
|
|
||||||
// If oneshot was found, return it.
|
// If oneshot was found, return it.
|
||||||
|
// The specification says the one-shot timeout shows the menu on this boot,
|
||||||
|
// and a value of 0 means the menu waits for the user.
|
||||||
if let Some(oneshot) = oneshot {
|
if let Some(oneshot) = oneshot {
|
||||||
return Ok(oneshot);
|
return Ok(match oneshot {
|
||||||
|
BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuForce,
|
||||||
|
BootloaderInterfaceTimeout::Timeout(value) => {
|
||||||
|
BootloaderInterfaceTimeout::MenuForceTimeout(value)
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to acquire the value of the LoaderConfigTimeout variable.
|
// Attempt to acquire the value of the LoaderConfigTimeout variable.
|
||||||
@@ -284,8 +291,12 @@ impl BootloaderInterface {
|
|||||||
.context("unable to check for LoaderConfigTimeout variable")?;
|
.context("unable to check for LoaderConfigTimeout variable")?;
|
||||||
|
|
||||||
// If direct was found, return it.
|
// If direct was found, return it.
|
||||||
|
// The specification says that a value of 0 means that the menu should be hidden.
|
||||||
if let Some(direct) = direct {
|
if let Some(direct) = direct {
|
||||||
return Ok(direct);
|
return Ok(match direct {
|
||||||
|
BootloaderInterfaceTimeout::Timeout(0) => BootloaderInterfaceTimeout::MenuHidden,
|
||||||
|
other => other,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, we know that neither variable was set.
|
// If we reach here, we know that neither variable was set.
|
||||||
@@ -315,9 +326,10 @@ impl BootloaderInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove the oneshot entry from the bootloader interface.
|
// Remove the oneshot entry from the bootloader interface.
|
||||||
Self::VENDOR
|
// If removal fails, the value is still used, as it was meant for this boot.
|
||||||
.remove("LoaderEntryOneShot")
|
if let Err(error) = Self::VENDOR.remove("LoaderEntryOneShot") {
|
||||||
.context("unable to remove oneshot entry")?;
|
warn!("unable to remove LoaderEntryOneShot variable: {:#}", error);
|
||||||
|
}
|
||||||
|
|
||||||
// Return the oneshot value.
|
// Return the oneshot value.
|
||||||
// An empty value is treated as if the oneshot entry was not set.
|
// An empty value is treated as if the oneshot entry was not set.
|
||||||
|
|||||||
+18
-19
@@ -1,7 +1,7 @@
|
|||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result};
|
||||||
use uefi::proto::loaded_image::{LoadOptionsError, LoadedImage};
|
use uefi::proto::loaded_image::LoadedImage;
|
||||||
|
|
||||||
/// Loads the command-line arguments passed to the current image.
|
/// Loads the command-line arguments passed to the current image.
|
||||||
pub fn args() -> Result<Vec<String>> {
|
pub fn args() -> Result<Vec<String>> {
|
||||||
@@ -13,26 +13,25 @@ pub fn args() -> Result<Vec<String>> {
|
|||||||
.context("unable to open loaded image protocol for current image")?;
|
.context("unable to open loaded image protocol for current image")?;
|
||||||
|
|
||||||
// Load the command-line argument string.
|
// Load the command-line argument string.
|
||||||
let options = match loaded_image.load_options_as_cstr16() {
|
// Load options are usually a null-terminated UCS-2 string, but firmware is not consistent
|
||||||
// Load options were passed. We will return them for processing.
|
// about this. Some firmware passes an odd number of bytes, leaves off the null terminator,
|
||||||
Ok(options) => options,
|
// or pads the string with extra nulls. We decode everything up to the first null and
|
||||||
|
// replace anything that isn't valid UCS-2, instead of refusing to boot.
|
||||||
|
let Some(options) = loaded_image.load_options_as_bytes() else {
|
||||||
// No load options were passed. We will return an empty vector.
|
// No load options were passed. We will return an empty vector.
|
||||||
Err(LoadOptionsError::NotSet) => {
|
return Ok(Vec::new());
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(LoadOptionsError::NotAligned) => {
|
|
||||||
bail!("load options are not properly aligned");
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(LoadOptionsError::InvalidString(error)) => {
|
|
||||||
bail!("load options are not a valid string: {}", error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert the options to a string.
|
// Decode the options as little-endian UTF-16, stopping at the first null.
|
||||||
let options = options.to_string();
|
// A trailing odd byte can't be part of a character, so it is dropped.
|
||||||
|
let options = options
|
||||||
|
.as_chunks::<2>()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|pair| u16::from_le_bytes(*pair))
|
||||||
|
.take_while(|c| *c != 0)
|
||||||
|
.collect::<Vec<u16>>();
|
||||||
|
let options = String::from_utf16_lossy(&options);
|
||||||
|
|
||||||
// Use shlex to parse the options.
|
// Use shlex to parse the options.
|
||||||
// If shlex fails, we will perform a simple whitespace split.
|
// If shlex fails, we will perform a simple whitespace split.
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ impl MediaLoaderHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Boot policy must not be true, and if it is, that is special behavior that is irrelevant
|
// Boot policy must not be true, and if it is, that is special behavior that is irrelevant
|
||||||
// for the media loader concept.
|
// for the media loader concept. Any non-zero value is true.
|
||||||
if boot_policy == Boolean::TRUE {
|
if bool::from(boot_policy) {
|
||||||
return Status::UNSUPPORTED;
|
return Status::UNSUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-39
@@ -6,7 +6,9 @@ use anyhow::{Context, Result};
|
|||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use uefi::fs::{FileSystem, Path};
|
use uefi::fs::{FileSystem, Path};
|
||||||
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
|
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
|
||||||
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
|
use uefi::proto::device_path::{
|
||||||
|
DevicePath, DevicePathNode, DeviceSubType, DeviceType, PoolDevicePath,
|
||||||
|
};
|
||||||
use uefi::proto::media::fs::SimpleFileSystem;
|
use uefi::proto::media::fs::SimpleFileSystem;
|
||||||
use uefi::{CString16, Handle};
|
use uefi::{CString16, Handle};
|
||||||
|
|
||||||
@@ -40,10 +42,17 @@ impl ResolvedPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a [CString16] contains a char `c`.
|
/// Checks if the device path `node` is a file path node, rather than a device node.
|
||||||
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
|
fn is_file_path_node(node: &DevicePathNode) -> bool {
|
||||||
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
|
node.device_type() == DeviceType::MEDIA && node.sub_type() == DeviceSubType::MEDIA_FILE_PATH
|
||||||
string.to_string().contains(c)
|
}
|
||||||
|
|
||||||
|
/// Converts the device path `node` to its text form.
|
||||||
|
fn node_to_string(node: &DevicePathNode) -> Result<String> {
|
||||||
|
Ok(node
|
||||||
|
.to_string16(DisplayOnly(false), AllowShortcuts(false))
|
||||||
|
.context("unable to convert device path node to string")?
|
||||||
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the input `path` as a [DevicePath].
|
/// Parses the input `path` as a [DevicePath].
|
||||||
@@ -67,20 +76,9 @@ pub fn text_to_device_path(path: impl AsRef<str>) -> Result<PoolDevicePath> {
|
|||||||
pub fn device_path_root(path: &DevicePath) -> Result<String> {
|
pub fn device_path_root(path: &DevicePath) -> Result<String> {
|
||||||
let mut path = path
|
let mut path = path
|
||||||
.node_iter()
|
.node_iter()
|
||||||
.filter_map(|item| {
|
.filter(|node| !is_file_path_node(node))
|
||||||
let item = item.to_string16(DisplayOnly(false), AllowShortcuts(false));
|
.map(node_to_string)
|
||||||
if item
|
.collect::<Result<Vec<_>>>()?
|
||||||
.as_ref()
|
|
||||||
.map(|item| cstring16_contains_char(item, '('))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
Some(item.unwrap_or_default())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|item| item.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("/");
|
.join("/");
|
||||||
path.push('/');
|
path.push('/');
|
||||||
Ok(path)
|
Ok(path)
|
||||||
@@ -92,20 +90,9 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
|
|||||||
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
|
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
|
||||||
let path = path
|
let path = path
|
||||||
.node_iter()
|
.node_iter()
|
||||||
.filter_map(|item| {
|
.filter(|node| is_file_path_node(node))
|
||||||
let item = item.to_string16(DisplayOnly(false), AllowShortcuts(false));
|
.map(node_to_string)
|
||||||
if item
|
.collect::<Result<Vec<_>>>()?
|
||||||
.as_ref()
|
|
||||||
.map(|item| cstring16_contains_char(item, '('))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(item.unwrap_or_default())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|item| item.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\\");
|
.join("\\");
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
@@ -120,15 +107,12 @@ pub fn resolve_path(
|
|||||||
let mut input = input.to_string();
|
let mut input = input.to_string();
|
||||||
|
|
||||||
let mut path = text_to_device_path(&input).context("unable to convert text to path")?;
|
let mut path = text_to_device_path(&input).context("unable to convert text to path")?;
|
||||||
|
// A path that starts with a device node has a device, while a path that starts
|
||||||
|
// with a file path node is relative to the default root path.
|
||||||
let path_has_device = path
|
let path_has_device = path
|
||||||
.node_iter()
|
.node_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|it| {
|
.is_some_and(|node| !is_file_path_node(node));
|
||||||
it.to_string16(DisplayOnly(false), AllowShortcuts(false))
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.map(|it| it.to_string().contains('('))
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !path_has_device {
|
if !path_has_device {
|
||||||
if !input.starts_with('\\') {
|
if !input.starts_with('\\') {
|
||||||
input.insert(0, '\\');
|
input.insert(0, '\\');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Architecturally modified to support UEFI and remove x86 (32-bit) support.
|
// Architecturally modified to support UEFI and remove x86 (32-bit) support.
|
||||||
|
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
/// Support for aarch64 timers.
|
/// Support for aarch64 timers.
|
||||||
#[cfg(target_arch = "aarch64")]
|
#[cfg(target_arch = "aarch64")]
|
||||||
@@ -35,7 +36,11 @@ impl TickFrequency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Produce a duration from the provided elapsed `ticks` value.
|
/// Produce a duration from the provided elapsed `ticks` value.
|
||||||
|
/// If the frequency is unknown, the duration is zero.
|
||||||
fn duration(&self, ticks: u64) -> Duration {
|
fn duration(&self, ticks: u64) -> Duration {
|
||||||
|
if self.ticks() == 0 {
|
||||||
|
return Duration::ZERO;
|
||||||
|
}
|
||||||
let accuracy = self.nanos();
|
let accuracy = self.nanos();
|
||||||
let nanos = ticks as f64 * accuracy;
|
let nanos = ticks as f64 * accuracy;
|
||||||
Duration::from_nanos(nanos as u64)
|
Duration::from_nanos(nanos as u64)
|
||||||
@@ -56,9 +61,10 @@ fn arch_frequency() -> TickFrequency {
|
|||||||
let frequency = aarch64::frequency();
|
let frequency = aarch64::frequency();
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
let frequency = x86_64::frequency();
|
let frequency = x86_64::frequency();
|
||||||
// If the frequency is 0, then something went very wrong and we should panic.
|
// Some firmware does not program the timer frequency. The timer is only used to report
|
||||||
|
// timestamps to the bootloader interface, so this should not prevent booting.
|
||||||
if frequency.ticks() == 0 {
|
if frequency.ticks() == 0 {
|
||||||
panic!("timer frequency is zero");
|
warn!("timer frequency is zero, timestamps will not be reported");
|
||||||
}
|
}
|
||||||
frequency
|
frequency
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use log::warn;
|
||||||
use uefi::ResultExt;
|
use uefi::ResultExt;
|
||||||
use uefi::boot::ScopedProtocol;
|
use uefi::boot::ScopedProtocol;
|
||||||
use uefi::proto::tcg::PcrIndex;
|
use uefi::proto::tcg::PcrIndex;
|
||||||
use uefi::proto::tcg::v2::{PcrEventInputs, Tcg};
|
use uefi::proto::tcg::v2::{PcrEventInputs, Tcg};
|
||||||
|
use uefi_raw::Status;
|
||||||
use uefi_raw::protocol::tcg::EventType;
|
use uefi_raw::protocol::tcg::EventType;
|
||||||
use uefi_raw::protocol::tcg::v2::{Tcg2HashLogExtendEventFlags, Tcg2Protocol, Tcg2Version};
|
use uefi_raw::protocol::tcg::v2::{Tcg2HashLogExtendEventFlags, Tcg2Protocol, Tcg2Version};
|
||||||
|
|
||||||
@@ -121,18 +123,33 @@ impl PlatformTpm {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Encode the description as UTF-8.
|
// Encode the description as UTF-8.
|
||||||
let description = description.as_bytes().to_vec();
|
let encoded_description = description.as_bytes().to_vec();
|
||||||
|
|
||||||
// Construct an event input for the TPM.
|
// Construct an event input for the TPM.
|
||||||
let event = PcrEventInputs::new_in_box(pcr_index, EventType::IPL, &description)
|
let event = PcrEventInputs::new_in_box(pcr_index, EventType::IPL, &encoded_description)
|
||||||
.discard_errdata()
|
.discard_errdata()
|
||||||
.context("unable to construct pcr event inputs")?;
|
.context("unable to construct pcr event inputs")?;
|
||||||
|
|
||||||
// Log the event into the TPM.
|
// Log the event into the TPM.
|
||||||
handle
|
let result = handle.protocol().hash_log_extend_event(
|
||||||
.protocol()
|
Tcg2HashLogExtendEventFlags::empty(),
|
||||||
.hash_log_extend_event(Tcg2HashLogExtendEventFlags::empty(), buffer, &event)
|
buffer,
|
||||||
.context("unable to log event to tpm")?;
|
&event,
|
||||||
|
);
|
||||||
|
|
||||||
|
// A full event log means the PCR was extended, but the event could not be logged.
|
||||||
|
// The measurement still happened, so this should not prevent booting.
|
||||||
|
if let Err(ref error) = result
|
||||||
|
&& error.status() == Status::VOLUME_FULL
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"tpm event log is full, unable to log event: {}",
|
||||||
|
description
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.context("unable to log event to tpm")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,16 +82,71 @@ impl SecurityHook {
|
|||||||
status.is_success()
|
status.is_success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call the original EFI_SECURITY_ARCH hook with `this`, `status`, and `path`.
|
||||||
|
/// This lets the firmware decide on images that the shim can't verify.
|
||||||
|
unsafe fn original_file_authentication_state(
|
||||||
|
this: *const SecurityArchProtocol,
|
||||||
|
status: u32,
|
||||||
|
path: *const FfiDevicePath,
|
||||||
|
) -> Status {
|
||||||
|
// Acquire the global hook state to grab the original hook.
|
||||||
|
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
|
||||||
|
// The hook state is available, so we can acquire the original hook.
|
||||||
|
Some(state) => state.original_hook.file_authentication_state,
|
||||||
|
|
||||||
|
// The hook state is not available, so we can't call the original hook.
|
||||||
|
None => {
|
||||||
|
warn!("global hook state is not available, unable to call original hook");
|
||||||
|
return Status::LOAD_ERROR;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the original hook function to see what it reports.
|
||||||
|
// SAFETY: This function is safe to call as it is stored by us and is required
|
||||||
|
// in the UEFI specification.
|
||||||
|
unsafe { function(this, status, path) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call the original EFI_SECURITY_ARCH2 hook with `this`, `path`, `file_buffer`,
|
||||||
|
/// `file_size`, and `boot_policy`.
|
||||||
|
/// This lets the firmware decide on images that the shim can't verify.
|
||||||
|
unsafe fn original_file_authentication(
|
||||||
|
this: *const SecurityArch2Protocol,
|
||||||
|
path: *const FfiDevicePath,
|
||||||
|
file_buffer: *const u8,
|
||||||
|
file_size: usize,
|
||||||
|
boot_policy: Boolean,
|
||||||
|
) -> Status {
|
||||||
|
// Acquire the global hook state to grab the original hook.
|
||||||
|
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
|
||||||
|
// The hook state is available, so we can acquire the original hook.
|
||||||
|
Some(state) => state.original_hook2.file_authentication,
|
||||||
|
|
||||||
|
// The hook state is not available, so we can't call the original hook.
|
||||||
|
None => {
|
||||||
|
warn!("global hook state is not available, unable to call original hook");
|
||||||
|
return Status::LOAD_ERROR;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the original hook function to see what it reports.
|
||||||
|
// SAFETY: This function is safe to call as it is stored by us and is required
|
||||||
|
// in the UEFI specification.
|
||||||
|
unsafe { function(this, path, file_buffer, file_size, boot_policy) }
|
||||||
|
}
|
||||||
|
|
||||||
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
|
/// File authentication state verifier for the EFI_SECURITY_ARCH protocol.
|
||||||
/// Takes the `path` and determines the verification.
|
/// Takes the `path` and determines the verification.
|
||||||
|
/// Anything the shim can't verify is passed to the original hook.
|
||||||
unsafe extern "efiapi" fn arch_file_authentication_state(
|
unsafe extern "efiapi" fn arch_file_authentication_state(
|
||||||
this: *const SecurityArchProtocol,
|
this: *const SecurityArchProtocol,
|
||||||
status: u32,
|
status: u32,
|
||||||
path: *const FfiDevicePath,
|
path: *const FfiDevicePath,
|
||||||
) -> Status {
|
) -> Status {
|
||||||
// Verify the path is not null.
|
// Without a path, there is nothing for the shim to read and verify.
|
||||||
if path.is_null() {
|
if path.is_null() {
|
||||||
return Status::INVALID_PARAMETER;
|
// SAFETY: The arguments are passed through unchanged from the firmware.
|
||||||
|
return unsafe { Self::original_file_authentication_state(this, status, path) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a shim input from the path.
|
// Construct a shim input from the path.
|
||||||
@@ -100,31 +155,19 @@ impl SecurityHook {
|
|||||||
// Convert the input to an owned data buffer.
|
// Convert the input to an owned data buffer.
|
||||||
let input = match input.into_owned_data_buffer() {
|
let input = match input.into_owned_data_buffer() {
|
||||||
Ok(input) => input,
|
Ok(input) => input,
|
||||||
// If an error occurs, log the error and return the not found status.
|
// If the data can't be read, such as for a path the shim can't access,
|
||||||
|
// let the original hook decide.
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("unable to read data to be authenticated: {}", error);
|
warn!("unable to read data to be authenticated: {}", error);
|
||||||
return Status::NOT_FOUND;
|
// SAFETY: The arguments are passed through unchanged from the firmware.
|
||||||
|
return unsafe { Self::original_file_authentication_state(this, status, path) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the input, if it fails, call the original hook.
|
// Verify the input, if it fails, call the original hook.
|
||||||
if !Self::verify(input) {
|
if !Self::verify(input) {
|
||||||
// Acquire the global hook state to grab the original hook.
|
// SAFETY: The arguments are passed through unchanged from the firmware.
|
||||||
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
|
unsafe { Self::original_file_authentication_state(this, status, path) }
|
||||||
// The hook state is available, so we can acquire the original hook.
|
|
||||||
Some(state) => state.original_hook.file_authentication_state,
|
|
||||||
|
|
||||||
// The hook state is not available, so we can't call the original hook.
|
|
||||||
None => {
|
|
||||||
warn!("global hook state is not available, unable to call original hook");
|
|
||||||
return Status::LOAD_ERROR;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the original hook function to see what it reports.
|
|
||||||
// SAFETY: This function is safe to call as it is stored by us and is required
|
|
||||||
// in the UEFI specification.
|
|
||||||
unsafe { function(this, status, path) }
|
|
||||||
} else {
|
} else {
|
||||||
Status::SUCCESS
|
Status::SUCCESS
|
||||||
}
|
}
|
||||||
@@ -132,6 +175,7 @@ impl SecurityHook {
|
|||||||
|
|
||||||
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
|
/// File authentication verifier for the EFI_SECURITY_ARCH2 protocol.
|
||||||
/// Takes the `path` and a file buffer to determine the verification.
|
/// Takes the `path` and a file buffer to determine the verification.
|
||||||
|
/// Anything the shim can't verify is passed to the original hook.
|
||||||
unsafe extern "efiapi" fn arch2_file_authentication(
|
unsafe extern "efiapi" fn arch2_file_authentication(
|
||||||
this: *const SecurityArch2Protocol,
|
this: *const SecurityArch2Protocol,
|
||||||
path: *const FfiDevicePath,
|
path: *const FfiDevicePath,
|
||||||
@@ -139,14 +183,13 @@ impl SecurityHook {
|
|||||||
file_size: usize,
|
file_size: usize,
|
||||||
boot_policy: Boolean,
|
boot_policy: Boolean,
|
||||||
) -> Status {
|
) -> Status {
|
||||||
// Verify the path and file buffer are not null.
|
// The path and file buffer are optional, and the shim can't verify without them.
|
||||||
if path.is_null() || file_buffer.is_null() {
|
// The shim also doesn't support the boot policy.
|
||||||
return Status::INVALID_PARAMETER;
|
if path.is_null() || file_buffer.is_null() || bool::from(boot_policy) {
|
||||||
}
|
// SAFETY: The arguments are passed through unchanged from the firmware.
|
||||||
|
return unsafe {
|
||||||
// If the boot policy is true, we can't continue as we don't support that.
|
Self::original_file_authentication(this, path, file_buffer, file_size, boot_policy)
|
||||||
if bool::from(boot_policy) {
|
};
|
||||||
return Status::INVALID_PARAMETER;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a slice out of the file buffer and size.
|
// Construct a slice out of the file buffer and size.
|
||||||
@@ -157,22 +200,10 @@ impl SecurityHook {
|
|||||||
|
|
||||||
// Verify the input, if it fails, call the original hook.
|
// Verify the input, if it fails, call the original hook.
|
||||||
if !Self::verify(input) {
|
if !Self::verify(input) {
|
||||||
// Acquire the global hook state to grab the original hook.
|
// SAFETY: The arguments are passed through unchanged from the firmware.
|
||||||
let function = match GLOBAL_HOOK_STATE.lock().as_ref() {
|
unsafe {
|
||||||
// The hook state is available, so we can acquire the original hook.
|
Self::original_file_authentication(this, path, file_buffer, file_size, boot_policy)
|
||||||
Some(state) => state.original_hook2.file_authentication,
|
}
|
||||||
|
|
||||||
// The hook state is not available, so we can't call the original hook.
|
|
||||||
None => {
|
|
||||||
warn!("global hook state is not available, unable to call original hook");
|
|
||||||
return Status::LOAD_ERROR;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the original hook function to see what it reports.
|
|
||||||
// SAFETY: This function is safe to call as it is stored by us and is required
|
|
||||||
// in the UEFI specification.
|
|
||||||
unsafe { function(this, path, file_buffer, file_size, boot_policy) }
|
|
||||||
} else {
|
} else {
|
||||||
Status::SUCCESS
|
Status::SUCCESS
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-10
@@ -90,6 +90,18 @@ pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> Strin
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The alignment of each initrd when initrds are concatenated.
|
||||||
|
const INITRD_ALIGNMENT: usize = 4;
|
||||||
|
|
||||||
|
/// Append the `initrd` to the concatenated `initrds`, padding with zeros so that it starts
|
||||||
|
/// on a 4-byte boundary. Linux only finds a cpio archive at a 4-byte aligned offset when
|
||||||
|
/// unpacking concatenated initrds, as GRUB and systemd-boot pad them the same way.
|
||||||
|
pub fn append_initrd(initrds: &mut Vec<u8>, initrd: &[u8]) {
|
||||||
|
let padded = initrds.len().next_multiple_of(INITRD_ALIGNMENT);
|
||||||
|
initrds.resize(padded, 0);
|
||||||
|
initrds.extend_from_slice(initrd);
|
||||||
|
}
|
||||||
|
|
||||||
/// Produce a unique hash for the input.
|
/// Produce a unique hash for the input.
|
||||||
/// This uses SHA-256, which is unique enough but relatively short.
|
/// This uses SHA-256, which is unique enough but relatively short.
|
||||||
pub fn unique_hash(input: &str) -> String {
|
pub fn unique_hash(input: &str) -> String {
|
||||||
@@ -140,14 +152,24 @@ pub fn match_kernel_prefix<'a>(name: &str, kernel_prefixes: &[&'a str]) -> Optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate initramfs candidate filenames by combining each entry of `initramfs_prefixes`
|
/// Generate initramfs candidate filenames by combining each entry of `initramfs_prefixes`
|
||||||
/// with `suffix`. The caller is expected to check which candidates actually exist.
|
/// with `suffix`, and then with `suffix` followed by `.img`, which many distributions use.
|
||||||
pub fn initramfs_candidates<'a>(
|
/// For example, Arch Linux pairs `vmlinuz-linux` with `initramfs-linux.img`.
|
||||||
suffix: &'a str,
|
/// The caller is expected to check which candidates actually exist.
|
||||||
initramfs_prefixes: &'a [&'a str],
|
pub fn initramfs_candidates(
|
||||||
) -> impl Iterator<Item = String> + 'a {
|
suffix: &str,
|
||||||
initramfs_prefixes
|
initramfs_prefixes: &[&str],
|
||||||
.iter()
|
) -> impl Iterator<Item = String> {
|
||||||
.map(move |prefix| format!("{}{}", prefix, suffix))
|
let mut candidates: Vec<String> = Vec::new();
|
||||||
|
for extension in ["", ".img"] {
|
||||||
|
for prefix in initramfs_prefixes {
|
||||||
|
let candidate = format!("{}{}{}", prefix, suffix, extension);
|
||||||
|
// Skip duplicates, like "initrd" with ".img" when "initrd.img" is also a prefix.
|
||||||
|
if !candidates.contains(&candidate) {
|
||||||
|
candidates.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -348,6 +370,24 @@ mod tests {
|
|||||||
assert!(config.ends_with('\n'));
|
assert!(config.ends_with('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append_initrd_pads_to_four_bytes() {
|
||||||
|
let mut initrds = Vec::new();
|
||||||
|
append_initrd(&mut initrds, b"abcde");
|
||||||
|
assert_eq!(initrds, b"abcde");
|
||||||
|
append_initrd(&mut initrds, b"fg");
|
||||||
|
assert_eq!(initrds, b"abcde\0\0\0fg");
|
||||||
|
append_initrd(&mut initrds, b"h");
|
||||||
|
assert_eq!(initrds, b"abcde\0\0\0fg\0\0h");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append_initrd_aligned_needs_no_padding() {
|
||||||
|
let mut initrds = b"abcd".to_vec();
|
||||||
|
append_initrd(&mut initrds, b"efgh");
|
||||||
|
assert_eq!(initrds, b"abcdefgh");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn kernel_prefix_exact_match() {
|
fn kernel_prefix_exact_match() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -399,14 +439,44 @@ mod tests {
|
|||||||
let candidates: Vec<_> = initramfs_candidates("-6.1.0", LINUX_INITRAMFS_PREFIXES).collect();
|
let candidates: Vec<_> = initramfs_candidates("-6.1.0", LINUX_INITRAMFS_PREFIXES).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
candidates,
|
candidates,
|
||||||
&["initramfs-6.1.0", "initrd-6.1.0", "initrd.img-6.1.0"]
|
&[
|
||||||
|
"initramfs-6.1.0",
|
||||||
|
"initrd-6.1.0",
|
||||||
|
"initrd.img-6.1.0",
|
||||||
|
"initramfs-6.1.0.img",
|
||||||
|
"initrd-6.1.0.img",
|
||||||
|
"initrd.img-6.1.0.img",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initramfs_candidates_include_img_extension() {
|
||||||
|
// Arch Linux, and Fedora or Gentoo with dracut, add .img to the initramfs name.
|
||||||
|
let arch: Vec<_> = initramfs_candidates("-linux", LINUX_INITRAMFS_PREFIXES).collect();
|
||||||
|
assert!(arch.iter().any(|c| c == "initramfs-linux.img"));
|
||||||
|
let fedora: Vec<_> =
|
||||||
|
initramfs_candidates("-6.8.5-301.fc40.x86_64", LINUX_INITRAMFS_PREFIXES).collect();
|
||||||
|
assert!(
|
||||||
|
fedora
|
||||||
|
.iter()
|
||||||
|
.any(|c| c == "initramfs-6.8.5-301.fc40.x86_64.img")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initramfs_candidates_empty_suffix() {
|
fn initramfs_candidates_empty_suffix() {
|
||||||
let candidates: Vec<_> = initramfs_candidates("", LINUX_INITRAMFS_PREFIXES).collect();
|
let candidates: Vec<_> = initramfs_candidates("", LINUX_INITRAMFS_PREFIXES).collect();
|
||||||
assert_eq!(candidates, &["initramfs", "initrd", "initrd.img"]);
|
assert_eq!(
|
||||||
|
candidates,
|
||||||
|
&[
|
||||||
|
"initramfs",
|
||||||
|
"initrd",
|
||||||
|
"initrd.img",
|
||||||
|
"initramfs.img",
|
||||||
|
"initrd.img.img"
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ $ cp /usr/lib/shim/fbaa64.efi.signed /boot/efi/EFI/sprout/fbaa64.efi
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 4: Sign Sprout for Secure Boot
|
## Step 4: Sign Sprout for Secure Boot
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ $ cp /boot/efi/EFI/fedora/mmaa64.efi /boot/efi/EFI/sprout/mmaa64.efi
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 4: Sign Sprout for Secure Boot
|
## Step 4: Sign Sprout for Secure Boot
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ $ cp /usr/share/efi/aarch64/fallback.efi /boot/efi/EFI/sprout/fallback.efi
|
|||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi`
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi`
|
||||||
file.
|
file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 4: Sign Sprout for Secure Boot
|
## Step 4: Sign Sprout for Secure Boot
|
||||||
|
|
||||||
@@ -78,8 +78,10 @@ $ sbsign \
|
|||||||
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
|
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
|
||||||
If `/boot` is FAT32 or ExFAT, you can skip this step.
|
If `/boot` is FAT32 or ExFAT, you can skip this step.
|
||||||
|
|
||||||
Most Debian systems use an ext4 filesystem for `/boot`.
|
openSUSE puts `/boot` on a Btrfs filesystem by default, but it may be ext4 or another filesystem depending on how
|
||||||
|
the system was installed. This guide uses an ext4 driver as the example, so use the driver for your filesystem instead.
|
||||||
You can download an EFI filesystem driver from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
|
You can download an EFI filesystem driver from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
|
||||||
|
For Btrfs, download the `btrfs` file for your platform.
|
||||||
For ext4, download the `ext2` file for your platform. It should work for ext4 filesystems too.
|
For ext4, download the `ext2` file for your platform. It should work for ext4 filesystems too.
|
||||||
|
|
||||||
If you have an EFI driver, copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing.
|
If you have an EFI driver, copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ $ cp /usr/lib/shim/fbaa64.efi /boot/efi/EFI/sprout/fbaa64.efi
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 4: Sign Sprout for Secure Boot
|
## Step 4: Sign Sprout for Secure Boot
|
||||||
|
|
||||||
@@ -129,8 +129,6 @@ If you do not have any drivers, exclude the drivers section entirely.
|
|||||||
|
|
||||||
## Step 7: Configure Sprout Boot Entry
|
## Step 7: Configure Sprout Boot Entry
|
||||||
|
|
||||||
## Step 7: Configure Sprout Boot Entry
|
|
||||||
|
|
||||||
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
|
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
|
||||||
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
|
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
|
||||||
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
|
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/boot/sprout.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/boot/sprout.efi` on your EFI System Partition.
|
||||||
|
|
||||||
Additionally, you will want to install the `efifs` package, which provides the filesystem support for Sprout.
|
Additionally, you will want to install the `efifs` package, which provides the filesystem support for Sprout.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install the efifs package which provides filesystem support for Sprout.
|
# Install the efifs package which provides filesystem support for Sprout.
|
||||||
$ apk install efifs
|
$ apk add efifs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 2: Configure Sprout
|
## Step 2: Configure Sprout
|
||||||
@@ -102,10 +102,12 @@ You will need to install the `efibootmgr` package:
|
|||||||
$ apk add efibootmgr
|
$ apk add efibootmgr
|
||||||
```
|
```
|
||||||
|
|
||||||
Once `efibootmgr` is installed, find the partition device of your EFI System Partition and run the following:
|
Once `efibootmgr` is installed, find the disk and partition number of your EFI System Partition and run the following.
|
||||||
|
For example, if your EFI System Partition is `/dev/nvme0n1p1`, the disk is `/dev/nvme0n1` and the partition
|
||||||
|
number is `1`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\boot\sprout.efi'
|
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -C -L 'Sprout' -l '\EFI\boot\sprout.efi'
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/BOOT/sprout.efi` on your EFI System Partition.
|
Copy the downloaded file to `/boot/efi/EFI/BOOT/sprout.efi` on your EFI System Partition.
|
||||||
|
|
||||||
Additionally, you will want to install the `edk2-ext4` package, which provides the ext4 filesystem support for Sprout.
|
Additionally, you will want to install the `edk2-ext4` package, which provides the ext4 filesystem support for Sprout.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ $ [ ! -d /boot/grub2/x86_64-efi ] && sudo cp -r /usr/lib/grub/x86_64-efi /boot/g
|
|||||||
# Install ARM64 GRUB modules.
|
# Install ARM64 GRUB modules.
|
||||||
$ sudo dnf install grub2-efi-aa64-modules
|
$ sudo dnf install grub2-efi-aa64-modules
|
||||||
# Copy ARM64 GRUB modules to /boot/grub2 for use by GRUB if it isn't installed already.
|
# Copy ARM64 GRUB modules to /boot/grub2 for use by GRUB if it isn't installed already.
|
||||||
$ [ ! -d /boot/grub2/arm64-efi ] && sudo cp -r /usr/lib/grub/arm64-efi /boot/grub2/x86_64-efi
|
$ [ ! -d /boot/grub2/arm64-efi ] && sudo cp -r /usr/lib/grub/arm64-efi /boot/grub2/arm64-efi
|
||||||
```
|
```
|
||||||
|
|
||||||
You will need to find the UUID of your EFI System Partition. You can do this by running the following command:
|
You will need to find the UUID of your EFI System Partition. You can do this by running the following command:
|
||||||
@@ -107,10 +107,12 @@ You may now reboot your system and select Sprout from the GRUB menu.
|
|||||||
|
|
||||||
You can configure your EFI boot menu to show Sprout as an option.
|
You can configure your EFI boot menu to show Sprout as an option.
|
||||||
|
|
||||||
To do so, please find the partition device of your EFI System Partition and run the following:
|
To do so, please find the disk and partition number of your EFI System Partition and run the following.
|
||||||
|
For example, if your EFI System Partition is `/dev/nvme0n1p1`, the disk is `/dev/nvme0n1` and the partition
|
||||||
|
number is `1`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\BOOT\sprout.efi'
|
$ sudo efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -C -L 'Sprout' -l '\EFI\BOOT\sprout.efi'
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ First, identify the path to your EFI System Partition. On most systems, this is
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `/EFI/BOOT/sprout.efi` on your EFI System Partition.
|
Copy the downloaded file to `/EFI/BOOT/sprout.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 2: Copy kernel and optional initramfs
|
## Step 2: Copy kernel and optional initramfs
|
||||||
|
|
||||||
@@ -52,10 +52,12 @@ They will be concatenated by a space and passed to the kernel.
|
|||||||
Since Sprout is still experimental, the following commands will add a boot entry to your EFI firmware for sprout but
|
Since Sprout is still experimental, the following commands will add a boot entry to your EFI firmware for sprout but
|
||||||
intentionally do not set it as the default boot entry.
|
intentionally do not set it as the default boot entry.
|
||||||
|
|
||||||
To add the entry, please find the partition device of your EFI System Partition and run the following:
|
To add the entry, please find the disk and partition number of your EFI System Partition and run the following.
|
||||||
|
For example, if your EFI System Partition is `/dev/nvme0n1p1`, the disk is `/dev/nvme0n1` and the partition
|
||||||
|
number is `1`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\BOOT\sprout.efi'
|
$ sudo efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -C -L 'Sprout' -l '\EFI\BOOT\sprout.efi'
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ Standard editors can, however, be used to edit files on the ESP.
|
|||||||
|
|
||||||
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
|
||||||
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
|
||||||
Copy the downloaded `sprout.efi` file to `X:\EFI\BOOT\sprout.efi` on your EFI System Partition.
|
Copy the downloaded file to `X:\EFI\BOOT\sprout.efi` on your EFI System Partition.
|
||||||
|
|
||||||
## Step 3: Configure Sprout
|
## Step 2: Configure Sprout
|
||||||
|
|
||||||
Write the following file to `X:\sprout.toml`:
|
Write the following file to `X:\sprout.toml`:
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ version = 1
|
|||||||
autoconfigure = true
|
autoconfigure = true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 4: Configure EFI Firmware to boot Sprout
|
## Step 3: Configure EFI Firmware to boot Sprout
|
||||||
|
|
||||||
It is not trivial to add an EFI boot entry inside Windows.
|
It is not trivial to add an EFI boot entry inside Windows.
|
||||||
However, most firmware lets you load arbitrary EFI files from the firmware settings.
|
However, most firmware lets you load arbitrary EFI files from the firmware settings.
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] && [ -e "/dev
|
|||||||
QEMU_ACCEL="kvm"
|
QEMU_ACCEL="kvm"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$(uname)" = "Darwin" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
|
if [ -z "${QEMU_ACCEL}" ] && [ "$(uname)" = "Darwin" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
|
||||||
[ "$(sysctl -n kern.hv_support 2>&1 || true)" = "1" ]; then
|
[ "$(sysctl -n kern.hv_support 2>&1 || true)" = "1" ]; then
|
||||||
QEMU_ACCEL="hvf"
|
QEMU_ACCEL="hvf"
|
||||||
fi
|
fi
|
||||||
+6
-1
@@ -15,7 +15,12 @@ set --
|
|||||||
if [ "${TARGET_ARCH}" = "x86_64" ]; then
|
if [ "${TARGET_ARCH}" = "x86_64" ]; then
|
||||||
set -- "${@}" qemu-system-x86_64 -M q35 -cpu SandyBridge,vmx=on
|
set -- "${@}" qemu-system-x86_64 -M q35 -cpu SandyBridge,vmx=on
|
||||||
elif [ "${TARGET_ARCH}" = "aarch64" ]; then
|
elif [ "${TARGET_ARCH}" = "aarch64" ]; then
|
||||||
set -- "${@}" qemu-system-aarch64 -M virt -cpu cortex-a57
|
# Hardware acceleration on aarch64 only supports the host CPU model.
|
||||||
|
if [ -n "${QEMU_ACCEL}" ] && [ "${QEMU_ACCEL}" != "tcg" ]; then
|
||||||
|
set -- "${@}" qemu-system-aarch64 -M virt -cpu host
|
||||||
|
else
|
||||||
|
set -- "${@}" qemu-system-aarch64 -M virt -cpu cortex-a57
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${QEMU_ACCEL}" ]; then
|
if [ -n "${QEMU_ACCEL}" ]; then
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
|
|||||||
cp "target/${RUST_TARGET}/${RUST_TARGET_SUBDIR}/sprout.efi" "${FINAL_DIR}/sprout.efi"
|
cp "target/${RUST_TARGET}/${RUST_TARGET_SUBDIR}/sprout.efi" "${FINAL_DIR}/sprout.efi"
|
||||||
else
|
else
|
||||||
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}:${DOCKER_TAG}" \
|
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}:${DOCKER_TAG}" \
|
||||||
|
--build-arg="RUST_PROFILE=${RUST_PROFILE}" \
|
||||||
--build-arg="RUST_TARGET_SUBDIR=${RUST_TARGET_SUBDIR}" \
|
--build-arg="RUST_TARGET_SUBDIR=${RUST_TARGET_SUBDIR}" \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
copy_from_image "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}" "sprout.efi" "${FINAL_DIR}/sprout.efi"
|
copy_from_image "${DOCKER_PREFIX}/sprout-${TARGET_ARCH}" "sprout.efi" "${FINAL_DIR}/sprout.efi"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ has-item = "\\EFI\\BOOT\\xen.efi"
|
|||||||
[actions.boot-edera]
|
[actions.boot-edera]
|
||||||
edera.xen = "$boot\\EFI\\BOOT\\xen.efi"
|
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.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 = "$boot\\vmlinuz"
|
||||||
edera.kernel-options = ["console=hvc0", "edera-xen=yes"]
|
edera.kernel-options = ["console=hvc0", "edera-xen=yes"]
|
||||||
edera.initrd = "$boot\\initramfs"
|
edera.initrd = "$boot\\initramfs"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
# Without globstar, ** matches like *, so only scripts one directory deep are formatted.
|
||||||
|
shopt -s globstar
|
||||||
|
|
||||||
cd "$(dirname "${0}")/.." || exit 1
|
cd "$(dirname "${0}")/.." || exit 1
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user