From 9127e968a77514cd65df60cca0952707fef3330b Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 4 Oct 2026 14:08:02 -0700 Subject: [PATCH] docs(config): add CONFIG.md and move the configuration reference out of the README CONFIG.md documents every section of sprout.toml, the command line options, BLS support and strict mode. The README keeps the overview and minimal examples and links to it. Adds an update-config-docs skill to keep the reference in sync with the code. --- .claude/skills/update-config-docs/SKILL.md | 83 +++ CLAUDE.md | 3 +- CONFIG.md | 561 +++++++++++++++++++++ README.md | 191 +------ crates/config/src/lib.rs | 2 +- 5 files changed, 655 insertions(+), 185 deletions(-) create mode 100644 .claude/skills/update-config-docs/SKILL.md create mode 100644 CONFIG.md diff --git a/.claude/skills/update-config-docs/SKILL.md b/.claude/skills/update-config-docs/SKILL.md new file mode 100644 index 0000000..a961d79 --- /dev/null +++ b/.claude/skills/update-config-docs/SKILL.md @@ -0,0 +1,83 @@ +--- +name: update-config-docs +description: Bring CONFIG.md back in line with the code. Use after changing anything in crates/config, the command line options, loader.conf handling, strict mode, or autoconfiguration, or when asked to check that CONFIG.md is accurate. +--- + +# Update CONFIG.md + +`CONFIG.md` is the reference for `sprout.toml`, the command line options, BLS support and strict mode. +The README only links to it, so a setting that is missing from CONFIG.md is undocumented. + +## 1. Find what changed + +Start from the diff, or from the whole config surface if there is none: + +```bash +git diff main -- crates/config crates/boot/src/options.rs crates/boot/src/config crates/boot/src/autoconfigure crates/bls +``` + +The sources of truth, and the section of CONFIG.md each one feeds: + +| Source | Section | +|-----------------------------------------------------------|-------------------------------------------| +| `crates/config/src/lib.rs` | file at a glance, `options` | +| `crates/config/src/drivers.rs` | `drivers` | +| `crates/config/src/extractors*` | `extractors` | +| `crates/config/src/actions*` | `actions` | +| `crates/config/src/entries.rs` | `entries` | +| `crates/config/src/generators*` | `generators`, `variants`, `exclude` | +| `crates/config/src/phases.rs` | `phases` | +| `crates/boot/src/options.rs` | command line options | +| `crates/boot/src/main.rs` | boot order, defaults, timeout and default entry order, strict mode | +| `crates/boot/src/autoconfigure*` | autoconfiguration | +| `crates/boot/src/generators/bls.rs`, `crates/bls` | BLS values, boot counting, `loader.conf` | + +## 2. Check each key + +For every struct field in `crates/config`, CONFIG.md needs its TOML name (look at `#[serde(rename)]`, as +most multi-word keys use dashes), its type, its default, and what it does. Read the code that uses it in +`crates/boot`, not only the doc comment, because the behavior is what the reader depends on. + +Look for the usual drift: + +- a field added, renamed or removed +- a default that changed (`#[serde(default = ...)]`, the `Default` impls, constants such as + `DEFAULT_MENU_TIMEOUT_SECONDS`) +- a new BLS value set with `context.set`, or a new option in `SproutOptions` +- a new row to add to, or remove from, the strict mode table +- a changed order: the default entry sources, the menu timeout sources, or the phases +- `LATEST_VERSION` changing, which the intro and `version` text must follow + +Don't document what you can't confirm in the code. If a behavior is unclear, read the function that +implements it. + +## 3. Write the change + +- Keep each section's shape: a short explanation, a TOML example, then a table of keys. +- Examples must parse. Check that every key in one exists and is spelled the way serde expects. +- Update the table of contents if you add or rename a heading. +- Keep README.md short. It gets the feature list and the minimal examples, and CONFIG.md gets everything else. +- Don't duplicate the strict mode table or the default entry order anywhere else. + +## 4. Write like a person + +Plain sentences, said once. Skip these: + +- filler like "robust", "seamless", "powerful", "leverage", "ensure", "note that", "it's important to" +- groups of three added for rhythm, and a summary sentence at the end of every section +- bullet lists for what a sentence says better, and bold used as decoration +- em dashes (use a comma, colon or parentheses) + +Say what a setting does and what happens when it's unset or wrong. Name real things (`bootctl`, `PCR 12`, +`\EFI\Linux`) instead of describing them. + +## 5. Verify + +```bash +cargo build --target x86_64-unknown-uefi +cargo build --target aarch64-unknown-uefi +cargo fmt --all --check +``` + +Then re-read the diff of CONFIG.md against the code once more for any key whose name or default you +touched. If the change was only to docs, skip the builds. diff --git a/CLAUDE.md b/CLAUDE.md index ece2a87..353ad37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,8 @@ Sprout is a programmable UEFI bootloader written in Rust, for `x86_64` and `aarch64`. It is a `no_std` workspace built for the `*-unknown-uefi` targets, so it cannot be run or tested like a normal binary. -See [README.md](README.md) for the configuration format and features. +See [README.md](README.md) for features and [CONFIG.md](CONFIG.md) for the configuration format. +After changing configuration, run the `update-config-docs` skill to keep CONFIG.md accurate. ## Layout diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 0000000..2fe662c --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,561 @@ +# Configuring Sprout + +Sprout reads a TOML file, `\sprout.toml`, from the root of the EFI partition it was loaded from. This page +covers everything that file can hold, the command line options that override it, and the BLS behavior that +sits next to it. For installing Sprout, see the [setup guides](./docs/setup). + +- [Command line options](#command-line-options) +- [The file at a glance](#the-file-at-a-glance) +- [Values](#values) +- [`options`](#options) +- [`drivers`](#drivers) +- [`extractors`](#extractors) +- [`actions`](#actions) +- [`entries`](#entries) +- [`generators`](#generators) +- [`phases`](#phases) +- [Autoconfiguration](#autoconfiguration) +- [Bootloader Specification support](#bootloader-specification-support) +- [Strict mode](#strict-mode) + +Sprout reads the file once at startup. A key it doesn't know is ignored with a warning that names the key, so +a typo shows up in the boot log instead of silently doing nothing. A `version` other than `1` stops Sprout. + +## Command line options + +These are passed to `sprout.efi` as load options, which is how GRUB or `efibootmgr` hand them over. They +override the matching setting in the file. + +| Option | What it does | +|---------------------------|-----------------------------------------------------------------------------| +| `--config=PATH` | Load a configuration file other than `\sprout.toml`. | +| `--autoconfigure` | Skip the configuration file and build one from what Sprout finds. | +| `--boot=ENTRY` | Boot an entry without showing the menu. See [entries](#entries) for matching. | +| `--force-menu` | Show the menu even when `--boot` picked an entry. | +| `--menu-style=STYLE` | `basic`, `simple` or `graphical`. | +| `--menu-timeout=SECONDS` | How long the menu waits before booting the default entry. | +| `--retain-boot-console` | Don't clear the screen before handing off to the entry. | +| `--bls-strict-mode` | Turn on [strict mode](#strict-mode). | +| `--help` | Print the options. | + +```bash +# Boot a specific configuration file. +$ sprout.efi --config=\path\to\config.toml +# Boot "Boot Xen" at once, but only show the menu if it can't be found. +$ sprout.efi --boot="Boot Xen" +# Show the graphical menu for 10 seconds. +$ sprout.efi --menu-style=graphical --menu-timeout=10 +``` + +An argument Sprout can't parse is dropped with a warning and the rest are used, since a typo in a boot +entry shouldn't leave a machine unbootable. + +## The file at a glance + +Every section is optional. An empty file is a valid configuration that has no entries. + +```toml +version = 1 + +[options] # settings for Sprout itself +[values] # named strings for use anywhere with $name +[drivers.x] # EFI drivers to load +[extractors.x] # values worked out at boot +[actions.x] # things Sprout can do +[entries.x] # items in the boot menu +[generators.x] # makes entries from data +[[phases.early]] # actions to run at set points +``` + +The pieces fit together like this: an **entry** is a menu item that runs **actions**; an action is configured +with strings that can contain **values**; values come from the `[values]` table, from **extractors**, and +from **generators**. **Drivers** run first so the filesystems the rest of the file points at exist. + +Order at boot: + +1. `phases.early` +2. drivers load +3. autoconfiguration, if enabled +4. extractors run +5. `phases.startup` +6. entries and generators produce the menu +7. an entry is chosen +8. `phases.late`, then the entry's actions + +## Values + +A value is a named string. Anywhere a string is accepted in an entry, action, driver path or phase, `$name` +is replaced with the value called `name`. + +```toml +[values] +root-options = "root=/dev/sda2 ro" +kernel = "\\vmlinuz" + +[actions.boot] +chainload.path = "$kernel" +chainload.options = ["$root-options", "quiet"] +``` + +Some details that matter in practice: + +- Values can refer to other values. Sprout keeps substituting until nothing changes, so + `a = "$b"`, `b = "x"` gives `x` for both. A value that never settles, such as one that contains itself, + is an error once it hits an iteration or length limit. +- When two keys share a prefix, the longer one wins, so `$kernel-options` is never read as `$kernel` plus + `-options`. +- A `$name` that has no value is left in the string as written. +- Values are layered. A key in a generator or entry overrides the same key set globally, and only for that + entry. From weakest to strongest: `[values]`, extractor results, generator values (and `variants`), then the + `values` of the entry itself. +- Windows-style paths need doubled backslashes in TOML strings: `"\\EFI\\Linux"`. Literal strings + (`'\EFI\Linux'`) work too. + +## `options` + +```toml +[options] +default-entry = "boot-linux" +menu-timeout = 5 +menu-style = "graphical" +autoconfigure = false +bls-strict-mode = false +``` + +| Key | Type | Default | Meaning | +|-------------------|---------|----------|-------------------------------------------------------------------------| +| `default-entry` | string | none | The entry that is selected first, and booted when the timeout ends. Without it, the first entry in the menu is. | +| `menu-timeout` | integer | see below | Seconds to show the menu before booting the default entry. | +| `menu-style` | string | `simple` | `basic` prints entries and takes a number. `simple` is a full-screen list moved with the arrow keys. `graphical` also takes the mouse. | +| `autoconfigure` | boolean | `false` | Add entries for what Sprout finds. See [autoconfiguration](#autoconfiguration). | +| `bls-strict-mode` | boolean | `false` | See [strict mode](#strict-mode). | + +When `menu-timeout` is not set anywhere, the menu shows for 10 seconds, or is hidden in strict mode. The +sources are ranked in [The bootloader interface](#the-bootloader-interface). + +## `drivers` + +Drivers are EFI executables that add features, most often a filesystem driver for something the firmware +can't read, such as ext4. Each is started once and expected to return. Afterwards Sprout reconnects every +controller so the new filesystems appear. Drivers have no defined load order. + +```toml +[drivers.ext4] +path = "\\sprout\\drivers\\ext4.efi" +``` + +| Key | Meaning | +|--------|--------------------------------------------------------| +| `path` | Path to the driver image. Required. Can use values. | + +The name (`ext4` above) is only for error messages and doesn't have to match the file. + +## `extractors` + +An extractor works out a value when Sprout boots, so a configuration doesn't need to hardcode where things +are. The name of the extractor is the name of the value it sets. + +There is one kind, `filesystem-device-match`, which finds a filesystem and returns its device root. The +result has a trailing slash, so you append a path to it directly: `$boot\\vmlinuz`. + +```toml +[extractors.boot] +filesystem-device-match.has-label = "BOOT" +filesystem-device-match.fallback = "PciRoot(0x0)/Pci(0x1,0x1)/Sata(0,0,0)/HD(1,GPT,c2a4f1b6-0000-0000-0000-000000000000)/" +``` + +| Key | Meaning | +|--------------------------|----------------------------------------------------------------------| +| `has-label` | The filesystem volume label equals this. | +| `has-item` | This file or directory exists on the filesystem. | +| `has-partition-uuid` | The unique GUID of the partition. | +| `has-partition-type-uuid`| The partition type GUID, such as the one for XBOOTLDR. | +| `fallback` | What to use when no filesystem matches. Without it, no match is an error. | + +Every criterion you give must match, and you must give at least one. The first matching filesystem wins. +`has-label` and `has-item` can use values. + +## `actions` + +An action is a named, configured piece of work. Entries and phases refer to actions by name, and an action +can be used by as many of them as you like. + +Each action sets exactly one of `chainload`, `print` or `edera`. If you set more than one Sprout warns and +uses the first of them in that order. + +### `chainload` + +Loads and starts another EFI image. That covers Linux through its EFI stub, Windows, and any other EFI +application. If the image returns, Sprout returns to its own flow. + +```toml +[actions.boot-linux] +chainload.path = "\\vmlinuz" +chainload.options = ["root=/dev/sda1", "quiet"] +chainload.linux-initrd = "\\initrd" +``` + +| Key | Type | Meaning | +|----------------------|-----------------|----------------------------------------------------------------------| +| `path` | string | The image to start. Required. | +| `options` | list of strings | Joined with spaces and given to the image as its command line. | +| `linux-initrd` | string | One initrd, handed to the Linux EFI stub through the initrd media loader. | +| `linux-initrd-chain` | list of strings | Several initrds, concatenated in order into one. Can't be used together with `linux-initrd`. | +| `devicetree` | string | A flattened devicetree to install until the image returns. | + +Notes: + +- `linux-initrd` is better than an `initrd=` option, because it doesn't depend on the stub resolving a path. +- Entries in `linux-initrd-chain` that are empty after substitution are skipped, which is what makes a + chain with spare slots safe. +- The devicetree is ignored with a warning when Secure Boot is on, as Sprout can't verify it. If it can't be + installed, Sprout warns and boots without it, unless [strict mode](#strict-mode) is on. +- When the image is a Windows boot manager or another non-Linux image, leave the initrd keys out. + +### `print` + +Writes a line to the console. It's mostly for phases, such as a message while drivers load. + +```toml +[actions.hello] +print.text = "Booting $version" +``` + +### `edera` + +Boots the Edera hypervisor and its root operating system, as an extension of the Xen EFI stub. Sprout hands +the Xen config, dom0 kernel and optional initrd to Xen as media loaders, then starts Xen. + +```toml +[actions.edera] +edera.xen = "\\EFI\\edera\\xen.efi" +edera.kernel = "\\EFI\\edera\\kernel" +edera.initrd = "\\EFI\\edera\\initrd" +edera.xen-options = ["dom0_mem=512M"] +edera.kernel-options = ["console=hvc0"] +``` + +| Key | Type | Meaning | +|------------------|-----------------|-------------------------------------------------------------| +| `xen` | string | The Xen EFI image. Required. | +| `kernel` | string | The dom0 kernel. Required. | +| `initrd` | string | The dom0 initrd. | +| `initrd-chain` | list of strings | Several initrds, concatenated in order. Can't be used with `initrd`. | +| `kernel-options` | list of strings | Command line of the dom0 kernel. | +| `xen-options` | list of strings | Command line of Xen. | + +When a TPM is present, the Xen options and dom0 command line are measured into PCR 12, the kernel into +PCR 11, and the initrd into PCR 9. + +## `entries` + +An entry is something the user can pick from the menu. The table name identifies it. + +```toml +[entries.boot-linux] +title = "Boot Linux" +actions = ["boot-linux"] +values.kernel = "\\vmlinuz" +sort-key = "10" +``` + +| Key | Type | Meaning | +|------------|-----------------|------------------------------------------------------------------------| +| `title` | string | The text in the menu. Required, and can use values. | +| `actions` | list of strings | Actions to run, in order, when the entry is chosen. | +| `values` | table | Values that exist only for this entry. | +| `sort-key` | string | Orders the entry in the menu. Compared as versions, not as text, so `10` sorts after `9`. Defaults to the entry's name. | + +The menu lists entries in reverse sort order, newest first. That is what you want for kernel versions and +is why `sort-key = "$version"` gives a sensible menu. + +`--boot` and the bootloader interface find an entry by its name, by its title, or by its position in the +menu counted from 0. A `*` in the pattern matches anything. + +If an entry fails Sprout reports the error and waits ten seconds. Then it exits to the firmware, or resets the +machine when `reboot-on-error` applies (see [boot counting](#boot-counting)). + +## `generators` + +A generator makes entries from data when Sprout starts. Each has a kind (`matrix`, `list` or `bls`), and +optionally `variants` and `exclude`, which work on the result of any kind. Set only one kind per generator. + +The entries a generator makes are named `-`, with `n` counting from 0, and a BLS generator +uses the name of the entry file instead. Every one of them is built from the `entry` template you give, +which takes the same keys as an [entry](#entries). + +### `matrix` + +Makes an entry for every combination of the lists you give. + +```toml +[generators.kernels] +matrix.entry.title = "Boot $kernel with $console" +matrix.entry.actions = ["boot-kernel"] +matrix.values.kernel = ["\\vmlinuz", "\\vmlinuz-lts"] +matrix.values.console = ["ttyS0", "tty0"] +``` + +That is four entries. A list of two and a list of three give six. + +### `list` + +Makes one entry per item. Each item is a table of values. Use it when the combinations aren't a clean grid. + +```toml +[generators.systems] +list.entry.title = "Boot $name" +list.entry.actions = ["boot-system"] +list.values = [ + { name = "Alpine", kernel = "\\alpine\\vmlinuz" }, + { name = "Debian", kernel = "\\debian\\vmlinuz" }, +] +``` + +### `bls` + +Makes entries from the files of the [Bootloader Specification](#bootloader-specification-support). + +```toml +[generators.bls] +bls.path = "\\loader" +bls.entry.title = "$title" +bls.entry.actions = ["boot-bls"] +``` + +| Key | Type | Default | Meaning | +|--------------|---------|-------------------|-------------------------------------------------------------------| +| `entry` | table | | The template entry. Required. | +| `path` | string | `\loader` | The directory that holds the `entries` directory. Can include a device, like `$boot\\loader`. | +| `uki-path` | string | `\EFI\Linux` on the device of `path` | Where to look for unified kernel images. An empty string turns them off. | +| `xbootldr` | boolean | `false` | Also read the extended boot loader partition of the same disk. | +| `pin-names` | boolean | `true` | Keep the entry file's name as the entry name, instead of prefixing the generator name. | + +Two generators reading the same directory produce entries with the same name. Sprout warns about it, since +picking by name becomes ambiguous. `pin-names` is what makes names match the ids that `bootctl` uses. + +The values each BLS entry provides are listed under [BLS values](#bls-values). + +### `variants` + +Variants multiply the entries of a generator. Each key is an axis, and each axis is a list of choices. +Every entry is made once for each combination of choices, taking one from each axis. + +```toml +[generators.kernels] +matrix.entry.title = "Boot $kernel ($console)" +matrix.entry.actions = ["boot-kernel"] +matrix.values.kernel = ["\\vmlinuz", "\\vmlinuz-lts"] +variants.console = [ + { name = "serial", values.console-options = "console=ttyS0" }, + { name = "graphics", values.console-options = "console=tty0" }, +] +``` + +This makes "Boot \vmlinuz (serial)", "Boot \vmlinuz (graphics)" and the same two for `\vmlinuz-lts`. Four +entries from a matrix of two kernels and two choices. + +A choice has a `name` and optional `values`. The axis name becomes a value holding the choice's name +(`$console` is `serial` here), and the choice's own values are added next to it. Names are appended to the +entry name, so the entries above are called `kernels-0-serial`, `kernels-0-graphics` and so on. Axes apply in +alphabetical order, choices in the order written. An axis with no choices, or two choices that share a name, +is an error. + +### `exclude` + +Drops generated entries. It's a list of rules, and a rule is a table of value names to patterns, where `*` +matches anything. An entry is dropped if every value in at least one rule matches. It runs after variants, +and compares values before they are substituted. + +```toml +[generators.bls] +# ... +exclude = [ + { version = "0-rescue-*" }, + { console = "graphics", mode = "debug" }, +] +``` + +The first rule drops BLS rescue entries. The second drops only the one combination. + +## `phases` + +Phases run actions at fixed points in the boot. Each phase is a list, and each item names actions and can +add values for them. + +| Phase | Runs | +|-----------|------------------------------------------------------------| +| `early` | Before drivers load. | +| `startup` | After drivers, autoconfiguration and extractors, before the menu is built. | +| `late` | After an entry is chosen, before its actions run. | + +```toml +[[phases.early]] +actions = ["splash"] + +[[phases.startup]] +actions = ["announce"] +values.message = "drivers are loaded" +``` + +Items run in order, and an action that fails stops the boot with an error. Note the double brackets: each +item is one element of a list. + +## Autoconfiguration + +With `autoconfigure = true`, or `--autoconfigure`, Sprout looks at every filesystem it can read and adds +configuration for what it finds. With the option, the result is added to your file. With the flag, your file +is not read at all, which makes it a good way to get a machine booting before you've written one. + +For each filesystem: + +- **BLS**: if it has `\loader\loader.conf`, a `\loader\entries` directory with `.conf` files, or `.efi` files + in `\EFI\Linux`, Sprout adds a `bls` generator and the action that boots its entries. These are named + `auto-bls-`. +- **Linux**: if there was no BLS, kernels in `\boot` and `\` are found by name (`vmlinuz` or `Image`, + optionally followed by `-` and a version), paired with an `initramfs`, `initrd` or `initrd.img` of the same + suffix. The entries are named `auto-linux-` and titled "Boot Linux" plus the path. Command line + options come from the value `linux-options`, which you can set in `[values]` and which otherwise is the + placeholder `placeholder`. +- **Windows**: if `\EFI\Microsoft\Boot\bootmgfw.efi` exists, an entry "Boot Windows" named + `auto-windows-` chainloads it. + +`` is a short hash of the filesystem's device path, so two disks never collide. [Strict +mode](#strict-mode) narrows which filesystems the BLS part reads. + +## Bootloader Specification support + +Sprout implements the [Boot Loader Specification](https://uapi-group.org/specifications/specs/boot_loader_specification/) +to read the entries that distributions install, and sorts them as the specification says. + +It reads Type #1 entries from `\loader\entries` and Type #2 unified kernel images (UKIs) from `\EFI\Linux`. A +UKI with several profiles is one entry per profile. Entries for another architecture than the machine's are +hidden. In a Type #1 entry Sprout uses the keys `title`, `version`, `machine-id`, `sort-key`, `linux`, `efi`, +`uki`, `initrd`, `options`, `devicetree`, `architecture` and `profile`. + +The simplest way to get this is `autoconfigure = true`. To write it out by hand, you need a `bls` generator +and an action that boots what it makes: + +```toml +[generators.bls] +bls.entry.title = "$title" +bls.entry.actions = ["boot-bls"] +bls.xbootldr = true + +[actions.boot-bls] +chainload.path = "$entry-root\\$chainload" +chainload.options = ["$options"] +chainload.devicetree = "$entry-root\\$devicetree" +chainload.linux-initrd-chain = [ + "$entry-root\\$initrd-0", + "$entry-root\\$initrd-1", + "$entry-root\\$initrd-2", + "$entry-root\\$initrd-3", +] +``` + +An entry can have up to 32 initrds, and Sprout warns when it has one that the chain doesn't list, because +the kernel likely can't then find its root. Autoconfiguration lists all 32. Unused slots are empty and +skipped, so listing more than you need costs nothing. + +### BLS values + +| Value | Holds | +|----------------------|----------------------------------------------------------------------------| +| `$title` | The title, with the version added unless it already contains it. | +| `$title-base` | The title as written in the entry. | +| `$version` | The `version` field. | +| `$machine-id` | The `machine-id` field. | +| `$chainload` | The path of the `linux`, `efi` or `uki` file. | +| `$options` | The `options` field, with the profile prepended for a UKI profile. | +| `$cmdline` | The command line embedded in a UKI. | +| `$uname` | The kernel version embedded in a UKI. | +| `$devicetree` | The `devicetree` path. | +| `$initrd-0` to `$initrd-31` | The initrds, in order. A slot with none is empty. | +| `$initrd` | The last initrd. | +| `$entry-root` | The device the entry's files are on. Empty for Sprout's own partition. | + +`$entry-root` matters once `xbootldr` is on, since an entry on the XBOOTLDR partition has its files there, +not next to Sprout. Always write BLS paths as `$entry-root\\$chainload`. + +### Boot counting + +A file named like `fedora+3.conf` or `fedora+3.efi` has three tries. Each boot renames it, say to +`fedora+2-1.conf`, and tells the system the new path in `LoaderBootCountPath` so `systemd-bless-boot` can +remove the counter once the boot works. + +Entries with no tries left sort last and are never the default, but can still be booted by hand. If an entry +fails to start after it used a try, and it had tries left, the machine resets so the next boot can use the +next try or pick another entry. This is the `reboot-on-error` setting of `loader.conf`. Without boot counting +a failure to start goes back to the firmware. + +### `loader.conf` + +Sprout reads `\loader\loader.conf` from the partition it was loaded from, as systemd-boot does. It is +measured into the TPM, since it changes how Sprout boots. + +| Key | Value | +|-------------------|--------------------------------------------------------------------| +| `default` | A pattern for the id of the default entry, or `@saved`. | +| `preferred` | Like `default`, but entries with no boot counter tries are skipped. | +| `timeout` | Seconds, `menu-hidden`, `menu-disabled` or `menu-force`. | +| `reboot-on-error` | `auto` (the default), `yes` or `no`. | + +The id of an entry is its file name without the boot counter, like `fedora.conf` or `fedora.efi`. Patterns +ignore case and can use `*`, `?` and `[a-z]`. With `@saved` the entry booted last is the default. + +`reboot-on-error = yes` resets after every failed start, which can loop forever. `auto` only resets when a +try was used up and some were left. Other keys are ignored with a warning. + +A hidden menu, from a timeout of zero or `menu-hidden`, still opens when a key is pressed. `menu-disabled` +does not. + +### The bootloader interface + +Sprout uses the same firmware variables as systemd-boot, so `bootctl`, `systemctl reboot --boot-loader-entry` +and `systemd-bless-boot` work with it. It publishes `LoaderEntries`, `LoaderEntrySelected`, +`LoaderBootCountPath`, `LoaderFeatures` and `LoaderInfo`, and reads `LoaderEntryDefault`, +`LoaderEntryPreferred`, `LoaderEntryOneShot`, `LoaderEntryLastBooted`, `LoaderConfigTimeout` and +`LoaderConfigTimeoutOneShot`. + +What `bootctl` sets outranks `sprout.toml`, which outranks `loader.conf`. The default entry is the first of +these that matches an entry: + +1. `LoaderEntryPreferred`, then `preferred` in `loader.conf` +2. `LoaderEntryDefault` +3. `default-entry` in `sprout.toml` +4. `default` in `loader.conf` + +The menu timeout is the first of these that is set: + +1. the one-shot timeout, `LoaderConfigTimeoutOneShot` +2. `--menu-timeout` +3. `LoaderConfigTimeout` +4. `menu-timeout` in `sprout.toml` +5. `timeout` in `loader.conf` + +A one-shot entry (`LoaderEntryOneShot`) is booted at once and without the menu, unless strict mode is on. + +## Strict mode + +By default Sprout departs from systemd-boot in a few places, where it is friendlier or safer for a +bootloader that reads the same files. Each departure is logged when it applies. `--bls-strict-mode`, or +`bls-strict-mode = true` under `[options]`, turns all of them off. + +| Behavior | By default | In strict mode | +|---------------------------------------------------|---------------------------------------------------------|---------------------------------------------------------| +| One-shot entry (`LoaderEntryOneShot`) | Booted at once, without the menu. | Only the default for this boot. The menu and its timeout still apply. | +| Menu timeout that nothing sets | The menu is shown for 10 seconds. | The menu is hidden. | +| Default entry with no boot counter tries | Skipped for another entry. | Used, as `default` ignores the tries. | +| Entry with more than one of `linux`, `efi`, `uki` | Boots the first of them. | Hidden. | +| Entry whose file does not exist | Shown, and it fails when booted. | Hidden. | +| UKI without a name | Named after its file. | Hidden. | +| Name and version of a UKI | Name is `PRETTY_NAME` or `ID`. Version is `IMAGE_VERSION`, `VERSION_ID`, `BUILD_ID`, then `.uname`. | What systemd-boot uses. Name is `PRETTY_NAME`, `IMAGE_ID`, `NAME` or `ID`. Version is `IMAGE_VERSION`, `VERSION`, `VERSION_ID` or `BUILD_ID`. | +| Boot counter of a plain `efi` entry | Counted. | Not counted, but its tries still make it bad. | +| Extended boot loader partition | Read when `xbootldr` is set on the generator. | Always read. | +| Autoconfiguration | Reads BLS entries from every disk, one generator each. | Reads BLS entries from Sprout's partition and the XBOOTLDR of its disk, as one generator. Windows and Linux entries are found as before. | +| Boot entry that returns | Sprout exits to the firmware. | The menu is shown again. | +| Devicetree that can't be installed | Warns and boots without it. | The entry fails. | + +In strict mode the actions of a hand-written generator have to use `$entry-root` for the files of an entry, +as entries can come from the extended boot loader partition. diff --git a/README.md b/README.md index 7949fe9..e3cd2ee 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ We recommend running Sprout without Secure Boot for development, and with Secure ### Project Documentation +- [Configuration Reference] - [Development Guide] - [Contributing Guide] - [Sprout License] @@ -110,39 +111,9 @@ We recommend running Sprout without Secure Boot for development, and with Secure Sprout is provided as a single EFI binary called `sprout.efi`. It can be chainloaded from GRUB or other UEFI bootloaders or booted into directly. -Sprout will look for \sprout.toml in the root of the EFI partition it was loaded from. -See [Configuration](#configuration) for how to configure sprout. - -## Configuration - -Sprout is configured using a TOML file at `\sprout.toml` on the root of the EFI partition sprout was booted from. - -### Command Line Options - -Sprout supports some command line options that can be combined to modify behavior without the configuration file. - -```bash -# Boot Sprout with a specific configuration file. -$ sprout.efi --config=\path\to\config.toml -# Boot a specific entry, bypassing the menu. -$ sprout.efi --boot="Boot Xen" -# Autoconfigure Sprout, without loading a configuration file. -$ sprout.efi --autoconfigure -# Use the basic boot menu instead of the simple one. -$ sprout.efi --menu-style=basic -# Use the graphical boot menu, which can be used with the mouse. -$ sprout.efi --menu-style=graphical -# Show the boot menu for 10 seconds before booting the default entry. -$ sprout.efi --menu-timeout=10 -# Show the boot menu even if an entry was chosen with --boot. -$ sprout.efi --force-menu -# Keep the boot console as it is when an entry is booted. -$ sprout.efi --retain-boot-console -# Follow the BLS specification and systemd-boot exactly. -$ sprout.efi --bls-strict-mode -``` +Sprout reads its configuration from `\sprout.toml` in the root of the EFI partition it was loaded from. -### Boot Linux from ESP +This is a configuration that boots a Linux kernel from the EFI partition: ```toml # sprout configuration: version 1 @@ -164,170 +135,24 @@ chainload.options = ["root=/dev/sda1"] chainload.linux-initrd = "\\initrd" ``` -### Bootloader Specification (BLS) Support +On a system that follows the Boot Loader Specification, you may not need to write one. With an ext4 driver +loaded, autoconfiguration finds the entries on its own: ```toml -# sprout configuration: version 1 version = 1 -# load an EFI driver for ext4. [drivers.ext4] path = "\\sprout\\drivers\\ext4.efi" -# global options. [options] -# enable autoconfiguration by detecting bls enabled -# filesystems and generating boot entries for them. autoconfigure = true ``` -Sprout reads Type #1 entries from `\loader\entries` and Type #2 unified kernel images (UKIs) from -`\EFI\Linux`, and sorts them as the specification says. A unified kernel image with several profiles -is one entry for each profile. Entries that are not for the architecture of the machine are hidden. -Type #1 entries can use `linux`, `efi`, `uki`, `initrd`, `options`, `devicetree`, `architecture`, -`profile`, `sort-key`, `version`, `machine-id`, and `title`. - -To set up the generator by hand instead of using autoconfiguration, add a generator and an action -that boots the entries. The entry values `$chainload`, `$options`, `$initrd-0` to `$initrd-7`, -`$devicetree`, `$cmdline`, `$title`, `$version`, and `$entry-root` are available. - -```toml -[generators.bls] -# the directory that has the entries directory. this is the default. -bls.path = "\\loader" -# the directory of unified kernel images. by default, this is \EFI\Linux on -# the device of the path. an empty path turns unified kernel images off. -bls.uki-path = "\\EFI\\Linux" -# also read the extended boot loader partition (XBOOTLDR) of the same disk, -# which is sorted with the other entries. strict mode always does. its files are on that partition, so -# the action has to use $entry-root, which is empty for Sprout's own partition. -bls.xbootldr = false -# keep the name of the entry file as the name of the entry, so it matches -# the ids that bootctl uses. -bls.pin-names = true -bls.entry.title = "$title" -bls.entry.actions = ["boot-bls"] - -[actions.boot-bls] -chainload.path = "$entry-root\\$chainload" -chainload.options = ["$options"] -chainload.devicetree = "$entry-root\\$devicetree" -# an entry can have up to 32 initrds, as $initrd-0 to $initrd-31. unused ones are skipped. -# only the first eight are listed here. add the others to boot an entry with more, -# as Sprout warns when an entry has an initrd that the chain does not list. -chainload.linux-initrd-chain = [ - "$entry-root\\$initrd-0", - "$entry-root\\$initrd-1", - "$entry-root\\$initrd-2", - "$entry-root\\$initrd-3", - "$entry-root\\$initrd-4", - "$entry-root\\$initrd-5", - "$entry-root\\$initrd-6", - "$entry-root\\$initrd-7", -] -``` - -An entry that names a `devicetree` boots with that devicetree installed for the image, which is put -back when the image returns. It is not used when Secure Boot is enabled, as it can't be verified. -If it can't be installed, Sprout warns and boots without it, unless strict mode is on. - -#### Boot counting - -An entry file named like `fedora+3.conf` or `fedora+3.efi` has three tries. Each time Sprout boots it, the -file is renamed, such as to `fedora+2-1.conf`, and the new path is given to the system in -`LoaderBootCountPath` so `systemd-bless-boot` can remove the counter once the boot works. -Entries that have no tries left are sorted last and are not picked as the default entry, but they can still be -booted by hand. By default, if an entry fails to start after a try was used up and it had tries left, the machine -resets, so the next boot can use the next try or another entry. This is the `reboot-on-error` setting -in `loader.conf`. Without boot counting, a failure to start returns to the firmware. - -#### loader.conf - -Sprout reads `\loader\loader.conf` from the partition it was loaded from, as systemd-boot does. - -| Key | Value | -|-------------------|--------------------------------------------------------------------| -| `default` | A pattern for the id of the default entry, or `@saved`. | -| `preferred` | Like `default`, but entries with no boot counter tries are skipped. | -| `timeout` | Seconds, `menu-hidden`, `menu-disabled`, or `menu-force`. | -| `reboot-on-error` | `auto` (the default), `yes`, or `no`. | - -The id of an entry is the name of its file without the boot counter, such as `fedora.conf` or `fedora.efi`. -Patterns ignore case and can use `*`, `?`, and `[a-z]`. With `@saved`, the entry that was booted last is the -default. With `reboot-on-error`, `yes` always resets after an entry fails to start, which can loop forever, -and `auto` only does when a boot counter try was used up and there were tries left. -Other keys are ignored with a warning. - -A hidden menu, from a timeout of zero or `menu-hidden`, still opens when a key is pressed. `menu-disabled` does not. - -#### Bootloader interface - -Sprout uses the same variables as systemd-boot, so `bootctl`, `systemctl reboot --boot-loader-entry`, and -`systemd-bless-boot` can work with it. It publishes `LoaderEntries`, `LoaderEntrySelected`, -`LoaderBootCountPath`, `LoaderFeatures`, and `LoaderInfo`, and reads `LoaderEntryDefault`, -`LoaderEntryPreferred`, `LoaderEntryOneShot`, `LoaderEntryLastBooted`, `LoaderConfigTimeout`, -and `LoaderConfigTimeoutOneShot`. - -The values that tools like `bootctl` set in the bootloader interface outrank `sprout.toml`, which outranks -`loader.conf`. In strict mode, `LoaderEntryOneShot` is tried before everything else for the default entry. - -The default entry comes from the first of these that matches an entry: - -1. `LoaderEntryPreferred`, then `preferred` in `loader.conf` -2. `LoaderEntryDefault` -3. `default-entry` in `sprout.toml` -4. `default` in `loader.conf` - -The menu timeout comes from the first of the one-shot timeout, `--menu-timeout`, `LoaderConfigTimeout`, -`menu-timeout` in `sprout.toml`, and `loader.conf`. - -#### Strict mode - -By default, Sprout differs from systemd-boot in a few places that are friendlier or safer for a bootloader that -reads the same files. Each of them is logged when it applies. `--bls-strict-mode`, or `bls-strict-mode = true` -in the options of `sprout.toml`, removes all of them. - -| Behavior | By default | In strict mode | -|------------------------------------------|-----------------------------------------------------------|-----------------------------------------------------------| -| One-shot entry (`LoaderEntryOneShot`) | Booted at once, without the menu. | Only the default for this boot. The menu and its timeout still apply. | -| Menu timeout that nothing sets | The menu is shown for 10 seconds. | The menu is hidden. | -| Default entry with no boot counter tries | Skipped for another entry. | Used, as `default` ignores the tries. | -| Entry with more than one of `linux`, `efi`, `uki` | Boots the first of them. | Hidden. | -| Entry whose file does not exist | Shown, and it fails when booted. | Hidden. | -| Unified kernel image without a name | Named after its file. | Hidden. | -| Name and version of a unified kernel image | The name is `PRETTY_NAME` or `ID`, and the version is `IMAGE_VERSION`, `VERSION_ID`, `BUILD_ID`, then `.uname`. | The fields systemd-boot uses: the name is `PRETTY_NAME`, `IMAGE_ID`, `NAME` or `ID`, and the version is `IMAGE_VERSION`, `VERSION`, `VERSION_ID` or `BUILD_ID`. | -| Boot counter of a plain `efi` entry | Counted. | Not counted, but its tries still make it bad. | -| The extended boot loader partition | Read when `xbootldr` is set on the generator. | Always read. | -| Autoconfiguration | Reads BLS entries from every disk, one generator each. | Reads BLS entries from Sprout's partition and the XBOOTLDR of its disk, as one generator. Windows and Linux entries are found as before. | -| Boot entry that returns | Sprout exits to the firmware. | The menu is shown again. | -| Devicetree that can't be installed | Warns and boots without it. | The entry fails. | - -In strict mode, the actions of a hand-written generator have to use `$entry-root` for the files of -an entry, as entries can come from the extended boot loader partition. - -### Generators - -Generators make entries when Sprout starts. The `matrix` generator makes an entry for every combination of -its values, the `list` generator makes an entry for each item of a list, and the `bls` generator makes -entries from BLS files. Variants multiply the entries of any generator, and `exclude` removes some of them. - -```toml -# makes an entry for each kernel and each console, such as "Boot \vmlinuz (serial)". -[generators.kernels] -matrix.entry.title = "Boot $kernel ($console)" -matrix.entry.actions = ["boot-kernel"] -matrix.values.kernel = ["\\vmlinuz", "\\vmlinuz-lts"] -variants.console = [ - { name = "serial", values.console-options = "console=ttyS0" }, - { name = "graphics", values.console-options = "console=tty0" }, -] - -[actions.boot-kernel] -chainload.path = "$kernel" -chainload.options = ["$console-options"] -``` +The full list of settings, the command line options, generators, and how Sprout treats BLS entries, +`loader.conf` and strict mode are in [CONFIG.md](./CONFIG.md). [Edera]: https://edera.dev +[Configuration Reference]: ./CONFIG.md [Development Guide]: ./DEVELOPMENT.md [Contributing Guide]: ./CONTRIBUTING.md [Sprout License]: ./LICENSE diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index ee1c180..e65e219 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -94,7 +94,7 @@ pub struct OptionsConfiguration { /// Makes Sprout follow the Boot Loader Specification and systemd-boot exactly where it /// otherwise differs for convenience or safety. For example, the extended boot loader /// partition is always read, a one-shot entry only sets the default entry, and entries with - /// missing files are hidden. See the README for everything it changes. + /// missing files are hidden. See CONFIG.md for everything it changes. #[serde(rename = "bls-strict-mode", default)] pub bls_strict_mode: bool, }