Compare commits

..
82 Commits
Author SHA1 Message Date
alex 5afbd15fd0 sprout: version 0.0.29 (#99) 2026-09-26 19:07:20 -07:00
alex 2da1cea25c fix(autoconfigure): skip filesystems that can't be opened
If the device path or filesystem protocol of any detected filesystem
could not be opened, autoconfiguration failed entirely and boot was
aborted. Log a warning and skip that filesystem instead, so the other
filesystems are still autoconfigured.
2026-09-26 18:58:54 -07:00
alex 84bd84b7d2 fix(phases): run the late phase after the entry is chosen
The late phase is documented to run after the entry is chosen but
before its actions are executed, but it ran before the boot menu was
shown. Run it after the entry is selected and reported to the
bootloader interface, right before executing the entry's actions.
2026-09-26 18:58:54 -07:00
alex db0edb1b6e fix(bls): sort entries with a version before entries without
compare_versions_optional treated a missing version as newer than any
version, so among entries with equal sort-key and machine-id, an entry
without a version sorted first. Treat a missing version as the oldest,
matching systemd's strverscmp_improved.
2026-09-26 18:58:54 -07:00
alex 282dc5369a fix(bls): sort entries with a sort-key before entries without
Sort keys were compared as Options, so an entry without a sort-key
sorted before one with a sort-key, and machine-id and version were
compared even when neither entry had a sort-key. Per the Boot Loader
Specification and systemd-boot, an entry with a sort-key sorts first,
and machine-id and version are only used when both entries have one.
2026-09-26 18:58:54 -07:00
alex 44c8ab28af fix(bls): compare version numbers of any length
Numeric segments were parsed into a saturating u64, so any two numbers
above u64::MAX compared equal. Compare the digits instead, by length
and then digit by digit, as systemd does.
2026-09-26 18:58:54 -07:00
alex 29e405f11f fix(bls): sort numeric version segments newer than letters
When only one version had a digit at the current position, both sides
were parsed as numbers and the letter side read as 0. "0" then compared
equal to "a" and the result depended on what followed. Per the
specification, the side with digits is newer.
2026-09-26 18:58:54 -07:00
alex 9d6d88c5e0 fix(bls): sort pre-release versions before releases
The '~' check ran after the end-of-string check, so "1~rc1" compared as
newer than "1". Per the UAPI version format specification, '~' must be
handled first so a pre-release sorts older than the release it
precedes. An empty string is kept as the oldest version, matching
systemd's strverscmp_improved.
2026-09-26 18:58:54 -07:00
alex dd35c2153b fix(bls): skip unreadable or invalid BLS entries
A single BLS entry that could not be read, was not valid UTF-8, or
failed to parse aborted entry generation and therefore the whole boot,
contrary to the generator's documented best-effort behavior. Log a
warning and skip the entry instead.
2026-09-26 18:58:54 -07:00
alex dbc292e369 fix(bls): allow BLS directories without an entries directory
Autoconfigure detects BLS when either \loader\loader.conf or
\loader\entries exists, but the BLS generator required the entries
directory and failed the boot without it. This broke ESPs that only
have a loader.conf, such as UKI-only systemd-boot setups. Generate no
entries when the directory is missing.
2026-09-26 18:58:54 -07:00
alex 6f84a1014a fix(chainload): skip initrd paths without a file component
Autoconfigured BLS actions use "$root\\$initrd" as the initrd path.
For BLS entries without an initrd line this stamps to the root of the
filesystem, and Sprout tried to read that directory as the initrd,
failing the boot. Treat a path that resolves to no file as no initrd.
2026-09-26 18:58:54 -07:00
alex 9967a7ab80 fix(chainload): release LoadedImage before starting the image
The chainload action held the child image's LoadedImage protocol open
exclusively while calling start_image. Any image that opens its own
LoadedImage protocol exclusively, such as another uefi-rs application
or Sprout itself, then failed with ACCESS_DENIED. Scope the protocol to
setting the load options.
2026-09-26 18:58:54 -07:00
alex bcfb7dc680 fix(shim): take security arch2 boot_policy as a UEFI Boolean
The firmware passes BootPolicy as a UEFI BOOLEAN (a u8). Declaring it
as a Rust bool in the extern signature is undefined behavior for any
value other than 0 or 1. Use uefi_raw::Boolean instead.
2026-09-26 18:58:54 -07:00
alex bcc78fd4a3 fix(shim): don't overwrite original handlers when already hooked
If the security hook was installed while already installed, the
protocols contained Sprout's own handlers, which were then saved as the
"original" handlers. A verification failure would then call back into
the hook forever. Keep the existing saved state in that case.
2026-09-26 18:58:54 -07:00
alex ad37a2bcf3 fix(variables): accept UTF-16 values without a NUL terminator
EFI string variables written without a trailing NUL, or with extra NUL
padding, failed to decode and were silently treated as unset. Decode up
to the first NUL and terminate the string ourselves, as systemd-boot
does.

Because empty values now decode to an empty string rather than failing,
treat empty LoaderEntryDefault and LoaderEntryOneShot values as unset so
an empty one-shot entry doesn't fail to match any entry.
2026-09-26 18:58:54 -07:00
alex df5d8f5c39 fix(bootloader-interface): ignore invalid timeout values
A LoaderConfigTimeout or LoaderConfigTimeoutOneShot value that is not a
number or a known keyword made Sprout abort boot. Log a warning and
treat the variable as unset instead, matching systemd-boot.
2026-09-26 18:58:54 -07:00
alex 48e3bc48a9 fix(loader): install the security hook right before LoadImage
The hook was installed before retaining the shim protocol and reading
the image, so an error in either step returned without uninstalling
it. Install it immediately before LoadImage instead.
2026-09-26 18:58:54 -07:00
alex 5c2724fd22 fix(env): filter all ASCII control characters from leading args
The check for unprintable leading arguments added by firmware used
c < 0x1f, which missed 0x1f itself. Use is_ascii_control() instead.
2026-09-26 18:58:54 -07:00
alex f7e7a9122b fix(menu): ignore key events without an available key
Some firmware signals the key event without a key being available.
This previously aborted boot with "no key was pressed"; treat it as a
no-op and keep waiting instead.
2026-09-26 18:58:54 -07:00
alex 1f5bc07c22 fix(autoconfigure): match arm64 Image kernels
File names are lowercased before matching kernel prefixes, but the
arm64 prefix was "Image", so it could never match and arm64 kernels
were not autoconfigured. Use a lowercase "image" prefix.
2026-09-26 18:58:54 -07:00
dependabot[bot] 724180618b chore(deps): bump the actions-updates group across 1 directory with 9 updates (#93)
Bumps the actions-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.17.0` | `2.21.1` |
| [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `7.0.1` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `8.0.0` | `10.1.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.35.1` | `4.38.1` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `4.1.0` | `4.2.2` |
| [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `3.0.0` | `3.2.0` |



Updates `step-security/harden-runner` from 2.17.0 to 2.21.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/f808768d1510423e83855289c910610ca9b43176...e14015d583714f6e62063499dc959a02595150a1)

Updates `actions/checkout` from 6.0.2 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `astral-sh/setup-uv` from 8.0.0 to 10.1.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/cec208311dfd045dd5311c1add060b2062131d57...bec219d24cd3e171d82865faccec33120bb574f4)

Updates `github/codeql-action/upload-sarif` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/init` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.35.1 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `actions/attest-build-provenance` from 4.1.0 to 4.2.2
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32...4d101475d8b20a2381f78447822ac1eab6504dd8)

Updates `actions/create-github-app-token` from 3.0.0 to 3.2.0
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/create-github-app-token/compare/f8d387b68d61c58ab83c6c016672934102569859...bcd2ba49218906704ab6c1aa796996da409d3eb1)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/create-github-app-token
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/init
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: step-security/harden-runner
  dependency-version: 2.19.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-26 18:37:06 -07:00
Ben Leggett 8b39f703c6 fix(ci): scope token (#94) 2026-09-26 18:24:44 -07:00
alex 29cdc75776 chore(deps): bump rust to 1.98.1, bump all images, and dependencies (#97) 2026-09-25 11:28:32 -07:00
alex 0e4452b625 chore(upgrade): upgrade to rust v1.97.1, bump deps and migrate, bump images (#95) 2026-08-02 19:02:09 -07:00
alex aafda6e3da chore(deps): upgrade spin and shlex (#86) 2026-05-19 11:51:37 -05:00
Luca Di Maio 863aa25971 Merge pull request #85 from edera-dev/89luca89/tpm_work_ws2
sprout: measure xen and dom0 payloads and cmdlines into PCRs 9/11/12
2026-05-18 13:24:50 +02:00
Luca Di Maio 2c1c380ab5 sprout: measure xen and dom0 payloads and cmdlines into PCRs 9/11/12
Extend PCR 9 with the initrd, PCR 11 with the dom0 kernel,
and PCR 12 with the Xen options and dom0 cmdline.

Signed-off-by: Luca Di Maio <[email protected]>
2026-05-18 12:43:41 +02:00
dependabot[bot] 076be95306 chore(deps): bump the actions-updates group across 1 directory with 3 updates (#81)
Bumps the actions-updates group with 3 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `step-security/harden-runner` from 2.16.0 to 2.16.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594...f808768d1510423e83855289c910610ca9b43176)

Updates `astral-sh/setup-uv` from 7.2.0 to 8.0.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/61cb8a9741eeb8a550a1b8544337180c0fc8476b...cec208311dfd045dd5311c1add060b2062131d57)

Updates `github/codeql-action` from 4.31.2 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/0499de31b99561a6d14a36a5f662c2a54f91beee...c10b8064de6f491fea524254123dbe5e09572f13)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 14:45:25 -07:00
alex 6f9c4a2852 chore(deps): upgrade rust and dependencies (#82) 2026-04-16 21:43:36 +00:00
Ben Leggett d75fce041c Merge pull request #75 from edera-dev/bleggett/bls-unit-tests
Split out unit testable code
2026-03-30 18:08:53 -04:00
dependabot[bot] f9391261d5 chore(deps): bump the actions-updates group across 1 directory with 4 updates (#74)
Bumps the actions-updates group with 4 updates in the / directory: [step-security/harden-runner](https://github.com/step-security/harden-runner), [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) and [actions/create-github-app-token](https://github.com/actions/create-github-app-token).


Updates `step-security/harden-runner` from 2.14.1 to 2.16.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/e3f713f2d8f53843e71c69a996d56f51aa9adfb9...fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

Updates `actions/attest-build-provenance` from 3.2.0 to 4.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/96278af6caaf10aea03fd8d33a09a777ca52d62f...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32)

Updates `actions/create-github-app-token` from 3.0.0.pre.beta.2 to 3
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/bf559f85448f9380bcfa2899dbdc01eb5b37be3a...f8d387b68d61c58ab83c6c016672934102569859)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 20:23:45 +01:00
Benjamin Leggett c44b429553 fmt 2026-03-25 16:13:46 -04:00
Benjamin Leggett c1115af841 Skip the UEFI crates during cargo test 2026-03-25 16:07:21 -04:00
Benjamin Leggett b53d21cea5 Split out parsing stuff as well, and test it 2026-03-25 16:01:28 -04:00
Benjamin Leggett 133476a0df Split out some BLS stuff into a separate testable crate without UEFI types 2026-03-25 15:31:47 -04:00
alex c70d1e4d80 sprout: version 0.0.28 (#64) 2026-02-09 12:34:08 -08:00
Ariadne Conill 1f6fc237c6 Merge pull request #66 from edera-dev/chore/upgrade
chore(upgrade): bump all dependencies, rust, and images
2026-02-09 09:52:12 -08:00
alex c86215f196 chore(upgrade): bump all dependencies, rust, and images 2026-02-09 09:38:57 -08:00
dependabot[bot] 425ce8ede4 chore(deps): bump actions/attest-build-provenance (#65)
Bumps the actions-updates group with 1 update: [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/attest-build-provenance` from 3.1.0 to 3.2.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8...96278af6caaf10aea03fd8d33a09a777ca52d62f)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 08:55:39 -08:00
dependabot[bot] 656fde2044 chore(deps): bump step-security/harden-runner (#63)
Bumps the actions-updates group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.14.0 to 2.14.1
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/20cf305ff2072d973412fa9b1e3a4f227bda3c76...e3f713f2d8f53843e71c69a996d56f51aa9adfb9)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.14.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 15:20:17 -05:00
Ariadne Conill 5740c14e31 Merge pull request #62 from edera-dev/chore/code-cleanup-stamp-iter
chore(sprout): implement iterator stamping to cleanup code
2026-02-02 19:12:14 +01:00
alex c295de4951 chore(sprout): implement iterator stamping to cleanup code 2026-01-26 20:13:52 -08:00
dependabot[bot] 95e0360721 chore(deps): bump toml in the cargo-updates group (#59)
Bumps the cargo-updates group with 1 update: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.9.10+spec-1.1.0 to 0.9.11+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.10...toml-v0.9.11)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.11+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 08:54:07 -08:00
dependabot[bot] 1370e1b39b chore(deps): bump the actions-updates group across 1 directory with 3 updates (#61)
Bumps the actions-updates group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/checkout` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `astral-sh/setup-uv` from 7.1.6 to 7.2.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/681c641aba71e4a1c380be3ab5e12ad51f415867...61cb8a9741eeb8a550a1b8544337180c0fc8476b)

Updates `actions/attest-build-provenance` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/977bb373ede98d70efdf65b84cb5f73e068dcc2a...00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: actions/attest-build-provenance
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 08:53:40 -08:00
alex b60c345958 sprout: version 0.0.27 (#57) 2025-12-23 22:51:26 -08:00
dependabot[bot] 45270d3bf3 chore(deps): bump the actions-updates group with 3 updates (#56)
Bumps the actions-updates group with 3 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `step-security/harden-runner` from 2.13.2 to 2.14.0
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/95d9a5deda9de15063e7595e9719c11c38c90ae2...20cf305ff2072d973412fa9b1e3a4f227bda3c76)

Updates `astral-sh/setup-uv` from 7.1.2 to 7.1.6
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41...681c641aba71e4a1c380be3ab5e12ad51f415867)

Updates `actions/upload-artifact` from 5.0.0 to 6.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-updates
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-23 18:54:25 -08:00
alex 6a57c72869 feat(boot): introduce sort keys, which are sorted in reverse order, to make boot order more configurable (#55) 2025-12-21 14:11:26 -08:00
alex d50f22a386 chore(docs): fix all rustdoc warnings (#54) 2025-12-21 14:11:10 -08:00
alex a8a3774c35 chore(upgrade): upgrade dependencies, rust => 1.92.0, dev kernel => 6.18.2, alpine => v3.23 (#53) 2025-12-20 23:00:51 -08:00
alex 81c8217ee0 fix(boot): on boot handoff, clear the screen using clear rather than reset (#52)
On some Dell firmware, the reset operation doesn't actually clear the screen.
This change uses clear instead of reset to ensure the screen is properly cleared.
2025-12-20 22:29:44 -08:00
alex 17e729d068 fix(eficore): unregister media loader on drop to prevent more opportunities for leaks (#50) 2025-12-20 22:01:04 -08:00
alex 2da457ee7c feat(boot): clear screen before boot handoff, if --retain-boot-console is not specified on the command-line (#51) 2025-12-20 21:45:35 -08:00
dependabot[bot] fafabe234e chore(deps): bump log from 0.4.28 to 0.4.29 in the cargo-updates group (#48)
Bumps the cargo-updates group with 1 update: [log](https://github.com/rust-lang/log).


Updates `log` from 0.4.28 to 0.4.29
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.28...0.4.29)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 19:55:23 -08:00
dependabot[bot] c2cafe6c20 chore(deps): bump rust in the docker-updates group (#46)
Bumps the docker-updates group with 1 update: rust.


Updates `rust` from `fbcca3e` to `8efbfb7`

---
updated-dependencies:
- dependency-name: rust
  dependency-version: 1.91.1-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 10:53:01 -08:00
dependabot[bot] bf28558a83 chore(deps): bump actions/checkout in the actions-updates group (#49)
Bumps the actions-updates group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5.0.0 to 6.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...8e8c483db84b4bee98b60c0593521ed34d9990e8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 11:39:56 -08:00
Ariadne Conill 0b75e547f7 Merge pull request #43 from edera-dev/chore/dev-alpine-clean
chore(dev): make alpine boot fully clean with kernel module infra
2025-11-27 20:33:10 -08:00
alex 5605056c82 chore(dev): make alpine boot fully clean with kernel module infra 2025-11-26 23:05:01 -08:00
alex d4fcba18c0 Merge pull request #39 from edera-dev/dependabot/github_actions/actions-updates-c5043b94ad
chore(deps): bump step-security/harden-runner from 2.13.1 to 2.13.2 in the actions-updates group
2025-11-25 21:35:22 -08:00
a dinosaur 5dcd763db9 chore(options):use jaarg alloc-less api, removing map middleman (#41) 2025-11-25 21:34:18 -08:00
alex 4f8abadb3a Merge pull request #42 from edera-dev/fix/pr-workflow-attest
fix(workflows): disable artifact publish attestation for pull requests
2025-11-25 20:43:24 -08:00
alex 57e90a4623 fix(workflows): disable artifact publish attestation for pull requests 2025-11-25 20:38:49 -08:00
dependabot[bot] 136b899844 chore(deps): bump step-security/harden-runner
Bumps the actions-updates group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner).


Updates `step-security/harden-runner` from 2.13.1 to 2.13.2
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a...95d9a5deda9de15063e7595e9719c11c38c90ae2)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-24 11:01:47 +00:00
alex 0dbd011648 Merge pull request #40 from edera-dev/chore/release-v0.0.26
chore(release): sprout: version 0.0.26
2025-11-17 10:18:11 -08:00
alex 65c392944b chore(release): sprout: version 0.0.26 2025-11-17 10:14:58 -08:00
Ariadne Conill 2683229bd3 Merge pull request #35 from edera-dev/chore/dev-improve-alpine
chore(dev): autologin to alpine and writable rootfs
2025-11-17 10:01:17 -08:00
alex a7a9554875 Merge pull request #38 from edera-dev/feat/partial-match-default
feat(entries): support '*' suffix as a partial match to an entry
2025-11-15 21:25:59 -08:00
alex 5ad617c54f feat(entries): support '*' suffix as a partial match to an entry 2025-11-15 20:36:31 -08:00
alex 0aa7a46808 Merge pull request #27 from edera-dev/feat/jaarg
feat(options): replace options parser with jaarg
2025-11-15 18:02:46 -08:00
alex 8711c54074 feat(boot): utilize jaarg for options parsing 2025-11-15 17:31:46 -08:00
Ariadne Conill c21c140039 Merge pull request #37 from edera-dev/fix/linux-auto-detect-aarch64-images
fix(autoconfigure/linux): detect Image as a kernel, which openSUSE uses
2025-11-15 14:50:18 -08:00
alex c053f62b88 fix(autoconfigure/linux): detect Image as a kernel, which openSUSE uses 2025-11-15 13:41:07 -08:00
alex e0bd703511 chore(dev): autologin to alpine and writable rootfs 2025-11-15 13:37:50 -08:00
alex 2a9c9f6907 Merge pull request #34 from edera-dev/chore/release-0.0.25
chore(release): sprout: version 0.0.25
2025-11-14 10:49:09 -05:00
alex 16755acdfe chore(release): sprout: version 0.0.25 2025-11-14 10:45:16 -05:00
alex ebb1f0bb44 Merge pull request #33 from edera-dev/chore/upgrade-deps
chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps
2025-11-14 00:30:48 -05:00
alex ecba8a5e02 chore(deps): upgrade rust to 1.91.1 and bump cargo and docker deps 2025-11-14 00:24:09 -05:00
alex 0028e3eefc Merge pull request #32 from rkratky/typo 2025-11-12 10:15:49 -05:00
Robert Krátký 8603794c44 Fix a typo: grubaa64.efi -> grubx64.efi 2025-11-12 16:11:13 +01:00
alex 4fae4080a2 Merge pull request #29 from edera-dev/dependabot/github_actions/actions-updates-1be5372544
chore(deps): bump the actions-updates group with 2 updates
2025-11-10 10:09:58 -05:00
dependabot[bot] da5e0daa51 chore(deps): bump the actions-updates group with 2 updates
Bumps the actions-updates group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `astral-sh/setup-uv` from 7.1.1 to 7.1.2
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/2ddd2b9cb38ad8efd50337e8ab201519a34c9f24...85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41)

Updates `github/codeql-action` from 4.31.0 to 4.31.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/4e94bd11f71e507f7f87df81788dff88d1dacbfb...0499de31b99561a6d14a36a5f662c2a54f91beee)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
- dependency-name: github/codeql-action
  dependency-version: 4.31.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-updates
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-10 12:27:22 +00:00
alex 0fb54a948b chore(doc): add fedora setup guide and tweak existing guides 2025-11-06 23:51:57 -05:00
alex c4475ad42d fix(boot): ensure top-level error is printed 2025-11-06 13:04:05 -05:00
69 changed files with 2193 additions and 1198 deletions

No files matched your search

+4 -4
View File
@@ -25,17 +25,17 @@ jobs:
actions: read # Needed to analyze action metadata. actions: read # Needed to analyze action metadata.
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: setup uv - name: setup uv
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: zizmor - name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -43,7 +43,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload - name: upload
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor
+6 -6
View File
@@ -21,12 +21,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -51,12 +51,12 @@ jobs:
name: 'build ${{ matrix.arch }}' name: 'build ${{ matrix.arch }}'
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -80,12 +80,12 @@ jobs:
name: 'clippy ${{ matrix.arch }}' name: 'clippy ${{ matrix.arch }}'
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
+4 -4
View File
@@ -37,23 +37,23 @@ jobs:
build-mode: none build-mode: none
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: initialize codeql - name: initialize codeql
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+5 -4
View File
@@ -25,12 +25,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -43,13 +43,14 @@ jobs:
- name: 'upload artifacts' - name: 'upload artifacts'
id: upload id: upload
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: artifacts name: artifacts
path: target/assemble/* path: target/assemble/*
- name: 'attest artifacts' - name: 'attest artifacts'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with: with:
subject-name: artifacts.zip subject-name: artifacts.zip
subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}" subject-digest: "sha256:${{ steps.upload.outputs.artifact-digest }}"
if: github.event_name != 'pull_request'
+7 -4
View File
@@ -25,12 +25,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: harden runner - name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with: with:
egress-policy: audit egress-policy: audit
- name: checkout - name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -42,16 +42,19 @@ jobs:
run: ./hack/assemble.sh run: ./hack/assemble.sh
- name: 'attest release artifacts' - name: 'attest release artifacts'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with: with:
subject-path: target/assemble/* subject-path: target/assemble/*
- name: 'generate cultivator token' - name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2 uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.0.0-beta.2
id: generate-token id: generate-token
with: with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}" app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}" private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
# Scope to what uploading assets and `gh release edit` need: write to
# release contents.
permission-contents: write
if: ${{ github.event.inputs.release-tag != '' }} if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts' - name: 'upload release artifacts'
Generated
+97 -81
View File
@@ -4,9 +4,9 @@ version = 4
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
@@ -16,63 +16,72 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [ dependencies = [
"generic-array", "hybrid-array",
] ]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [ dependencies = [
"generic-array", "hybrid-array",
"typenum",
] ]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
] ]
[[package]] [[package]]
name = "edera-sprout-boot" name = "edera-sprout-bls"
version = "0.0.24" version = "0.0.29"
dependencies = [ dependencies = [
"anyhow", "anyhow",
]
[[package]]
name = "edera-sprout-boot"
version = "0.0.29"
dependencies = [
"anyhow",
"edera-sprout-bls",
"edera-sprout-build", "edera-sprout-build",
"edera-sprout-config", "edera-sprout-config",
"edera-sprout-eficore", "edera-sprout-eficore",
"edera-sprout-parsing",
"hex", "hex",
"jaarg",
"log", "log",
"sha2", "sha2",
"toml", "toml",
@@ -82,18 +91,18 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout-build" name = "edera-sprout-build"
version = "0.0.24" version = "0.0.29"
[[package]] [[package]]
name = "edera-sprout-config" name = "edera-sprout-config"
version = "0.0.24" version = "0.0.29"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "edera-sprout-eficore" name = "edera-sprout-eficore"
version = "0.0.24" version = "0.0.29"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
@@ -105,13 +114,11 @@ dependencies = [
] ]
[[package]] [[package]]
name = "generic-array" name = "edera-sprout-parsing"
version = "0.14.9" version = "0.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [ dependencies = [
"typenum", "hex",
"version_check", "sha2",
] ]
[[package]] [[package]]
@@ -121,10 +128,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "libc" name = "hybrid-array"
version = "0.2.177" version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
[[package]]
name = "jaarg"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534d589df1ef528a238f4bc4b1db081a1280f3aedf2695fd8971e9853a7fa4f6"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -137,33 +159,33 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "ptr_meta" name = "ptr_meta"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
dependencies = [ dependencies = [
"ptr_meta_derive", "ptr_meta_derive",
] ]
[[package]] [[package]]
name = "ptr_meta_derive" name = "ptr_meta_derive"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -172,9 +194,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.41" version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -187,9 +209,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive", "serde_derive",
@@ -197,18 +219,18 @@ dependencies = [
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.228" version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -217,18 +239,18 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.0.3" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@@ -237,24 +259,24 @@ dependencies = [
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10"
dependencies = [ dependencies = [
"lock_api", "lock_api",
] ]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.108" version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -263,9 +285,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.8" version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_spanned", "serde_spanned",
@@ -276,27 +298,27 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.3" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.4" version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]] [[package]]
name = "ucs2" name = "ucs2"
@@ -309,9 +331,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi" name = "uefi"
version = "0.36.1" version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe9058b73ee2b6559524af9e33199c13b2485ddbf3ad1181b68051cdc50c17" checksum = "368cef7d94514897e6dad4350945c7910a9f5bf9021191d1c58ee3ec09962697"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -325,9 +347,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi-macros" name = "uefi-macros"
version = "0.19.0" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4687412b5ac74d245d5bfb1733ede50c31be19bf8a4b6a967a29b451bab49e67" checksum = "643efa8b64be89780bc498b290fab04337c7c4d171cc82cce2e133a74616dcc4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -336,9 +358,9 @@ dependencies = [
[[package]] [[package]]
name = "uefi-raw" name = "uefi-raw"
version = "0.13.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f64fe59e11af447d12fd60a403c74106eb104309f34b4c6dbce6e927d97da9d" checksum = "3636327a6d88a38c0f2567fb695ec7ba27b3483d1877730486605aa0866da201"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"uguid", "uguid",
@@ -352,18 +374,12 @@ checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[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"
version = "0.7.13" 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 = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+18 -11
View File
@@ -4,24 +4,26 @@ members = [
"crates/build", "crates/build",
"crates/config", "crates/config",
"crates/eficore", "crates/eficore",
"crates/bls",
"crates/parsing",
] ]
resolver = "3" resolver = "3"
[workspace.package] [workspace.package]
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.24" version = "0.0.29"
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"
[workspace.dependencies] [workspace.dependencies]
bitflags = "2.10.0" bitflags = "2.13.2"
log = "0.4.28" log = "0.4.34"
spin = "0.10.0" spin = "0.12.3"
uefi-raw = "0.13.0" uefi-raw = "0.17.0"
[workspace.dependencies.anyhow] [workspace.dependencies.anyhow]
version = "1.0.100" version = "1.0.104"
default-features = false default-features = false
[workspace.dependencies.hex] [workspace.dependencies.hex]
@@ -29,26 +31,31 @@ version = "0.4.3"
default-features = false default-features = false
features = ["alloc"] features = ["alloc"]
[workspace.dependencies.jaarg]
version = "0.2.2"
default-features = false
features = ["alloc"]
[workspace.dependencies.serde] [workspace.dependencies.serde]
version = "1.0.228" version = "1.0.229"
default-features = false default-features = false
features = ["alloc", "derive"] features = ["alloc", "derive"]
[workspace.dependencies.sha2] [workspace.dependencies.sha2]
version = "0.10.9" version = "0.11.0"
default-features = false default-features = false
[workspace.dependencies.shlex] [workspace.dependencies.shlex]
version = "1.3.0" version = "2.0.1"
default-features = false default-features = false
[workspace.dependencies.toml] [workspace.dependencies.toml]
version = "0.9.8" version = "1.1.6"
default-features = false default-features = false
features = ["serde", "parse"] features = ["serde", "parse"]
[workspace.dependencies.uefi] [workspace.dependencies.uefi]
version = "0.36.1" version = "0.41.0"
default-features = false default-features = false
features = ["alloc", "global_allocator", "panic_handler"] features = ["alloc", "global_allocator", "panic_handler"]
+1 -2
View File
@@ -1,8 +1,7 @@
# syntax=docker/dockerfile:1.7-labs
ARG RUST_PROFILE=release ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rust:1.91.0-alpine@sha256:a3e3d30122c08c0ed85dcd8867d956f066be23c32ed67a0453bc04ce478ad69b AS build FROM --platform=$BUILDPLATFORM rust:1.98.1-alpine@sha256:7cc1c22d77d9432f7fe012a70e6d3e555af54c2a6832700ed7d553f1769ae89f AS build
RUN apk --no-cache add musl-dev busybox-static RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build RUN adduser -S -s /bin/sh build
+2 -1
View File
@@ -47,8 +47,9 @@ We recommend running Sprout without Secure Boot for development, and with Secure
| Operating System | Secure Boot Enabled | Link | | Operating System | Secure Boot Enabled | Link |
|------------------|---------------------|-------------------------------------------------------| |------------------|---------------------|-------------------------------------------------------|
| Ubuntu | ✅ | [Setup Guide](./docs/setup/signed/ubuntu.md) | | Fedora | ✅ | [Setup Guide](./docs/setup/signed/fedora.md) |
| Debian | ✅ | [Setup Guide](./docs/setup/signed/debian.md) | | Debian | ✅ | [Setup Guide](./docs/setup/signed/debian.md) |
| Ubuntu | ✅ | [Setup Guide](./docs/setup/signed/ubuntu.md) |
| openSUSE | ✅ | [Setup Guide](./docs/setup/signed/opensuse.md) | | openSUSE | ✅ | [Setup Guide](./docs/setup/signed/opensuse.md) |
| Fedora | ❌ | [Setup Guide](./docs/setup/unsigned/fedora.md) | | Fedora | ❌ | [Setup Guide](./docs/setup/unsigned/fedora.md) |
| Alpine Edge | ❌ | [Setup Guide](./docs/setup/unsigned/alpine-edge.md) | | Alpine Edge | ❌ | [Setup Guide](./docs/setup/unsigned/alpine-edge.md) |
+18
View File
@@ -0,0 +1,18 @@
# This crate explicitly does not have uefi/uefi-raw dependencies,
# so that the contents can be unit-testable on non-UEFI target hosts.
# Do not add uefi or uefi-raw as dependencies.
[package]
name = "edera-sprout-bls"
description = "Sprout BLS Utilities (UEFI-free)"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
[lib]
name = "edera_sprout_bls"
path = "src/lib.rs"
+800
View File
@@ -0,0 +1,800 @@
#![no_std]
extern crate alloc;
use alloc::string::{String, ToString};
use anyhow::{Error, Result};
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)]
pub struct BlsEntry {
/// The title of the entry.
pub title: Option<String>,
/// The options to pass to the entry.
pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
/// The sort key for the entry.
pub sort_key: Option<String>,
/// The version of the entry.
pub version: Option<String>,
/// The machine id of the entry.
pub machine_id: Option<String>,
}
/// Parser for a BLS entry.
impl FromStr for BlsEntry {
type Err = Error;
/// Parses the `input` as a BLS entry file.
fn from_str(input: &str) -> Result<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
let mut sort_key: Option<String> = None;
let mut version: Option<String> = None;
let mut machine_id: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() {
let line = line.trim();
// Skip over empty lines and comments.
if line.is_empty() || line.starts_with('#') {
continue;
}
// Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};
// Match the key to a field we understand.
match key {
// The title of the entry.
"title" => {
title = Some(value.trim().to_string());
}
// The options to pass to the entry.
"options" => {
options = Some(value.trim().to_string());
}
// The path to the linux kernel.
"linux" => {
linux = Some(value.trim().to_string());
}
// The path to the initrd.
"initrd" => {
initrd = Some(value.trim().to_string());
}
// The path to an EFI image.
"efi" => {
efi = Some(value.trim().to_string());
}
"sort-key" => {
sort_key = Some(value.trim().to_string());
}
"version" => {
version = Some(value.trim().to_string());
}
"machine-id" => {
machine_id = Some(value.trim().to_string());
}
// Ignore any other key.
_ => {
continue;
}
}
}
// Produce a BLS entry from the parsed fields.
Ok(Self {
title,
options,
linux,
initrd,
efi,
sort_key,
version,
machine_id,
})
}
}
impl BlsEntry {
/// Checks if this BLS entry is something we can actually boot in Sprout.
pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some()
}
/// Fetches the path to an EFI bootable image to boot, if any.
/// This prioritizes the linux field over efi.
/// It also converts / to \\ to match EFI path style.
pub fn chainload_path(&self) -> Option<String> {
self.linux
.clone()
.or(self.efi.clone())
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the path to an initrd to pass to the kernel, if any.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the options to pass to the kernel, if any.
pub fn options(&self) -> Option<String> {
self.options.clone()
}
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> {
self.title.clone()
}
/// Fetches the sort key of the entry, if any.
pub fn sort_key(&self) -> Option<String> {
self.sort_key.clone()
}
/// Fetches the version of the entry, if any.
pub fn version(&self) -> Option<String> {
self.version.clone()
}
/// Fetches the machine id of the entry, if any.
pub fn machine_id(&self) -> Option<String> {
self.machine_id.clone()
}
}
/// Sorts two BLS entries according to the BLS sort system.
/// `a_name` and `b_name` are the entry filenames (without `.conf`) used as the
/// final tiebreaker when all other fields are equal.
/// Reference: <https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
pub fn sort_bls(a_bls: &BlsEntry, a_name: &str, b_bls: &BlsEntry, b_name: &str) -> Ordering {
// Grab the sort keys from both entries.
let a_sort_key = a_bls.sort_key();
let b_sort_key = b_bls.sort_key();
// Sort keys, machine-id and version are only considered when both entries have a sort key.
// If only one entry has a sort key, that entry sorts first.
let ordering = match (a_sort_key, b_sort_key) {
(Some(a_sort_key), Some(b_sort_key)) => a_sort_key
.cmp(&b_sort_key)
// If the sort keys are equal, sort by machine-id.
.then_with(|| a_bls.machine_id().cmp(&b_bls.machine_id()))
// If the machine-id values are equal, sort by version, sorting newer versions first.
.then_with(|| {
compare_versions_optional(a_bls.version().as_deref(), b_bls.version().as_deref())
.reverse()
}),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
};
// If all else is equal, compare the file names of both entries, sorting newer entries first.
ordering.then_with(|| compare_versions(a_name, b_name).reverse())
}
/// Handles single character advancement and comparison.
macro_rules! handle_single_char {
($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => {
match ($ca == $c, $cb == $c) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
$a_chars.next();
$b_chars.next();
continue;
}
_ => {}
}
};
}
/// Compares two strings using the BLS version comparison specification.
/// Handles optional values as well by comparing only if both are specified.
/// A missing value is considered older than any specified value.
pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) {
// If both have values, compare them.
(Some(a), Some(b)) => compare_versions(a, b),
// If the second value is None, then `a` is greater than `b`.
(Some(_a), None) => Ordering::Greater,
// If the first value is None, the `a` is less than `b`.
(None, Some(_b)) => Ordering::Less,
// If both values are None, return that they are equal.
(None, None) => Ordering::Equal,
}
}
/// Compares two strings using the BLS version comparison specification.
/// See: <https://uapi-group.org/specifications/specs/version_format_specification/>
pub fn compare_versions(a: &str, b: &str) -> Ordering {
// An empty string is older than any other string.
if a.is_empty() || b.is_empty() {
return a.len().cmp(&b.len());
}
// Acquire a peekable iterator for each string.
let mut a_chars = a.chars().peekable();
let mut b_chars = b.chars().peekable();
// Loop until we have reached the end of one of the strings.
loop {
// Skip invalid characters in both strings.
skip_invalid(&mut a_chars);
skip_invalid(&mut b_chars);
// Handle the ~ character. This must happen before the end of string check,
// as a string with ~ is older than a string that has ended (1~rc1 < 1).
match (a_chars.peek() == Some(&'~'), b_chars.peek() == Some(&'~')) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
a_chars.next();
b_chars.next();
continue;
}
(false, false) => {}
}
// Check if either string has ended.
match (a_chars.peek(), b_chars.peek()) {
// No more characters in either string.
(None, None) => return Ordering::Equal,
// One string has ended, the other hasn't.
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
// Both strings have characters left.
(Some(&ca), Some(&cb)) => {
// Handle '-' character.
handle_single_char!(ca, cb, a_chars, b_chars, '-');
// Handle the '^' character.
handle_single_char!(ca, cb, a_chars, b_chars, '^');
// Handle the '.' character.
handle_single_char!(ca, cb, a_chars, b_chars, '.');
// Handle digits with numerical comparison.
// If only one string has digits at this position, that string is newer.
if ca.is_ascii_digit() || cb.is_ascii_digit() {
if !ca.is_ascii_digit() {
return Ordering::Less;
}
if !cb.is_ascii_digit() {
return Ordering::Greater;
}
let result = compare_numeric(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
// Handle letters with alphabetical comparison.
// We key off of the A character being alphabetical intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() {
let result = compare_alphabetic(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
}
}
}
}
/// Skips characters that are not in the valid character set.
fn skip_invalid<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
while let Some(&c) = iter.peek() {
if is_valid_char(c) {
break;
}
iter.next();
}
}
/// Checks if a character is in the valid character set for comparison.
fn is_valid_char(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^')
}
/// Compares numerical prefixes by extracting numbers.
/// Numbers are compared by their digit count first and then digit by digit,
/// which supports numbers of any length.
fn compare_numeric<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
let num_a = extract_number(iter_a);
let num_b = extract_number(iter_b);
num_a
.len()
.cmp(&num_b.len())
.then_with(|| num_a.cmp(&num_b))
}
/// Extracts the digits of a number from the iterator, skipping leading zeros.
fn extract_number<I: Iterator<Item = char>>(iter: &mut Peekable<I>) -> String {
// Skip leading zeros
while let Some(&'0') = iter.peek() {
iter.next();
}
let mut num = String::new();
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
iter.next();
num.push(c);
} else {
break;
}
}
num
}
/// Compares alphabetical prefixes.
/// Capital letters compare lower than lowercase letters (B < a).
fn compare_alphabetic<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
loop {
return match (iter_a.peek(), iter_b.peek()) {
(Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => {
if ca == cb {
// Same character, we should continue.
iter_a.next();
iter_b.next();
continue;
}
// Different characters found.
// All capital letters compare lower than lowercase letters.
match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) {
(true, false) => Ordering::Less, // uppercase < lowercase
(false, true) => Ordering::Greater, // lowercase > uppercase
(true, true) => ca.cmp(&cb), // both are uppercase
(false, false) => ca.cmp(&cb), // both are lowercase
}
}
(Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => {
// a has letters, b doesn't
Ordering::Greater
}
(Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => {
// b has letters, a doesn't
Ordering::Less
}
(Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater,
(None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less,
_ => Ordering::Equal,
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::cmp::Ordering;
fn sort_entry(
sort_key: Option<&str>,
machine_id: Option<&str>,
version: Option<&str>,
) -> BlsEntry {
BlsEntry {
sort_key: sort_key.map(|s| s.to_string()),
machine_id: machine_id.map(|s| s.to_string()),
version: version.map(|s| s.to_string()),
linux: Some("/vmlinuz".to_string()),
..BlsEntry::default()
}
}
#[test]
fn parse_empty_input_gives_all_none() {
let entry: BlsEntry = "".parse().unwrap();
assert!(entry.title.is_none());
assert!(entry.linux.is_none());
assert!(entry.efi.is_none());
assert!(entry.initrd.is_none());
assert!(entry.options.is_none());
assert!(entry.sort_key.is_none());
assert!(entry.version.is_none());
assert!(entry.machine_id.is_none());
}
#[test]
fn parse_all_known_fields() {
let input = "\
title Fedora Linux 6.5.6
version 6.5.6-300.fc39.x86_64
machine-id abc123def456
linux /boot/vmlinuz-6.5.6
initrd /boot/initrd-6.5.6.img
options root=/dev/sda1 ro quiet
sort-key fedora
efi /EFI/fedora/shimx64.efi
";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("Fedora Linux 6.5.6"));
assert_eq!(entry.version.as_deref(), Some("6.5.6-300.fc39.x86_64"));
assert_eq!(entry.machine_id.as_deref(), Some("abc123def456"));
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz-6.5.6"));
assert_eq!(entry.initrd.as_deref(), Some("/boot/initrd-6.5.6.img"));
assert_eq!(entry.options.as_deref(), Some("root=/dev/sda1 ro quiet"));
assert_eq!(entry.sort_key.as_deref(), Some("fedora"));
assert_eq!(entry.efi.as_deref(), Some("/EFI/fedora/shimx64.efi"));
}
#[test]
fn parse_skips_blank_lines() {
let input = "\n\ntitle My Entry\n\n\nlinux /vmlinuz\n\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_skips_comment_lines() {
let input = "# this is a comment\ntitle My Entry\n# another comment\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_trims_leading_whitespace_from_value() {
let input = "title Padded Title\nlinux /boot/vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("Padded Title"));
assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz"));
}
#[test]
fn parse_ignores_unknown_keys() {
let input = "title My Entry\nunknown-key some-value\nfuture-field value\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn parse_skips_lines_without_whitespace_separator() {
// A line with no whitespace cannot be split into key+value, so it is skipped
let input = "title My Entry\nnovalueline\nlinux /vmlinuz\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.title.as_deref(), Some("My Entry"));
assert_eq!(entry.linux.as_deref(), Some("/vmlinuz"));
}
#[test]
fn is_valid_when_linux_present() {
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
assert!(entry.is_valid());
}
#[test]
fn is_valid_when_efi_present() {
let entry: BlsEntry = "efi /EFI/boot/bootx64.efi\n".parse().unwrap();
assert!(entry.is_valid());
}
#[test]
fn not_valid_without_linux_or_efi() {
let entry: BlsEntry = "title Just a Title\noptions quiet\n".parse().unwrap();
assert!(!entry.is_valid());
}
#[test]
fn chainload_path_normalises_forward_slashes_to_backslashes() {
let entry: BlsEntry = "linux /boot/vmlinuz\n".parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("boot\\vmlinuz"));
}
#[test]
fn chainload_path_strips_leading_backslash() {
let entry: BlsEntry = "linux \\EFI\\boot\\kernel\n".parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("EFI\\boot\\kernel"));
}
#[test]
fn chainload_path_prefers_linux_over_efi() {
let input = "linux /boot/vmlinuz\nefi /EFI/boot/bootx64.efi\n";
let entry: BlsEntry = input.parse().unwrap();
assert_eq!(entry.chainload_path().as_deref(), Some("boot\\vmlinuz"));
}
#[test]
fn chainload_path_falls_back_to_efi_when_no_linux() {
let entry: BlsEntry = "efi /EFI/Microsoft/Boot/bootmgfw.efi\n".parse().unwrap();
assert_eq!(
entry.chainload_path().as_deref(),
Some("EFI\\Microsoft\\Boot\\bootmgfw.efi")
);
}
#[test]
fn chainload_path_none_when_neither_linux_nor_efi() {
let entry: BlsEntry = "title Only Title\n".parse().unwrap();
assert!(entry.chainload_path().is_none());
}
#[test]
fn initrd_path_normalises_slashes() {
let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap();
assert_eq!(entry.initrd_path().as_deref(), Some("boot\\initrd.img"));
}
#[test]
fn initrd_path_none_when_not_set() {
let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap();
assert!(entry.initrd_path().is_none());
}
#[test]
fn sort_key_is_primary_criterion() {
let a = sort_entry(Some("alpine"), None, None);
let b = sort_entry(Some("fedora"), None, None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn machine_id_is_secondary_criterion() {
let a = sort_entry(Some("linux"), Some("aaa"), None);
let b = sort_entry(Some("linux"), Some("bbb"), None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn version_is_tertiary_criterion_newer_first() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.4.0"));
// newer version (a) sorts before older version (b), so a < b in sort order
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn name_is_final_tiebreaker_newer_first() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
// name comparison via compare_versions, reversed — higher name sorts first
assert_eq!(sort_bls(&a, "entry-2", &b, "entry-1"), Ordering::Less);
assert_eq!(sort_bls(&a, "entry-1", &b, "entry-2"), Ordering::Greater);
}
#[test]
fn identical_entries_are_equal() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
assert_eq!(sort_bls(&a, "entry-1", &b, "entry-1"), Ordering::Equal);
}
#[test]
fn equal_strings_are_equal() {
assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
}
#[test]
fn empty_strings_are_equal() {
assert_eq!(compare_versions("", ""), Ordering::Equal);
}
#[test]
fn simple_numeric_less() {
assert_eq!(compare_versions("1", "2"), Ordering::Less);
}
#[test]
fn simple_numeric_greater() {
assert_eq!(compare_versions("2", "1"), Ordering::Greater);
}
#[test]
fn numeric_is_not_lexicographic() {
// "10" > "9", not "10" < "9" as in lexicographic order
assert_eq!(compare_versions("10", "9"), Ordering::Greater);
assert_eq!(compare_versions("1.10", "1.9"), Ordering::Greater);
}
#[test]
fn leading_zeros_are_ignored() {
assert_eq!(compare_versions("01", "1"), Ordering::Equal);
assert_eq!(compare_versions("1.00", "1.0"), Ordering::Equal);
assert_eq!(compare_versions("007", "7"), Ordering::Equal);
}
#[test]
fn multi_component_comparison() {
assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
}
#[test]
fn more_components_is_greater() {
assert_eq!(compare_versions("1.0.0", "1.0"), Ordering::Greater);
assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Less);
assert_eq!(compare_versions("1.0.0.0", "1.0.0"), Ordering::Greater);
}
#[test]
fn alphabetic_sections_compare_lexicographically() {
assert_eq!(compare_versions("1.0a", "1.0b"), Ordering::Less);
assert_eq!(compare_versions("1.0b", "1.0a"), Ordering::Greater);
assert_eq!(compare_versions("1.0abc", "1.0abd"), Ordering::Less);
}
#[test]
fn uppercase_letters_sort_below_lowercase() {
// Capital letters compare lower than lowercase (B < a)
assert_eq!(compare_versions("B", "a"), Ordering::Less);
assert_eq!(compare_versions("a", "B"), Ordering::Greater);
assert_eq!(compare_versions("Z", "a"), Ordering::Less);
}
#[test]
fn uppercase_letters_compare_among_themselves() {
assert_eq!(compare_versions("A", "B"), Ordering::Less);
assert_eq!(compare_versions("B", "A"), Ordering::Greater);
assert_eq!(compare_versions("A", "A"), Ordering::Equal);
}
#[test]
fn invalid_characters_are_skipped() {
// Characters not in [a-zA-Z0-9.-~^] are skipped before comparison
assert_eq!(compare_versions("##1.0", "1.0"), Ordering::Equal);
assert_eq!(compare_versions("1.0##", "1.0"), Ordering::Equal);
}
#[test]
fn tilde_between_two_present_tilde_strings() {
// When both have ~ at the same position, they are consumed and comparison continues
assert_eq!(compare_versions("1~alpha", "1~beta"), Ordering::Less);
assert_eq!(compare_versions("1~rc1", "1~rc2"), Ordering::Less);
assert_eq!(compare_versions("1~rc1", "1~rc1"), Ordering::Equal);
}
#[test]
fn tilde_when_only_one_side_has_it() {
// When a has ~ but b doesn't at the same position, a < b
assert_eq!(compare_versions("1~rc1", "1.0"), Ordering::Less);
// When b has ~ but a doesn't, a > b
assert_eq!(compare_versions("1.0", "1~rc1"), Ordering::Greater);
}
#[test]
fn tilde_is_older_than_end_of_string() {
// A pre-release is older than the release it precedes.
assert_eq!(compare_versions("1~rc1", "1"), Ordering::Less);
assert_eq!(compare_versions("1", "1~rc1"), Ordering::Greater);
assert_eq!(compare_versions("6.1~rc3", "6.1"), Ordering::Less);
}
#[test]
fn empty_string_is_oldest() {
assert_eq!(compare_versions("", "~1"), Ordering::Less);
assert_eq!(compare_versions("~1", ""), Ordering::Greater);
assert_eq!(compare_versions("", "1"), Ordering::Less);
}
#[test]
fn digits_are_newer_than_letters() {
assert_eq!(compare_versions("0", "a"), Ordering::Greater);
assert_eq!(compare_versions("a", "0"), Ordering::Less);
assert_eq!(compare_versions("1.0", "1.a"), Ordering::Greater);
assert_eq!(compare_versions("1.a", "1.0"), Ordering::Less);
}
#[test]
fn large_numbers_do_not_saturate() {
assert_eq!(
compare_versions("99999999999999999999", "99999999999999999998"),
Ordering::Greater
);
assert_eq!(
compare_versions("1.99999999999999999998", "1.99999999999999999999"),
Ordering::Less
);
}
#[test]
fn entry_with_sort_key_sorts_before_entry_without() {
let with_key = sort_entry(Some("fedora"), None, None);
let without_key = sort_entry(None, None, None);
assert_eq!(sort_bls(&with_key, "a", &without_key, "b"), Ordering::Less);
assert_eq!(
sort_bls(&without_key, "b", &with_key, "a"),
Ordering::Greater
);
}
#[test]
fn entries_without_sort_key_sort_by_name_only() {
// Without a sort key, machine-id and version are ignored.
let a = sort_entry(None, Some("aaa"), Some("1.0"));
let b = sort_entry(None, Some("bbb"), Some("2.0"));
assert_eq!(sort_bls(&a, "entry-2", &b, "entry-1"), Ordering::Less);
assert_eq!(sort_bls(&b, "entry-1", &a, "entry-2"), Ordering::Greater);
}
#[test]
fn entry_with_version_sorts_before_entry_without() {
let a = sort_entry(Some("linux"), Some("abc"), Some("6.5.0"));
let b = sort_entry(Some("linux"), Some("abc"), None);
assert_eq!(sort_bls(&a, "a", &b, "b"), Ordering::Less);
assert_eq!(sort_bls(&b, "b", &a, "a"), Ordering::Greater);
}
#[test]
fn optional_both_none_equal() {
assert_eq!(compare_versions_optional(None, None), Ordering::Equal);
}
#[test]
fn optional_some_vs_none_is_greater() {
// A missing version is older than any specified version.
assert_eq!(
compare_versions_optional(Some("1.0"), None),
Ordering::Greater
);
}
#[test]
fn optional_none_vs_some_is_less() {
assert_eq!(compare_versions_optional(None, Some("1.0")), Ordering::Less);
}
#[test]
fn optional_both_some_delegates_to_compare_versions() {
assert_eq!(
compare_versions_optional(Some("1.0"), Some("2.0")),
Ordering::Less
);
assert_eq!(
compare_versions_optional(Some("2.0"), Some("1.0")),
Ordering::Greater
);
assert_eq!(
compare_versions_optional(Some("1.0"), Some("1.0")),
Ordering::Equal
);
}
}
+4
View File
@@ -11,7 +11,10 @@ edition.workspace = true
anyhow.workspace = true anyhow.workspace = true
edera-sprout-config.path = "../config" edera-sprout-config.path = "../config"
edera-sprout-eficore.path = "../eficore" edera-sprout-eficore.path = "../eficore"
edera-sprout-bls.path = "../bls"
edera-sprout-parsing.path = "../parsing"
hex.workspace = true hex.workspace = true
jaarg.workspace = true
sha2.workspace = true sha2.workspace = true
toml.workspace = true toml.workspace = true
log.workspace = true log.workspace = true
@@ -24,3 +27,4 @@ edera-sprout-build.path = "../build"
[[bin]] [[bin]]
name = "sprout" name = "sprout"
path = "src/main.rs" path = "src/main.rs"
test = false
+48 -33
View File
@@ -1,15 +1,15 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::utils; use crate::phases::before_handoff;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::rc::Rc; use alloc::rc::Rc;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_parsing::{combine_options, empty_is_none};
use eficore::bootloader_interface::BootloaderInterface; use eficore::bootloader_interface::BootloaderInterface;
use eficore::loader::source::ImageSource; use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader}; use eficore::loader::{ImageLoadRequest, ImageLoader};
use eficore::media_loader::MediaLoaderHandle; use eficore::media_loader::MediaLoaderHandle;
use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use log::error;
use uefi::CString16; use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
@@ -21,7 +21,7 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// Resolve the path to the image to chainload. // Resolve the path to the image to chainload.
let resolved = eficore::path::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&configuration.path), context.stamp(&configuration.path),
) )
.context("unable to resolve chainload path")?; .context("unable to resolve chainload path")?;
@@ -32,14 +32,8 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// It will determine if the image needs to be loaded via the shim or can be loaded directly. // It will determine if the image needs to be loaded via the shim or can be loaded directly.
let image = ImageLoader::load(request)?; let image = ImageLoader::load(request)?;
// Open the LoadedImage protocol of the image to chainload.
let mut loaded_image_protocol =
uefi::boot::open_protocol_exclusive::<LoadedImage>(*image.handle())
.context("unable to open loaded image protocol")?;
// Stamp and combine the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = let options = combine_options(context.stamp_iter(configuration.options.iter()));
utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
// Pass the load options to the image. // Pass the load options to the image.
// If no options are provided, the resulting string will be empty. // If no options are provided, the resulting string will be empty.
@@ -50,16 +44,26 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.context("unable to convert chainloader options to CString16")?, .context("unable to convert chainloader options to CString16")?,
); );
// Ensure the chainloader options limit is not exceeded.
if options.num_bytes() > u32::MAX as usize { if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large"); bail!("chainloader options too large");
} }
// SAFETY: option size is checked to validate it is safe to pass. // Open the LoadedImage protocol of the image to chainload to pass the load options.
// Additionally, the pointer is allocated and retained on heap, which makes // This is done in a block to release the protocol before the image is started, as the
// passing the `options` pointer safe to the next image. // image may itself need to open its LoadedImage protocol exclusively.
unsafe { {
loaded_image_protocol let mut loaded_image_protocol =
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32); uefi::boot::open_protocol_exclusive::<LoadedImage>(*image.handle())
.context("unable to open loaded image protocol")?;
// SAFETY: option size is checked to validate it is safe to pass.
// Additionally, the pointer is allocated and retained on heap, which makes
// passing the `options` pointer safe to the next image.
unsafe {
loaded_image_protocol
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32);
}
} }
// Stamp the initrd path, if provided. // Stamp the initrd path, if provided.
@@ -68,45 +72,56 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.as_ref() .as_ref()
.map(|item| context.stamp(item)); .map(|item| context.stamp(item));
// The initrd can be None or empty, so we need to collapse that into a single Option. // The initrd can be None or empty, so we need to collapse that into a single Option.
let initrd = utils::empty_is_none(initrd); let initrd = empty_is_none(initrd);
// If an initrd is provided, register it with the EFI stack. // If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(linux_initrd) = initrd { if let Some(linux_initrd) = initrd {
let content = eficore::path::read_file_contents( let resolved_initrd =
Some(context.root().loaded_image_path()?), eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &linux_initrd)
&linux_initrd, .context("unable to resolve linux initrd path")?;
)
.context("unable to read linux initrd")?; // A path without a file component refers to the root of the filesystem, not an initrd.
let handle = // This happens when a path template like "$root\\$initrd" is stamped with an empty
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) // initrd value, such as a BLS entry without an initrd.
.context("unable to register linux initrd")?; let initrd_subpath = eficore::path::device_path_subpath(&resolved_initrd.full_path)
initrd_handle = Some(handle); .context("unable to get linux initrd subpath")?;
if !initrd_subpath.trim_matches('\\').is_empty() {
let content = resolved_initrd
.read_file()
.context("unable to read linux initrd")?;
let handle = MediaLoaderHandle::register(
LINUX_EFI_INITRD_MEDIA_GUID,
content.into_boxed_slice(),
)
.context("unable to register linux initrd")?;
initrd_handle = Some(handle);
}
} }
// Mark execution of an entry in the bootloader interface. // Mark execution of an entry in the bootloader interface.
BootloaderInterface::mark_exec(context.root().timer()) BootloaderInterface::mark_exec(context.root().timer())
.context("unable to mark execution of boot entry in bootloader interface")?; .context("unable to mark execution of boot entry in bootloader interface")?;
// Since we are about to hand off control to another image, we need to execute the handoff hook.
// This will perform operations like clearing the screen.
before_handoff(&context).context("unable to execute before handoff hook")?;
// 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.handle()); let result = uefi::boot::start_image(*image.handle());
// Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle
&& let Err(error) = initrd_handle.unregister()
{
error!("unable to unregister linux initrd: {}", error);
}
// Assert there was no error starting the image. // Assert there was no error starting the image.
result.context("unable to start image")?; result.context("unable to start image")?;
// Explicitly drop the options to clarify the lifetime. // Explicitly drop the options to clarify the lifetime.
drop(options); drop(options);
// Explicitly drop the initrd handle to clarify when it should be unregistered.
drop(initrd_handle);
// Return control to sprout. // Return control to sprout.
Ok(()) Ok(())
} }
+75 -91
View File
@@ -1,119 +1,107 @@
use crate::{ use crate::{actions, context::SproutContext};
actions,
context::SproutContext,
utils::{self},
};
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::{String, ToString}; use alloc::vec::Vec;
use alloc::{format, vec}; use alloc::{format, vec};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::actions::edera::EderaConfiguration; use edera_sprout_config::actions::edera::EderaConfiguration;
use edera_sprout_parsing::{build_xen_config, combine_options, empty_is_none};
use eficore::media_loader::{ use eficore::media_loader::{
MediaLoaderHandle, MediaLoaderHandle,
constants::xen::{ constants::xen::{
XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID, XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID,
}, },
}; };
use log::error; use eficore::platform::tpm::PlatformTpm;
use uefi::Guid; use uefi::Guid;
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. /// Register a media loader for the provided `bytes` with the vendor `guid`.
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
[
// global section
"[global]".to_string(),
// default configuration section
"default=sprout".to_string(),
// configuration section for sprout
"[sprout]".to_string(),
// xen options
format!("options={}", xen_options),
// kernel options, stub replaces the kernel path
// the kernel is provided via media loader
format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored
"".to_string(),
]
.join("\n")
}
/// Register a media loader for some `text` with the vendor `guid`.
/// `what` should indicate some identifying value for error messages /// `what` should indicate some identifying value for error messages
/// like `config` or `kernel`. /// like `config` or `kernel`.
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader. /// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> { fn register_media_loader_bytes(
MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice())
.context(format!("unable to register {} media loader", what)) /* */
}
/// Register a media loader for the file `path` with the vendor `guid`.
/// `what` should indicate some identifying value for error messages
/// like `config` or `kernel`.
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
fn register_media_loader_file(
context: &Rc<SproutContext>,
guid: Guid, guid: Guid,
what: &str, what: &str,
path: &str, bytes: Vec<u8>,
) -> Result<MediaLoaderHandle> { ) -> Result<MediaLoaderHandle> {
// Stamp the path to the file. MediaLoaderHandle::register(guid, bytes.into_boxed_slice())
.context(format!("unable to register {} media loader", what))
}
/// Read the contents of the loader payload at `path` relative to the sprout image.
/// `what` should indicate some identifying value for error messages
/// like `kernel` or `initrd`.
fn read_loader_payload(context: &Rc<SproutContext>, what: &str, path: &str) -> Result<Vec<u8>> {
let path = context.stamp(path); let path = context.stamp(path);
// Read the file contents. eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path)
let content = .context(format!("unable to read {} file", what))
eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path)
.context(format!("unable to read {} file", what))?;
// Register the media loader.
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
.context(format!("unable to register {} media loader", what))?;
Ok(handle)
} }
/// Executes the edera action which will boot the Edera hypervisor with the specified /// Executes the edera action which will boot the Edera hypervisor with the specified
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. /// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> { pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Build the Xen config file content for this configuration. // Only register the initrd media loader if the user actually configured one.
let config = build_xen_config(context.clone(), configuration); let xen_opts = combine_options(context.stamp_iter(configuration.xen_options.iter()));
let dom0_args = combine_options(context.stamp_iter(configuration.kernel_options.iter()));
// Register the media loader for the config. // Measure xen options and dom0 cmdline into PCR 12 in a fixed order.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) PlatformTpm::log_event(
.context("unable to register config media loader")?; PlatformTpm::PCR_KERNEL_PARAMETERS,
xen_opts.as_bytes(),
// Register the media loaders for the kernel. "sprout: xen options",
let kernel = register_media_loader_file(
&context,
XEN_EFI_KERNEL_MEDIA_GUID,
"kernel",
&configuration.kernel,
) )
.context("unable to register kernel media loader")?; .context("unable to measure xen options into the TPM")?;
PlatformTpm::log_event(
PlatformTpm::PCR_KERNEL_PARAMETERS,
dom0_args.as_bytes(),
"sprout: dom0 cmdline",
)
.context("unable to measure dom0 cmdline into the TPM")?;
// Create a vector of media loaders to unregister on error. // Build the Xen config text and register it as the config media loader. The
let mut media_loaders = vec![config, kernel]; // assembled text is a derived artifact, intentionally not measured.
let config_handle = register_media_loader_bytes(
XEN_EFI_CONFIG_MEDIA_GUID,
"config",
build_xen_config(&xen_opts, &dom0_args).into_bytes(),
)
.context("unable to register config media loader")?;
// Read the dom0 kernel, measure it into PCR 11, then register the kernel
// media loader.
let kernel_bytes = read_loader_payload(&context, "kernel", &configuration.kernel)?;
PlatformTpm::log_event(
PlatformTpm::PCR_KERNEL,
&kernel_bytes,
"sprout: dom0 kernel",
)
.context("unable to measure dom0 kernel into the TPM")?;
let kernel_handle =
register_media_loader_bytes(XEN_EFI_KERNEL_MEDIA_GUID, "kernel", kernel_bytes)
.context("unable to register kernel media loader")?;
// Extend PCR 9 with the initrd bytes (empty when no initrd is
// configured).
let initrd_bytes = match empty_is_none(configuration.initrd.as_ref()) {
Some(p) => read_loader_payload(&context, "initrd", p)?,
None => Vec::new(),
};
PlatformTpm::log_event(
PlatformTpm::PCR_INITRD,
&initrd_bytes,
"sprout: dom0 initrd",
)
.context("unable to measure dom0 initrd into the TPM")?;
// Create a vector of media loaders to drop them only after this function completes.
let mut media_loaders = vec![config_handle, kernel_handle];
// Register the initrd if it is provided. // Register the initrd if it is provided.
if let Some(initrd) = utils::empty_is_none(configuration.initrd.as_ref()) { if !initrd_bytes.is_empty() {
let initrd = let initrd_handle =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_bytes(XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd_bytes)
.context("unable to register initrd media loader")?; .context("unable to register initrd media loader")?;
media_loaders.push(initrd); media_loaders.push(initrd_handle);
} }
// Chainload to the Xen EFI stub. // Chainload to the Xen EFI stub.
@@ -127,12 +115,8 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders when an error happens. // Explicitly drop the media loaders to clarify when they should be unregistered.
for media_loader in media_loaders { drop(media_loaders);
if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error);
}
}
result result
} }
+16 -6
View File
@@ -1,5 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use edera_sprout_config::RootConfiguration; use edera_sprout_config::RootConfiguration;
use log::warn;
use uefi::fs::FileSystem; use uefi::fs::FileSystem;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
@@ -25,15 +26,24 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
// For each filesystem that was detected, scan it for supported autoconfig mechanisms. // For each filesystem that was detected, scan it for supported autoconfig mechanisms.
for handle in filesystem_handles { for handle in filesystem_handles {
// Acquire the device path root for the filesystem. // Acquire the device path root for the filesystem.
let root = { // A filesystem that can't be opened is skipped, as it should not prevent
uefi::boot::open_protocol_exclusive::<DevicePath>(handle) // autoconfiguring the other filesystems.
.context("unable to get root for filesystem")? let root = match uefi::boot::open_protocol_exclusive::<DevicePath>(handle) {
.to_boxed() Ok(root) => root.to_boxed(),
Err(error) => {
warn!("skipping filesystem, unable to get its root: {}", error);
continue;
}
}; };
// Open the filesystem that was detected. // Open the filesystem that was detected.
let filesystem = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle) let filesystem = match uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(handle) {
.context("unable to open filesystem")?; Ok(filesystem) => filesystem,
Err(error) => {
warn!("skipping filesystem, unable to open it: {}", error);
continue;
}
};
// Trade the filesystem protocol for the uefi filesystem helper. // Trade the filesystem protocol for the uefi filesystem helper.
let mut filesystem = FileSystem::new(filesystem); let mut filesystem = FileSystem::new(filesystem);
+3 -3
View File
@@ -1,4 +1,3 @@
use crate::utils;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::{format, vec}; use alloc::{format, vec};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@@ -8,6 +7,7 @@ use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration; use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration; use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::bls::BlsConfiguration; use edera_sprout_config::generators::bls::BlsConfiguration;
use edera_sprout_parsing::unique_hash;
use uefi::cstr16; use uefi::cstr16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
@@ -30,14 +30,14 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration. // Convert the device path root to a string we can use in the configuration.
let mut root = root let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root); let root_unique_hash = unique_hash(&root);
// Whether we have a loader.conf file. // Whether we have a loader.conf file.
let has_loader_conf = filesystem let has_loader_conf = filesystem
+15 -24
View File
@@ -1,5 +1,3 @@
use crate::utils;
use crate::utils::vercmp;
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -11,6 +9,10 @@ use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration; use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_config::generators::GeneratorDeclaration; use edera_sprout_config::generators::GeneratorDeclaration;
use edera_sprout_config::generators::list::ListConfiguration; use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_parsing::{
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
unique_hash,
};
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path, PathBuf}; use uefi::fs::{FileSystem, Path, PathBuf};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
@@ -24,12 +26,6 @@ const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The empty string represents the root of the filesystem. /// The empty string represents the root of the filesystem.
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"]; 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", "initrd.img"];
/// This is really silly, but if what we are booting is the Canonical stubble stub, /// This is really silly, but if what we are booting is the Canonical stubble stub,
/// there is a chance it will assert that the load options are non-empty. /// there is a chance it will assert that the load options are non-empty.
/// Technically speaking, load options can be empty. However, it assumes load options /// Technically speaking, load options can be empty. However, it assumes load options
@@ -37,9 +33,9 @@ const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// and then uses that code improperly by asserting that the pointer is non-null. /// and then uses that code improperly by asserting that the pointer is non-null.
/// To give a good user experience, we place a placeholder value here to ensure it's non-empty. /// To give a good user experience, we place a placeholder value here to ensure it's non-empty.
/// For stubble, this code ensures the command line pointer becomes null: /// For stubble, this code ensures the command line pointer becomes null:
/// https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L61-L64 /// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L61-L64>
/// Then this code asserts on it, stopping the boot process: /// Then this code asserts on it, stopping the boot process:
/// https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L27 /// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L27>
const DEFAULT_LINUX_OPTIONS: &str = "placeholder"; const DEFAULT_LINUX_OPTIONS: &str = "placeholder";
/// Pair of kernel and initramfs. /// Pair of kernel and initramfs.
@@ -109,9 +105,7 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Find a kernel prefix that matches, if any. // Find a kernel prefix that matches, if any.
// This is case-insensitive to ensure we pick up all possibilities. // This is case-insensitive to ensure we pick up all possibilities.
let Some(prefix) = KERNEL_PREFIXES.iter().find(|prefix| { let Some(prefix) = match_kernel_prefix(&name_for_match, LINUX_KERNEL_PREFIXES) else {
name_for_match == **prefix || name_for_match.starts_with(&format!("{}-", prefix))
}) else {
// Skip over anything that doesn't match a kernel prefix. // Skip over anything that doesn't match a kernel prefix.
continue; continue;
}; };
@@ -119,15 +113,14 @@ fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelP
// Acquire the suffix of the name, this will be used to match an initramfs. // Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..]; let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any. // Find a matching initramfs by trying each candidate name, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter(); let mut candidates = initramfs_candidates(suffix, LINUX_INITRAMFS_PREFIXES);
let matched_initramfs_path = loop { let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else { let Some(candidate) = candidates.next() else {
break None; break None;
}; };
// Construct an initramfs path. // Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix); let initramfs = CString16::try_from(candidate.as_str())
let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?; .context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path_for_join.clone(); let mut initramfs_path = path_for_join.clone();
initramfs_path.push(Path::new(&initramfs)); initramfs_path.push(Path::new(&initramfs));
@@ -165,14 +158,14 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration. // Convert the device path root to a string we can use in the configuration.
let mut root = root let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root); let root_unique_hash = unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list. // Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS { for location in SCAN_LOCATIONS {
@@ -186,17 +179,15 @@ pub fn scan(
return Ok(false); return Ok(false);
} }
// Sort the kernel pairs by kernel version, if it has one, newer kernels first.
pairs.sort_by(|a, b| vercmp::compare_versions(&a.kernel, &b.kernel).reverse());
// Generate a unique name for the linux chainload action. // Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,); let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash);
// Kernel pairs are detected, generate a list configuration for it. // Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration { let generator = ListConfiguration {
entry: EntryDeclaration { entry: EntryDeclaration {
title: "Boot Linux $name".to_string(), title: "Boot Linux $name".to_string(),
actions: vec![chainload_action_name.clone()], actions: vec![chainload_action_name.clone()],
sort_key: Some("$kernel".to_string()),
..Default::default() ..Default::default()
}, },
values: pairs values: pairs
+5 -4
View File
@@ -1,4 +1,3 @@
use crate::utils;
use alloc::string::ToString; use alloc::string::ToString;
use alloc::{format, vec}; use alloc::{format, vec};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@@ -6,6 +5,7 @@ use edera_sprout_config::RootConfiguration;
use edera_sprout_config::actions::ActionDeclaration; use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::chainload::ChainloadConfiguration;
use edera_sprout_config::entries::EntryDeclaration; use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_parsing::unique_hash;
use uefi::CString16; use uefi::CString16;
use uefi::fs::{FileSystem, Path}; use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
@@ -38,26 +38,27 @@ pub fn scan(
// Convert the device path root to a string we can use in the configuration. // Convert the device path root to a string we can use in the configuration.
let mut root = root let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.to_string(); .to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed. // Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/'); root.push('/');
// Generate a unique hash of the root path. // Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root); let root_unique_hash = unique_hash(&root);
// Generate a unique name for the Windows chainload action. // Generate a unique name for the Windows chainload action.
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,); let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows. // Generate an entry name for Windows.
let entry_name = format!("auto-windows-{}", root_unique_hash,); let entry_name = format!("auto-windows-{}", root_unique_hash);
// Create an entry for Windows and insert it into the configuration. // Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration { let entry = EntryDeclaration {
title: "Boot Windows".to_string(), title: "Boot Windows".to_string(),
actions: vec![chainload_action_name.clone()], actions: vec![chainload_action_name.clone()],
values: Default::default(), values: Default::default(),
sort_key: None, // Use the default sort key.
}; };
config.entries.insert(entry_name, entry); config.entries.insert(entry_name, entry);
+12 -45
View File
@@ -1,14 +1,13 @@
use crate::options::SproutOptions; use crate::options::SproutOptions;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use core::cmp::Reverse;
use edera_sprout_config::actions::ActionDeclaration; use edera_sprout_config::actions::ActionDeclaration;
use edera_sprout_parsing::stamp_values;
use eficore::platform::timer::PlatformTimer; use eficore::platform::timer::PlatformTimer;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
@@ -197,7 +196,7 @@ impl SproutContext {
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 {
let (changed, result) = Self::stamp_values(&current_values, value); let (changed, result) = stamp_values(&current_values, value);
if changed { if changed {
// If the value changed, we need to re-stamp it. // If the value changed, we need to re-stamp it.
did_change = true; did_change = true;
@@ -221,52 +220,20 @@ impl SproutContext {
}) })
} }
/// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;
// 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);
if result != next_result {
did_change = true;
}
result = next_result;
}
(did_change, result)
}
/// Stamps the input `text` with all the values in this [SproutContext] and it's parents. /// Stamps the input `text` with all the values in this [SproutContext] and it's parents.
/// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce /// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce
/// "hello\\b" as an output string. /// "hello\\b" as an output string.
pub fn stamp(&self, text: impl AsRef<str>) -> String { pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1 stamp_values(&self.all_values(), text.as_ref()).1
}
/// Stamps all the items from the iterator `input` with all the values in this [SproutContext]
/// and it's parents. This calls [self.stamp] on each item.
pub fn stamp_iter(
&self,
input: impl Iterator<Item = impl AsRef<str>>,
) -> impl Iterator<Item = String> {
input.map(|item| self.stamp(item))
} }
/// Unloads a [SproutContext] back into an owned context. This /// Unloads a [SproutContext] back into an owned context. This
+2 -2
View File
@@ -18,7 +18,7 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
// Resolve the path to the driver image. // Resolve the path to the driver image.
let resolved = eficore::path::resolve_path( let resolved = eficore::path::resolve_path(
Some(context.root().loaded_image_path()?), Some(context.root().loaded_image_path()?),
&context.stamp(&driver.path), context.stamp(&driver.path),
) )
.context("unable to resolve path to driver")?; .context("unable to resolve path to driver")?;
@@ -49,7 +49,7 @@ fn reconnect() -> Result<()> {
for handle in handles.iter() { for handle in handles.iter() {
// Ignore the result as there is nothing we can do if reconnecting a controller fails. // Ignore the result as there is nothing we can do if reconnecting a controller fails.
// This is also likely to fail in some cases but should fail safely. // This is also likely to fail in some cases but should fail safely.
let _ = uefi::boot::connect_controller(*handle, None, None, true); let _ = uefi::boot::connect_controller(*handle, &[], None, true);
} }
Ok(()) Ok(())
+28
View File
@@ -12,6 +12,7 @@ pub struct BootableEntry {
declaration: EntryDeclaration, declaration: EntryDeclaration,
default: bool, default: bool,
pin_name: bool, pin_name: bool,
sort_key: Option<String>,
} }
impl BootableEntry { impl BootableEntry {
@@ -29,6 +30,7 @@ impl BootableEntry {
declaration, declaration,
default: false, default: false,
pin_name: false, pin_name: false,
sort_key: None,
} }
} }
@@ -93,10 +95,36 @@ impl BootableEntry {
} }
/// Determine if this entry matches `needle` by comparing to the name or title of the entry. /// Determine if this entry matches `needle` by comparing to the name or title of the entry.
/// If `needle` ends with *, we will match a partial match.
pub fn is_match(&self, needle: &str) -> bool { pub fn is_match(&self, needle: &str) -> bool {
// If the needle ends with '*', we will accept a partial match.
if needle.ends_with("*") {
// Strip off any '*' at the end.
let partial = needle.trim_end_matches("*");
// Check if the name or title start with the partial match.
return self.name.starts_with(partial) || self.title.starts_with(partial);
}
// Standard quality matching rules.
self.name == needle || self.title == needle self.name == needle || self.title == needle
} }
/// Set the sort key of the entry. This is used to sort entries via version comparison.
pub fn set_sort_key(&mut self, sort_key: String) {
self.sort_key = Some(sort_key);
}
/// Retrieve a reference to the sort key of the entry. If one is not specified, we will use the
/// name of the entry.
pub fn sort_key(&self) -> &str {
// Use the sort key specified in the bootable entry, or use the declaration sort key,
// or use the name of the entry.
self.sort_key
.as_deref()
.or(self.declaration.sort_key.as_deref())
.unwrap_or(&self.name)
}
/// Find an entry by `needle` inside the entry iterator `haystack`. /// Find an entry by `needle` inside the entry iterator `haystack`.
/// This will search for an entry by name, title, or index. /// This will search for an entry by name, title, or index.
pub fn find<'a>( pub fn find<'a>(
+60 -69
View File
@@ -1,22 +1,22 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::bls::entry::BlsEntry; use alloc::{
use crate::utils::vercmp; format,
use alloc::format; rc::Rc,
use alloc::rc::Rc; string::{String, ToString},
use alloc::string::{String, ToString}; vec::Vec,
use alloc::vec::Vec; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use core::cmp::Ordering; use core::{cmp::Ordering, str::FromStr};
use core::str::FromStr; use edera_sprout_bls::{BlsEntry, sort_bls};
use edera_sprout_config::generators::bls::BlsConfiguration; use edera_sprout_config::generators::bls::BlsConfiguration;
use uefi::cstr16; use log::warn;
use uefi::fs::{FileSystem, PathBuf}; use uefi::{
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; cstr16,
use uefi::proto::media::fs::SimpleFileSystem; fs::{FileSystem, PathBuf},
proto::device_path::text::{AllowShortcuts, DisplayOnly},
/// BLS entry parser. proto::media::fs::SimpleFileSystem,
mod entry; };
// TODO(azenla): remove this once variable substitution is implemented. // TODO(azenla): remove this once variable substitution is implemented.
/// This function is used to remove the `tuned_initrd` variable from entry values. /// This function is used to remove the `tuned_initrd` variable from entry values.
@@ -26,57 +26,11 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
} }
/// Sorts two entries according to the BLS sort system. /// Sorts two entries according to the BLS sort system.
/// Reference: https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting /// Reference: <https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting>
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering { fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
// Grab the components of both entries.
let (a_bls, a_boot) = a; let (a_bls, a_boot) = a;
let (b_bls, b_boot) = b; let (b_bls, b_boot) = b;
sort_bls(a_bls, a_boot.name(), b_bls, b_boot.name())
// Grab the sort keys from both entries.
let a_sort_key = a_bls.sort_key();
let b_sort_key = b_bls.sort_key();
// Compare the sort keys of both entries.
match a_sort_key.cmp(&b_sort_key) {
// If A and B sort keys are equal, sort by machine-id.
Ordering::Equal => {
// Grab the machine-id from both entries.
let a_machine_id = a_bls.machine_id();
let b_machine_id = b_bls.machine_id();
// Compare the machine-id of both entries.
match a_machine_id.cmp(&b_machine_id) {
// If both machine-id values are equal, sort by version.
Ordering::Equal => {
// Grab the version from both entries.
let a_version = a_bls.version();
let b_version = b_bls.version();
// Compare the version of both entries, sorting newer versions first.
match vercmp::compare_versions_optional(
a_version.as_deref(),
b_version.as_deref(),
)
.reverse()
{
// If both versions are equal, sort by file name in reverse order.
Ordering::Equal => {
// Grab the file name from both entries.
let a_name = a_boot.name();
let b_name = b_boot.name();
// Compare the file names of both entries, sorting newer entries first.
vercmp::compare_versions(a_name, b_name).reverse()
}
other => other,
}
}
other => other,
}
}
other => other,
}
} }
/// 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
@@ -96,7 +50,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let mut entries_path = PathBuf::from( let mut entries_path = PathBuf::from(
bls_resolved bls_resolved
.sub_path .sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert bls path to string")?, .context("unable to convert bls path to string")?,
); );
entries_path.push(cstr16!("entries")); entries_path.push(cstr16!("entries"));
@@ -107,6 +61,15 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
.context("unable to open bls filesystem")?; .context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs); let mut fs = FileSystem::new(fs);
// The entries directory is optional, as a BLS directory may only contain a loader.conf file.
// If it does not exist, there are no entries to generate.
if !fs
.try_exists(&entries_path)
.context("unable to check for bls entries directory")?
{
return Ok(Vec::new());
}
// 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)
@@ -143,15 +106,33 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
full_entry_path.push(entry.file_name()); full_entry_path.push(entry.file_name());
// Read the entry file. // Read the entry file.
let content = fs // Entries that can't be read or parsed are skipped, as one broken entry
.read(full_entry_path) // should not prevent booting any of the other entries.
.context("unable to read bls file")?; let content = match fs.read(full_entry_path) {
Ok(content) => content,
Err(error) => {
warn!("unable to read bls entry {}: {}", name, error);
continue;
}
};
// Parse the entry file as a UTF-8 string. // Parse the entry file as a UTF-8 string.
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?; let content = match String::from_utf8(content) {
Ok(content) => content,
Err(error) => {
warn!("unable to read bls entry {} as utf8: {}", name, error);
continue;
}
};
// Parse the entry file as a BLS entry. // Parse the entry file as a BLS entry.
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?; let entry = match BlsEntry::from_str(&content) {
Ok(entry) => entry,
Err(error) => {
warn!("unable to parse bls entry {}: {}", name, error);
continue;
}
};
// Ignore entries that are not valid for Sprout. // Ignore entries that are not valid for Sprout.
if !entry.is_valid() { if !entry.is_valid() {
@@ -206,6 +187,16 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
// Sort all the entries according to the BLS sort system. // Sort all the entries according to the BLS sort system.
entries.sort_by(sort_entries); entries.sort_by(sort_entries);
// Grab the number of entries that we have, so we can calculate a reverse index.
let entry_count = entries.len();
// Set the sort keys of all the bootable entries to a semi-unique prefix + the BLS sort order.
// The final comparison happens using version comparison, so this will sort
// things properly.
for (idx, (_bls, boot)) in entries.iter_mut().enumerate() {
boot.set_sort_key(format!("bls-{}-{}", path, entry_count - idx - 1));
}
// Collect all the bootable entries and return them. // Collect all the bootable entries and return them.
Ok(entries.into_iter().map(|(_, boot)| boot).collect()) Ok(entries.into_iter().map(|(_, boot)| boot).collect())
} }
-168
View File
@@ -1,168 +0,0 @@
use alloc::string::{String, ToString};
use anyhow::{Error, Result};
use core::str::FromStr;
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)]
pub struct BlsEntry {
/// The title of the entry.
pub title: Option<String>,
/// The options to pass to the entry.
pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
/// The sort key for the entry.
pub sort_key: Option<String>,
/// The version of the entry.
pub version: Option<String>,
/// The machine id of the entry.
pub machine_id: Option<String>,
}
/// Parser for a BLS entry.
impl FromStr for BlsEntry {
type Err = Error;
/// Parses the `input` as a BLS entry file.
fn from_str(input: &str) -> Result<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
let mut sort_key: Option<String> = None;
let mut version: Option<String> = None;
let mut machine_id: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() {
// Trim the line.
let line = line.trim();
// Skip over empty lines and comments.
if line.is_empty() || line.starts_with('#') {
continue;
}
// Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};
// Match the key to a field we understand.
match key {
// The title of the entry.
"title" => {
title = Some(value.trim().to_string());
}
// The options to pass to the entry.
"options" => {
options = Some(value.trim().to_string());
}
// The path to the linux kernel.
"linux" => {
linux = Some(value.trim().to_string());
}
// The path to the initrd.
"initrd" => {
initrd = Some(value.trim().to_string());
}
// The path to an EFI image.
"efi" => {
efi = Some(value.trim().to_string());
}
"sort-key" => {
sort_key = Some(value.trim().to_string());
}
"version" => {
version = Some(value.trim().to_string());
}
"machine-id" => {
machine_id = Some(value.trim().to_string());
}
// Ignore any other key.
_ => {
continue;
}
}
}
// Produce a BLS entry from the parsed fields.
Ok(Self {
title,
options,
linux,
initrd,
efi,
sort_key,
version,
machine_id,
})
}
}
impl BlsEntry {
/// Checks if this BLS entry is something we can actually boot in Sprout.
pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some()
}
/// Fetches the path to an EFI bootable image to boot, if any.
/// This prioritizes the linux field over efi.
/// It also converts / to \\ to match EFI path style.
pub fn chainload_path(&self) -> Option<String> {
self.linux
.clone()
.or(self.efi.clone())
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the path to an initrd to pass to the kernel, if any.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the options to pass to the kernel, if any.
pub fn options(&self) -> Option<String> {
self.options.clone()
}
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> {
self.title.clone()
}
/// Fetches the sort key of the entry, if any.
pub fn sort_key(&self) -> Option<String> {
self.sort_key.clone()
}
/// Fetches the version of the entry, if any.
pub fn version(&self) -> Option<String> {
self.version.clone()
}
/// Fetches the machine id of the entry, if any.
pub fn machine_id(&self) -> Option<String> {
self.machine_id.clone()
}
}
+4 -5
View File
@@ -22,11 +22,10 @@ pub fn generate(
// Stamp the entry title and actions from the template. // Stamp the entry title and actions from the template.
let mut entry = list.entry.clone(); let mut entry = list.entry.clone();
entry.actions = entry
.actions // Stamp all the actions this entry references.
.into_iter() entry.actions = context.stamp_iter(entry.actions.into_iter()).collect();
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context. // Push the entry into the list with the new context.
entries.push(BootableEntry::new( entries.push(BootableEntry::new(
index.to_string(), index.to_string(),
+1 -35
View File
@@ -1,46 +1,12 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::list; use crate::generators::list;
use alloc::collections::BTreeMap;
use alloc::rc::Rc; use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::Result; use anyhow::Result;
use edera_sprout_config::generators::list::ListConfiguration; use edera_sprout_config::generators::list::ListConfiguration;
use edera_sprout_config::generators::matrix::MatrixConfiguration; use edera_sprout_config::generators::matrix::MatrixConfiguration;
use edera_sprout_parsing::build_matrix;
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce:
/// x: a, y: c
/// x: a, y: d
/// x: b, y: c
/// x: b, y: d
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
for (key, values) in items {
let mut new_result = Vec::new();
// Produce all the combinations of the input values.
for combination in &result {
for value in &values {
let mut new_combination = combination.clone();
new_combination.insert(key.clone(), value.clone());
new_result.push(new_combination);
}
}
result = new_result;
}
result.into_iter().filter(|item| !item.is_empty()).collect()
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`. /// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate( pub fn generate(
+25 -26
View File
@@ -3,28 +3,26 @@
#![no_main] #![no_main]
extern crate alloc; extern crate alloc;
use crate::context::{RootContext, SproutContext}; use crate::{
use crate::entries::BootableEntry; context::{RootContext, SproutContext},
use crate::options::SproutOptions; entries::BootableEntry,
use crate::options::parser::OptionsRepresentable; options::SproutOptions,
use crate::phases::phase; phases::phase,
use alloc::collections::BTreeMap; };
use alloc::format; use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
use alloc::string::ToString;
use alloc::vec::Vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use core::ops::Deref; use core::{ops::Deref, time::Duration};
use core::time::Duration; use edera_sprout_bls::compare_versions;
use edera_sprout_config::RootConfiguration; use edera_sprout_config::RootConfiguration;
use eficore::bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout}; use eficore::{
use eficore::partition::PartitionGuidForm; bootloader_interface::{BootloaderInterface, BootloaderInterfaceTimeout},
use eficore::platform::timer::PlatformTimer; partition::PartitionGuidForm,
use eficore::platform::tpm::PlatformTpm; platform::{timer::PlatformTimer, tpm::PlatformTpm},
use eficore::secure::SecureBoot; secure::SecureBoot,
use eficore::setup; setup,
};
use log::{error, info, warn}; use log::{error, info, warn};
use uefi::entry; use uefi::{entry, proto::device_path::LoadedImageDevicePath};
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi_raw::Status; use uefi_raw::Status;
/// actions: Code that can be configured and executed by Sprout. /// actions: Code that can be configured and executed by Sprout.
@@ -63,9 +61,6 @@ pub mod phases;
/// sbat: Secure Boot Attestation section. /// sbat: Secure Boot Attestation section.
pub mod sbat; pub mod sbat;
/// utils: Utility functions that are used by other parts of Sprout.
pub mod utils;
/// The delay to wait for when an error occurs in Sprout. /// The delay to wait for when an error occurs in Sprout.
const DELAY_ON_ERROR: Duration = Duration::from_secs(10); const DELAY_ON_ERROR: Duration = Duration::from_secs(10);
@@ -258,13 +253,14 @@ fn run() -> Result<()> {
} }
} }
// Sort the entries by their sort key, finalizing the order to show entries. This happens
// in reverse order so that entries that would come last show up first in the menu.
entries.sort_by(|a, b| compare_versions(a.sort_key(), b.sort_key()).reverse());
// Tell the bootloader interface what entries are available. // Tell the bootloader interface what entries are available.
BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name())) BootloaderInterface::set_entries(entries.iter().map(|entry| entry.name()))
.context("unable to set entries in bootloader interface")?; .context("unable to set entries in bootloader interface")?;
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Acquire the timeout setting from the bootloader interface. // Acquire the timeout setting from the bootloader interface.
let bootloader_interface_timeout = let bootloader_interface_timeout =
BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?; BootloaderInterface::get_timeout().context("unable to get bootloader interface timeout")?;
@@ -356,6 +352,9 @@ fn run() -> Result<()> {
BootloaderInterface::set_selected_entry(entry.name().to_string()) BootloaderInterface::set_selected_entry(entry.name().to_string())
.context("unable to set selected entry in bootloader interface")?; .context("unable to set selected entry in bootloader interface")?;
// Execute the late phase, now that the entry is chosen but before its actions are executed.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Execute all the actions for the selected entry. // Execute all the actions for the selected entry.
for action in &entry.declaration().actions { for action in &entry.declaration().actions {
let action = entry.context().stamp(action); let action = entry.context().stamp(action);
@@ -385,7 +384,7 @@ fn efi_main() -> Status {
let result = run(); let result = run();
if let Err(ref error) = result { if let Err(ref error) = result {
// Print an error trace. // Print an error trace.
error!("sprout encountered an error:"); error!("sprout encountered an error: {}", error);
for (index, stack) in error.chain().enumerate() { for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack); error!("[{}]: {}", index, stack);
} }
+7 -13
View File
@@ -1,6 +1,6 @@
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use alloc::vec; use alloc::vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result};
use core::time::Duration; use core::time::Duration;
use eficore::bootloader_interface::BootloaderInterface; use eficore::bootloader_interface::BootloaderInterface;
use eficore::platform::timer::PlatformTimer; use eficore::platform::timer::PlatformTimer;
@@ -44,23 +44,15 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
.context("unable to create timer event")? .context("unable to create timer event")?
}; };
// The timeout is in increments of 100 nanoseconds.
let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration. // Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64); let trigger = TimerTrigger::Relative(*timeout);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?; uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = vec![timer_event, key_event]; let events = vec![timer_event, key_event];
// Wait for either the timer event or the key event to trigger. // Wait for either the timer event or the key event to trigger.
// Store the result so that we can free the timer event. // Store the result so that we can free the timer event.
let event_result = uefi::boot::wait_for_event(&mut events) let event_result = uefi::boot::wait_for_event(&events)
.discard_errdata() .discard_errdata()
.context("unable to wait for event"); .context("unable to wait for event");
@@ -94,8 +86,10 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
} }
// If we reach here, there is a key event. // If we reach here, there is a key event.
// Some firmware signals the key event without a key being available,
// in which case there is nothing to do.
let Some(key) = input.read_key().context("unable to read key")? else { let Some(key) = input.read_key().context("unable to read key")? else {
bail!("no key was pressed"); return Ok(MenuOperation::Nop);
}; };
match key { match key {
+102 -90
View File
@@ -1,10 +1,12 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, anyhow};
use core::ptr::null_mut;
/// The Sprout options parser. use jaarg::{
pub mod parser; ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts,
ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter,
};
use log::{error, info};
use uefi_raw::Status;
/// Default configuration file path. /// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
@@ -22,6 +24,8 @@ pub struct SproutOptions {
pub force_menu: bool, pub force_menu: bool,
/// The timeout for the boot menu in seconds. /// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>, pub menu_timeout: Option<u64>,
/// Retains the boot console before boot.
pub retain_boot_console: bool,
} }
/// The default Sprout options. /// The default Sprout options.
@@ -33,102 +37,110 @@ impl Default for SproutOptions {
boot: None, boot: None,
force_menu: false, force_menu: false,
menu_timeout: None, menu_timeout: None,
retain_boot_console: false,
} }
} }
} }
/// The options parser mechanism for Sprout. /// The options parser mechanism for Sprout.
impl OptionsRepresentable for SproutOptions { impl SproutOptions {
/// Produce the [SproutOptions] structure. /// Produces [SproutOptions] from the arguments provided by the UEFI core.
type Output = Self; /// Internally, we use the `jaarg` argument parser which has excellent no_std support.
pub fn parse() -> Result<Self> {
enum ArgID {
Help,
AutoConfigure,
Config,
Boot,
ForceMenu,
MenuTimeout,
RetainBootConsole,
}
/// All the Sprout options that are defined. // All the options for the Sprout executable.
fn options() -> &'static [(&'static str, OptionDescription<'static>)] { const OPTIONS: Opts<ArgID> = Opts::new(&[
&[ Opt::help_flag(ArgID::Help, &["--help"]).help_text("Display Sprout Help"),
( Opt::flag(ArgID::AutoConfigure, &["--autoconfigure"])
"autoconfigure", .help_text("Enable Sprout autoconfiguration"),
OptionDescription { Opt::value(ArgID::Config, &["--config"], "PATH")
description: "Enable Sprout Autoconfiguration", .help_text("Path to Sprout configuration file"),
form: OptionForm::Flag, Opt::value(ArgID::Boot, &["--boot"], "ENTRY")
}, .help_text("Entry to boot, bypassing the menu"),
), Opt::flag(ArgID::ForceMenu, &["--force-menu"]).help_text("Force showing the boot menu"),
( Opt::value(ArgID::MenuTimeout, &["--menu-timeout"], "TIMEOUT")
"config", .help_text("Boot menu timeout, in seconds"),
OptionDescription { Opt::flag(ArgID::RetainBootConsole, &["--retain-boot-console"])
description: "Path to Sprout configuration file", .help_text("Retain boot console before boot"),
form: OptionForm::Value, ]);
},
), // Acquire the arguments as determined by the UEFI core.
( let args = eficore::env::args()?;
"boot",
OptionDescription {
description: "Entry to boot, bypassing the menu",
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",
OptionDescription {
description: "Display Sprout Help",
form: OptionForm::Help,
},
),
]
}
/// Produces [SproutOptions] from the parsed raw `options` map.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self> {
// Use the default value of sprout options and have the raw options be parsed into it. // Use the default value of sprout options and have the raw options be parsed into it.
let mut result = Self::default(); let mut result = Self::default();
for (key, value) in options { // Parse the OPTIONS into a map using jaarg.
match key.as_str() { match OPTIONS.parse(
"autoconfigure" => { "sprout",
// Enable autoconfiguration. args.iter(),
result.autoconfigure = true; |program_name, id, _opt, _name, value| {
match id {
ArgID::AutoConfigure => {
// Enable autoconfiguration.
result.autoconfigure = true;
}
ArgID::Config => {
// The configuration file to load.
result.config = value.into();
}
ArgID::Boot => {
// The entry to boot.
result.boot = Some(value.into());
}
ArgID::ForceMenu => {
// Force showing of the boot menu.
result.force_menu = true;
}
ArgID::MenuTimeout => {
// The timeout for the boot menu in seconds.
result.menu_timeout = Some(value.parse::<u64>()?);
}
ArgID::RetainBootConsole => {
// Retain the boot console before booting.
result.retain_boot_console = true;
}
ArgID::Help => {
let ctx = HelpWriterContext {
options: &OPTIONS,
program_name,
};
info!("{}", StandardFullHelpWriter::new(ctx));
return Ok(ParseControl::Quit);
}
} }
Ok(ParseControl::Continue)
},
|program_name, error| {
let ctx = ErrorUsageWriterContext {
options: &OPTIONS,
program_name,
error,
};
error!("{}", StandardErrorUsageWriter::new(ctx));
},
) {
ParseResult::ContinueSuccess => Ok(result),
ParseResult::ExitSuccess => unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut())
.context("unable to exit")?;
Err(anyhow!("unable to exit"))
},
"config" => { ParseResult::ExitError => unsafe {
// The configuration file to load. uefi::boot::exit(uefi::boot::image_handle(), Status::ABORTED, 0, null_mut())
result.config = value.context("--config option requires a value")?; .context("unable to exit")?;
} Err(anyhow!("unable to exit"))
},
"boot" => {
// The entry to boot.
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}"),
}
} }
Ok(result)
} }
} }
-153
View File
@@ -1,153 +0,0 @@
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use anyhow::{Context, Result, bail};
use core::ptr::null_mut;
use eficore::env;
use log::info;
use uefi_raw::Status;
/// The type of option. This disambiguates different behavior
/// of how options are handled.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum OptionForm {
/// A flag, like --verbose.
Flag,
/// A value, in the form --abc 123 or --abc=123.
Value,
/// Help flag, like --help.
Help,
}
/// The description of an option, used in the option parser
/// to make decisions about how to progress.
#[derive(Debug, Clone)]
pub struct OptionDescription<'a> {
/// The description of the option.
pub description: &'a str,
/// The type of option to parse as.
pub form: OptionForm,
}
/// Represents a type that can be parsed from command line arguments.
/// This is a super minimal options parser mechanism just for Sprout.
pub trait OptionsRepresentable {
/// The output type that parsing will produce.
type Output;
/// The configured options for this type. This should describe all the options
/// that are valid to produce the type. The left-hand side is the name of the option,
/// and the right-hand side is the description.
fn options() -> &'static [(&'static str, OptionDescription<'static>)];
/// Produces the type by taking the `options` and processing it into the output.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self::Output>;
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support the format: --abc=123
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument, which is the path to our executable.
let args = env::args()?;
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the option doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if let Some((part_key, part_value)) = option.split_once('=') {
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option");
}
// Find the description of the configured option, if any.
let Some(description) = configured.get(option.as_str()) else {
bail!("invalid option: --{option}");
};
// Check if the option requires a value and error if none was provided.
if description.form == OptionForm::Value && value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is None.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// If the option form does not support a value and there is a value, error.
if description.form != OptionForm::Value && value.is_some() {
bail!("option --{} does not take a value", option);
}
// Handle the --help flag case.
if description.form == OptionForm::Help {
// Generic configured options output.
info!("Configured Options:");
for (name, description) in &configured {
info!(
" --{}{}: {}",
name,
if description.form == OptionForm::Value {
" <value>"
} else {
""
},
description.description
);
}
// Exit because the help has been displayed.
unsafe {
uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut());
};
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce].
fn parse() -> Result<Self::Output> {
// Parse the program arguments into a raw map.
let options = Self::parse_raw().context("unable to parse options")?;
// Produce the options from the map.
Self::produce(options)
}
}
+16 -1
View File
@@ -6,7 +6,7 @@ use anyhow::{Context, Result};
use edera_sprout_config::phases::PhaseConfiguration; use edera_sprout_config::phases::PhaseConfiguration;
/// Executes the specified [phase] of the boot process. /// Executes the specified [phase] of the boot process.
/// The value [phase] should be a reference of a specific phase in the [PhasesConfiguration]. /// The value [phase] should be a reference of a specific phase in the `PhasesConfiguration`.
/// Any error from the actions is propagated into the [Result] and will interrupt further /// Any error from the actions is propagated into the [Result] and will interrupt further
/// execution of phase actions. /// execution of phase actions.
pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> { pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result<()> {
@@ -24,3 +24,18 @@ pub fn phase(context: Rc<SproutContext>, phase: &[PhaseConfiguration]) -> Result
} }
Ok(()) Ok(())
} }
/// Manual hook called by code in the bootloader that hands off to another image.
/// This is used to perform actions like clearing the screen.
pub fn before_handoff(context: &SproutContext) -> Result<()> {
// If we have not been asked to retain the boot console, then we should clear the screen.
if !context.root().options().retain_boot_console {
// Clear the screen. We use clear here instead of reset because some firmware,
// particularly Dell firmware, does not clear the screen on reset.
// We clear both stdout and stderr because it's not guaranteed that they are the same
// text output.
uefi::system::with_stdout(|stdout| stdout.clear()).context("unable to clear screen")?;
uefi::system::with_stderr(|stderr| stderr.clear()).context("unable to clear screen")?;
}
Ok(())
}
-26
View File
@@ -1,26 +0,0 @@
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
/// Implements a version comparison algorithm according to the BLS specification.
pub mod vercmp;
/// 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 {
hex::encode(Sha256::digest(input.as_bytes()))
}
/// 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())
}
-184
View File
@@ -1,184 +0,0 @@
use core::cmp::Ordering;
use core::iter::Peekable;
/// Handles single character advancement and comparison.
macro_rules! handle_single_char {
($ca: expr, $cb:expr, $a_chars:expr, $b_chars:expr, $c:expr) => {
match ($ca == $c, $cb == $c) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
$a_chars.next();
$b_chars.next();
continue;
}
_ => {}
}
};
}
/// Compares two strings using the BLS version comparison specification.
/// Handles optional values as well by comparing only if both are specified.
pub fn compare_versions_optional(a: Option<&str>, b: Option<&str>) -> Ordering {
match (a, b) {
// If both have values, compare them.
(Some(a), Some(b)) => compare_versions(a, b),
// If the second value is None, then `a` is less than `b`.
(Some(_a), None) => Ordering::Less,
// If the first value is None, the `a` is greater than `b`.
(None, Some(_b)) => Ordering::Greater,
// If both values are None, return that they are equal.
(None, None) => Ordering::Equal,
}
}
/// Compares two strings using the BLS version comparison specification.
/// See: https://uapi-group.org/specifications/specs/version_format_specification/
pub fn compare_versions(a: &str, b: &str) -> Ordering {
// Acquire a peekable iterator for each string.
let mut a_chars = a.chars().peekable();
let mut b_chars = b.chars().peekable();
// Loop until we have reached the end of one of the strings.
loop {
// Skip invalid characters in both strings.
skip_invalid(&mut a_chars);
skip_invalid(&mut b_chars);
// Check if either string has ended.
match (a_chars.peek(), b_chars.peek()) {
// No more characters in either string.
(None, None) => return Ordering::Equal,
// One string has ended, the other hasn't.
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
// Both strings have characters left.
(Some(&ca), Some(&cb)) => {
// Handle the ~ character.
handle_single_char!(ca, cb, a_chars, b_chars, '~');
// Handle '-' character.
handle_single_char!(ca, cb, a_chars, b_chars, '-');
// Handle the '^' character.
handle_single_char!(ca, cb, a_chars, b_chars, '^');
// Handle the '.' character.
handle_single_char!(ca, cb, a_chars, b_chars, '.');
// Handle digits with numerical comparison.
// We key off of the A character being a digit intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_digit() || cb.is_ascii_digit() {
let result = compare_numeric(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
// Handle letters with alphabetical comparison.
// We key off of the A character being alphabetical intentionally as we presume
// this indicates it will be the same at this position.
if ca.is_ascii_alphabetic() || cb.is_ascii_alphabetic() {
let result = compare_alphabetic(&mut a_chars, &mut b_chars);
if result != Ordering::Equal {
return result;
}
continue;
}
}
}
}
}
/// Skips characters that are not in the valid character set.
fn skip_invalid<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
while let Some(&c) = iter.peek() {
if is_valid_char(c) {
break;
}
iter.next();
}
}
/// Checks if a character is in the valid character set for comparison.
fn is_valid_char(c: char) -> bool {
matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '.' | '~' | '^')
}
/// Compares numerical prefixes by extracting numbers.
fn compare_numeric<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
let num_a = extract_number(iter_a);
let num_b = extract_number(iter_b);
num_a.cmp(&num_b)
}
/// Extracts a number from the iterator, skipping leading zeros.
fn extract_number<I: Iterator<Item = char>>(iter: &mut Peekable<I>) -> u64 {
// Skip leading zeros
while let Some(&'0') = iter.peek() {
iter.next();
}
let mut num = 0u64;
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() {
iter.next();
num = num.saturating_mul(10).saturating_add(c as u64 - '0' as u64);
} else {
break;
}
}
num
}
/// Compares alphabetical prefixes
/// Capital letters compare lower than lowercase letters (B < a)
fn compare_alphabetic<I: Iterator<Item = char>>(
iter_a: &mut Peekable<I>,
iter_b: &mut Peekable<I>,
) -> Ordering {
loop {
return match (iter_a.peek(), iter_b.peek()) {
(Some(&ca), Some(&cb)) if ca.is_ascii_alphabetic() && cb.is_ascii_alphabetic() => {
if ca == cb {
// Same character, we should continue.
iter_a.next();
iter_b.next();
continue;
}
// Different characters found.
// All capital letters compare lower than lowercase letters.
match (ca.is_ascii_uppercase(), cb.is_ascii_uppercase()) {
(true, false) => Ordering::Less, // uppercase < lowercase
(false, true) => Ordering::Greater, // lowercase > uppercase
(true, true) => ca.cmp(&cb), // both are uppercase
(false, false) => ca.cmp(&cb), // both are lowercase
}
}
(Some(&ca), Some(_)) if ca.is_ascii_alphabetic() => {
// a has letters, b doesn't
Ordering::Greater
}
(Some(_), Some(&cb)) if cb.is_ascii_alphabetic() => {
// b has letters, a doesn't
Ordering::Less
}
(Some(&ca), None) if ca.is_ascii_alphabetic() => Ordering::Greater,
(None, Some(&cb)) if cb.is_ascii_alphabetic() => Ordering::Less,
_ => Ordering::Equal,
};
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
/// Define the SBAT attestation by including the sbat.csv file. /// Define the SBAT attestation by including the sbat.csv file.
/// See this document for more details: https://github.com/rhboot/shim/blob/main/SBAT.md /// See this document for more details: <https://github.com/rhboot/shim/blob/main/SBAT.md>
/// NOTE: This data must be aligned by 512 bytes. /// NOTE: This data must be aligned by 512 bytes.
#[used] #[used]
#[unsafe(link_section = ".sbat")] #[unsafe(link_section = ".sbat")]
+1 -1
View File
@@ -13,7 +13,7 @@ pub struct ChainloadConfiguration {
#[serde(default)] #[serde(default)]
pub options: Vec<String>, pub options: Vec<String>,
/// An optional path to a Linux initrd. /// An optional path to a Linux initrd.
/// This uses the [LINUX_EFI_INITRD_MEDIA_GUID] mechanism to load the initrd into the EFI stack. /// This uses the `LINUX_EFI_INITRD_MEDIA_GUID` mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is /// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future. /// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")] #[serde(default, rename = "linux-initrd")]
+3
View File
@@ -18,4 +18,7 @@ pub struct EntryDeclaration {
/// The values to insert into the context when the entry is selected. /// The values to insert into the context when the entry is selected.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
/// The key to sort entries, via version comparison.
#[serde(default, rename = "sort-key")]
pub sort_key: Option<String>,
} }
+1 -2
View File
@@ -78,8 +78,7 @@ pub struct RootConfiguration {
/// Options configuration for Sprout, used when the corresponding options are not specified. /// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct OptionsConfiguration { pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to mark as the default entry, instead of the first entry.
/// If not specified, a boot menu is shown.
#[serde(rename = "default-entry", default)] #[serde(rename = "default-entry", default)]
pub default_entry: Option<String>, pub default_entry: Option<String>,
/// The timeout of the boot menu. /// The timeout of the boot menu.
+1
View File
@@ -19,3 +19,4 @@ uefi-raw.workspace = true
[lib] [lib]
name = "eficore" name = "eficore"
path = "src/lib.rs" path = "src/lib.rs"
test = false
+15 -6
View File
@@ -5,6 +5,7 @@ use alloc::format;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::warn;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::{Guid, guid}; use uefi::{Guid, guid};
use uefi_raw::table::runtime::VariableVendor; use uefi_raw::table::runtime::VariableVendor;
@@ -245,9 +246,14 @@ impl BootloaderInterface {
} }
// Parse the value as a u64 to decode an numeric value. // Parse the value as a u64 to decode an numeric value.
let value = value // An invalid value is ignored, as a bad variable should not prevent booting.
.parse::<u64>() let value = match value.parse::<u64>() {
.context("unable to parse timeout value")?; Ok(value) => value,
Err(error) => {
warn!("ignoring invalid {} value '{}': {}", key, value, error);
return Ok(None);
}
};
// The specification says that a value of 0 means that the menu should be hidden. // The specification says that a value of 0 means that the menu should be hidden.
if value == 0 { if value == 0 {
@@ -289,9 +295,11 @@ impl BootloaderInterface {
/// Get the default entry set by the bootloader interface. /// Get the default entry set by the bootloader interface.
pub fn get_default_entry() -> Result<Option<String>> { pub fn get_default_entry() -> Result<Option<String>> {
Self::VENDOR // An empty value is treated as if the default entry was not set.
Ok(Self::VENDOR
.get_cstr16("LoaderEntryDefault") .get_cstr16("LoaderEntryDefault")
.context("unable to get default entry from bootloader interface") .context("unable to get default entry from bootloader interface")?
.filter(|value| !value.is_empty()))
} }
/// Get the oneshot entry set by the bootloader interface. /// Get the oneshot entry set by the bootloader interface.
@@ -312,6 +320,7 @@ impl BootloaderInterface {
.context("unable to remove oneshot entry")?; .context("unable to remove oneshot entry")?;
// Return the oneshot value. // Return the oneshot value.
Ok(Some(value)) // An empty value is treated as if the oneshot entry was not set.
Ok(Some(value).filter(|value| !value.is_empty()))
} }
} }
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn args() -> Result<Vec<String>> {
.next() .next()
// Filter out unprintable characters and backticks. // Filter out unprintable characters and backticks.
// Both of which have been observed in the wild. // Both of which have been observed in the wild.
.map(|c| c < 0x1f as char || c == '`') .map(|c| c.is_ascii_control() || c == '`')
.unwrap_or(false) .unwrap_or(false)
}) })
.collect(); .collect();
+5 -2
View File
@@ -1,5 +1,8 @@
//! Sprout EFI Core. //! Sprout EFI core.
//! This crate provides tools for working with the EFI environment. //! This crate provides core EFI functionality for Sprout.
// For some reason this triggers, and I can't figure out why.
#![allow(rustdoc::bare_urls)]
#![no_std] #![no_std]
extern crate alloc; extern crate alloc;
+12 -10
View File
@@ -83,16 +83,6 @@ impl ImageLoader {
// If the image loader is installed, we can skip over the security hook. // If the image loader is installed, we can skip over the security hook.
let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available; let requires_security_hook = secure_boot && shim_loaded && !shim_loader_available;
// If the security hook is required, we will bail for now.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// If the shim is loaded, we will need to retain the shim protocol to allow // If the shim is loaded, we will need to retain the shim protocol to allow
// loading multiple images. // loading multiple images.
if shim_loaded { if shim_loaded {
@@ -114,6 +104,18 @@ impl ImageLoader {
file_path: input.file_path(), file_path: input.file_path(),
}; };
// If the security hook is required, install it right before loading the image.
// Installing it here ensures that an error while preparing the image can't
// return without uninstalling the hook.
if requires_security_hook {
// Install the security hook, if possible. If it's not, this is necessary to continue,
// so we should bail.
let installed = SecurityHook::install().context("unable to install security hook")?;
if !installed {
bail!("unable to install security hook required for this platform");
}
}
// Loads the image using Boot Services LoadImage function. // Loads the image using Boot Services LoadImage function.
let result = uefi::boot::load_image(current_image, source).context("unable to load image"); let result = uefi::boot::load_image(current_image, source).context("unable to load image");
+14 -3
View File
@@ -3,6 +3,7 @@ use alloc::vec::Vec;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use core::ffi::c_void; use core::ffi::c_void;
use core::ptr; use core::ptr;
use log::error;
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::build::DevicePathBuilder; use uefi::proto::device_path::build::DevicePathBuilder;
use uefi::proto::device_path::build::media::Vendor; use uefi::proto::device_path::build::media::Vendor;
@@ -33,8 +34,7 @@ struct MediaLoaderProtocol {
} }
/// Represents a media loader which has been registered in the UEFI stack. /// Represents a media loader which has been registered in the UEFI stack.
/// You MUST call [MediaLoaderHandle::unregister] when ready to unregister. /// Calling `drop` on this handle will unregister the media loader.
/// [Drop] is not implemented for this type.
pub struct MediaLoaderHandle { pub struct MediaLoaderHandle {
/// The handle of the media loader in the UEFI stack. /// The handle of the media loader in the UEFI stack.
handle: Handle, handle: Handle,
@@ -255,7 +255,7 @@ impl MediaLoaderHandle {
/// Unregisters a media loader from the UEFI stack. /// Unregisters a media loader from the UEFI stack.
/// This will free the memory allocated by the passed data. /// This will free the memory allocated by the passed data.
pub fn unregister(self) -> Result<()> { fn unregister(&self) -> Result<()> {
// SAFETY: We know that the media loader is registered if the handle is valid, // SAFETY: We know that the media loader is registered if the handle is valid,
// so we can safely uninstall it. // so we can safely uninstall it.
// We should have allocated the pointers involved, so we can safely free them. // We should have allocated the pointers involved, so we can safely free them.
@@ -293,3 +293,14 @@ impl MediaLoaderHandle {
Ok(()) Ok(())
} }
} }
/// Implement drop for the handle to automatically unregister the media loader.
impl Drop for MediaLoaderHandle {
fn drop(&mut self) {
// If unregister fails, print an error to the log.
// This may leak stuff, but the only other option is to panic.
if let Err(error) = self.unregister() {
error!("unable to unregister media loader: {}", error);
}
}
}
+13 -9
View File
@@ -34,7 +34,7 @@ impl ResolvedPath {
let mut fs = FileSystem::new(fs); let mut fs = FileSystem::new(fs);
let path = self let path = self
.sub_path .sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))?; .to_string16(DisplayOnly(false), AllowShortcuts(false))?;
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")
} }
@@ -48,8 +48,8 @@ fn cstring16_contains_char(string: &CString16, c: char) -> bool {
/// 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: impl AsRef<str>) -> Result<PoolDevicePath> {
let path = CString16::try_from(path).context("unable to convert path to CString16")?; let path = CString16::try_from(path.as_ref()).context("unable to convert path to CString16")?;
let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>( let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>(
uefi::boot::get_handle_for_protocol::<DevicePathFromText>() uefi::boot::get_handle_for_protocol::<DevicePathFromText>()
.context("no device path from text protocol")?, .context("no device path from text protocol")?,
@@ -68,7 +68,7 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
let mut path = path let mut path = path
.node_iter() .node_iter()
.filter_map(|item| { .filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); let item = item.to_string16(DisplayOnly(false), AllowShortcuts(false));
if item if item
.as_ref() .as_ref()
.map(|item| cstring16_contains_char(item, '(')) .map(|item| cstring16_contains_char(item, '('))
@@ -93,7 +93,7 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let path = path let path = path
.node_iter() .node_iter()
.filter_map(|item| { .filter_map(|item| {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); let item = item.to_string16(DisplayOnly(false), AllowShortcuts(false));
if item if item
.as_ref() .as_ref()
.map(|item| cstring16_contains_char(item, '(')) .map(|item| cstring16_contains_char(item, '('))
@@ -113,19 +113,23 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
/// 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: Option<&DevicePath>, input: &str) -> Result<ResolvedPath> { pub fn resolve_path(
let mut path = text_to_device_path(input).context("unable to convert text to path")?; default_root_path: Option<&DevicePath>,
input: impl ToString,
) -> Result<ResolvedPath> {
let mut input = input.to_string();
let mut path = text_to_device_path(&input).context("unable to convert text to path")?;
let path_has_device = path let path_has_device = path
.node_iter() .node_iter()
.next() .next()
.map(|it| { .map(|it| {
it.to_string(DisplayOnly(false), AllowShortcuts(false)) it.to_string16(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();
if !input.starts_with('\\') { if !input.starts_with('\\') {
input.insert(0, '\\'); input.insert(0, '\\');
} }
+9
View File
@@ -38,6 +38,15 @@ impl PlatformTpm {
/// The PCR for measuring the bootloader configuration into. /// The PCR for measuring the bootloader configuration into.
pub const PCR_BOOT_LOADER_CONFIG: PcrIndex = PcrIndex(5); pub const PCR_BOOT_LOADER_CONFIG: PcrIndex = PcrIndex(5);
/// The PCR for measuring the dom0 initrd payload into.
pub const PCR_INITRD: PcrIndex = PcrIndex(9);
/// The PCR for measuring the dom0 kernel payload into.
pub const PCR_KERNEL: PcrIndex = PcrIndex(11);
/// The PCR for measuring kernel command line and xen options into.
pub const PCR_KERNEL_PARAMETERS: PcrIndex = PcrIndex(12);
/// Acquire access to the TPM protocol handle, if possible. /// Acquire access to the TPM protocol handle, if possible.
/// Returns None if TPM is not available. /// Returns None if TPM is not available.
fn protocol() -> Result<Option<TpmProtocolHandle>> { fn protocol() -> Result<Option<TpmProtocolHandle>> {
+2 -2
View File
@@ -82,9 +82,9 @@ impl<'a> ShimInput<'a> {
}; };
// Convert the underlying path to a string. // Convert the underlying path to a string.
let path = path let path = path
.to_string(DisplayOnly(false), AllowShortcuts(false)) .to_string16(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device path to string")?; .context("unable to convert device path to string")?;
let path = crate::path::resolve_path(None, &path.to_string()) let path = crate::path::resolve_path(None, path.to_string())
.context("unable to resolve path")?; .context("unable to resolve path")?;
// Read the file path. // Read the file path.
let data = path.read_file()?; let data = path.read_file()?;
+14 -6
View File
@@ -2,11 +2,11 @@ use crate::shim::{ShimInput, ShimSupport, ShimVerificationOutput};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use core::slice; use core::slice;
use log::warn; use log::warn;
use spin::{Lazy, Mutex}; use spin::{LazyLock, Mutex};
use uefi::proto::device_path::FfiDevicePath; use uefi::proto::device_path::FfiDevicePath;
use uefi::proto::unsafe_protocol; use uefi::proto::unsafe_protocol;
use uefi::{Guid, guid}; use uefi::{Guid, guid};
use uefi_raw::Status; use uefi_raw::{Boolean, Status};
/// GUID for the EFI_SECURITY_ARCH protocol. /// GUID for the EFI_SECURITY_ARCH protocol.
const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839"); const SECURITY_ARCH_GUID: Guid = guid!("a46423e3-4617-49f1-b9ff-d1bfa9115839");
@@ -33,7 +33,7 @@ pub struct SecurityArch2Protocol {
path: *const FfiDevicePath, path: *const FfiDevicePath,
file_buffer: *const u8, file_buffer: *const u8,
file_size: usize, file_size: usize,
boot_policy: bool, boot_policy: Boolean,
) -> Status, ) -> Status,
} }
@@ -45,7 +45,8 @@ struct SecurityHookState {
/// Global state for the security hook. /// Global state for the security hook.
/// This is messy, but it is safe given the mutex. /// This is messy, but it is safe given the mutex.
static GLOBAL_HOOK_STATE: Lazy<Mutex<Option<SecurityHookState>>> = Lazy::new(|| Mutex::new(None)); static GLOBAL_HOOK_STATE: LazyLock<Mutex<Option<SecurityHookState>>> =
LazyLock::new(|| Mutex::new(None));
/// Security hook helper. /// Security hook helper.
pub struct SecurityHook; pub struct SecurityHook;
@@ -136,7 +137,7 @@ impl SecurityHook {
path: *const FfiDevicePath, path: *const FfiDevicePath,
file_buffer: *const u8, file_buffer: *const u8,
file_size: usize, file_size: usize,
boot_policy: bool, boot_policy: Boolean,
) -> Status { ) -> Status {
// Verify the path and file buffer are not null. // Verify the path and file buffer are not null.
if path.is_null() || file_buffer.is_null() { if path.is_null() || file_buffer.is_null() {
@@ -144,7 +145,7 @@ impl SecurityHook {
} }
// If the boot policy is true, we can't continue as we don't support that. // If the boot policy is true, we can't continue as we don't support that.
if boot_policy { if bool::from(boot_policy) {
return Status::INVALID_PARAMETER; return Status::INVALID_PARAMETER;
} }
@@ -215,6 +216,13 @@ impl SecurityHook {
// Acquire the lock to the global state and replace it. // Acquire the lock to the global state and replace it.
let mut global_state = GLOBAL_HOOK_STATE.lock(); let mut global_state = GLOBAL_HOOK_STATE.lock();
// If the hook is already installed, the protocols contain our hooks.
// Storing them as the original hooks would cause infinite recursion on
// verification failure, so keep the existing state instead.
if global_state.is_some() {
return Ok(true);
}
global_state.replace(state); global_state.replace(state);
// Install the hooks into the UEFI stack. // Install the hooks into the UEFI stack.
+10 -2
View File
@@ -10,13 +10,21 @@ pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
} }
// Convert the bytes to UTF-16 data. // Convert the bytes to UTF-16 data.
let data = bytes let mut data = bytes
// Chunk everything into two bytes. // Chunk everything into two bytes.
.chunks_exact(2) .as_chunks::<2>()
.0
.iter()
// Reinterpret the bytes as u16 little-endian. // Reinterpret the bytes as u16 little-endian.
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
// Stop at the first null terminator, if any.
// This ignores any trailing data, such as extra null padding.
.take_while(|c| *c != 0)
// Collect the result into a vector. // Collect the result into a vector.
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Add the null terminator, as the input is not guaranteed to be null-terminated.
data.push(0);
CString16::try_from(data).context("unable to convert utf16 bytes to CString16") CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
} }
+19
View File
@@ -0,0 +1,19 @@
# This crate explicitly does not have uefi/uefi-raw dependencies,
# so that the contents can be unit-testable on non-UEFI target hosts.
# Do not add uefi or uefi-raw as dependencies.
[package]
name = "edera-sprout-parsing"
description = "Sprout Parsing Utilities (UEFI-free)"
license.workspace = true
version.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
[dependencies]
hex.workspace = true
sha2.workspace = true
[lib]
name = "edera_sprout_parsing"
path = "src/lib.rs"
+417
View File
@@ -0,0 +1,417 @@
#![no_std]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::Reverse;
use sha2::{Digest, Sha256};
/// Stamps the `text` value with the specified `values` map. The returned value indicates
/// whether the `text` has been changed and the value that was stamped and changed.
///
/// Stamping works like this:
/// - Start with the input text.
/// - Sort all the keys in reverse length order (longest keys first)
/// - For each key, if the key is not empty, replace $KEY in the text.
/// - Each follow-up iteration acts upon the last iterations result.
/// - We keep track if the text changes during the replacement.
/// - We return both whether the text changed during any iteration and the final result.
pub fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;
// 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);
if result != next_result {
did_change = true;
}
result = next_result;
}
(did_change, result)
}
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce:
/// x: a, y: c
/// x: a, y: d
/// x: b, y: c
/// x: b, y: d
pub fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = alloc::vec![BTreeMap::new()];
for (key, values) in items {
let mut new_result = Vec::new();
// Produce all the combinations of the input values.
for combination in &result {
for value in &values {
let mut new_combination = combination.clone();
new_combination.insert(key.clone(), value.clone());
new_result.push(new_combination);
}
}
result = new_result;
}
result.into_iter().filter(|item| !item.is_empty()).collect()
}
/// 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 {
hex::encode(Sha256::digest(input.as_bytes()))
}
/// 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())
}
/// Build a Xen EFI stub configuration file from pre-stamped `xen_options` and `kernel_options`.
/// The returned string is in the Xen ini-like config file format.
pub fn build_xen_config(xen_options: &str, kernel_options: &str) -> String {
[
// global section
"[global]",
// default configuration section
"default=sprout",
// configuration section for sprout
"[sprout]",
// xen options
&format!("options={}", xen_options),
// kernel options, stub replaces the kernel path
// the kernel is provided via media loader
&format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored
"",
]
.join("\n")
}
/// Filename prefixes used to identify Linux kernel images.
/// These must be lowercase, as file names are lowercased before matching.
pub const LINUX_KERNEL_PREFIXES: &[&str] = &["vmlinuz", "image"];
/// Filename prefixes used to identify initramfs images paired with a kernel.
pub const LINUX_INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// Check whether `name` (already lowercased) matches one of the `kernel_prefixes`,
/// either exactly or as a dash-separated prefix (e.g. `"vmlinuz-6.1"`).
/// Returns the matched prefix string if found.
pub fn match_kernel_prefix<'a>(name: &str, kernel_prefixes: &[&'a str]) -> Option<&'a str> {
kernel_prefixes
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
.copied()
}
/// Generate initramfs candidate filenames by combining each entry of `initramfs_prefixes`
/// with `suffix`. The caller is expected to check which candidates actually exist.
pub fn initramfs_candidates<'a>(
suffix: &'a str,
initramfs_prefixes: &'a [&'a str],
) -> impl Iterator<Item = String> + 'a {
initramfs_prefixes
.iter()
.map(move |prefix| format!("{}{}", prefix, suffix))
}
#[cfg(test)]
mod tests {
use super::*;
fn map(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn stamp_replaces_known_key() {
let values = map(&[("name", "world")]);
let (changed, result) = stamp_values(&values, "hello $name");
assert!(changed);
assert_eq!(result, "hello world");
}
#[test]
fn stamp_no_match_returns_unchanged() {
let values = map(&[("name", "world")]);
let (changed, result) = stamp_values(&values, "hello there");
assert!(!changed);
assert_eq!(result, "hello there");
}
#[test]
fn stamp_longer_key_takes_precedence_over_shorter() {
// Without longest-first ordering, "$ab" would be partially matched by "$a"
let values = map(&[("a", "WRONG"), ("ab", "RIGHT")]);
let (changed, result) = stamp_values(&values, "$ab");
assert!(changed);
assert_eq!(result, "RIGHT");
}
#[test]
fn stamp_empty_key_is_skipped() {
let values = map(&[("", "should-not-appear"), ("x", "val")]);
let (_, result) = stamp_values(&values, "$x");
assert_eq!(result, "val");
assert!(!result.contains("should-not-appear"));
}
#[test]
fn stamp_multiple_keys_replaced() {
let values = map(&[("a", "foo"), ("b", "bar")]);
let (changed, result) = stamp_values(&values, "$a and $b");
assert!(changed);
assert_eq!(result, "foo and bar");
}
#[test]
fn stamp_empty_text_returns_empty() {
let values = map(&[("a", "foo")]);
let (changed, result) = stamp_values(&values, "");
assert!(!changed);
assert_eq!(result, "");
}
#[test]
fn stamp_empty_map_returns_unchanged() {
let values = map(&[]);
let (changed, result) = stamp_values(&values, "hello $name");
assert!(!changed);
assert_eq!(result, "hello $name");
}
fn matrix_map(pairs: &[(&str, &[&str])]) -> BTreeMap<String, Vec<String>> {
pairs
.iter()
.map(|(k, vs)| (k.to_string(), vs.iter().map(|v| v.to_string()).collect()))
.collect()
}
#[test]
fn matrix_single_key_produces_one_entry_per_value() {
let input = matrix_map(&[("x", &["a", "b", "c"])]);
let result = build_matrix(&input);
assert_eq!(result.len(), 3);
}
#[test]
fn matrix_two_keys_produces_cartesian_product() {
let input = matrix_map(&[("x", &["a", "b"]), ("y", &["c", "d"])]);
let result = build_matrix(&input);
assert_eq!(result.len(), 4);
// Every combination of x and y should be present.
for x in &["a", "b"] {
for y in &["c", "d"] {
assert!(
result
.iter()
.any(|m| m.get("x").map(|s| s.as_str()) == Some(x)
&& m.get("y").map(|s| s.as_str()) == Some(y))
);
}
}
}
#[test]
fn matrix_empty_input_produces_no_entries() {
let input = matrix_map(&[]);
let result = build_matrix(&input);
assert!(result.is_empty());
}
#[test]
fn matrix_key_with_empty_values_produces_no_entries() {
let input = matrix_map(&[("x", &[])]);
let result = build_matrix(&input);
assert!(result.is_empty());
}
#[test]
fn combine_options_joins_with_space() {
let result = combine_options(["a", "b", "c"].iter().copied());
assert_eq!(result, "a b c");
}
#[test]
fn combine_options_skips_empty_strings() {
let result = combine_options(["a", "", "b"].iter().copied());
assert_eq!(result, "a b");
}
#[test]
fn combine_options_all_empty_returns_empty() {
let result = combine_options(["", ""].iter().copied());
assert_eq!(result, "");
}
#[test]
fn combine_options_empty_iterator_returns_empty() {
let result = combine_options(core::iter::empty::<&str>());
assert_eq!(result, "");
}
#[test]
fn empty_is_none_returns_none_for_empty_string() {
assert!(empty_is_none(Some("")).is_none());
}
#[test]
fn empty_is_none_returns_some_for_nonempty_string() {
assert_eq!(empty_is_none(Some("x")), Some("x"));
}
#[test]
fn empty_is_none_passthrough_on_none() {
assert!(empty_is_none(None::<&str>).is_none());
}
#[test]
fn unique_hash_is_deterministic() {
assert_eq!(unique_hash("hello"), unique_hash("hello"));
}
#[test]
fn unique_hash_differs_for_different_inputs() {
assert_ne!(unique_hash("hello"), unique_hash("world"));
}
#[test]
fn unique_hash_empty_string() {
// SHA-256 of "" is well-known; just check it produces a non-empty hex string.
let h = unique_hash("");
assert!(!h.is_empty());
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn xen_config_contains_global_and_sprout_sections() {
let config = build_xen_config("", "");
assert!(config.contains("[global]"));
assert!(config.contains("[sprout]"));
assert!(config.contains("default=sprout"));
}
#[test]
fn xen_config_embeds_xen_options() {
let config = build_xen_config("--no-real-mode --iommu=no", "");
assert!(config.contains("options=--no-real-mode --iommu=no"));
}
#[test]
fn xen_config_embeds_kernel_options() {
let config = build_xen_config("", "quiet splash");
assert!(config.contains("kernel=stub quiet splash"));
}
#[test]
fn xen_config_ends_with_newline() {
// Required or the last line will be ignored by the Xen config parser.
let config = build_xen_config("", "");
assert!(config.ends_with('\n'));
}
#[test]
fn kernel_prefix_exact_match() {
assert_eq!(
match_kernel_prefix("vmlinuz", LINUX_KERNEL_PREFIXES),
Some("vmlinuz")
);
}
#[test]
fn kernel_prefix_dash_suffix_match() {
assert_eq!(
match_kernel_prefix("vmlinuz-6.1.0", LINUX_KERNEL_PREFIXES),
Some("vmlinuz")
);
}
#[test]
fn kernel_prefix_matches_lowercased_arm64_image() {
// Callers lowercase file names before matching, so "Image-6.1" arrives as "image-6.1".
assert_eq!(
match_kernel_prefix("image-6.1", LINUX_KERNEL_PREFIXES),
Some("image")
);
assert_eq!(
match_kernel_prefix("image", LINUX_KERNEL_PREFIXES),
Some("image")
);
}
#[test]
fn kernel_prefix_case_sensitive_no_match() {
// match_kernel_prefix expects the caller to lowercase first; uppercase input won't match.
assert!(match_kernel_prefix("VMLINUZ-6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn kernel_prefix_no_match() {
assert!(match_kernel_prefix("initramfs-6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn kernel_prefix_partial_no_match() {
// "vmlinuz6.1" has no dash separator — should not match.
assert!(match_kernel_prefix("vmlinuz6.1", LINUX_KERNEL_PREFIXES).is_none());
}
#[test]
fn initramfs_candidates_with_suffix() {
let candidates: Vec<_> = initramfs_candidates("-6.1.0", LINUX_INITRAMFS_PREFIXES).collect();
assert_eq!(
candidates,
&["initramfs-6.1.0", "initrd-6.1.0", "initrd.img-6.1.0"]
);
}
#[test]
fn initramfs_candidates_empty_suffix() {
let candidates: Vec<_> = initramfs_candidates("", LINUX_INITRAMFS_PREFIXES).collect();
assert_eq!(candidates, &["initramfs", "initrd", "initrd.img"]);
}
#[test]
fn initramfs_candidates_empty_prefixes() {
let candidates: Vec<_> = initramfs_candidates("-6.1.0", &[]).collect();
assert!(candidates.is_empty());
}
}
+8 -6
View File
@@ -60,7 +60,7 @@ Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.e
## Step 4: Sign Sprout for Secure Boot ## Step 4: Sign Sprout for Secure Boot
```bash ```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call. # For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \ $ sbsign \
--key /etc/sprout/secure-boot/mok.key \ --key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \ --cert /etc/sprout/secure-boot/mok.crt \
@@ -120,8 +120,8 @@ path = "\\EFI\\sprout\\ext4.efi"
# global options. # global options.
[options] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting installed kernels
# filesystems and generating boot entries for them. # generating boot entries for them.
autoconfigure = true autoconfigure = true
``` ```
@@ -130,14 +130,16 @@ If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry ## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/ESP_PARTITION with the actual path to the ESP partition block device. In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash ```bash
# For x86_64, run this command to add Sprout as the default boot entry. # For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi' $ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry. # For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi' $ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
``` ```
Reboot your machine and it should boot into Sprout. Reboot your machine and it should boot into Sprout.
+172
View File
@@ -0,0 +1,172 @@
# Setup Sprout for Fedora with Secure Boot
## Prerequisites
- Modern Fedora release: tested on Fedora 43 x86_64.
- EFI System Partition mounted on `/boot/efi` (the default)
- You will need the following packages installed: `openssl`, `mokutil`, `sbsigntools`, `efibootmgr`
**NOTE**: Fedora on ARM64 itself does not support Secure Boot consistently.
## Step 1: Generate and Install Secure Boot Key
```bash
# Create a directory to store the Secure Boot MOK key and certificates.
$ mkdir -p /etc/sprout/secure-boot
# Change to the created directory.
$ cd /etc/sprout/secure-boot
# Generate a MOK key and certificate.
$ openssl req \
-newkey rsa:4096 -nodes -keyout mok.key \
-new -x509 -sha256 -days 3650 -subj '/CN=Sprout Secure Boot/' \
-out mok.crt
# Generate a DER encoded certificate for enrollment.
$ openssl x509 -outform DER -in mok.crt -out mok.cer
# Import the certificate into the Secure Boot environment.
# This will ask you to make a password that will be used during enrollment.
$ mokutil --import mok.cer
# Reboot your machine.
# During boot, MOK enrollment should appear. If it doesn't, ensure you are booting into the shim.
# Press any key to begin MOK management. Select "Enroll MOK".
# Select "View key 0", and ensure the subject says "CN=Sprout Secure Boot".
# If the subject does not match, something has gone wrong with MOK enrollment.
# Press Enter to continue, then select the "Continue" option.
# When it asks to enroll the key, select the "Yes" option.
# Enter the password that you created during the mokutil --import step.
# Select "Reboot" to boot back into your Operating System.
```
## Step 2: Prepare the Secure Boot Environment
```bash
# Create a directory for Sprout EFI artifacts.
$ mkdir -p /boot/efi/EFI/sprout
# For x86_64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimx64.efi /boot/efi/EFI/sprout/shimx64.efi
$ cp /boot/efi/EFI/fedora/mmx64.efi /boot/efi/EFI/sprout/mmx64.efi
# For aarch64, copy the following artifacts to the Sprout EFI directory.
$ cp /boot/efi/EFI/fedora/shimaa64.efi /boot/efi/EFI/sprout/shimaa64.efi
$ cp /boot/efi/EFI/fedora/mmaa64.efi /boot/efi/EFI/sprout/mmaa64.efi
```
## Step 3: Install Unsigned Sprout
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM64 systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.efi` on your EFI System Partition.
## Step 4: Sign Sprout for Secure Boot
```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubx64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
# For aarch64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/grubaa64.efi \
/boot/efi/EFI/sprout/sprout.unsigned.efi
```
## Step 5: Install and Sign EFI Drivers
You will need a filesystem EFI driver if `/boot` is not FAT32 or ExFAT.
### ext4
Most Fedora systems use an ext4 filesystem for `/boot`, if that is the case, use the ext4 instructions here:
Install the necessary `edk2-ext4` package:
```bash
# Install the ext4 driver from the package manager.
$ dnf install edk2-ext4
```
Copy the ext4 driver to `/boot/efi/EFI/sprout/ext4.unsigned.efi`:
```bash
# For x86_64, copy the ext4x64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4x64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
# For aarch64, copy the ext4aa64.efi driver to the Sprout EFI directory.
$ cp /usr/share/edk2/drivers/ext4aa64.efi /boot/efi/EFI/sprout/ext4.unsigned.efi
```
```bash
# Sign the ext4 driver at ext4.unsigned.efi, placing it at ext4.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/ext4.efi \
/boot/efi/EFI/sprout/ext4.unsigned.efi
```
### Other Filesystems
If you need another driver, you can download EFI filesystem drivers from [EfiFs releases](https://github.com/pbatard/EfiFs/releases).
Copy the driver to `/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi` for signing, then sign it like this:
```bash
# Sign your driver, placing it at DRIVER_NAME.efi, which will be used in the configuration.
$ sbsign \
--key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \
--output /boot/efi/EFI/sprout/DRIVER_NAME.efi \
/boot/efi/EFI/sprout/DRIVER_NAME.unsigned.efi
```
You will add the driver in your Sprout configuration below, like this:
```toml
[drivers.DRIVER_NAME]
path = "\\EFI\\sprout\\DRIVER_NAME.efi"
```
## Step 6: Create Sprout Configuration
Write the following to the file `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an ext4 EFI driver.
# skip this if you do not have a filesystem driver.
# if your filesystem driver is not named ext4, change accordingly.
[drivers.ext4]
path = "\\EFI\\sprout\\ext4.efi"
# global options.
[options]
# enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them.
autoconfigure = true
```
Ensure you add the signed driver paths to the configuration, not the unsigned ones.
If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash
# For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
```
Reboot your machine and it should boot into Sprout.
If Sprout fails to boot, it should boot into the original bootloader.
+4 -2
View File
@@ -128,11 +128,13 @@ If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry ## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/ESP_PARTITION with the actual path to the ESP partition block device. In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash ```bash
# Run this command to add Sprout as the default boot entry. # Run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shim.efi' $ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shim.efi'
``` ```
Reboot your machine and it should boot into Sprout. Reboot your machine and it should boot into Sprout.
+10 -6
View File
@@ -59,7 +59,7 @@ Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/sprout/sprout.unsigned.e
## Step 4: Sign Sprout for Secure Boot ## Step 4: Sign Sprout for Secure Boot
```bash ```bash
# For x86_64, sign the unsigned Sprout artifact and name it grubaa64.efi which is what the shim will call. # For x86_64, sign the unsigned Sprout artifact and name it grubx64.efi which is what the shim will call.
$ sbsign \ $ sbsign \
--key /etc/sprout/secure-boot/mok.key \ --key /etc/sprout/secure-boot/mok.key \
--cert /etc/sprout/secure-boot/mok.crt \ --cert /etc/sprout/secure-boot/mok.crt \
@@ -119,8 +119,8 @@ path = "\\EFI\\sprout\\ext4.efi"
# global options. # global options.
[options] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting installed kernels
# filesystems and generating boot entries for them. # generating boot entries for them.
autoconfigure = true autoconfigure = true
``` ```
@@ -129,14 +129,18 @@ If you do not have any drivers, exclude the drivers section entirely.
## Step 7: Configure Sprout Boot Entry ## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/ESP_PARTITION with the actual path to the ESP partition block device. ## Step 7: Configure Sprout Boot Entry
In the following commands, replace /dev/BLOCK_DEVICE with the device that houses your GPT partition table,
and PARTITION_NUMBER with the partition number of the EFI System Partition. For example, if your EFI System Partition is
`/dev/sda1`, the BLOCK_DEVICE would be `/dev/sda` and the PARTITION_NUMBER would be `1`
```bash ```bash
# For x86_64, run this command to add Sprout as the default boot entry. # For x86_64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi' $ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimx64.efi'
# For aarch64, run this command to add Sprout as the default boot entry. # For aarch64, run this command to add Sprout as the default boot entry.
$ efibootmgr -d /dev/ESP_PARTITION -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi' $ efibootmgr -d /dev/BLOCK_DEVICE -p PARTITION_NUMBER -c -L 'Sprout' -l '\EFI\sprout\shimaa64.efi'
``` ```
Reboot your machine and it should boot into Sprout. Reboot your machine and it should boot into Sprout.
+12 -6
View File
@@ -30,9 +30,9 @@ if [ "${QEMU_GDB_WAIT}" = "1" ]; then
set -- "${@}" "-S" set -- "${@}" "-S"
fi fi
set -- "${@}" -smp 2 -m 4096 set -- "${@}" -nodefaults -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then if [ "${NO_GRAPHICAL}" = "1" ]; then
set -- "${@}" -nographic set -- "${@}" -nographic
else else
if [ "${GRAPHICAL_ONLY}" != "1" ]; then if [ "${GRAPHICAL_ONLY}" != "1" ]; then
@@ -40,9 +40,9 @@ else
set -- "${@}" -serial stdio set -- "${@}" -serial stdio
else else
set -- "${@}" \ set -- "${@}" \
-device virtio-serial-pci,id=vs0 \ -device 'virtio-serial-pci,id=vs0' \
-chardev stdio,id=stdio0 \ -chardev 'stdio,id=stdio0,signal=off' \
-device virtconsole,chardev=stdio0,id=console0 -device 'virtconsole,chardev=stdio0,id=console0,name=alpine'
fi fi
fi fi
@@ -62,6 +62,12 @@ if [ "${NO_INPUT}" != "1" ]; then
-device usb-mouse -device usb-mouse
fi fi
if [ "${NO_NETWORK}" != "1" ]; then
set -- "${@}" \
-netdev 'user,id=network0' \
-device 'virtio-net-pci,netdev=network0'
fi
rm -f "${FINAL_DIR}/ovmf-boot.fd" rm -f "${FINAL_DIR}/ovmf-boot.fd"
cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd" cp "${FINAL_DIR}/ovmf.fd" "${FINAL_DIR}/ovmf-boot.fd"
if [ "${TARGET_ARCH}" = "aarch64" ]; then if [ "${TARGET_ARCH}" = "aarch64" ]; then
@@ -70,7 +76,7 @@ fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
set -- "${@}" \ 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'
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"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS build FROM --platform=$BUILDPLATFORM debian:trixie@sha256:9cc080028c43b27d2074d63a5f9caf7166d731494965616c1a6d2827a004585c AS build
ARG BUILDPLATFORM ARG BUILDPLATFORM
ARG EFI_NAME ARG EFI_NAME
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
+7 -19
View File
@@ -48,7 +48,7 @@ copy_from_image_polyfill() {
SOURCE="${2}" SOURCE="${2}"
TARGET="${3}" TARGET="${3}"
docker build -t "${IMAGE}-copy-polyfill:${DOCKER_TAG}" --build-arg "TARGET_IMAGE=${IMAGE}:${DOCKER_TAG}" \ docker build --platform="${DOCKER_TARGET}" -t "${IMAGE}-copy-polyfill:${DOCKER_TAG}" --build-arg "TARGET_IMAGE=${IMAGE}:${DOCKER_TAG}" \
-f hack/dev/utils/Dockerfile.copy-polyfill hack -f hack/dev/utils/Dockerfile.copy-polyfill hack
# note: the -w '//' is a workaround for Git Bash where / is magically rewritten. # note: the -w '//' is a workaround for Git Bash where / is magically rewritten.
docker run --rm -i -w '//' "${IMAGE}-copy-polyfill:${DOCKER_TAG}" cat "image/${SOURCE}" >"${TARGET}" docker run --rm -i -w '//' "${IMAGE}-copy-polyfill:${DOCKER_TAG}" cat "image/${SOURCE}" >"${TARGET}"
@@ -72,6 +72,7 @@ if [ "${SKIP_KERNEL_BUILD}" != "1" ]; then
fi fi
copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.efi" "${FINAL_DIR}/kernel.efi" copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.efi" "${FINAL_DIR}/kernel.efi"
copy_from_image "${DOCKER_PREFIX}/sprout-kernel-${TARGET_ARCH}" "kernel.modules.tgz" "${FINAL_DIR}/kernel.modules.tgz"
fi fi
if [ "${SKIP_VM_BUILD}" != "1" ]; then if [ "${SKIP_VM_BUILD}" != "1" ]; then
@@ -80,8 +81,12 @@ if [ "${SKIP_VM_BUILD}" != "1" ]; then
-f hack/dev/vm/Dockerfile.ovmf "${FINAL_DIR}" -f hack/dev/vm/Dockerfile.ovmf "${FINAL_DIR}"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "ovmf.fd" "${FINAL_DIR}/ovmf.fd" copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "ovmf.fd" "${FINAL_DIR}/ovmf.fd"
copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "shell.efi" "${FINAL_DIR}/shell.efi" copy_from_image "${DOCKER_PREFIX}/sprout-ovmf-${TARGET_ARCH}" "shell.efi" "${FINAL_DIR}/shell.efi"
rm -rf "${FINAL_DIR}/initramfs.build"
mkdir -p "${FINAL_DIR}/initramfs.build"
cp -r "hack/dev/vm/files" "${FINAL_DIR}/initramfs.build/files"
cp "${FINAL_DIR}/kernel.modules.tgz" "${FINAL_DIR}/initramfs.build/kernel.modules.tgz"
docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}:${DOCKER_TAG}" \ docker build --platform="${DOCKER_TARGET}" -t "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}:${DOCKER_TAG}" \
-f hack/dev/vm/Dockerfile.initramfs "${FINAL_DIR}" -f hack/dev/vm/Dockerfile.initramfs "${FINAL_DIR}/initramfs.build"
copy_from_image "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}" "initramfs" "${FINAL_DIR}/initramfs" copy_from_image "${DOCKER_PREFIX}/sprout-initramfs-${TARGET_ARCH}" "initramfs" "${FINAL_DIR}/initramfs"
if [ -n "${SPROUT_XEN_EFI_OVERRIDE}" ]; then if [ -n "${SPROUT_XEN_EFI_OVERRIDE}" ]; then
@@ -108,23 +113,6 @@ 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/configs/bls.conf" "${FINAL_DIR}/bls.conf" cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf"
mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI"
if [ -f "${FINAL_DIR}/kernel.efi" ]; then
cp "${FINAL_DIR}/kernel.efi" "${FINAL_DIR}/efi/EFI/BOOT/KERNEL.EFI"
fi
if [ -f "${FINAL_DIR}/shell.efi" ]; then
cp "${FINAL_DIR}/shell.efi" "${FINAL_DIR}/efi/EFI/BOOT/SHELL.EFI"
fi
if [ -f "${FINAL_DIR}/xen.efi" ]; then
cp "${FINAL_DIR}/xen.efi" "${FINAL_DIR}/efi/EFI/BOOT/XEN.EFI"
fi
if [ -f "${FINAL_DIR}/xen.cfg" ]; then
cp "${FINAL_DIR}/xen.cfg" "${FINAL_DIR}/efi/EFI/BOOT/XEN.CFG"
fi
cp "${FINAL_DIR}/sprout.toml" "${FINAL_DIR}/efi/SPROUT.TOML"
cp "${FINAL_DIR}/initramfs" "${FINAL_DIR}/efi/INITRAMFS"
fi fi
if [ "${SKIP_BOOT_BUILD}" != "1" ]; then if [ "${SKIP_BOOT_BUILD}" != "1" ]; then
+1 -1
View File
@@ -8,7 +8,7 @@ has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\vmlinuz" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0", "overlaytmpfs=yes"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel] [entries.kernel]
+1 -1
View File
@@ -9,7 +9,7 @@ has-item = "\\vmlinuz"
[actions.chainload-kernel] [actions.chainload-kernel]
chainload.path = "$boot\\vmlinuz" chainload.path = "$boot\\vmlinuz"
chainload.options = ["console=hvc0"] chainload.options = ["console=hvc0", "overlaytmpfs=yes"]
chainload.linux-initrd = "$boot\\initramfs" chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel] [entries.kernel]
+2 -2
View File
@@ -3,5 +3,5 @@ default=sprout
[sprout] [sprout]
options=clocksource=tsc smp=on smt=on ioapic_ack=new dom0_vcpus_pin=on spec-ctrl=gds-mit=no noreboot console=com1 options=clocksource=tsc smp=on smt=on ioapic_ack=new dom0_vcpus_pin=on spec-ctrl=gds-mit=no noreboot console=com1
kernel=\EFI\BOOT\KERNEL.EFI console=hvc0 kernel=\VMLINUZ console=hvc0
ramdisk=\initramfs ramdisk=\INITRAMFS
+6 -3
View File
@@ -1,7 +1,7 @@
ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.17.2.tar.xz ARG KERNEL_SOURCE_URL=https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.54.tar.xz
ARG KERNEL_CHECKSUM=sha256:fdebcb065065f5c1b8dc68a6fb59cda50cdddbf9103d207c2196d55ea764f57f ARG KERNEL_CHECKSUM=sha256:9df30b02dd8102bbd0be52556288ef6889ddbe7f1ddb96fbf847d0becf3eacac
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS buildenv FROM --platform=$BUILDPLATFORM debian:trixie@sha256:9cc080028c43b27d2074d63a5f9caf7166d731494965616c1a6d2827a004585c AS buildenv
RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y \
build-essential squashfs-tools python3-yaml \ build-essential squashfs-tools python3-yaml \
patch diffutils sed mawk findutils zstd \ patch diffutils sed mawk findutils zstd \
@@ -32,6 +32,9 @@ ENV BUILDPLATFORM=${BUILDPLATFORM}
ENV TARGETPLATFORM=${TARGETPLATFORM} ENV TARGETPLATFORM=${TARGETPLATFORM}
WORKDIR /build/src WORKDIR /build/src
RUN /build/docker-build.sh RUN /build/docker-build.sh
COPY --chown=build:build docker-install.sh /build/docker-install.sh
RUN /build/docker-install.sh
FROM scratch AS final FROM scratch AS final
COPY --from=build /build/src/kernel.image /kernel.efi COPY --from=build /build/src/kernel.image /kernel.efi
COPY --from=build /build/src/kernel.modules.tgz /kernel.modules.tgz
+19 -1
View File
@@ -28,21 +28,39 @@ else
exit 1 exit 1
fi fi
echo "CROSS_COMPILE=${MAYBE_CROSS_COMPILE}" > kernel.buildenv
echo "TARGET_KARCH=${TARGET_KARCH}" >> kernel.buildenv
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" ]; then if [ "${TARGET_KARCH}" = "x86_64" ]; 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
fi fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2yesconfig
./scripts/config -e IPV6
./scripts/config -e BPF_SYSCALL
./scripts/config -e UEVENT_HELPER
./scripts/config --set-str UEVENT_HELPER_PATH "/sbin/hotplug"
./scripts/config -e SYN_COOKIES
./scripts/config -d DEBUG_STACK_USAGE
./scripts/config -e DRM_VIRTIO_GPU ./scripts/config -e DRM_VIRTIO_GPU
./scripts/config -e FRAMEBUFFER_CONSOLE ./scripts/config -e FRAMEBUFFER_CONSOLE
./scripts/config -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY ./scripts/config -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY
./scripts/config -e LOGO
./scripts/config -e XEN_DOM0 ./scripts/config -e XEN_DOM0
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2noconfig
make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}"
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image [ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image [ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image
exit 0 exit 0
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
. /build/src/kernel.buildenv
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" INSTALL_MOD_PATH="/build/install" modules_install
cd /build/install
tar czpf /build/src/kernel.modules.tgz .
+1 -1
View File
@@ -1 +1 @@
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 FROM --platform=$BUILDPLATFORM debian:trixie@sha256:9cc080028c43b27d2074d63a5f9caf7166d731494965616c1a6d2827a004585c
+1 -1
View File
@@ -1,4 +1,4 @@
ARG TARGET_IMAGE=scratch ARG TARGET_IMAGE=scratch
FROM ${TARGET_IMAGE} AS image FROM ${TARGET_IMAGE} AS image
FROM --platform=$BUILDPLATFORM debian:trixie@sha256:fd8f5a1df07b5195613e4b9a0b6a947d3772a151b81975db27d47f093f60c6e6 AS final FROM --platform=$BUILDPLATFORM debian:trixie@sha256:9cc080028c43b27d2074d63a5f9caf7166d731494965616c1a6d2827a004585c AS final
COPY --from=image / /image COPY --from=image / /image
+8 -5
View File
@@ -1,5 +1,5 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS rootfs FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS rootfs
RUN apk --no-cache add alpine-base tzdata RUN apk --no-cache add alpine-base tzdata wireless-regdb ifupdown-ng agetty
RUN rc-update add devfs sysinit && \ RUN rc-update add devfs sysinit && \
rc-update add dmesg sysinit && \ rc-update add dmesg sysinit && \
rc-update add mdev sysinit && \ rc-update add mdev sysinit && \
@@ -7,6 +7,7 @@ RUN rc-update add devfs sysinit && \
rc-update add sysctl boot && \ rc-update add sysctl boot && \
rc-update add hostname boot && \ rc-update add hostname boot && \
rc-update add bootmisc boot && \ rc-update add bootmisc boot && \
rc-update add networking boot && \
rc-update add syslog boot && \ rc-update add syslog boot && \
rc-update add mount-ro shutdown && \ rc-update add mount-ro shutdown && \
rc-update add killprocs shutdown && \ rc-update add killprocs shutdown && \
@@ -14,11 +15,13 @@ RUN rc-update add devfs sysinit && \
ln -s /sbin/init /init && \ ln -s /sbin/init /init && \
echo 'root:root' | chpasswd && \ echo 'root:root' | chpasswd && \
echo 'sprout' > /etc/hostname && \ echo 'sprout' > /etc/hostname && \
echo '' > /etc/motd && \ rm /etc/motd && \
ln -s /usr/share/zoneinfo/UTC /etc/localtime && \ ln -s /usr/share/zoneinfo/UTC /etc/localtime && \
echo 'hvc0::respawn:/sbin/getty -L hvc0 115200 vt100' >> /etc/inittab echo 'hvc0::respawn:/sbin/agetty --autologin root -L hvc0 115200 vt100' >> /etc/inittab
ADD kernel.modules.tgz /
COPY files/interfaces /etc/network/interfaces
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS build
COPY --from=rootfs / /rootfs COPY --from=rootfs / /rootfs
WORKDIR /rootfs WORKDIR /rootfs
RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs RUN find . | cpio -R 0:0 --ignore-devno --renumber-inodes -o -H newc --quiet > /initramfs
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/x86_64" ]; then \ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/x86_64" ]; then \
apk --no-cache add ovmf edk2-shell; cp /usr/share/ovmf/bios.bin /ovmf.fd; fi apk --no-cache add ovmf edk2-shell; cp /usr/share/ovmf/bios.bin /ovmf.fd; fi
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 AS build FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apk add --no-cache xen-hypervisor && cp /usr/lib/efi/xen.efi /xen.efi RUN apk add --no-cache xen-hypervisor && cp /usr/lib/efi/xen.efi /xen.efi
+3
View File
@@ -0,0 +1,3 @@
auto eth0
iface eth0
use dhcp
+1 -1
View File
@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.91.0" channel = "1.98.1"
components = ["rustfmt", "clippy"] components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"] targets = ["x86_64-unknown-uefi", "aarch64-unknown-uefi"]