65 Commits

Author SHA1 Message Date
50f7bc11aa sprout: version 0.0.12 2025-10-27 18:41:32 -04:00
2200ba74f6 fix(cargo): force dev profiles to use opt-level = 2 to workaround hardware acceleration 2025-10-27 18:35:18 -04:00
7a3db08e1d fix(cargo): remove transitive dependency on tokio 2025-10-27 18:26:53 -04:00
e7f5be30dd feat(autoconfigure): generate names using a unique hash 2025-10-27 18:21:28 -04:00
3bbe6561ef fix(docs): fedora setup guide should use [options] 2025-10-27 17:57:29 -04:00
3b5e110d1e feat(config): rename [defaults] to [options] 2025-10-27 17:56:38 -04:00
26315fb4c4 fix(options): stamp initrd and combine options safely by ignoring empty strings 2025-10-27 17:44:30 -04:00
a76f9770dc fix(chainload): support an empty initrd path, which will result in no initrd 2025-10-27 16:27:39 -04:00
59edd63a12 fix(doc): list generator is not the matrix generator 2025-10-27 16:17:33 -04:00
8a2e8c8127 fix(sprout): correct rustdoc and clarify safety in some places 2025-10-27 16:16:09 -04:00
6086778dc0 fix(menu): free timer event to avoid leak 2025-10-27 16:03:25 -04:00
e729d6a60b feat(sprout): cleanup default logging 2025-10-27 15:44:29 -04:00
d6e8fe0245 feat(autoconfigure): find vmlinuz and initramfs pairs with linux autoconfigure module 2025-10-27 15:41:29 -04:00
99653b5192 fix(menu): hide the entry name from the menu since it can be long with autoconfigure 2025-10-27 12:25:22 -04:00
2a76e4f798 chore(code): move bls autoconfigure to separate module 2025-10-27 04:28:25 -04:00
a10a5cb342 sprout: version 0.0.11 2025-10-27 04:00:37 -04:00
fdc5f0e1d2 chore(docs): mention autoconfiguration support for bls and use it in the docs 2025-10-27 03:52:39 -04:00
f60cf4b365 Merge pull request #18 from edera-dev/azenla/autoconfigure
Autoconfiguration Support
2025-10-27 03:48:51 -04:00
0ca9ff4fec fix(bls-autoconfigure): generate a unique chainload action for each filesystem 2025-10-27 03:45:10 -04:00
1799419bfa fix(autoconfigure): apply the actions properly in the root 2025-10-27 03:37:09 -04:00
facd2000a5 feat(autoconfigure): initial attempt at bls autoconfiguration 2025-10-27 02:38:40 -04:00
7dd910a74f sprout: version 0.0.10 2025-10-27 00:53:35 -04:00
187a84fcf8 fix(bls): entries path should be constructed from the subpath, not the device path 2025-10-27 00:50:17 -04:00
e47c813536 fix(bls): swap from intaking \\loader\\entries to just \\loader
This prepares for further BLS integration.
2025-10-27 00:31:07 -04:00
094128de58 fix(log): swap everything to use logging 2025-10-27 00:21:24 -04:00
e8a4fa5053 Revert "fix(log): make all logging debug! instead of info!"
This reverts commit 717e7716ba.
2025-10-27 00:20:42 -04:00
717e7716ba fix(log): make all logging debug! instead of info! 2025-10-27 00:16:00 -04:00
1d32855d22 Merge pull request #16 from edera-dev/azenla/alpine-edge
chore(docs): alpine edge setup guide
2025-10-27 00:15:28 -04:00
93c7a35c62 Merge pull request #17 from edera-dev/azenla/basic-menu
feat(boot): basic boot menu
2025-10-27 00:04:54 -04:00
8b6317f221 chore(doc): document the basic boot menu as a supported feature 2025-10-27 00:01:05 -04:00
4bbac3e4d5 feat(boot): implement basic boot menu 2025-10-26 23:59:50 -04:00
1f48d26385 chore(docs): alpine edge setup guide 2025-10-26 22:29:30 -04:00
9d2e25183b Merge pull request #15 from edera-dev/azenla/defaults-boot
feat(config): support for setting the default entry to boot
2025-10-24 21:30:03 -07:00
734ff117db feat(config): support for setting the default entry to boot 2025-10-24 21:19:38 -07:00
fbebedd66a fix(hack): format should use bash to use glob 2025-10-24 20:09:32 -07:00
b3bf564b65 fix(hack): formatting fixes 2025-10-24 20:08:46 -07:00
340c280c00 fix(hack): check kvm with /dev/kvm instead of cpu flags 2025-10-24 20:07:41 -07:00
7a72b7af5b Merge pull request #14 from edera-dev/azenla/fixes
Repair automated bug analysis issues.
2025-10-24 20:00:49 -07:00
0c2303d789 fix(framebuffer): add proper bounds checking for accessing a pixel 2025-10-24 19:54:28 -07:00
6cd502ef18 fix(actions): if edera action returns successfully, an intended unreachable line could be reached 2025-10-24 19:51:08 -07:00
e243228f15 fix(framebuffer): check width, height and implement proper checking when accessing pixels 2025-10-24 19:44:26 -07:00
2253fa2a1f fix(context): make sure to actually iterate longest first for key replacement 2025-10-24 19:40:40 -07:00
057c48f9f7 fix(bls): parser should skip over empty lines and comments 2025-10-24 19:31:01 -07:00
45d7cd2d3b fix(doc): incorrect comment for startup phase execution 2025-10-24 19:28:38 -07:00
482db0b763 fix(media-loader): eliminate usage of unwrap and swap to result 2025-10-24 19:27:43 -07:00
a15c92a749 fix(context): ensure longer keys are replaced first, fixing key replacement edge case 2025-10-24 19:24:29 -07:00
7d5248e2ee fix(context): skip over empty keys to avoid replacing $ and breaking other values 2025-10-24 19:12:43 -07:00
41fbca6f76 fix(utils): clarify that the to_string().contains() is necessary due to CString16 2025-10-24 19:11:17 -07:00
d39fbae168 fix(splash): check for zero-sized images 2025-10-24 19:08:02 -07:00
0b0b4dc19d chore(perf): replace some string replacement and comparison with characters for performance 2025-10-24 18:59:15 -07:00
86fa00928e fix(bls): convert less safe path concatenation to use path buffer 2025-10-24 18:56:11 -07:00
4c7b1d70ef fix(bls): parsing of entries should split by whitespace, not just spaces 2025-10-24 18:51:34 -07:00
9d2c31f77f fix(options): clarify code that checks for --abc=123 option form 2025-10-24 18:49:14 -07:00
fc710ec391 fix(options): --help should exit with code zero 2025-10-24 18:47:34 -07:00
9f7ca672ea chore(filesystem-device-match): add clarity to statement which is unreachable 2025-10-24 18:46:45 -07:00
2a2aa74c09 fix(context): add context finalization iteration limit
This prevents any possibility of an infinite loop during finalization.
2025-10-24 18:44:54 -07:00
2e3399f33f chore(deps): upgrade to uefi v0.36.0 2025-10-24 18:34:18 -07:00
160a7737fb sprout: version 0.0.9 2025-10-24 18:17:27 -07:00
68220d0de1 chore(workflows): repin all actions 2025-10-24 18:09:24 -07:00
e9b842a81f chore(workflows): separate release workflow 2025-10-24 18:04:55 -07:00
efb357d62b chore(workflows): add concurrency limiter 2025-10-24 18:01:50 -07:00
30600f0c81 fix(main): repair contextual replacement for entries 2025-10-24 17:16:42 -07:00
e10e98d669 chore(assets): update sprout logo 2025-10-24 16:49:34 -07:00
911b617d92 feat(options): --boot now supports selecting by entry name or index, not just title 2025-10-24 16:32:48 -07:00
d3f9e876fb fix(docs): cleanup rustdoc warnings 2025-10-24 15:55:56 -07:00
53 changed files with 1582 additions and 343 deletions

View File

@@ -9,6 +9,10 @@ on:
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs: jobs:
zizmor: zizmor:
name: zizmor name: zizmor
@@ -24,12 +28,12 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: setup uv - name: setup uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7 uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
- name: zizmor - name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -37,7 +41,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload - name: upload
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4 uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -11,6 +11,10 @@ on:
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt name: rustfmt
@@ -22,7 +26,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -52,7 +56,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -81,7 +85,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -11,6 +11,10 @@ on:
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs: jobs:
analyze: analyze:
name: analyze (${{ matrix.language }}) name: analyze (${{ matrix.language }})
@@ -36,18 +40,18 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: initialize codeql - name: initialize codeql
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v4 uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis - name: perform codeql analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v4 uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -1,13 +1,6 @@
name: publish name: publish
on: on:
workflow_dispatch:
inputs:
release-tag:
description: 'Release Tag'
required: true
type: string
push: push:
branches: branches:
- main - main
@@ -25,11 +18,15 @@ on:
permissions: permissions:
contents: read # Needed to checkout the repository. contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs: jobs:
artifacts: artifacts:
name: artifacts name: artifacts
permissions: permissions:
contents: write # Needed to upload release assets and artifacts. contents: write # Needed to upload artifacts.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
@@ -38,7 +35,7 @@ jobs:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false persist-credentials: false
@@ -50,35 +47,13 @@ jobs:
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact' - name: 'upload sprout-x86_64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: sprout-x86_64.efi name: sprout-x86_64.efi
path: target/assemble/sprout-x86_64.efi path: target/assemble/sprout-x86_64.efi
- name: 'upload sprout-aarch64.efi artifact' - name: 'upload sprout-aarch64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: sprout-aarch64.efi name: sprout-aarch64.efi
path: target/assemble/sprout-aarch64.efi path: target/assemble/sprout-aarch64.efi
- name: 'generate cultivator token'
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}

62
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: release
on:
workflow_dispatch:
inputs:
release-tag:
description: 'Release Tag'
required: true
type: string
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.inputs.release-tag }}"
cancel-in-progress: true
jobs:
release:
name: release
permissions:
contents: write # Needed to upload release assets.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: 'install rust toolchain'
run: |
cargo version
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}

165
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -28,9 +39,18 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.4" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@@ -45,10 +65,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "cfg-if" name = "bytes"
version = "1.0.3" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
@@ -59,14 +94,35 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.8" version = "0.0.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
"log", "log",
"serde", "serde",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
@@ -89,20 +145,36 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.3" version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.8" version = "0.25.8"
@@ -118,14 +190,20 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.11.4" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
] ]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.28"
@@ -175,9 +253,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.101" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -204,9 +282,9 @@ dependencies = [
[[package]] [[package]]
name = "pxfm" name = "pxfm"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [ dependencies = [
"num-traits", "num-traits",
] ]
@@ -259,6 +337,29 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -266,9 +367,9 @@ source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.106" version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -314,6 +415,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "ucs2" name = "ucs2"
version = "0.3.3" version = "0.3.3"
@@ -325,9 +432,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi" name = "uefi"
version = "0.35.0" version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7569ceafb898907ff764629bac90ac24ba4203c38c33ef79ee88c74aa35b11" checksum = "f123e69767fc287c44d70ee19af3b39d1bfb735dbaff5090e95b5b13cd656d16"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -341,9 +448,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi-macros" name = "uefi-macros"
version = "0.18.1" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dad47b3af8f99116c0f6d4d669c439487d9aaf1c8d9480d686cda6f3a8aa23" checksum = "4687412b5ac74d245d5bfb1733ede50c31be19bf8a4b6a967a29b451bab49e67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -352,9 +459,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi-raw" name = "uefi-raw"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cad96b8baaf1615d3fdd0f03d04a0b487d857c1b51b19dcbfe05e2e3c447b78" checksum = "8aff2f4f2b556a36a201d335a1e0a57754967a96857b1f47a52d5a23825cac84"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"uguid", "uguid",
@@ -362,15 +469,21 @@ dependencies = [
[[package]] [[package]]
name = "uguid" name = "uguid"
version = "2.2.0" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533" checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.19" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "winnow" name = "winnow"

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.8" version = "0.0.12"
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"
@@ -13,7 +13,7 @@ toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
[dependencies.image] [dependencies.image]
version = "0.25.6" version = "0.25.8"
default-features = false default-features = false
features = ["png"] features = ["png"]
optional = true optional = true
@@ -22,17 +22,26 @@ optional = true
version = "1.0.228" version = "1.0.228"
features = ["derive"] features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi] [dependencies.uefi]
version = "0.35.0" version = "0.36.0"
features = ["alloc", "logger"] features = ["alloc", "logger"]
[dependencies.uefi-raw] [dependencies.uefi-raw]
version = "0.11.0" version = "0.12.0"
[features] [features]
default = ["splash"] default = ["splash"]
splash = ["dep:image"] splash = ["dep:image"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly.
opt-level = 2
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "symbols" strip = "symbols"
@@ -46,6 +55,7 @@ debug = 1
inherits = "dev" inherits = "dev"
strip = "debuginfo" strip = "debuginfo"
debug = 0 debug = 0
opt-level = 2
[patch.crates-io.simd-adler32] [patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git" git = "https://github.com/edera-dev/sprout-patched-deps.git"

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
![Sprout Logo](assets/logo.png) ![Sprout Logo](assets/logo-small.png)
# Sprout # Sprout
@@ -39,6 +39,7 @@ simplify installation and usage.
- [Fedora Setup Guide] - [Fedora Setup Guide]
- [Generic Linux Setup Guide] - [Generic Linux Setup Guide]
- [Alpine Edge Setup Guide]
- [Windows Setup Guide] - [Windows Setup Guide]
- [Development Guide] - [Development Guide]
- [Contributing Guide] - [Contributing Guide]
@@ -48,8 +49,8 @@ simplify installation and usage.
## Features ## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently NOTE: Currently, Sprout is experimental and is not intended for production use.
have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or fails. The boot menu mechanism is very rudimentary.
### Current ### Current
@@ -59,11 +60,12 @@ have secure boot support. In fact, as of writing, it doesn't even have a boot me
- [x] Linux boot support via EFI stub - [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload - [x] Windows boot support via chainload
- [x] Load Linux initrd from disk - [x] Load Linux initrd from disk
- [x] Boot first configured entry - [x] Basic boot menu
- [x] BLS autoconfiguration support
### Roadmap ### Roadmap
- [ ] Boot menu - [ ] Full-featured boot menu
- [ ] Secure Boot support: work in progress - [ ] Secure Boot support: work in progress
- [ ] UKI support: partial - [ ] UKI support: partial
- [ ] multiboot2 support - [ ] multiboot2 support
@@ -99,6 +101,8 @@ Sprout supports some command line options that can be combined to modify behavio
$ sprout.efi --config=\path\to\config.toml $ sprout.efi --config=\path\to\config.toml
# Boot a specific entry, bypassing the menu. # Boot a specific entry, bypassing the menu.
$ sprout.efi --boot="Boot Xen" $ sprout.efi --boot="Boot Xen"
# Autoconfigure Sprout, without loading a configuration file.
$ sprout.efi --autoconfigure
``` ```
### Boot Linux from ESP ### Boot Linux from ESP
@@ -133,32 +137,17 @@ version = 1
[drivers.ext4] [drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem # global options.
# that contains \loader\entries as a directory [options]
# into the value called "boot" # enable autoconfiguration by detecting bls enabled
[extractors.boot.filesystem-device-match] # filesystems and generating boot entries for them.
has-item = "\\loader\\entries" autoconfigure = true
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
``` ```
[Edera]: https://edera.dev [Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md [Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md [Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Alpine Edge Setup Guide]: ./docs/alpine-edge-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md [Windows Setup Guide]: ./docs/windows-setup.md
[Development Guide]: ./DEVELOPMENT.md [Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md [Contributing Guide]: ./CONTRIBUTING.md

BIN
assets/logo-large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/logo-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

113
docs/alpine-edge-setup.md Normal file
View File

@@ -0,0 +1,113 @@
# Setup Sprout on Alpine Edge
## Prerequisites
- Alpine Edge
- EFI System Partition mounted on `/boot/efi` (the default)
- ext4 or FAT32/exFAT formatted `/boot` partition
## Step 1: Base Installation
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/boot/sprout.efi` on your EFI System Partition.
Additionally, you will want to install the `efifs` package, which provides the filesystem support for Sprout.
```bash
# Install the efifs package which provides filesystem support for Sprout.
$ apk install efifs
```
## Step 2: Configure Sprout
Since Alpine uses standard image paths based on the `linux` package installed, it's quite easy to configure Sprout
to boot Alpine.
Write the following file to `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an EFI driver for ext2/ext3/ext4.
[drivers.ext2]
path = "\\EFI\\efifs\\ext2.efi"
# extract the full path of the first filesystem
# that contains \boot\vmlinuz-stable as a file
# into the value called "root"
[extractors.root.filesystem-device-match]
has-item = "\\boot\\vmlinuz-stable"
# add a boot entry for booting linux
# which will run the boot-linux action.
[entries.boot-linux-stable]
title = "Boot Linux Stable"
actions = ["boot-linux-stable"]
# use the chainload action to boot linux-stable via the efi stub.
# the options below are passed to the efi stub as the
# kernel command line. the initrd is loaded using the efi stub
# initrd loader mechanism.
[actions.boot-linux-stable]
chainload.path = "$root\\boot\\vmlinuz-stable"
chainload.options = ["root=/dev/sda1", "my-kernel-option"]
chainload.linux-initrd = "$root\\boot\\initramfs-stable"
```
You can replace `vmlinuz-stable` and `initramfs-stable` with the actual
files for the `linux` package you have installed. For example, for `linux-lts` it is `vmlinuz-lts` and `initramfs-lts`.
## Step 3, Option 1: Configure GRUB to load Sprout (recommended)
You can configure GRUB to add a boot entry for Sprout, so you can continue to use GRUB without interruption.
GRUB needs to be configured with the chainloader module to load Sprout.
You will need to find the UUID of your EFI System Partition. You can do this by running the following command:
```bash
$ grep "/boot/efi" /etc/fstab | awk '{print $1}' | awk -F '=' '{print $2}'
SAMPLE-VALUE
```
The GRUB configuration for Sprout is as follows, replace `SAMPLE-VALUE` with the UUID of your EFI System Partition:
```grub
menuentry 'Sprout' $menuentry_id_option 'sprout' {
insmod part_gpt
insmod fat
insmod chain
search --no-floppy --fs-uuid --set=root SAMPLE-VALUE
chainloader /EFI/boot/sprout.efi
}
```
You can append this to `/etc/grub.d/40_custom` and run the following command to update your configuration:
```bash
$ update-grub
```
To update your GRUB configuration.
You may now reboot your system and select Sprout from the GRUB menu.
## Step 3, Option 2: Configure your EFI firmware for Sprout
You can configure your EFI boot menu to show Sprout as an option.
You will need to install the `efibootmgr` package:
```
$ apk add efibootmgr
```
Once `efibootmgr` is installed, find the partition device of your EFI System Partition and run the following:
```bash
$ efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\boot\sprout.efi'
```
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
Now if you boot into your UEFI firmware, you should see Sprout as an option to boot.

View File

@@ -39,27 +39,11 @@ version = 1
[drivers.ext4] [drivers.ext4]
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem # global options.
# that contains \loader\entries as a directory [options]
# into the value called "boot" # enable autoconfiguration by detecting bls enabled
[extractors.boot.filesystem-device-match] # filesystems and generating boot entries for them.
has-item = "\\loader\\entries" autoconfigure = true
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
# entries in sprout, using the entry template
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
entry.title = "$title"
entry.actions = ["bls"]
# the action that is used for each bls entry above.
[actions.bls]
chainload.path = "$boot\\$chainload"
chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
``` ```
## Step 3, Option 1: Configure GRUB to load Sprout (recommended) ## Step 3, Option 1: Configure GRUB to load Sprout (recommended)

View File

@@ -36,9 +36,7 @@ DOCKER_TARGET="linux/${TARGET_ARCH}"
FINAL_DIR="target/final/${TARGET_ARCH}" FINAL_DIR="target/final/${TARGET_ARCH}"
ASSEMBLE_DIR="target/assemble" ASSEMBLE_DIR="target/assemble"
if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] && if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] && [ -e "/dev/kvm" ]; then
[ -f "/proc/cpuinfo" ] &&
grep -E '^flags.*:.+ vmx .*' /proc/cpuinfo >/dev/null; then
QEMU_ACCEL="kvm" QEMU_ACCEL="kvm"
fi fi

View File

@@ -35,6 +35,7 @@ set -- "${@}" -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then
set -- "${@}" -nographic set -- "${@}" -nographic
else else
if [ "${GRAPHICAL_ONLY}" != "1" ]; then
if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then
set -- "${@}" -serial stdio set -- "${@}" -serial stdio
else else
@@ -43,6 +44,7 @@ else
-chardev stdio,id=stdio0 \ -chardev stdio,id=stdio0 \
-device virtconsole,chardev=stdio0,id=console0 -device virtconsole,chardev=stdio0,id=console0
fi fi
fi
if [ "${QEMU_LEGACY_VGA}" = "1" ]; then if [ "${QEMU_LEGACY_VGA}" = "1" ]; then
set -- "${@}" -vga std set -- "${@}" -vga std
@@ -63,13 +65,8 @@ set -- "${@}" \
-drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \ -drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \
-device nvme,drive=disk1,serial=cafebabe -device nvme,drive=disk1,serial=cafebabe
if [ "${DISK_BOOT}" = "1" ]; then
set -- "${@}" \ set -- "${@}" \
-drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on" -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"
else
set -- "${@}" \
-drive "if=none,file=fat:rw:${FINAL_DIR}/efi,format=raw,id=disk1"
fi
set -- "${@}" -name "sprout ${TARGET_ARCH}" set -- "${@}" -name "sprout ${TARGET_ARCH}"

View File

@@ -13,6 +13,7 @@ COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG COPY xen.cfg /work/XEN.CFG
COPY initramfs /work/INITRAMFS COPY initramfs /work/INITRAMFS
COPY edera-splash.png /work/EDERA-SPLASH.PNG COPY edera-splash.png /work/EDERA-SPLASH.PNG
COPY bls.conf /work/BLS.CONF
RUN truncate -s128MiB sprout.img && \ RUN truncate -s128MiB sprout.img && \
parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \ parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \
parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \ parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \
@@ -20,6 +21,8 @@ RUN truncate -s128MiB sprout.img && \
mkfs.vfat -F32 -n EFI sprout.img && \ mkfs.vfat -F32 -n EFI sprout.img && \
mmd -i sprout.img ::/EFI && \ mmd -i sprout.img ::/EFI && \
mmd -i sprout.img ::/EFI/BOOT && \ mmd -i sprout.img ::/EFI/BOOT && \
mmd -i sprout.img ::/LOADER && \
mmd -i sprout.img ::/LOADER/ENTRIES && \
mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \
@@ -28,6 +31,7 @@ RUN truncate -s128MiB sprout.img && \
mcopy -i sprout.img SPROUT.TOML ::/ && \ mcopy -i sprout.img SPROUT.TOML ::/ && \
mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \ mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \
mcopy -i sprout.img INITRAMFS ::/ && \ mcopy -i sprout.img INITRAMFS ::/ && \
mcopy -i sprout.img BLS.CONF ::/LOADER/ENTRIES/ && \
mv sprout.img /sprout.img mv sprout.img /sprout.img
FROM scratch AS final FROM scratch AS final

View File

@@ -11,7 +11,7 @@ if [ "${TARGET_ARCH}" = "aarch64" ]; then
fi fi
if [ -z "${SPROUT_CONFIG_NAME}" ]; then if [ -z "${SPROUT_CONFIG_NAME}" ]; then
SPROUT_CONFIG_NAME="kernel" SPROUT_CONFIG_NAME="all"
fi fi
echo "[build] ${TARGET_ARCH} ${RUST_PROFILE}" echo "[build] ${TARGET_ARCH} ${RUST_PROFILE}"
@@ -108,6 +108,7 @@ if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then
cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml" cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml"
cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg" cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg"
cp "hack/dev/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png" cp "hack/dev/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png"
cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf"
mkdir -p "${FINAL_DIR}/efi/EFI/BOOT" mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI" cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI"

View File

@@ -0,0 +1,30 @@
version = 1
[options]
default-entry = "kernel"
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi"
[actions.chainload-kernel]
chainload.path = "$boot\\EFI\\BOOT\\kernel.efi"
chainload.options = ["console=hvc0"]
chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel]
title = "Boot Linux"
actions = ["chainload-kernel"]
[actions.chainload-shell]
chainload.path = "$boot\\EFI\\BOOT\\shell.efi"
[entries.shell]
title = "Boot Shell"
actions = ["chainload-shell"]
[actions.chainload-xen]
chainload.path = "$boot\\EFI\\BOOT\\xen.efi"
[entries.xen]
title = "Boot Xen"
actions = ["chainload-xen"]

View File

@@ -0,0 +1,4 @@
version = 1
[options]
autoconfigure = true

View File

@@ -0,0 +1,4 @@
title Boot Linux
linux /efi/boot/kernel.efi
options console=hvc0
initrd /initramfs

View File

@@ -1,5 +1,9 @@
version = 1 version = 1
[options]
default-entry = "edera"
menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi" has-item = "\\EFI\\BOOT\\xen.efi"

View File

@@ -1,5 +1,9 @@
version = 1 version = 1
[options]
default-entry = "kernel"
menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi" has-item = "\\EFI\\BOOT\\kernel.efi"

View File

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

View File

@@ -1,5 +1,9 @@
version = 1 version = 1
[options]
default-entry = "xen"
menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi" has-item = "\\EFI\\BOOT\\xen.efi"

View File

@@ -29,8 +29,7 @@ else
fi fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig
if [ "${TARGET_KARCH}" = "x86_64" ] if [ "${TARGET_KARCH}" = "x86_64" ]; then
then
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config
./scripts/config -e XEN_PV ./scripts/config -e XEN_PV
./scripts/config -e XEN_PV_DOM0 ./scripts/config -e XEN_PV_DOM0

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/bash
set -e set -e
cd "$(dirname "${0}")/.." || exit 1 cd "$(dirname "${0}")/.." || exit 1
cargo fmt --all cargo fmt --all || true
shfmt -w hack/**/*.sh shfmt -w hack/**/*.sh || true

View File

@@ -1,5 +1,5 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use anyhow::{Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
@@ -19,7 +19,7 @@ pub mod splash;
/// that you can specify via other concepts. /// that you can specify via other concepts.
/// ///
/// Actions are the main work that Sprout gets done, like booting Linux. /// Actions are the main work that Sprout gets done, like booting Linux.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ActionDeclaration { pub struct ActionDeclaration {
/// Chainload to another EFI application. /// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system /// This allows you to load any EFI application, either to boot an operating system
@@ -50,7 +50,10 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
bail!("unknown action '{}'", name.as_ref()); bail!("unknown action '{}'", name.as_ref());
}; };
// Finalize the context and freeze it. // Finalize the context and freeze it.
let context = context.finalize().freeze(); let context = context
.finalize()
.context("unable to finalize context")?
.freeze();
// Execute the action. // Execute the action.
if let Some(chainload) = &action.chainload { if let Some(chainload) = &action.chainload {
@@ -61,6 +64,7 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
return Ok(()); return Ok(());
} else if let Some(edera) = &action.edera { } else if let Some(edera) = &action.edera {
edera::edera(context.clone(), edera)?; edera::edera(context.clone(), edera)?;
return Ok(());
} }
#[cfg(feature = "splash")] #[cfg(feature = "splash")]

View File

@@ -10,7 +10,7 @@ use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
/// The configuration of the chainload action. /// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration { pub struct ChainloadConfiguration {
/// The path to the image to chainload. /// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
@@ -53,18 +53,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?; .context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = configuration let options =
.options utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
// Pass the options to the image, if any are provided. // Pass the options to the image, if any are provided.
// The holder must drop at the end of this function to ensure the options are not leaked, // The holder must drop at the end of this function to ensure the options are not leaked,
// and the holder here ensures it outlives the if block here, as a pointer has to be // and the holder here ensures it outlives the if block here, as a pointer has to be
// passed to the image. This has been hand-validated to be safe. // passed to the image.
// SAFETY: The options outlive the usage of the image, and the image is not used after this.
let mut options_holder: Option<Box<CString16>> = None; let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() { if !options.is_empty() {
let options = Box::new( let options = Box::new(
@@ -88,10 +85,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
options_holder = Some(options); options_holder = Some(options);
} }
// The initrd can be None or empty, so we need to collapse that into a single Option.
let initrd = utils::empty_is_none(configuration.linux_initrd.as_ref());
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(ref linux_initrd) = configuration.linux_initrd { if let Some(linux_initrd) = initrd {
let initrd_path = context.stamp(linux_initrd); // Stamp the path to the initrd.
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path) let linux_initrd = context.stamp(linux_initrd);
let content = utils::read_file_contents(context.root().loaded_image_path()?, &linux_initrd)
.context("unable to read linux initrd")?; .context("unable to read linux initrd")?;
let handle = let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
@@ -99,15 +101,11 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
initrd_handle = Some(handle); initrd_handle = Some(handle);
} }
// Retrieve the base and size of the loaded image to display.
let (base, size) = loaded_image_protocol.info();
info!("loaded image: base={:#x} size={:#x}", base.addr(), size);
// 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
// after the optional initrd has been unregistered. // after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image).context("unable to start image"); let result = uefi::boot::start_image(image);
// Unregister the initrd if it was registered. // Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle

View File

@@ -22,7 +22,7 @@ use crate::{
/// The configuration of the edera action which boots the Edera hypervisor. /// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack. /// Edera is based on Xen but modified significantly with a Rust stack.
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen. /// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EderaConfiguration { pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image. /// The path to the Xen hypervisor EFI image.
pub xen: String, pub xen: String,
@@ -40,7 +40,23 @@ pub struct EderaConfiguration {
} }
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. /// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
fn build_xen_config(configuration: &EderaConfiguration) -> String { fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String {
// Stamp xen options and combine them.
let xen_options = utils::combine_options(
configuration
.xen_options
.iter()
.map(|item| context.stamp(item)),
);
// Stamp kernel options and combine them.
let kernel_options = utils::combine_options(
configuration
.kernel_options
.iter()
.map(|item| context.stamp(item)),
);
// xen config file format is ini-like // xen config file format is ini-like
[ [
// global section // global section
@@ -50,10 +66,10 @@ fn build_xen_config(configuration: &EderaConfiguration) -> String {
// configuration section for sprout // configuration section for sprout
"[sprout]".to_string(), "[sprout]".to_string(),
// xen options // xen options
format!("options={}", configuration.xen_options.join(" ")), format!("options={}", xen_options),
// kernel options, stub replaces the kernel path // kernel options, stub replaces the kernel path
// the kernel is provided via media loader // the kernel is provided via media loader
format!("kernel=stub {}", configuration.kernel_options.join(" ")), format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored // required or else the last line will be ignored
"".to_string(), "".to_string(),
] ]
@@ -94,7 +110,7 @@ fn register_media_loader_file(
/// `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<()> {
// Build the Xen config file content for this configuration. // Build the Xen config file content for this configuration.
let config = build_xen_config(configuration); let config = build_xen_config(context.clone(), configuration);
// Register the media loader for the config. // Register the media loader for the config.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
@@ -113,7 +129,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
let mut media_loaders = vec![config, kernel]; let mut media_loaders = vec![config, kernel];
// Register the initrd if it is provided. // Register the initrd if it is provided.
if let Some(ref initrd) = configuration.initrd { if let Some(initrd) = utils::empty_is_none(configuration.initrd.as_ref()) {
let initrd = let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?; .context("unable to register initrd media loader")?;
@@ -131,7 +147,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders on error. // Unregister the media loaders when an error happens.
for media_loader in media_loaders { for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() { if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error); error!("unable to unregister media loader: {}", error);

View File

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

View File

@@ -15,7 +15,7 @@ use uefi::proto::console::gop::GraphicsOutput;
const DEFAULT_SPLASH_TIME: u32 = 0; const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action. /// The configuration of the splash action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SplashConfiguration { pub struct SplashConfiguration {
/// The path to the image to display. /// The path to the image to display.
/// Currently, only PNG images are supported. /// Currently, only PNG images are supported.
@@ -52,6 +52,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
height: image.height(), height: image.height(),
}; };
// Handle the case where the image is zero-sized.
if input.height == 0 || input.width == 0 {
return input;
}
// Calculate the ratio of the image dimensions. // Calculate the ratio of the image dimensions.
let input_ratio = input.width as f32 / input.height as f32; let input_ratio = input.width as f32 / input.height as f32;
@@ -66,6 +71,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
height: frame.height, height: frame.height,
}; };
// Handle the case where the output is zero-sized.
if output.height == 0 || output.width == 0 {
return output;
}
if input_ratio < frame_ratio { if input_ratio < frame_ratio {
output.width = (frame.height as f32 * input_ratio).floor() as u32; output.width = (frame.height as f32 * input_ratio).floor() as u32;
output.height = frame.height; output.height = frame.height;
@@ -110,7 +120,8 @@ fn draw(image: DynamicImage) -> Result<()> {
let image = resize_to_fit(&image, fit); let image = resize_to_fit(&image, fit);
// Create a framebuffer to draw the image on. // Create a framebuffer to draw the image on.
let mut framebuffer = Framebuffer::new(width, height); let mut framebuffer =
Framebuffer::new(width, height).context("unable to create framebuffer")?;
// Iterate over the pixels in the image and put them on the framebuffer. // Iterate over the pixels in the image and put them on the framebuffer.
for (x, y, pixel) in image.enumerate_pixels() { for (x, y, pixel) in image.enumerate_pixels() {

50
src/autoconfigure.rs Normal file
View File

@@ -0,0 +1,50 @@
use crate::config::RootConfiguration;
use anyhow::{Context, Result};
use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath;
use uefi::proto::media::fs::SimpleFileSystem;
/// bls: autodetect and configure BLS-enabled filesystems.
pub mod bls;
/// linux: autodetect and configure Linux kernels.
/// This autoconfiguration module should not be activated
/// on BLS-enabled filesystems as it may make duplicate entries.
pub mod linux;
/// Generate a [RootConfiguration] based on the environment.
/// Intakes a `config` to use as the basis of the autoconfiguration.
pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
// Find all the filesystems that are on the system.
let filesystem_handles =
uefi::boot::find_handles::<SimpleFileSystem>().context("unable to scan filesystems")?;
// For each filesystem that was detected, scan it for supported autoconfig mechanisms.
for handle in filesystem_handles {
// Acquire the device path root for the filesystem.
let root = {
uefi::boot::open_protocol_exclusive::<DevicePath>(handle)
.context("unable to get root for filesystem")?
.to_boxed()
};
// Open the filesystem that was detected.
let filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
.context("unable to open filesystem")?;
// Trade the filesystem protocol for the uefi filesystem helper.
let mut filesystem = FileSystem::new(filesystem);
// Scan the filesystem for BLS supported configurations.
let bls_found = bls::scan(&mut filesystem, &root, config)
.context("unable to scan for bls configurations")?;
// If BLS was not found, scan for Linux configurations.
if !bls_found {
linux::scan(&mut filesystem, &root, config)
.context("unable to scan for linux configurations")?;
}
}
Ok(())
}

101
src/autoconfigure/bls.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the BLS chainload action that will be used
/// by the BLS generator to chainload entries.
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}

213
src/autoconfigure/linux.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Linux chainload action that will be used to boot Linux.
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// We will check for symlinks and if this directory is a symlink, we will skip it.
const SCAN_LOCATIONS: &[&str] = &["/boot", "/"];
/// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd"];
/// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find.
struct KernelPair {
/// The path to a kernel.
kernel: String,
/// The path to an initramfs, if any.
initramfs: Option<String>,
}
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
// All the discovered kernel pairs.
let mut pairs = Vec::new();
// Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
let path = path.to_path_buf();
// Check if the path exists and is a directory.
let exists = filesystem
.metadata(&path)
.ok()
.map(|metadata| metadata.is_directory())
.unwrap_or(false);
// If the path does not exist, return an empty list.
if !exists {
return Ok(pairs);
}
// Open a directory iterator on the path to scan.
// Ignore errors here as in some scenarios this might fail due to symlinks.
let Some(directory) = filesystem.read_dir(&path).ok() else {
return Ok(pairs);
};
// For each item in the directory, find a kernel.
for item in directory {
let item = item.context("unable to read directory item")?;
// Skip over any items that are not regular files.
if !item.is_regular_file() {
continue;
}
// Convert the name from a CString16 to a String.
let name = item.file_name().to_string();
// Find a kernel prefix that matches, if any.
let Some(prefix) = KERNEL_PREFIXES
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let initramfs = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
break None;
};
// Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path.clone();
initramfs_path.push(Path::new(&initramfs));
// Check if the initramfs path exists, if it does, break out of the loop.
if filesystem
.try_exists(&initramfs_path)
.context("unable to check if initramfs path exists")?
{
break Some(initramfs_path);
}
};
// Construct a kernel path from the kernel name.
let mut kernel = path.clone();
kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string();
let initramfs = initramfs.map(|initramfs| initramfs.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
pairs.push(pair);
}
Ok(pairs)
}
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
let mut pairs = Vec::new();
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS {
let scanned = scan_directory(filesystem, location)
.with_context(|| format!("unable to scan directory {}", location))?;
pairs.extend(scanned);
}
// If no kernel pairs were found, return false.
if pairs.is_empty() {
return Ok(false);
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $kernel".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
values: pairs
.into_iter()
.map(|pair| {
BTreeMap::from_iter(vec![
("kernel".to_string(), pair.kernel),
("initrd".to_string(), pair.initramfs.unwrap_or_default()),
])
})
.collect(),
};
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
},
);
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config
.values
.insert("linux-options".to_string(), "".to_string());
}
// Generate a chainload configuration for the list generator.
// The list will provide these values to us.
// Note that we don't need an extra \\ in the paths here.
// The root already contains a trailing slash.
let chainload = ChainloadConfiguration {
path: format!("{}$kernel", root),
options: vec!["$linux-options".to_string()],
linux_initrd: Some(format!("{}$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}

View File

@@ -14,14 +14,20 @@ pub mod loader;
/// This must be incremented when the configuration breaks compatibility. /// This must be incremented when the configuration breaks compatibility.
pub const LATEST_VERSION: u32 = 1; pub const LATEST_VERSION: u32 = 1;
/// The default timeout for the boot menu in seconds.
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
/// The Sprout configuration format. /// The Sprout configuration format.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, 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
/// the configuration is the latest version. /// the configuration is the latest version.
#[serde(default = "latest_version")] #[serde(default = "latest_version")]
pub version: u32, pub version: u32,
/// Default options for Sprout.
#[serde(default)]
pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context. /// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
@@ -59,6 +65,25 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
/// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown.
#[serde(rename = "default-entry", default)]
pub default_entry: Option<String>,
/// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64,
/// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool,
}
fn latest_version() -> u32 { fn latest_version() -> u32 {
LATEST_VERSION LATEST_VERSION
} }
fn default_menu_timeout() -> u64 {
DEFAULT_MENU_TIMEOUT_SECONDS
}

View File

@@ -1,11 +1,15 @@
use crate::actions::ActionDeclaration; use crate::actions::ActionDeclaration;
use crate::options::SproutOptions; use crate::options::SproutOptions;
use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::{Result, bail};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc; use std::rc::Rc;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
/// The maximum number of iterations that can be performed in [SproutContext::finalize].
const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// 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.
#[derive(Default)] #[derive(Default)]
@@ -79,6 +83,11 @@ impl SproutContext {
self.root.as_ref() self.root.as_ref()
} }
/// Access the root context to modify it, if possible.
pub fn root_mut(&mut self) -> Option<&mut RootContext> {
Rc::get_mut(&mut self.root)
}
/// Retrieve the value specified by `key` from this context or its parents. /// Retrieve the value specified by `key` from this context or its parents.
/// Returns `None` if the value is not found. /// Returns `None` if the value is not found.
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> { pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
@@ -109,7 +118,10 @@ impl SproutContext {
pub fn all_values(&self) -> BTreeMap<String, String> { pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for key in self.all_keys() { for key in self.all_keys() {
values.insert(key.clone(), self.get(key).cloned().unwrap_or_default()); // Acquire the value from the context. Since retrieving all the keys will give us
// a full view of the context, we can be sure that the key exists.
let value = self.get(&key).cloned().unwrap_or_default();
values.insert(key.clone(), value);
} }
values values
} }
@@ -151,11 +163,20 @@ impl SproutContext {
/// Finalizes a context by producing a context with no parent that contains all the values /// Finalizes a context by producing a context with no parent that contains all the values
/// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no /// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no
/// inheritance with other [SproutContext]s. It will still contain a [RootContext] however. /// inheritance with other [SproutContext]s. It will still contain a [RootContext] however.
pub fn finalize(&self) -> SproutContext { pub fn finalize(&self) -> Result<SproutContext> {
// Collect all the values from the context and its parents. // Collect all the values from the context and its parents.
let mut current_values = self.all_values(); let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail.
let mut iterations: usize = 0;
loop { loop {
iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization");
}
let mut did_change = false; let mut did_change = false;
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for (key, value) in &current_values { for (key, value) in &current_values {
@@ -176,11 +197,11 @@ impl SproutContext {
} }
// Produce the final context. // Produce the final context.
Self { Ok(Self {
root: self.root.clone(), root: self.root.clone(),
parent: None, parent: None,
values: current_values, values: current_values,
} })
} }
/// Stamps the `text` value with the specified `values` map. The returned value indicates /// Stamps the `text` value with the specified `values` map. The returned value indicates
@@ -188,7 +209,25 @@ impl SproutContext {
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) { fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string(); let mut result = text.as_ref().to_string();
let mut did_change = false; let mut did_change = false;
for (key, value) in values {
// Sort the keys by length. This is to ensure that we stamp the longest keys first.
// If we did not do this, "$abc" could be stamped by "$a" into an invalid result.
let mut keys = values.keys().collect::<Vec<_>>();
// Sort by key length, reversed. This results in the longest keys appearing first.
keys.sort_by_key(|key| Reverse(key.len()));
for key in keys {
// Empty keys are not supported.
if key.is_empty() {
continue;
}
// We can fetch the value from the map. It is verifiable that the key exists.
let Some(value) = values.get(key) else {
unreachable!("keys iterated over is collected on a map that cannot be modified");
};
let next_result = result.replace(&format!("${key}"), value); let next_result = result.replace(&format!("${key}"), value);
if result != next_result { if result != next_result {
did_change = true; did_change = true;
@@ -204,4 +243,10 @@ impl SproutContext {
pub fn stamp(&self, text: impl AsRef<str>) -> String { pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1 Self::stamp_values(&self.all_values(), text.as_ref()).1
} }
/// Unloads a [SproutContext] back into an owned context. This
/// may not succeed if something else is holding onto the value.
pub fn unload(self: Rc<SproutContext>) -> Option<SproutContext> {
Rc::into_inner(self)
}
} }

View File

@@ -12,14 +12,14 @@ use uefi::proto::device_path::LoadedImageDevicePath;
/// Drivers allow extending the functionality of Sprout. /// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support. /// Drivers are loaded at runtime and can provide extra functionality like filesystem support.
/// Drivers are loaded by their name, which is used to reference them in other concepts. /// Drivers are loaded by their name, which is used to reference them in other concepts.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DriverDeclaration { pub struct DriverDeclaration {
/// The filesystem path to the driver. /// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed. /// This file should be an EFI executable that can be located and executed.
pub path: String, pub path: String,
} }
/// Loads the driver specified by the [driver] declaration. /// Loads the driver specified by the `driver` declaration.
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> { fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image. // Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle(); let sprout_image = uefi::boot::image_handle();
@@ -33,8 +33,6 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
// Push the path of the driver from the root. // Push the path of the driver from the root.
full_path.push_str(&context.stamp(&driver.path)); full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path);
// Convert the path to a device path. // Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?; let device_path = utils::text_to_device_path(&full_path)?;

View File

@@ -1,13 +1,16 @@
use crate::context::SproutContext;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc;
/// Declares a boot entry to display in the boot menu. /// Declares a boot entry to display in the boot menu.
/// ///
/// Entries are the user-facing concept of Sprout, making it possible /// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context. /// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration { pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu. /// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value.
pub title: String, pub title: String,
/// The actions to run when the entry is selected. /// The actions to run when the entry is selected.
#[serde(default)] #[serde(default)]
@@ -16,3 +19,93 @@ pub struct EntryDeclaration {
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
} }
/// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)]
pub struct BootableEntry {
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
default: bool,
}
impl BootableEntry {
/// Create a new bootable entry to represent the full context of an entry.
pub fn new(
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
) -> Self {
Self {
name,
title,
context,
declaration,
default: false,
}
}
/// Fetch the name of the entry. This is usually a machine-identifiable key.
pub fn name(&self) -> &str {
&self.name
}
/// Fetch the title of the entry. This is usually a human-readable key.
pub fn title(&self) -> &str {
&self.title
}
/// Fetch the full context of the entry.
pub fn context(&self) -> Rc<SproutContext> {
Rc::clone(&self.context)
}
/// Fetch the declaration of the entry.
pub fn declaration(&self) -> &EntryDeclaration {
&self.declaration
}
/// Fetch whether the entry is the default entry.
pub fn is_default(&self) -> bool {
self.default
}
/// Swap out the context of the entry.
pub fn swap_context(&mut self, context: Rc<SproutContext>) {
self.context = context;
}
/// Restamp the title with the current context.
pub fn restamp_title(&mut self) {
self.title = self.context.stamp(&self.title);
}
/// Mark this entry as the default entry.
pub fn mark_default(&mut self) {
self.default = true;
}
/// Prepend the name of the entry with `prefix`.
pub fn prepend_name_prefix(&mut self, prefix: &str) {
self.name.insert_str(0, prefix);
}
/// Determine if this entry matches `needle` by comparing to the name or title of the entry.
pub fn is_match(&self, needle: &str) -> bool {
self.name == needle || self.title == needle
}
/// Find an entry by `needle` inside the entry iterator `haystack`.
/// This will search for an entry by name, title, or index.
pub fn find<'a>(
needle: &str,
haystack: impl Iterator<Item = &'a BootableEntry>,
) -> Option<&'a BootableEntry> {
haystack
.enumerate()
.find(|(index, entry)| entry.is_match(needle) || index.to_string() == needle)
.map(|(_index, entry)| entry)
}
}

View File

@@ -10,7 +10,7 @@ pub mod filesystem_device_match;
/// Declares an extractor configuration. /// Declares an extractor configuration.
/// Extractors allow calculating values at runtime /// Extractors allow calculating values at runtime
/// using built-in sprout modules. /// using built-in sprout modules.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ExtractorDeclaration { pub struct ExtractorDeclaration {
/// The filesystem device match extractor. /// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns /// This extractor finds a filesystem using some search criteria and returns

View File

@@ -18,9 +18,9 @@ use uefi_raw::Status;
/// the device root path that can concatenated with subpaths to access files /// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem. /// on a particular filesystem.
/// ///
/// This function only requires one of the criteria to match. /// This function only requires all the criteria to match.
/// The fallback value can be used to provide a value if none is found. /// The fallback value can be used to provide a value if none is found.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor { pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label. /// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")] #[serde(default, rename = "has-label")]
@@ -81,7 +81,7 @@ pub fn extract(
} else { } else {
// We should still handle other errors gracefully. // We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?; Err(error).context("unable to open filesystem partition info")?;
None unreachable!()
} }
} }
} }

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::EntryDeclaration; use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration; use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration; use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result; use anyhow::Result;
use anyhow::bail; use anyhow::bail;
@@ -8,11 +9,12 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
pub mod bls; pub mod bls;
pub mod list;
pub mod matrix; pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
/// Generators allow generating entries at runtime based on a set of data. /// Generators allow generating entries at runtime based on a set of data.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct GeneratorDeclaration { pub struct GeneratorDeclaration {
/// Matrix generator configuration. /// Matrix generator configuration.
/// Matrix allows you to specify multiple value-key values as arrays. /// Matrix allows you to specify multiple value-key values as arrays.
@@ -32,6 +34,9 @@ pub struct GeneratorDeclaration {
/// It will generate a sprout entry for every supported BLS entry. /// It will generate a sprout entry for every supported BLS entry.
#[serde(default)] #[serde(default)]
pub bls: Option<BlsConfiguration>, pub bls: Option<BlsConfiguration>,
/// List generator configuration.
/// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>,
} }
/// Runs the generator specified by the `generator` option. /// Runs the generator specified by the `generator` option.
@@ -40,11 +45,13 @@ pub struct GeneratorDeclaration {
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
generator: &GeneratorDeclaration, generator: &GeneratorDeclaration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> { ) -> Result<Vec<BootableEntry>> {
if let Some(matrix) = &generator.matrix { if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix) matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls { } else if let Some(bls) = &generator.bls {
bls::generate(context, bls) bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else { } else {
bail!("unknown generator configuration"); bail!("unknown generator configuration");
} }

View File

@@ -1,30 +1,30 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::EntryDeclaration; use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::bls::entry::BlsEntry; use crate::generators::bls::entry::BlsEntry;
use crate::utils; use crate::utils;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
use uefi::CString16; use uefi::cstr16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, PathBuf};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
/// BLS entry parser. /// BLS entry parser.
mod entry; mod entry;
/// The default path to the BLS entries directory. /// The default path to the BLS directory.
const BLS_TEMPLATE_PATH: &str = "\\loader\\entries"; const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator. /// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce /// The BLS uses the Bootloader Specification to produce
/// entries from an input template. /// entries from an input template.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration { pub struct BlsConfiguration {
/// The entry to use for as a template. /// The entry to use for as a template.
pub entry: EntryDeclaration, pub entry: EntryDeclaration,
/// The path to the BLS entries directory. /// The path to the BLS directory.
#[serde(default = "default_bls_path")] #[serde(default = "default_bls_path")]
pub path: String, pub path: String,
} }
@@ -42,36 +42,34 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
/// Generates entries from the BLS entries directory using the specified `bls` configuration and /// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries. /// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate( pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
context: Rc<SproutContext>,
bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
let mut entries = Vec::new(); let mut entries = Vec::new();
// Stamp the path to the BLS entries directory. // Stamp the path to the BLS directory.
let path = context.stamp(&bls.path); let path = context.stamp(&bls.path);
// Resolve the path to the BLS entries directory. // Resolve the path to the BLS directory.
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path) let bls_resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
.context("unable to resolve bls path")?; .context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory.
let mut entries_path = PathBuf::from(
bls_resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert bls path to string")?,
);
entries_path.push(cstr16!("entries"));
// Open exclusive access to the BLS filesystem. // Open exclusive access to the BLS filesystem.
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle) let fs =
uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(bls_resolved.filesystem_handle)
.context("unable to open bls filesystem")?; .context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs); let mut fs = FileSystem::new(fs);
// Convert the subpath to the BLS entries directory to a string.
let sub_text_path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert subpath to string")?;
// Produce a path to the BLS entries directory.
let entries_path = Path::new(&sub_text_path);
// Read the BLS entries directory. // Read the BLS entries directory.
let entries_iter = fs let entries_iter = fs
.read_dir(entries_path) .read_dir(&entries_path)
.context("unable to read bls entries")?; .context("unable to read bls entries")?;
// For each entry in the BLS entries directory, parse the entry and add it to the list. // For each entry in the BLS entries directory, parse the entry and add it to the list.
@@ -88,14 +86,13 @@ pub fn generate(
let name = entry.file_name().to_string(); let name = entry.file_name().to_string();
// Ignore files that are not .conf files. // Ignore files that are not .conf files.
if !name.ends_with(".conf") { if !name.to_lowercase().ends_with(".conf") {
continue; continue;
} }
// Produce the full path to the entry file. // Create a mutable path so we can append the file name to produce the full path.
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str()) let mut full_entry_path = entries_path.to_path_buf();
.context("unable to construct full entry path")?; full_entry_path.push(entry.file_name());
let full_entry_path = Path::new(&full_entry_path);
// Read the entry file. // Read the entry file.
let content = fs let content = fs
@@ -116,7 +113,7 @@ pub fn generate(
// 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();
let title = entry.title().unwrap_or(name); let title = entry.title().unwrap_or_else(|| name.clone());
let chainload = entry.chainload_path().unwrap_or_default(); let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default(); let options = entry.options().unwrap_or_default();
@@ -129,7 +126,12 @@ pub fn generate(
context.set("initrd", initrd); context.set("initrd", initrd);
// Add the entry to the list with a frozen context. // Add the entry to the list with a frozen context.
entries.push((context.freeze(), bls.entry.clone())); entries.push(BootableEntry::new(
name,
bls.entry.title.clone(),
context.freeze(),
bls.entry.clone(),
));
} }
Ok(entries) Ok(entries)

View File

@@ -36,8 +36,13 @@ impl FromStr for BlsEntry {
// Trim the line. // Trim the line.
let line = line.trim(); let line = line.trim();
// Split the line once by a space. // Skip over empty lines and comments.
let Some((key, value)) = line.split_once(" ") else { if line.is_empty() || line.starts_with('#') {
continue;
}
// Split the line once by whitespace.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue; continue;
}; };
@@ -99,7 +104,7 @@ impl BlsEntry {
self.linux self.linux
.clone() .clone()
.or(self.efi.clone()) .or(self.efi.clone())
.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 path to an initrd to pass to the kernel, if any.
@@ -107,7 +112,7 @@ impl BlsEntry {
pub fn initrd_path(&self) -> Option<String> { pub fn initrd_path(&self) -> Option<String> {
self.initrd self.initrd
.clone() .clone()
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string()) .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
} }
/// Fetches the options to pass to the kernel, if any. /// Fetches the options to pass to the kernel, if any.

52
src/generators/list.rs Normal file
View File

@@ -0,0 +1,52 @@
use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: Vec<BTreeMap<String, String>>,
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
list: &ListConfiguration,
) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in list.values.iter().enumerate() {
let mut context = context.fork();
// Insert the combination into the context.
context.insert(combination);
let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = list.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
}

View File

@@ -1,5 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::EntryDeclaration; use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::list;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -8,7 +9,7 @@ use std::rc::Rc;
/// Matrix generator configuration. /// Matrix generator configuration.
/// The matrix generator produces multiple entries based /// The matrix generator produces multiple entries based
/// on input values multiplicatively. /// on input values multiplicatively.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration { pub struct MatrixConfiguration {
/// The template entry to use for each generated entry. /// The template entry to use for each generated entry.
#[serde(default)] #[serde(default)]
@@ -54,29 +55,15 @@ fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, S
pub fn generate( pub fn generate(
context: Rc<SproutContext>, context: Rc<SproutContext>,
matrix: &MatrixConfiguration, matrix: &MatrixConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> { ) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values. // Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values); let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new(); // Use the list generator to generate entries for each combination.
list::generate(
// For each combination, create a new context and entry. context,
for combination in combinations { &list::ListConfiguration {
let mut context = context.fork(); entry: matrix.entry.clone(),
// Insert the combination into the context. values: combinations,
context.insert(&combination); },
let context = context.freeze(); )
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone();
entry.title = context.stamp(&entry.title);
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push((context, entry));
}
Ok(entries)
} }

View File

@@ -1,20 +1,25 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![feature(uefi_std)] #![feature(uefi_std)]
use crate::config::RootConfiguration;
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use log::info; use log::{error, info};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// actions: Code that can be configured and executed by Sprout. /// actions: Code that can be configured and executed by Sprout.
pub mod actions; pub mod actions;
/// autoconfigure: Autoconfigure Sprout based on the detected environment.
pub mod autoconfigure;
/// config: Sprout configuration mechanism. /// config: Sprout configuration mechanism.
pub mod config; pub mod config;
@@ -33,6 +38,9 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values. /// generators: Runtime code that can generate entries with specific values.
pub mod generators; pub mod generators;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// phases: Hooks into specific parts of the boot process. /// phases: Hooks into specific parts of the boot process.
pub mod phases; pub mod phases;
@@ -45,20 +53,21 @@ pub mod options;
/// utils: Utility functions that are used by other parts of Sprout. /// utils: Utility functions that are used by other parts of Sprout.
pub mod utils; pub mod utils;
/// The main entrypoint of sprout. /// Run Sprout, returning an error if one occurs.
/// It is possible this function will not return if actions that are executed fn run() -> Result<()> {
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// 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")?;
// If --autoconfigure is specified, we use a stub configuration.
let mut config = if options.autoconfigure {
info!("autoconfiguration enabled, configuration file will be ignored");
RootConfiguration::default()
} else {
// Load the configuration of sprout. // Load the configuration of sprout.
// At this point, the configuration has been validated and the specified // At this point, the configuration has been validated and the specified
// version is checked to ensure compatibility. // version is checked to ensure compatibility.
let config = config::loader::load(&options)?; config::loader::load(&options)?
};
// Load the root context. // Load the root context.
// This is done in a block to ensure the release of the LoadedImageDevicePath protocol. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol.
@@ -68,10 +77,6 @@ fn main() -> Result<()> {
>(uefi::boot::image_handle()) >(uefi::boot::image_handle())
.context("unable to get loaded image device path")?; .context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed(); let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path, options) RootContext::new(loaded_image_path, options)
}; };
@@ -93,6 +98,31 @@ fn main() -> Result<()> {
// Load all configured drivers. // Load all configured drivers.
drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?; drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?;
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
}
// Unload the context so that it can be modified.
let Some(mut context) = context.unload() else {
bail!("context safety violation while trying to unload context");
};
// Perform root context modification in a block to release the modification when complete.
{
// Modify the root context to include the autoconfigured actions.
let Some(root) = context.root_mut() else {
bail!("context safety violation while trying to modify root context");
};
// Extend the root context with the autoconfigured actions.
root.actions_mut().extend(config.actions);
}
// Refreeze the context to ensure that further operations can share the context.
let context = context.freeze();
// Run all the extractors declared in the configuration. // Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new(); let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors { for (name, extractor) in &config.extractors {
@@ -106,69 +136,120 @@ fn main() -> Result<()> {
context.insert(&extracted); context.insert(&extracted);
let context = context.freeze(); let context = context.freeze();
// Execute the late phase. // Execute the startup phase.
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?; phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
let mut staged_entries = Vec::new(); let mut entries = Vec::new();
// Insert all the static entries from the configuration into the entry list. // Insert all the static entries from the configuration into the entry list.
for (_name, entry) in config.entries { for (name, entry) in config.entries {
// Associate the main context with the static entry. // Associate the main context with the static entry.
staged_entries.push((context.clone(), entry)); entries.push(BootableEntry::new(
name,
entry.title.clone(),
context.clone(),
entry,
));
} }
// Run all the generators declared in the configuration. // Run all the generators declared in the configuration.
for (_name, generator) in config.generators { for (name, generator) in config.generators {
let context = context.fork().freeze(); let context = context.fork().freeze();
// We will prefix all entries with [name]-.
let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list. // Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry. // The generator specifies the context associated with the entry.
for entry in generators::generate(context.clone(), &generator)? { for mut entry in generators::generate(context.clone(), &generator)? {
staged_entries.push(entry); entry.prepend_name_prefix(&prefix);
entries.push(entry);
} }
} }
// Build a list of all the final boot entries. for entry in &mut entries {
let mut final_entries = Vec::new(); let mut context = entry.context().fork();
for (context, entry) in staged_entries {
let mut context = 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.values); context.insert(&entry.declaration().values);
let context = context.finalize().freeze(); let context = context
.finalize()
.context("unable to finalize context")?
.freeze();
// Provide the new context to the bootable entry.
entry.swap_context(context);
// Restamp the title with any values.
entry.restamp_title();
// Insert the entry configuration into final boot entries with the extended context. // Mark this entry as the default entry if it is declared as such.
final_entries.push((context, entry)); if let Some(ref default_entry) = config.options.default_entry {
// If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) {
entry.mark_default();
}
}
} }
// TODO(azenla): Implement boot menu here. // If no entries were the default, pick the first entry as the default entry.
// For now, we just print all of the entries. if entries.iter().all(|entry| !entry.is_default())
info!("entries:"); && let Some(entry) = entries.first_mut()
for (index, (context, entry)) in final_entries.iter().enumerate() { {
let title = context.stamp(&entry.title); entry.mark_default();
info!(" entry {}: {}", index + 1, title);
} }
// Execute the late phase. // Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Use the boot option if possible, otherwise pick the first entry. // If --boot is specified, boot that entry immediately.
let (context, entry) = if let Some(ref boot) = context.root().options().boot { let force_boot_entry = context.root().options().boot.as_ref();
final_entries // If --force-menu is specified, show the boot menu regardless of the value of --boot.
.iter() let force_boot_menu = context.root().options().force_menu;
.find(|(_context, entry)| &entry.title == boot)
.context(format!("unable to find entry: {boot}"))? // Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.options.menu_timeout);
let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
let entry = if !force_boot_menu && let Some(ref force_boot_entry) = force_boot_entry {
BootableEntry::find(force_boot_entry, entries.iter())
.context(format!("unable to find entry: {force_boot_entry}"))?
} else { } else {
final_entries.first().context("no entries found")? // Delegate to the menu to select an entry to boot.
menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")?
}; };
// Execute all the actions for the selected entry. // Execute all the actions for the selected entry.
for action in &entry.actions { for action in &entry.declaration().actions {
let action = context.stamp(action); let action = entry.context().stamp(action);
actions::execute(context.clone(), &action) actions::execute(entry.context().clone(), &action)
.context(format!("unable to execute action '{}'", action))?; .context(format!("unable to execute action '{}'", action))?;
} }
Ok(())
}
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
}
// Sprout doesn't necessarily guarantee anything was booted. // Sprout doesn't necessarily guarantee anything was booted.
// If we reach here, we will exit back to whoever called us. // If we reach here, we will exit back to whoever called us.
Ok(()) Ok(())

158
src/menu.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::entries::BootableEntry;
use anyhow::{Context, Result, bail};
use log::info;
use std::time::Duration;
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
use uefi::proto::console::text::{Input, Key, ScanCode};
use uefi_raw::table::boot::{EventType, Tpl};
/// 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'];
/// Represents the operation that can be performed by the boot menu.
#[derive(PartialEq, Eq)]
enum MenuOperation {
/// The user selected a numbered entry.
Number(usize),
/// The user selected the escape key to exit the boot menu.
Exit,
/// The user selected the enter key to display the entries again.
Continue,
/// Timeout occurred.
Timeout,
/// No operation should be performed.
Nop,
}
/// Read a key from the input device with a duration, returning the [MenuOperation] that was
/// performed.
fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
// The event to wait for a key press.
let key_event = input
.wait_for_key_event()
.context("unable to acquire key event")?;
// Timer event for timeout.
// SAFETY: The timer event creation allocated a timer pointer on the UEFI heap.
// This is validated safe as long as we are in boot services.
let timer_event = unsafe {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")?
};
// The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events)
.discard_errdata()
.context("unable to wait for event")?;
// Close the timer event that we acquired.
if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?;
}
// 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.
let Some(key) = input.read_key().context("unable to read key")? else {
bail!("no key was pressed");
};
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.
fn select_with_input<'a>(
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, entry.name());
}
}
// 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(timeout: Duration, entries: &[BootableEntry]) -> Result<&BootableEntry> {
// Acquire the standard input device and run the boot menu.
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
}

View File

@@ -11,18 +11,27 @@ const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
/// The parsed options of sprout. /// The parsed options of sprout.
#[derive(Debug)] #[derive(Debug)]
pub struct SproutOptions { pub struct SproutOptions {
/// Configures Sprout automatically based on the environment.
pub autoconfigure: bool,
/// Path to a configuration file to load. /// Path to a configuration file to load.
pub config: String, pub config: String,
/// Entry to boot without showing the boot menu. /// Entry to boot without showing the boot menu.
pub boot: Option<String>, pub boot: Option<String>,
/// Force display of the boot menu.
pub force_menu: bool,
/// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>,
} }
/// The default Sprout options. /// The default Sprout options.
impl Default for SproutOptions { impl Default for SproutOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
autoconfigure: false,
config: DEFAULT_CONFIG_PATH.to_string(), config: DEFAULT_CONFIG_PATH.to_string(),
boot: None, boot: None,
force_menu: false,
menu_timeout: None,
} }
} }
} }
@@ -49,6 +58,20 @@ impl OptionsRepresentable for SproutOptions {
form: OptionForm::Value, form: OptionForm::Value,
}, },
), ),
(
"force-menu",
OptionDescription {
description: "Force showing of the boot menu",
form: OptionForm::Flag,
},
),
(
"menu-timeout",
OptionDescription {
description: "Boot menu timeout, in seconds",
form: OptionForm::Value,
},
),
( (
"help", "help",
OptionDescription { OptionDescription {
@@ -66,6 +89,11 @@ impl OptionsRepresentable for SproutOptions {
for (key, value) in options { for (key, value) in options {
match key.as_str() { match key.as_str() {
"autoconfigure" => {
// Enable autoconfiguration.
result.autoconfigure = true;
}
"config" => { "config" => {
// The configuration file to load. // The configuration file to load.
result.config = value.context("--config option requires a value")?; result.config = value.context("--config option requires a value")?;
@@ -76,6 +104,20 @@ impl OptionsRepresentable for SproutOptions {
result.boot = Some(value.context("--boot option requires a value")?); result.boot = Some(value.context("--boot option requires a value")?);
} }
"force-menu" => {
// Force showing of the boot menu.
result.force_menu = true;
}
"menu-timeout" => {
// The timeout for the boot menu in seconds.
let value = value.context("--menu-timeout option requires a value")?;
let value = value
.parse::<u64>()
.context("menu-timeout must be a number")?;
result.menu_timeout = Some(value);
}
_ => bail!("unknown option: --{key}"), _ => bail!("unknown option: --{key}"),
} }
} }

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior /// The type of option. This disambiguates different behavior
@@ -72,11 +73,7 @@ pub trait OptionsRepresentable {
let mut value = None; let mut value = None;
// Check if the option is of the form --abc=123 // Check if the option is of the form --abc=123
if option.contains("=") { if let Some((part_key, part_value)) = option.split_once('=') {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string(); let part_key = part_key.to_string();
let part_value = part_value.to_string(); let part_value = part_value.to_string();
option = part_key; option = part_key;
@@ -117,9 +114,9 @@ pub trait OptionsRepresentable {
// Handle the --help flag case. // Handle the --help flag case.
if description.form == OptionForm::Help { if description.form == OptionForm::Help {
// Generic configured options output. // Generic configured options output.
println!("Configured Options:"); info!("Configured Options:");
for (name, description) in &configured { for (name, description) in &configured {
println!( info!(
" --{}{}: {}", " --{}{}: {}",
name, name,
if description.form == OptionForm::Value { if description.form == OptionForm::Value {
@@ -131,7 +128,7 @@ pub trait OptionsRepresentable {
); );
} }
// Exit because the help has been displayed. // Exit because the help has been displayed.
std::process::exit(1); std::process::exit(0);
} }
// Insert the option and the value into the map. // Insert the option and the value into the map.

View File

@@ -7,7 +7,7 @@ use std::rc::Rc;
/// Configures the various phases of the boot process. /// Configures the various phases of the boot process.
/// This allows hooking various phases to run actions. /// This allows hooking various phases to run actions.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhasesConfiguration { pub struct PhasesConfiguration {
/// The early phase is run before drivers are loaded. /// The early phase is run before drivers are loaded.
#[serde(default)] #[serde(default)]
@@ -23,7 +23,7 @@ pub struct PhasesConfiguration {
/// Configures a single phase of the boot process. /// Configures a single phase of the boot process.
/// There can be multiple phase configurations that are /// There can be multiple phase configurations that are
/// executed sequentially. /// executed sequentially.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhaseConfiguration { pub struct PhaseConfiguration {
/// The actions to run when the phase is executed. /// The actions to run when the phase is executed.
#[serde(default)] #[serde(default)]

View File

@@ -12,7 +12,7 @@ pub mod framebuffer;
/// Support code for the media loader protocol. /// Support code for the media loader protocol.
pub mod media_loader; pub mod media_loader;
/// Parses the input [path] as a [DevicePath]. /// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. /// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> { pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
let path = CString16::try_from(path).context("unable to convert path to CString16")?; let path = CString16::try_from(path).context("unable to convert path to CString16")?;
@@ -27,7 +27,13 @@ pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
.context("unable to convert text to device path") .context("unable to convert text to device path")
} }
/// Grabs the root part of the [path]. /// Checks if a [CString16] contains a char `c`.
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
string.to_string().contains(c)
}
/// Grabs the root part of the `path`.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" /// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)" /// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
pub fn device_path_root(path: &DevicePath) -> Result<String> { pub fn device_path_root(path: &DevicePath) -> Result<String> {
@@ -37,7 +43,7 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item if item
.as_ref() .as_ref()
.map(|item| item.to_string().contains("(")) .map(|item| cstring16_contains_char(item, '('))
.unwrap_or(false) .unwrap_or(false)
{ {
Some(item.unwrap_or_default()) Some(item.unwrap_or_default())
@@ -52,7 +58,7 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
Ok(path) Ok(path)
} }
/// Grabs the part of the [path] after the root. /// Grabs the part of the `path` after the root.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" /// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "\EFI\BOOT\BOOTX64.efi" /// it will give "\EFI\BOOT\BOOTX64.efi"
pub fn device_path_subpath(path: &DevicePath) -> Result<String> { pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
@@ -62,7 +68,7 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item if item
.as_ref() .as_ref()
.map(|item| item.to_string().contains("(")) .map(|item| cstring16_contains_char(item, '('))
.unwrap_or(false) .unwrap_or(false)
{ {
None None
@@ -92,8 +98,8 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle, pub filesystem_handle: Handle,
} }
/// Resolve a path specified by [input] to its various components. /// Resolve a path specified by `input` to its various components.
/// Uses [default_root_path] as the base root if one is not specified in the path. /// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components. /// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> { pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?; let mut path = text_to_device_path(input).context("unable to convert text to path")?;
@@ -104,11 +110,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
it.to_string(DisplayOnly(false), AllowShortcuts(false)) it.to_string(DisplayOnly(false), AllowShortcuts(false))
.unwrap_or_default() .unwrap_or_default()
}) })
.map(|it| it.to_string().contains("(")) .map(|it| it.to_string().contains('('))
.unwrap_or(false); .unwrap_or(false);
if !path_has_device { if !path_has_device {
let mut input = input.to_string(); let mut input = input.to_string();
if !input.starts_with("\\") { if !input.starts_with('\\') {
input.insert(0, '\\'); input.insert(0, '\\');
} }
input.insert_str( input.insert_str(
@@ -137,9 +143,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
}) })
} }
/// Read the contents of a file at the location specified with the [input] path. /// Read the contents of a file at the location specified with the `input` path.
/// Internally, this uses [resolve_path] to resolve the path to its various components. /// Internally, this uses [resolve_path] to resolve the path to its various components.
/// [resolve_path] is passed the [default_root_path] which should specify a base root. /// [resolve_path] is passed the `default_root_path` which should specify a base root.
/// ///
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved /// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with /// filesystem handle, so care must be taken to call this function outside a scope with
@@ -155,3 +161,23 @@ pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result
let content = fs.read(Path::new(&path)); let content = fs.read(Path::new(&path));
content.context("unable to read file contents") content.context("unable to read file contents")
} }
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}

View File

@@ -13,17 +13,33 @@ pub struct Framebuffer {
impl Framebuffer { impl Framebuffer {
/// Creates a new framebuffer of the specified `width` and `height`. /// Creates a new framebuffer of the specified `width` and `height`.
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize) -> Result<Self> {
Framebuffer { // Verify that the size is valid during multiplication.
let size = width
.checked_mul(height)
.context("framebuffer size overflow")?;
// Initialize the pixel buffer with black pixels, with the verified size.
let pixels = vec![BltPixel::new(0, 0, 0); size];
Ok(Framebuffer {
width, width,
height, height,
pixels: vec![BltPixel::new(0, 0, 0); width * height], pixels,
} })
} }
/// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate. /// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate.
pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> { pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> {
self.pixels.get_mut(y * self.width + x) // Verify that the coordinates are within the bounds of the framebuffer.
if x >= self.width || y >= self.height {
return None;
}
// Calculate the index of the pixel safely, returning None if it overflows.
let index = y.checked_mul(self.width)?.checked_add(x)?;
// Return the pixel at the index. If the index is out of bounds, this will return None.
self.pixels.get_mut(index)
} }
/// Blit the framebuffer to the specified `gop` [GraphicsOutput]. /// Blit the framebuffer to the specified `gop` [GraphicsOutput].

View File

@@ -46,8 +46,8 @@ pub struct MediaLoaderHandle {
impl MediaLoaderHandle { impl MediaLoaderHandle {
/// The behavior of this function is derived from how Linux calls it. /// The behavior of this function is derived from how Linux calls it.
/// ///
/// Linux calls this function by first passing a NULL [buffer]. /// Linux calls this function by first passing a NULL `buffer`.
/// We must set the size of the buffer it should allocate in [buffer_size]. /// We must set the size of the buffer it should allocate in `buffer_size`.
/// The next call will pass a buffer of the right size, and we should copy /// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on /// data into that buffer, checking whether it is safe to copy based on
/// the buffer size. /// the buffer size.
@@ -97,7 +97,7 @@ impl MediaLoaderHandle {
} }
/// Creates a new device path for the media loader based on a vendor `guid`. /// Creates a new device path for the media loader based on a vendor `guid`.
fn device_path(guid: Guid) -> Box<DevicePath> { fn device_path(guid: Guid) -> Result<Box<DevicePath>> {
// The buffer for the device path. // The buffer for the device path.
let mut path = Vec::new(); let mut path = Vec::new();
// Build a device path for the media loader with a vendor-specific guid. // Build a device path for the media loader with a vendor-specific guid.
@@ -106,18 +106,18 @@ impl MediaLoaderHandle {
vendor_guid: guid, vendor_guid: guid,
vendor_defined_data: &[], vendor_defined_data: &[],
}) })
.unwrap() // We know that the device path is valid, so we can unwrap. .context("unable to produce device path")?
.finalize() .finalize()
.unwrap(); // We know that the device path is valid, so we can unwrap. .context("unable to produce device path")?;
// Convert the device path to a boxed device path. // Convert the device path to a boxed device path.
// This is safer than dealing with a pooled device path. // This is safer than dealing with a pooled device path.
path.to_boxed() Ok(path.to_boxed())
} }
/// Checks if the media loader is already registered with the UEFI stack. /// Checks if the media loader is already registered with the UEFI stack.
fn already_registered(guid: Guid) -> Result<bool> { fn already_registered(guid: Guid) -> Result<bool> {
// Acquire the device path for the media loader. // Acquire the device path for the media loader.
let path = Self::device_path(guid); let path = Self::device_path(guid)?;
let mut existing_path = path.as_ref(); let mut existing_path = path.as_ref();
@@ -137,12 +137,12 @@ impl MediaLoaderHandle {
Ok(false) Ok(false)
} }
/// Registers the provided [data] with the UEFI stack as media loader. /// Registers the provided `data` with the UEFI stack as media loader.
/// This uses a special device path that other EFI programs will look at /// This uses a special device path that other EFI programs will look at
/// to load the data from. /// to load the data from.
pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> { pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> {
// Acquire the vendor device path for the media loader. // Acquire the vendor device path for the media loader.
let path = Self::device_path(guid); let path = Self::device_path(guid)?;
// Check if the media loader is already registered. // Check if the media loader is already registered.
// If it is, we can't register it again safely. // If it is, we can't register it again safely.