title: Generalize the bootloader subsystem and introduce Guix Bootspec
id: 00X
status: draft
discussion: TBD
authors: coopi <[email protected]>
sponsors: TBD
date: TBD
draft-date: TBD
discussion-date: TBD
deliberation-date: TBD
SPDX-License-Identifier: CC-BY-SA-4.0 OR GFDL-1.3-no-invariants-or-later
---

# Summary

Guix's bootloader subsystem was designed around GRUB, and its generic interface
still assumes an installer, a generated configuration file, and menu entries for
Guix system generations.  This GCD defines *Guix Bootspec*, a versioned,
bootloader-independent launch contract for a Guix system generation with a
closed core and typed namespaced extensions; backend-specific bootloader
configuration; a boot set describing the generations to publish; and a backend
contract applied to both live systems and system images.  It builds on the
[#72457 bootloader rewrite](https://issues.guix.gnu.org/72457) and the later
[bootloader-subsystem GCD draft](https://issues.guix.gnu.org/79248).  The
Bootspec boundary and terminology are directly inspired by [NixOS RFC 0125,
*Bootspec*](https://github.com/NixOS/rfcs/blob/master/rfcs/0125-bootspec.md).
System-generation selection and bootloader policy become independent: selecting
an older system generation keeps the active bootloader policy, while restoring
historical bootloader policy is a separate operation.

# Motivation

The current bootloader subsystem exposes several abstractions that are generic
in name but derive from GRUB's design.

A `<bootloader>` describes an installer, a disk-image installer, a configuration
file, and a configuration-file generator.  `<bootloader-configuration>` contains
settings whose meaning largely comes from GRUB, and `<menu-entry>` assumes that
a bootable system is represented as an entry in a generated menu.

The generation data stored by Guix already reflects this coupling.
`<boot-parameters>` contains a `store-device` whose documented semantics are
chosen specifically for GRUB: device file names such as `/dev/sda3` are filtered
out because GRUB cannot use them directly.  The same record also stores a
bootloader name and generic menu entries so that generation switching can later
reconstruct bootloader state.

Generation switching relies on those assumptions.
`guix system switch-generation` looks up the bootloader recorded by name,
creates a default `<bootloader-configuration>`, regenerates its configuration
file, and installs only that file.  That works for GRUB, where generation state
lives primarily in a generated configuration, but it cannot reproduce arbitrary
backend-specific policy.

Deployment is split in a different way.  Live installation and disk-image
construction use separate installer callbacks with different inputs.  Generic
image code also contains bootloader-specific behavior, including GRUB-specific
EFI partition initialization.  Existing U-Boot definitions exercise the other
end of the problem: several install boot images at board-specific raw offsets,
while their generation menu is provided through Extlinux.

The task is narrower than designing a universal model of boot firmware.  Guix
needs a stable description of its own bootable system generations, a backend
interface free of GRUB-specific representation, and one deployment model capable
of expressing the behavior already supported by the in-tree bootloaders.

These limitations motivated the [#72457 patch
series](https://issues.guix.gnu.org/72457), which proposed a broad rewrite after
observing that non-GRUB support had accumulated on top of an interface
originally designed for GRUB.  The subsequent [2025 bootloader-subsystem GCD
draft](https://issues.guix.gnu.org/79248) refined that work by proposing
backend-specific configuration records and target-specific installers.  It also
identified the generated configuration file as an unsuitable basis for generic
generation switching.

The diagnosis holds.  The main difference here is the placement of the stable
boundary: the proposal first separates the description of a bootable Guix system
from the mechanism used to publish it to a boot environment, instead of
centering the interface on a particular installer and target model.

This separation is directly inspired by [NixOS RFC
0125](https://github.com/NixOS/rfcs/blob/master/rfcs/0125-bootspec.md).  NixOS
Bootspec is a machine-readable intermediate representation between a system
generation and bootloader management tools.  Guix Bootspec adopts the same
architectural boundary, while the surrounding bootloader subsystem remains
Guix-specific.

The name should not be confused with the [Boot Loader
Specification](https://uapi-group.org/specifications/specs/boot_loader_specification/)
used by some EFI boot managers.  As RFC 0125 notes, that specification addresses
the representation and discovery of boot entries, rather than the description of
a system generation.

# Detailed Design

## Scope

The stable interface separates four kinds of state:

* a Guix Bootspec describes the launch contract of one realized system output;
* a boot set describes the generations to publish and the selected generation;
* backend configuration describes bootloader-specific policy; and
* the deployment environment describes the resources on which a backend acts.

Backend configuration languages, presentation options, custom boot entries,
chainloading facilities, and installation targets stay outside Bootspec; they
belong to backend configuration or deployment.  The generic boot set contains
Guix system generations.  Discovering and managing foreign operating-system boot
chains remains outside scope, although individual backends may provide custom or
foreign entries as policy.

Operation ordering and deployment-resource resolution are implementation details
of the backend interface; their internal representation is left open.

## Guix Bootspec

Guix Bootspec is the versioned interface between a system generation and
bootloader tooling.  It is an immutable launch contract for one realized system
output: it records the payload semantics Guix has already selected, not the
`operating-system` objects from which they were derived or a bootloader
representation of that payload.

Version 1 is stored as a data-only S-expression at
`SYSTEM-PATH/boot-specification`, where `SYSTEM-PATH` is the system store item.
Its canonical form is:

```scheme
(boot-specification
  (version 1)
  (system TARGET-SYSTEM)
  (label LABEL)
  (system-path SYSTEM-PATH)
  (payload
    (type TYPE)
    (entry ENTRY)
    (command-line COMMAND-LINE)
    (components
      ((boot-component
         (file FILE))
       …)))
  (extensions
    ((boot-extension
       (id EXTENSION-ID)
       (required? REQUIRED?)
       (artifacts (ARTIFACT …))
       (data DATA))
     …)))
```

A component with an associated command line contains an additional
`(command-line COMMAND-LINE)` field after `file`.  `extensions` is an empty list
when the generation carries no extensions.

In version 1, `TARGET-SYSTEM`, `LABEL`, `SYSTEM-PATH`, `ENTRY`, `FILE`,
`COMMAND-LINE`, `EXTENSION-ID`, and `ARTIFACT` are strings; `TYPE` is a symbol;
and `REQUIRED?` is a boolean.  `version` is the exact integer `1`.
`components`, `extensions`, and `artifacts` are proper lists.

`TARGET-SYSTEM` is the canonical Guix system identifier of the system being
booted, such as `x86_64-linux`, `aarch64-linux`, or `i586-gnu`.

`LABEL` is the generation's intrinsic human-readable label.  Generation number,
creation time, and other profile metadata are not part of it.

`SYSTEM-PATH` is the absolute store name of the system containing the Bootspec.
Bootspec is written while constructing that system output, so the output path
can appear in the serialized datum and in command lines without becoming an
input dependency of the system.

`ENTRY`, every component `FILE`, and every extension `ARTIFACT` are absolute
store file names.  Every store item named through one of these fields is
reachable from the system closure.  This permits generic boot tooling to retain
and copy boot artifacts without interpreting payload- or extension-specific
data.

`COMMAND-LINE` is the exact command-line text presented to the corresponding
entry or component, excluding any terminating NUL required by the payload
protocol.  It is not an `argv` representation.  Generic bootloader code does not
parse it.  A backend may quote the string as required by its own configuration
syntax, but does not change the text delivered to the payload.  NUL is invalid
in a Bootspec command line.  Version 1 treats Linux command lines as UTF-8 text.
This is a Guix Bootspec convention, not a claim that the Linux boot protocol
itself assigns a character encoding.  A `multiboot1` payload accepts only ASCII,
as required by Multiboot 1.  For a component, an omitted command line and an
empty command line are distinct.

The component sequence is ordered.  Its order is part of the payload semantics.

`TYPE` defines the semantics of the entry and components.  Version 1 defines two
payload types:

* `linux` identifies a Linux kernel image.  Its components are zero or more
  generation-owned initramfs fragments in the listed order.  A Linux component
  has no component command line.  The generation initramfs is their bytewise
  concatenation in that order.  A backend which accepts a single initramfs
  buffer performs that concatenation; a backend which can name multiple initrds
  can pass them separately while preserving the same ordering.  The [Linux
  initramfs buffer
  format](https://docs.kernel.org/driver-api/early-userspace/buffer-format.html)
  explicitly permits concatenated archives, and GRUB's
  [`initrd`](https://gnu.org/software/grub/manual/grub/html_node/initrd.html)
  command implements the same ordered composition.
* `multiboot1` identifies a Multiboot 1 entry image.  Its components are
  Multiboot modules in the listed order.  A component command line is the string
  associated with that module by Multiboot 1.  Entry and component command lines
  must be representable as the ASCII strings required by Multiboot 1.

The payload type identifies payload semantics, not a bootloader implementation.
Any mechanism is valid for a `linux` payload if it preserves the generation's
entry image, command line, and ordered initramfs contribution.  Active backend
policy may supplement that contribution as described under bootloader
configuration, while leaving the Bootspec payload unchanged.  A `multiboot1`
payload likewise describes the contract expected by the entry image, independent
of GRUB's `multiboot` and `module` commands.  The [Multiboot 1 specification](
https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
) defines that contract.

Bootspec contains no backend syntax.  In particular, Multiboot module command
lines contain the strings intended for the modules; GRUB-specific quoting
belongs to the GRUB backend.

### Extensions

Version 1 has a closed core schema and one extension point: the `extensions`
field.  Extensions add generation-associated information without adding fields
to the core Bootspec grammar.

`EXTENSION-ID` identifies one extension contract.  It is a lower-case ASCII,
reverse-domain-style name consisting of at least two dot-separated labels.  Each
label contains letters, digits, or hyphens and does not begin or end with a
hyphen.  For example, `org.gnu.guix.boot.device-tree` is valid.  The
reverse-domain prefix identifies the namespace owner, and `org.gnu.guix` is
reserved for Guix.  An identifier's semantics are stable.  Incompatible
semantics require a new identifier.

`REQUIRED?` states whether correct realization of the generation requires the
extension.  Requiredness is a property of the extension definition, not of an
individual instance.  The public construction API associates each extension type
with a fixed identifier, fixed requiredness, and validators for its artifacts
and data.  An extension instance supplies that type, its artifacts, and its
data; ordinary construction takes neither an independent identifier nor a
requiredness value.  The serialized `required?` field repeats the type's
requiredness so that an unaware consumer can determine whether realization may
continue.  A consumer which knows the extension verifies that the serialized
value agrees with the extension definition.

If ignoring any conforming instance of an extension can make the generation's
boot semantics incorrect, that extension type is required.  Version 1 has no
per-instance or conditional requiredness.  A contract whose instances genuinely
have different compatibility semantics uses distinct extension identifiers
instead of a user-selectable criticality bit.

A required extension describes information that has to be processed to preserve
the generation's boot semantics.  A Bootspec with an unknown required extension
remains parsable, but a deployment unable to satisfy it cannot realize that
system.  This check occurs before boot-state changes are performed.  An unknown
non-required extension can be ignored for realization.  A recognized extension
with malformed data is invalid regardless of requiredness.

Extensions are additive.  They do not replace, reinterpret, suppress, or reorder
core Bootspec fields or payload components, alter the meaning of a payload type,
or override another extension.  Information which changes those semantics
belongs in the core Bootspec or payload model instead.  Boot-set metadata,
bootloader policy, and deployment resources are likewise outside the extension
mechanism.

`ARTIFACT` lists the immutable store files required by the extension.  Every
store object whose contents are needed to interpret or realize an extension is
listed there even when its path also appears in `DATA`.  This list is part of
the generic closure contract, so unknown extensions can keep their required
store objects live without teaching generic code how to inspect their data.  A
known extension validates the relationship between its `artifacts` and `data`.

`DATA` is one data-only S-expression composed recursively of booleans, exact
integers, strings, symbols, bytevectors, and proper lists of those values.  It
is validated further by the extension type which defines it.  Generic readers
preserve unknown extension data and artifact lists without interpreting them.
Extension order has no semantic meaning, duplicate identifiers are invalid, and
the canonical writer orders extensions by identifier.  The extension mechanism
defines no generic dependency or precedence relation; a set of requirements
which cannot be satisfied together cannot be realized.

No process-global registry is required.  Guix and external channels can provide
first-class extension types and realization support for the identifiers they
implement.

### Command-line construction

Bootspec command lines are finalized during construction of the realized system
output.  They are not subsequently rewritten by deployment.

This distinction matters for system images.  Guix already specializes the
`operating-system` used for an image before constructing its system output:
`operating-system-for-image` assigns the image's deterministic root UUID and
root file-system type, and the resulting system is built from that specialized
value.  The image receives its own Bootspec and root arguments.  A launch
requirement which genuinely changes with the system being built belongs in that
system's Bootspec; topology which merely changes how the same store artifacts
are reached belongs to deployment.

Root-file-system data is an input to payload construction, not a Bootspec field.
For a `linux` payload, Guix converts it to the corresponding Linux root
arguments.  For a `multiboot1` GNU system, Guix constructs the GNU Mach
root-device argument.  The resulting command line is stored in Bootspec.

Payload construction also adds contextual Guix arguments, including `gnu.system`
and `gnu.load`.  Their values use `SYSTEM-PATH` directly.

The current `system-service-type` assembles the top-level system with
`file-union`, while the current `parameters` file is built separately.  The
Bootspec implementation changes only this final assembly step: the system
builder creates the directory entries and writes `boot-specification` after its
output path is bound.  As with ordinary Guix builders, the output path is
supplied by the store as the derivation output; embedding that string in the
output introduces no self-dependency.

System-generated arguments retain their current precedence over conflicting
user-supplied arguments.

Hurd bootstrap expressions stored in component command lines are the expressions
delivered to the component.  Any quoting required to embed them in GRUB syntax
is applied by the GRUB backend.

### Store topology

Bootspec names immutable store artifacts and leaves their reachability to the
deployment layer.

The current `<boot-parameters>` fields for store device, mount point, Btrfs
directory prefix, and encrypted devices move to the deployment environment.
Their values describe deployment topology, and some of their current semantics
are GRUB-specific.  For example, `store-device` filters device file names that
GRUB cannot use directly.

The active backend resolves the store paths named by each Bootspec against the
current deployment environment.

### Versioning and validation

Version 1 fixes the core field grammar and the meaning of those fields.  A
reader rejects an unsupported Bootspec version, unknown or duplicate core
fields, malformed values, duplicate extension identifiers, and trailing data
after the Bootspec datum.  The canonical writer emits core fields in the order
shown above and extensions in identifier order.

An incompatible change to the core grammar or to the meaning of an existing core
field uses a new Bootspec version.  Independent extensions use stable
identifiers of their own, without separate version fields.  The set of payload
types is also part of the versioned core contract: version 1 contains only
`linux` and `multiboot1`, and any other payload type is invalid.  Adding another
launch protocol requires a new Bootspec version.  Payload semantics stay out of
an unnamespaced secondary extension mechanism; the lighter-weight extension
facility remains available for orthogonal generation requirements.

Newer Bootspec versions coexist with older ones.  A writer emits the oldest
version whose contract can represent the generation without loss.  Thus a future
payload protocol introduced in version 2 need not turn otherwise unchanged
`linux` or `multiboot1` generations into version 2 documents.  Consumers can
support several Bootspec versions concurrently.

The file is UTF-8 text containing exactly one datum.  Its syntax is the [R7RS
external representation](https://standards.scheme.org/r7rs-html5/index.html)
restricted to the data types admitted by Bootspec: proper lists, strings,
symbols, booleans, exact integers, and bytevectors.  The canonical writer emits
`#t` and `#f`, base-10 integers, `#u8(…)` bytevectors with decimal octets, and
proper-list notation; it omits datum labels, dotted lists, vectors, characters,
abbreviations, comments, alternate radix or exactness prefixes, and alternate
boolean spellings.  Core symbols use lower-case ASCII identifiers.  Readers
reject values outside this subset even when an underlying Scheme reader would
accept them.  Reading a Bootspec never evaluates the datum.

The in-memory Bootspec representation mirrors the serialized model: a boot
specification, a boot payload, boot components, and boot extensions.  Public
Bootspec fields have the semantics specified above.  Constructor and procedure
names that do not affect those semantics are ordinary implementation details.

## Bootable systems and boot sets

A *bootable system* is the normalized in-memory value consumed internally by the
bootloader subsystem.  It is not a serialized compatibility format or a separate
public API.  It provides the system path, label, normalized payload type, entry,
command line, ordered components, generation extensions when present, and
profile metadata needed for selection and presentation.  For a generation
containing Bootspec v1, these launch fields come directly from that Bootspec.
Historical generations are converted from their legacy `parameters` data into
equivalent launch information without manufacturing a Bootspec v1 datum; their
extension set is empty.  Values absent from historical data, such as an explicit
target-system identifier, remain unknown unless the existing compatibility path
can derive them without changing legacy behavior.  Backends consume these
normalized launch fields and make no assumption that every bootable system
originated as a complete Bootspec v1 value.  Profile metadata includes the
generation number and can include the generation creation time.

A *boot set* is the collection of bootable systems to be published by a backend.
It identifies the selected system and can identify the currently booted system
when that distinction is useful.

For generation switching, the default boot set contains every generation still
present in the system profile, with the requested generation selected.  During a
reconfiguration, it contains the candidate system as the selected system
together with every generation currently retained by the profile; once the
profile switch succeeds, the candidate becomes the new profile generation.  This
matches the historical generations for which current Guix constructs boot
entries while also letting bootloader deployment be validated before the
candidate has to be treated as an existing profile generation.

The selected system is always a member of the boot set.  Deleting a
system-profile generation removes it from subsequent boot sets.  A future
explicit policy for publishing a smaller subset of unselected retained
generations can be added independently, but backends never impose such a policy
silently.  No system generation contains a snapshot of the other generations
that existed when it was created.

A backend maps the boot set to its own representation.  Generic Guix code does
not require the selected system to be the first menu entry, one menu entry per
generation, or a menu at all.

Accordingly, generic `<menu-entry>` objects are not the representation of Guix
system generations.  Backends can define entry types for backend-specific
features such as custom or foreign boot entries.

## Bootloader configuration

Bootloader configuration is backend-specific.

A configured bootloader associates a backend with configuration understood by
that backend.  Configuration for one backend is not interpreted by another.
Backend-specific options do not extend a universal configuration record.

Generic Guix code identifies the backend and supplies its configuration when
deployment is requested.  The exact Scheme representation of that association is
left open.

Backend policy can supplement a generation payload where the backend explicitly
provides such a facility.  Policy-owned additions are not part of generation
identity and follow the active policy when another generation is selected.  They
may neither remove nor reorder generation-owned payload components, nor rewrite
the Bootspec command line.  The existing GRUB `extra-initrd` maps cleanly to
this rule: its additional initrd can precede the generation initramfs as backend
policy without becoming a Bootspec component.

## Bootloader backend interface

A bootloader backend is defined by one backend-specific configuration and the
logic needed to realize boot state from:

* that configuration;
* a boot set; and
* a deployment environment.

The stable contract defines semantics, not a scheduler API.  A backend may lower
realization to one operation or several operations performed as their resources
become available.  The representation used to schedule those operations is
internal to the implementation.  This allows filesystem installation,
configuration publication, and raw-device installation to share one backend
definition without making a generic deployment graph part of the public
interface.

The interface requires neither a generated configuration file nor explicit menu
entries nor a single installation target.  The backend translates Bootspec
payloads into the bootloader's representation; root arguments, `gnu.system`,
`gnu.load`, and payload-specific component command lines have already been
constructed.

The deployment environment provides resources in generic system and image terms.
The backend translates those resources into any paths, device identifiers,
bootloader syntax, or on-disk representation it requires.

Backend realization can also add auxiliary material owned by backend policy or
by the deployment strategy when that material is needed to present the
generation payload.  Such material stays outside Bootspec and cannot be used to
remove or reorder generation-owned components or rewrite the generation command
line.  The GRUB path which injects its LUKS helper into the initramfs when the
store is encrypted is one existing instance of this distinction.

Before deployment changes boot state, realization performs the capability and
resource checks that can be established in advance for every system in the boot
set, including payload-type and required-extension support.  The boot set is
accepted or rejected as a whole: backends cannot drop systems they fail to
realize, and an unsupported system causes preflight failure.  Runtime I/O or
installer failures can still occur after preflight.  Any decision to publish
fewer generations is made when the boot set is constructed.

Backends preserve the installation guarantees of the boot mechanisms they
implement.  Configuration files and similar resources are staged and replaced
atomically where the current implementation already does so and the underlying
filesystem permits it.  Operations which are inherently non-transactional today,
such as board-specific raw-device writes, are not made less reliable by the
replacement interface.

Redeploying the same boot set with another selected system uses the currently
active policy realization.  Historical backend configuration is not read from
the selected system's Bootspec.

Objects used only to resolve resources, order operations, or integrate with
image construction are not part of the public backend API unless separately
specified.

## Deployment and system images

Live deployment and system-image construction use the same backend definition
and payload semantics.  They need not invoke one procedure at the same lifecycle
point.

The two environments can expose different concrete resources and different
lifecycle points.  During image construction, for example, individual
filesystems can exist before the complete disk image.  This lets a backend
perform operations as the required resources become available, without a
separate disk-image installer API.

Generic system and image code supplies deployment resources and lifecycle
information.  Environment adapters can ask the backend to perform the portions
of realization whose resources are available at a given lifecycle point.
Bootloader-specific installation logic remains in the backend or shared
bootloader support code, keeping bootloader syntax and board-specific write
offsets out of generic image code.

A deployment resource can have different concrete representations in different
environments while retaining the same semantic role.  This includes the location
and accessibility of store paths named by Bootspec and policy-owned resources
which are intentionally outside the store.  The internal representation of
deployment resources is not part of the stable interface.

## Generation switching and bootloader policy

System-generation selection and bootloader policy are independent state.

Selecting another system generation keeps the active bootloader policy and
redeploys the current boot set with the requested system selected.  A historical
generation therefore has no effect on which backend or configuration is active.

Restoring historical bootloader policy is a separate operation.

Guix persists bootloader-policy history independently from system-profile
history.  Backend configuration needs no general-purpose serialization format.
When a policy is created, Guix lowers the configured backend to an immutable
*policy realization* in the store.  That realization captures the backend code,
lowered configuration, and immutable store references needed to deploy future
boot sets under the policy without capturing a particular boot set.  References
to intentionally mutable, secret-bearing, or deployment-local resources remain
references instead of being copied into the store; deployment resolves and
checks them again.  The store is not used as confidential storage.  Generation
switching can supply the then-current boot set without re-evaluating an old
Scheme configuration or requiring the channel revision which originally
constructed the policy.  This adapts the useful retained-installer idea from the
[2025 bootloader-subsystem GCD draft](https://issues.guix.gnu.org/79248) without
freezing the generations that existed when the policy was created.

Policy history identifies these lowered realizations.  Explicit restoration
selects one from this history; no system generation owns or identifies a
bootloader policy.  Restoration is subject to ordinary preflight: an old policy
can be incompatible with a newer payload or required extension, and a referenced
deployment-local resource may no longer exist.

A retained policy realization must remain invocable for as long as its history
entry is retained.  Realizations therefore use a small internal invocation
contract through which the current boot set and deployment environment are
supplied.  The implementation retains whatever runner or compatibility support
is needed by existing history entries.  This contract is not a public backend
extension API, but its implementation cannot be changed in a way that strands
retained policy history without migrating or retaining support for that history.

A candidate policy realization and its closure are made GC-reachable before
deployment begins.  The previously active policy remains reachable throughout
deployment.  Activating policy has a separate commit point: the candidate is
recorded as active only after backend deployment succeeds.  A failed deployment
therefore leaves the previous policy active and keeps its closure reachable.
Policies retained for explicit historical restoration remain reachable for as
long as they remain in policy history.

The replacement provides an explicit policy-restoration operation before the
legacy interface is removed.  Its command-line syntax, the small persistent
index used to name policy realizations, and the history-retention policy are
left unspecified.

## User-facing configuration

Bootloader selection remains part of the `operating-system` declaration.

The selected backend carries backend-specific configuration instead of extending
a shared `<bootloader-configuration>` record with fields for all backends.

Low-level deployment machinery is not part of the user-facing interface unless a
backend chooses to expose some part of it as configuration.

Exact constructor and record names remain unspecified beyond the semantic
interfaces above.

## Migration and implementation

The bootloader subsystem has one coherent user-visible cutover.  Preparatory
internal changes can land incrementally with the current subsystem still active.
The cutover occurs only when Bootspec generation, compatibility conversion,
policy realization, generation switching, system-image integration, and all
existing in-tree backends operate through the replacement without loss of
supported behavior.

The transition does not depend on adding new bootloaders.  It is complete when
the bootloader choices already provided by Guix operate through the new
subsystem without loss of supported behavior.

Before the cutover, automated coverage includes:

* canonical Bootspec writing, parsing, validation, and rejection of malformed
  versions, payloads, and extensions;
* conversion of representative historical `parameters` versions without
  manufacturing unavailable Bootspec fields;
* equivalence of the Linux and GNU Mach/Hurd launch contracts before and after
  moving semantic construction out of backend renderers;
* the supported live-deployment and system-image paths of every in-tree backend,
  including raw-device U-Boot installers where they can be exercised in images;
* preservation of atomic replacement or staging on paths which provide it today;
* rejection of an unsupported boot set during preflight without boot-state
  writes; and
* failure injection around policy deployment and GC-root transitions, including
  preservation of the previously active realization after a failed deployment.

Hardware-specific paths which cannot be exercised reliably in automated tests
remain subject to targeted hardware validation before the legacy implementation
is removed.

The new API is the implementation used by in-tree backends.  The existing public
API remains as a deprecated compatibility interface where practical.  Old
bindings are defined in terms of the new API when this remains reasonably
simple; narrowly scoped compatibility code can preserve old behavior otherwise.
Backend-specific behavior currently exposed through the generic configuration,
such as GRUB's `extra-initrd`, is translated to the corresponding backend policy
instead of being promoted into Bootspec.

System generations created before the refactor contain `parameters` rather than
`boot-specification`.  A shared compatibility path converts that data into a
bootable system directly.  It can use the enclosing system path to recover
contextual arguments omitted by historical `parameters` files, without claiming
that the result is a Bootspec v1 value when legacy data lacks fields required by
that format.  Individual backends do not implement this conversion.

Compatibility bindings use `(guix deprecation)` where appropriate and follow the
[Guix Deprecation
Policy](https://guix.gnu.org/manual/devel/en/html_node/Deprecation-Policy.html).
Documentation for deprecated interfaces identifies their replacements.  The
bindings can be removed after the applicable deprecation period.

## Alternatives considered

### Keep the current interface

The existing subsystem can continue to add special cases for bootloaders which
do not fit its configuration-file and menu-entry model.

This avoids a large migration in the short term but preserves the structural
problem that motivated the previous rewrites.  It also leaves the representation
of a Guix system coupled to concepts inherited from GRUB.

### Target-specific installers

The [2025 bootloader-subsystem GCD draft](https://issues.guix.gnu.org/79248)
proposed installer objects associated with typed targets.

That design recognizes that different bootloaders operate on different resources
and that image construction exposes those resources at different stages.  It
also makes a particular representation of deployment central to the public
bootloader interface before several implementations have established that it is
the right shared abstraction.

Backends need to operate on different deployment resources, but target-specific
installer callbacks are not the stable architectural boundary.

### A universal boot entry

The existing `<menu-entry>` could instead be generalized until every bootloader
can translate it into its own representation.

Such an interface either describes only the features common to every backend or
accumulates backend-specific alternatives over time.  Bootspec instead describes
the Guix system itself.  Whether a backend creates a menu entry is a backend
decision.

### Separate live and image interfaces

Guix could continue to expose unrelated procedures for ordinary bootloader
installation and disk-image installation.

Each procedure stays simple, but the split duplicates the deployment model of
one bootloader and encourages generic image code to contain bootloader-specific
knowledge.  The proposed interface instead describes one backend whose
deployment can be realized in either environment.

### An opaque deployment program

A configured bootloader could be represented solely by a program invoked
whenever boot state needs to change.

Such a program provides considerable implementation freedom, but by itself says
nothing about the generation data it consumes, generation-switching semantics,
or the relationship between ordinary deployment and image construction.  For
that reason, an opaque program is not the public backend interface.  A lowered
program is nevertheless useful internally as the persistent policy realization
described above, where its input contract is already defined by the boot set and
deployment environment.

### A generic deployment graph

The public backend interface could expose arbitrary deployment actions and their
dependencies as a general graph.

A graph is expressive, but would also require generic semantics for ordering,
partial failure, cleanup, resource ownership, and conflicts between privileged
operations.  That machinery is unnecessary as a public abstraction for this
subsystem.  Dependency information can be used internally where needed.

### No Bootspec extension mechanism

Bootspec could require a new format version for every new generation-associated
requirement not represented by the core schema.  The format stays small, but
unrelated evolution then shares one global compatibility boundary.  A consumer
would have to reject an otherwise familiar generation merely because it uses an
independent feature irrelevant to that consumer.

The extension mechanism limits that incompatibility to generations which carry a
required extension.  Changes to the common Bootspec contract use the Bootspec
version.

### Unrestricted namespaced extensions

[NixOS RFC
0125](https://github.com/NixOS/rfcs/blob/master/rfcs/0125-bootspec.md) allows
namespaced extension data so consumers can attach information not covered by the
core schema.  Guix Bootspec retains namespacing but gives extensions a fixed
requiredness and typed construction API.

This makes unknown-extension behavior explicit and validates known extension
data without turning the extension field into another place for arbitrary
backend policy.  The generic mechanism intentionally provides no override,
precedence, dependency, or per-instance criticality rules.

## Cost of Reverting

The expected compatibility impact of the refactor is level 2.  Existing Scheme
configurations continue to work through the deprecated compatibility interface,
but generation switching intentionally changes in one uncommon case: selecting
an older system generation after the active bootloader policy has changed keeps
the current policy instead of reconstructing policy from generation data.  This
primarily affects advanced configurations which have changed bootloaders between
system generations.

The compatibility layer follows the Guix Deprecation Policy, so removal of the
legacy public bindings is a later and separately communicated incompatibility.
Until then, old bindings remain available while the in-tree implementation uses
the new subsystem.

Bootspec introduces a second compatibility boundary.  Once a Bootspec version is
consumed by external tools or third-party backends, its core semantics cannot be
changed incompatibly; such changes require a new version.  Published extension
identifiers are compatibility commitments of their own: identifier meaning is
stable, and required extensions must remain satisfiable for historical
generations that use them.  The same applies to the public backend contract once
downstream channels begin implementing it.  The normalized `bootable system`,
deployment scheduler, and concrete policy-realization representation are not
public extension APIs.  Retained policy realizations nevertheless carry an
internal compatibility obligation: their invocation support must remain
available for as long as policy history promises that they can be restored.

Reverting the implementation during the deprecation period remains possible by
restoring the old subsystem behind the compatibility surface.  After the legacy
API is removed, a reversion would itself require another public API transition
and continued support for any published Bootspec versions.

# Drawbacks and Open Issues

Bootspec v1 has a smaller scope than `<boot-parameters>`.  Porting the existing
backends requires moving payload construction out of backend rendering.  The
Hurd path is the sharpest case: GNU Mach root-device selection and semantic
Multiboot module command lines currently live partly in the GRUB generator,
while GRUB quoting is mixed into stored module arguments.  The refactor must
separate those responsibilities without changing the resulting Hurd bootstrap.

A self-contained Bootspec changes construction of the top-level system output.
The current `parameters` file is built independently and the final system is a
`file-union`.  The replacement final builder must instead create those links and
write Bootspec after the output path is known.  The construction change is
localized; it is not an unresolved data dependency.  Because it affects a
central system derivation, it needs dedicated reproducibility and substitution
tests.

Backend realization must nevertheless cover several different operations already
in Guix.  Extlinux installs files and boot code, U-Boot backends write
board-specific images at fixed disk offsets or install files for later flashing,
and system images expose partitions before the final disk image exists.  GRUB
also has separate live and image paths today.  The semantic boundary is now
fixed—one backend definition may lower to several lifecycle-specific
operations—while the scheduler and concrete resource records remain internal.
Porting all existing in-tree backends is the test that this internal model is
expressive enough before the legacy implementation is removed.  The replacement
must preserve atomic replacement and staging on paths which provide them today;
raw-device installers retain the same power-loss and partial-write limitations
they already have.

Independent bootloader-policy history introduces persistent state and additional
GC roots outside the system profile.  Lowered policy realizations avoid
requiring arbitrary backend configuration to remain serializable or
re-evaluable, but the persistent index, internal invocation contract, and
history-retention policy need implementation and compatibility tests.  The
implementation must keep each retained realization and its closure reachable for
as long as the corresponding history entry can be restored.  Mutable
deployment-local resources cannot be made reproducible by GC rooting;
restoration of a policy which refers to one can fail if that resource has
disappeared or changed.

The default boot set preserves every retained rollback generation.
Consequently, changing to a backend which cannot realize one of those
generations is rejected even if the selected generation itself is supported.
Deleting the incompatible system generation resolves the conflict today;
retaining a generation while suppressing its boot entry would require the
separate generic boot-set publication policy left open above.  The backend does
not make that choice implicitly.

The legacy compatibility surface includes `<menu-entry>`, which GRUB and
Extlinux interpret differently and for which some fields are GRUB-specific.
Deprecated configurations should translate to backend-specific equivalents when
straightforward.  Other legacy combinations can use narrowly scoped adapters;
the old generic entry type does not become part of the replacement core.

Historical `parameters` files do not contain every value available to Bootspec
v1, including the explicit target `system`, and older parameter versions contain
still less information.  Compatibility conversion produces a bootable system
directly and preserves the behavior of the existing reader.  Missing values are
not invented merely to manufacture a literal v1 Bootspec.

The extension mechanism reduces format-version churn but creates a narrower
compatibility commitment for each published identifier.  Code can enforce the
identifier syntax, fixed requiredness, artifact closure declarations, data
validation, duplicate rejection, and unknown-extension behavior; it cannot prove
that an extension was correctly classified as required or that the datum belongs
in Bootspec instead of backend policy.  That architectural judgment remains part
of review when an extension type is introduced.  Required extensions remain
exceptional because each one narrows the set of implementations capable of
realizing generations which use it.

Finally, the refactor changes the recovery path of every supported Guix System
bootloader.  In-tree U-Boot backends include board-specific raw-disk writes that
are difficult to validate fully without representative hardware, and a broken
bootloader cannot necessarily be repaired through an ordinary system rollback.
Preserving all existing bootloader choices is a merge criterion, but automated
tests cannot replace targeted image and hardware validation.

haiii!!

i've been poking at guix's bootloader subsystem for a while, and ended
up writing a gcd about it :3

the main problem is that the generic bootloader interface is still very
heavily shaped around grub: generated configuration files, menu entries,
installer callbacks, etc. this gets especially awkward for generation
switching, system images, and bootloaders which don't really fit that
model.

the draft introduces guix bootspec, which describes the launch contract
of a system generation independently from whichever bootloader happens
to publish it. it also separates the boot set, backend-specific policy,
and deployment environment, so generation switching doesn't have to
reconstruct bootloader state from old generation data anymore.

the draft is attached. i'm looking for a sponsor (or sponsors), and would
especially appreciate people willing to poke holes in the bootspec/backend
boundary before the discussion period starts ^w^

thanks!!

-- 
coopi
lost: one train of thought.  if found, please return.

Reply via email to