Subject: GCD006: Rewrite bootloader subsystem

From: Lilah Tascheter <[email-masked]>
Date: Fri, 15 Aug 2025 15:13:49 -0500

hey everyone!

about a year ago I proposed a patch series rewriting bootloader handling
in Guix. seeing the new GCD process, I figured it was perfect for
gathering discussion and consent for the rewrite. so, I rewrote
the rewrite, and I'm proposing it here!

this is a draft; if anyone has suggestions at this stage, please let me
know! I'll need at least one sponsor, as well, for this to move to the
discussion period.

lilah

title: Bootloader Subsystem Rewrite
id: 006
status: draft
discussion: https://issues.guix.gnu.org/79248
authors: Lilah Tascheter
sponsors: <Sponsor Name>
date: <date when the discussion period starts>
SPDX-License-Identifier: CC-BY-SA-4.0 OR GFDL-1.3-no-invariants-or-later
---

# Summary

Guix's bootloader handling was originally designed solely for GRUB but, with
greater adoption and widened system compatibility, this has proved insufficient.
Unfortunately, core aspects of bootloader handling have remained unchanged,
resulting in a system hard to fit new bootloaders into and confusing for
end-users to configure.

We propose an in-depth rewrite in order to create a more flexible and easier to
understand interface for both end users and bootloader packagers.

Prior work has been done as patchsets #69343, #73202, and #72457 on Mumi. This
GCD is a new proposal improving upon these patches.

# Motivation

We identify three main ways the current bootloader handling proves insufficient:
1. Structural assumptions of GRUB exist in both user-facing bootloader
   configuration options as well as the packager-facing bootloader interface.
2. Multiple special cases exist in bootloader packaging and installation, making
   it incredibly easy to package bootloaders that either install onto the host
   during disk-image building or fail during system rollback.
3. Bootloader packaging is ill-documented and relies on a confusing interface,
   which is especially bad for such a sensitive part of system configuration.

Take, first, the user-facing side of things. 11/14 fields of the
`<bootloader-configuration>` record are only used in GRUB, with multiple having
no imaginable use for non-GRUB bootloaders in the first place. Theoretically,
new bootloaders could add fields to the record if they need their own
configuration, but any external channels would not be able to do even that.

Disk image handling is not well-specified, especially when attempting to create
a disk image with a custom bootloader. Examples provide `/dev/vda1`, so a user
would assume that Guix looks for that magic string, but instead bootloader
handling uses a special install procedure. Notably, an unused target must be
specified anyway.

The `<menu-entry>` record is, also, made solely with GRUB in mind, even though
dual-booting is theoretically something that should be supported by bootloaders
in general. Fields are documented with semantics based solely on how GRUB
interprets them.

More notably, the current state of affairs hampers bootloader packaging
significantly. The `<bootloader>` record contains the following fields:
* `bootloader-name`: containing a symbol supposed to uniquely identify a
  bootloader package, for use in rollbacks. Notably, u-boot bootloaders ignore
  this requirement, to potentially disasterous consequences. It doesn't help
  that the only time one would notice problems with this (for example, a
  channel's bootloader defined outside gnu/bootloader, a bootloader defined as a
  procedure, or reused `bootloader-name`s) is when you already need to roll
  back to a different bootloader.
* `bootloader-package`: This is only useful for switching which GRUB package is
  used for GRUB bootloaders.
* `bootloader-configuration-file`: This is the sole mechanism through which
  rollbacks function. Few bootloaders utilize config file semantics, making
  bootloader packaging a pain of trying to coerce semantics to fit in this rigid
  box. This, also, prevents use of non-deterministic config files, such as those
  containing signed data for secure boot systems.
* `bootloader-installer` and `bootloader-disk-image-installer`: `installer` is
  meant to be used on mounted filesystems. `disk-image-installer` is meant to be
  used on devices. `disk-image-installer` is used for disk image building or as
  a fallback. Some bootloaders (even GRUB!) need access to both a filesystem and
  disk, and the two separate code paths mean development can lead to poorly
  tested bootloading code. The arguments to both procedures are, also, poorly
  documented (and weird choices in general; root-index is purely used by GRUB).

# Detailed Design

## Bootloader Definition
First, we go over the general architecture of bootloader packaging. Each
bootloader will have two things associated to it: a `<bootloader>` record
denoting what functions it provides, and a configuration record unique to each
type of bootloader. The `<bootloader>` record defines functions taking in the
configuration record, as a way for the bootloader system to extract the
information it needs from each bootloader's config.

Going into specifics, the `<bootloader>` record will be redefined as such:
* `bootloader-predicate`: A predicate denoting whether the configuration is of
  the right type, used to verify whether users provided the correct record.
* `bootloader-installers`: A list of `<bootloader-installer>` records.

`<bootloader-installer>` is a new record intended to split up individual
target installation methods. This is necessary as disk images do not have all
possible targets accessible at once (as the disk is generated strictly after
partitions). The record is defined as such:
* `<bootloader-installer-tag>`: An arbitrary symbol used by the bootloader to
  identify this installer and its targets. A `<bootloader>` may not have two
  `<bootloader-installers>` with the same tag.
* `<bootloader-installer-type>`: A symbol indicating the sort of values allowed
  as targets. Allowed the following values:
  - `'file`: Only strings allowed, interpreted as paths.
  - `'block`: Strings (interpreted as block devices), `file-system-label`s, and
    `uuid`s allowed. `uuid`s are interpreted as filesystem UUIDs, partition
    UUIDs, or partition table UUIDs, attempted in that order.
* `<bootloader-installer-targets>`: A function from the bootloader's
  configuration to a list of targets specified by the user. Typically this is
  just a configuration record field accessor.
* `<bootloader-installer-installer>`: A function from the bootloader's
  configuration, an alist of generation numbers to `<boot-parameters>`, the
  current generation number, and the boot-time target map (see below), to a gexp
  containing an installer procedure as specified below.

Installers are gexps containing a procedure taking keyword arguments. The
procedure is called once for each target in the `<bootloader-installer>`, and
provided keyword arguments depending on the `<bootloader-installer-type>`. Each
keyword argument has a side, either boot-side or install-side. Install-side data
is only valid during installation of the bootloader, and should not be used
during bootup. This data is generated from system state at install-time.
Boot-side data is usable during bootup, and generated from a given system's
`<file-system>` records. However, if a `'block` type installer is provided a
path to a block device, boot-time data must be generated at install-time. For
this reason, usage of a path `'block` is not recommended. Keyword arguments are
as follows:
* `#:mount` (`'file`, install-time): Absolute path to the mount point of the
  filesystem the file is stored on.
* `#:path` (`'file`, install-time): Absolute path to the specified file.
* `#:device` (`'block`, install-time): Device file for the block device.
* `#:offset` (`'file`, boot-time): Filesystem-relative path. Includes the btrfs
  subvolume prefix when applicable, so `#:path` may not always be
  `string-append`'ed `#:mount` and `#:offset`.
* `#:label` (all, boot-time): Filesystem label.
* `#:uuid` (all, boot-time): Filesystem uuid, unless `'block` and a partition
  UUID or partition table UUID is provided.

In the future, more installer types and keyword arguments may be provided
fully backwards-compatibly.

The boot-time target map provieded to `bootloader-installers` is an alist of
tags to lists of boot-time data, one for each target. Boot-time data is
exactly the boot-time keyword arguments provided to installer gexp procedures,
but are provided here for certain installers to know about others (for example,
the GRUB early loader installed to a disk knowing about the full GRUB
installation's partition and location).

## User Configuration
Users will specify the `<bootloader>` they wish to use directly in the
`operating-system-bootloader` field, which will be changed to allow `#f` (for no
bootloaders, as in virtualized images with direct linux booting).
`<operating-system>` will also receive a new
`operating-system-bootloader-configuration` field. This field will accept a
configuration used by the `operating-system-bootloader`, verified by
`bootloader-predicate`.

All further bootloader configuration, including targets and additional boot
options, will be provided in the `operating-system-bootloader-configuration`.

The current `<menu-entry>` and `<bootloader-configuration>` records will be
re-adapted into grub-specific configuration.

## Rollbacks
As the entire bootloader install process exists in gexps, each generation will
simply save its combined bootloader install script as a GC root to be called on
rollback, the exact way it's called on install. This eliminates the need for
bootloader module search and bootloader configuration guessing (as is currently
used), as well as making bootloader rollbacks more reliable.

Due to this, the following `<boot-parameters>` fields may be removed:
`boot-parameters-root-device`, `boot-parameter-bootloader-name`, and
`boot-parameter-menu-entries`. The record shall be updated to version 2 to
match.

## Disk Images
While disk images will work automatically with the above system through the
keyword argument discovery mechanisms, one instance of special-casing is
unfortunately required. Any `<bootloader-installer>` with the tag `'disk`
will, instead of using targets from `bootloader-installer-targets`, provide
a single target, generated from the resulting disk image file.

All other handling will be done invisibly to both end-users and bootloader
packagers, due to the above defined semantics of target conversion into keyword
arguments.

## Cascading Changes
These changes will necessitate some edits to specific bootloaders and
supporting code. The current GRUB bootloader installer calls `grub-install`,
which guesses a lot of information about the system it's currently run on. This
approach, of course, does not work for disk images, and so manual image creation
is used there. We propose unifying the two approaches, manually creating disk
images with the information given to us by the new targeting system.

Furthermore, a new utility procedure will be created to facilitate EFI
bootloaders, handling copying to the ESP, setting EFI boot options,
configuring removability, and ensuring sufficient storage space in the ESP.

## Documentation
A page will be added to the manual under the `Programming Interface` section
describing the new bootloader interfaces. This page will note the default
modules provided to installer gexps and the special-casing of the `'disk` tag.

The `Bootloader Configuration` page will be updated to act as a unified
documentation of the peculiarities of each bootloader and how to use it. Manual
pages pertaining to bootloader installation will be updated to document the new
user configuration format, and a blog post will be made to ease the transition.

## Bootloading Team
Lastly, a bootloading team will be created as a point of contact and
implementation group for the above changes. This team would develop the proposed
changes in a public fork of Guix in order to facilitate user testing across the
wide range of supported hardware. Merging with mainline will be done once patch
review and sufficiently diverse testing as to minimize user impact has been
completed.

## Examples
```scheme
(define-record-type* <simple-configuration>
  simple-configuration make-simple-configuration simple-configuration?
  (copy-to simple-configuration-copy-to))
(define (simple-bootloader-installer conf params current targets)
  #~(lambda* (#:key path #:allow-other-keys)
      (copy-file #$(file-append simple-bootloader-pkg "/share/config") path)))
(define simple-bootloader
  (bootloader
    (predicate simple-configuration?)
    (installers (bootloader-installer
                  (tag 'copy-to)
                  (type 'file)
                  (targets (compose list simple-configuration-copy-to))
                  (installer simple-bootloader-installer)))))
```
```scheme
(operating-system
  (bootloader simple-bootloader)
  (bootloader-configuration (simple-configuration (copy-to "/boot/config"))))
```

# Migration
Even though bootloader management may be considered part of the Guix API
surface, we do not attempt to provide a clean migration path. Thus, upon merge
immediate incompatibilities will occur for all channels packaging bootloaders.
This, thankfully, proves to be a very small number.

For operating system configuration, however, the existing
`<bootloader-configuration>` and `<menu-entry>` structures shall be deprecated.
`<operating-system>`s will go through a pass converting such structures into the
new format, through a `bootloader-migration` field in `<bootloader>` allowed to
be `#f`. This field may contain a function from `<bootloader-configuration>` to
the bootloader's configuration. The fixup procedure will operate as follows,
taking
```scheme
(operating-system
  (bootloader (bootloader-configuration
                (bootloader test-bootloader))))
```
to
```scheme
(operating-system
  (bootloader test-bootloader)
  (bootloader-configuration ((bootloader-migrate test-bootloader)
                             (bootloader-configuration
                               (bootloader test-bootloader)))))
```

Target handling would require little to no special-casing in such migration
functions.

After a year, this deprecation will expire, and the fixup code, old records, and
`bootloader-migration` field may be removed. This way, existing
`<operating-system>`s may be used as-is for the period necessitated by the
Deprecation Policy.

## Study of Affected Channels
We have taken an informal study of Guix channels as of 2025-08-13 in order to
estimate the impact of breaking API changes. 98 Guix channels were located
from the following roots:
* [whereiseverything channel toys](https://toys.whereis.social/channels)
* [Github awesome-guix](https://github.com/franzos/awesome-guix)
* [crafted-guix](https://codeberg.org/SystemCrafters/crafted-guix), an
  additional channel found through internet search.
* [gooy-guix](https://git.squircle.space/gooy-guix.git), mentioned to me by Ada
  Avery and posted on guix-devel.

Of those 98, we were able to locate exactly 4 which add bootloaders, none of
which managed to fully support Guix's existing bootloader handling:
* [rosenthal](https://codeberg.org/hako/Rosenthal), which just packages GRUB
  with an overridden package that supports LUKS2. Does not support rollbacks.
* [gooy-guix](https://git.squircle.space/gooy-guix.git), packaging a
  systemd-boot EFI bootloader. It is impossible for this bootloader to support
  disk images in the current system.
* [waggle](https://git.lunabee.space/waggle/files.html), my channel, which
  adds p-boot for the Pinephone and a UKI bootloader, whose roundabout
  implementation necessitated by the current system prompted me to begin this
  rewrite in the first place. Does not support rollbacks, and I didn't realize
  until after I needed one in the first place. It is impossible for these to
  support disk images in the current system.
* [sakura](https://g.freya.cat/freya/sakura). The bootloader added is entirely
  copied from my channel and the channel's owner has switched to Nix anyway.

We thus conclude change to be necessary, and the potential impact of breaking
changes minimal.

# Cost of Reverting
While these changes will drastically affect internals, the vast majority of
users should see a smooth transition. Similar methods to the above mentioned
migration path may be employed if this changeset needs to be reverted, at the
cost of another breaking change to bootloader-packaging channels.

However, another roadblock in reverting could take place in the removal of boot
parameter options. Thus, we propose postponing the boot parameter changes until
after reverting is precluded as a possibility. Only then will the version be
incremented and unnecessary fields dropped.

# Drawbacks and Open Issues
As these are bootloader changes, patches will need to be tested carefully in
order to ensure no devices get bricked. And, even then, there could still be a
risk due to how sensitiive bootloader installation and handling can be. This is
the main obstacle for implementation. It may be useful, then, to have a more
concrete procedure set in place to ensure proper testing after patch
development.

The deprecation process is not ideal, especially with the removal of the
`bootloader-migrate` field resulting in two breaking changes to
bootloader-packaging channels.

The special-casing of the `'disk` tag is also not ideal. We could not think of
any better method to provide such information.