diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 41d539dd5f1dd..32e0313cf8dbf 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -78,6 +78,8 @@
/nixos/doc/manual/man-nixos-option.xml @nbp
/nixos/modules/installer/tools/nixos-option.sh @nbp
/nixos/modules/system @dasJ
+/nixos/modules/system/activation/bootspec.nix @grahamc @cole-h @raitobezarius
+/nixos/modules/system/activation/bootspec.cue @grahamc @cole-h @raitobezarius
# NixOS integration test driver
/nixos/lib/test-driver @tfc
diff --git a/nixos/doc/manual/development/bootspec.chapter.md b/nixos/doc/manual/development/bootspec.chapter.md
new file mode 100644
index 0000000000000..96c12f24e7f1f
--- /dev/null
+++ b/nixos/doc/manual/development/bootspec.chapter.md
@@ -0,0 +1,36 @@
+# Experimental feature: Bootspec {#sec-experimental-bootspec}
+
+Bootspec is a experimental feature, introduced in the [RFC-0125 proposal](https://github.com/NixOS/rfcs/pull/125), the reference implementation can be found [there](https://github.com/NixOS/nixpkgs/pull/172237) in order to standardize bootloader support
+and advanced boot workflows such as SecureBoot and potentially more.
+
+You can enable the creation of bootspec documents through [`boot.bootspec.enable = true`](options.html#opt-boot.bootspec.enable), which will prompt a warning until [RFC-0125](https://github.com/NixOS/rfcs/pull/125) is officially merged.
+
+## Schema {#sec-experimental-bootspec-schema}
+
+The bootspec schema is versioned and validated against [a CUE schema file](https://cuelang.org/) which should considered as the source of truth for your applications.
+
+You will find the current version [here](../../../modules/system/activation/bootspec.cue).
+
+## Extensions mechanism {#sec-experimental-bootspec-extensions}
+
+Bootspec cannot account for all usecases.
+
+For this purpose, Bootspec offers a generic extension facility [`boot.bootspec.extensions`](options.html#opt-boot.bootspec.extensions) which can be used to inject any data needed for your usecases.
+
+An example for SecureBoot is to get the Nix store path to `/etc/os-release` in order to bake it into a unified kernel image:
+
+```nix
+{ config, lib, ... }: {
+ boot.bootspec.extensions = {
+ "org.secureboot.osRelease" = config.environment.etc."os-release".source;
+ };
+}
+```
+
+To reduce incompatibility and prevent names from clashing between applications, it is **highly recommended** to use a unique namespace for your extensions.
+
+## External bootloaders {#sec-experimental-bootspec-external-bootloaders}
+
+It is possible to enable your own bootloader through [`boot.loader.external.installHook`](options.html#opt-boot.loader.external.installHook) which can wrap an existing bootloader.
+
+Currently, there is no good story to compose existing bootloaders to enrich their features, e.g. SecureBoot, etc. It will be necessary to reimplement or reuse existing parts.
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
index 624ee3931659b..949468c9021df 100644
--- a/nixos/doc/manual/development/development.xml
+++ b/nixos/doc/manual/development/development.xml
@@ -12,6 +12,7 @@
+
diff --git a/nixos/doc/manual/from_md/development/bootspec.chapter.xml b/nixos/doc/manual/from_md/development/bootspec.chapter.xml
new file mode 100644
index 0000000000000..acf8ca76bf5cf
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/bootspec.chapter.xml
@@ -0,0 +1,73 @@
+
+ Experimental feature: Bootspec
+
+ Bootspec is a experimental feature, introduced in the
+ RFC-0125
+ proposal, the reference implementation can be found
+ there
+ in order to standardize bootloader support and advanced boot
+ workflows such as SecureBoot and potentially more.
+
+
+ You can enable the creation of bootspec documents through
+ boot.bootspec.enable = true,
+ which will prompt a warning until
+ RFC-0125
+ is officially merged.
+
+
+ Schema
+
+ The bootspec schema is versioned and validated against
+ a CUE schema file
+ which should considered as the source of truth for your
+ applications.
+
+
+ You will find the current version
+ here.
+
+
+
+ Extensions mechanism
+
+ Bootspec cannot account for all usecases.
+
+
+ For this purpose, Bootspec offers a generic extension facility
+ boot.bootspec.extensions
+ which can be used to inject any data needed for your usecases.
+
+
+ An example for SecureBoot is to get the Nix store path to
+ /etc/os-release in order to bake it into a
+ unified kernel image:
+
+
+{ config, lib, ... }: {
+ boot.bootspec.extensions = {
+ "org.secureboot.osRelease" = config.environment.etc."os-release".source;
+ };
+}
+
+
+ To reduce incompatibility and prevent names from clashing between
+ applications, it is highly
+ recommended to use a unique namespace for your
+ extensions.
+
+
+
+ External bootloaders
+
+ It is possible to enable your own bootloader through
+ boot.loader.external.installHook
+ which can wrap an existing bootloader.
+
+
+ Currently, there is no good story to compose existing bootloaders
+ to enrich their features, e.g. SecureBoot, etc. It will be
+ necessary to reimplement or reuse existing parts.
+
+
+
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 24dd30e157501..da56e7a8a692e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1241,6 +1241,7 @@
./services/x11/xserver.nix
./system/activation/activation-script.nix
./system/activation/specialisation.nix
+ ./system/activation/bootspec.nix
./system/activation/top-level.nix
./system/boot/binfmt.nix
./system/boot/emergency-mode.nix
@@ -1256,6 +1257,7 @@
./system/boot/loader/grub/grub.nix
./system/boot/loader/grub/ipxe.nix
./system/boot/loader/grub/memtest.nix
+ ./system/boot/loader/external/external.nix
./system/boot/loader/init-script/init-script.nix
./system/boot/loader/loader.nix
./system/boot/loader/raspberrypi/raspberrypi.nix
diff --git a/nixos/modules/system/activation/bootspec.cue b/nixos/modules/system/activation/bootspec.cue
new file mode 100644
index 0000000000000..3fc9ca381df77
--- /dev/null
+++ b/nixos/modules/system/activation/bootspec.cue
@@ -0,0 +1,17 @@
+#V1: {
+ init: string
+ initrd?: string
+ initrdSecrets?: string
+ kernel: string
+ kernelParams: [...string]
+ label: string
+ toplevel: string
+ specialisation?: {
+ [=~"^"]: #V1
+ }
+ extensions?: {...}
+}
+
+Document: {
+ v1: #V1
+}
diff --git a/nixos/modules/system/activation/bootspec.nix b/nixos/modules/system/activation/bootspec.nix
new file mode 100644
index 0000000000000..da76bf9084af8
--- /dev/null
+++ b/nixos/modules/system/activation/bootspec.nix
@@ -0,0 +1,124 @@
+# Note that these schemas are defined by RFC-0125.
+# This document is considered a stable API, and is depended upon by external tooling.
+# Changes to the structure of the document, or the semantics of the values should go through an RFC.
+#
+# See: https://github.com/NixOS/rfcs/pull/125
+{ config
+, pkgs
+, lib
+, ...
+}:
+let
+ cfg = config.boot.bootspec;
+ children = lib.mapAttrs (childName: childConfig: childConfig.configuration.system.build.toplevel) config.specialisation;
+ schemas = {
+ v1 = rec {
+ filename = "boot.json";
+ json =
+ pkgs.writeText filename
+ (builtins.toJSON
+ {
+ v1 = {
+ kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
+ kernelParams = config.boot.kernelParams;
+ initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
+ initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
+ label = "NixOS ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
+
+ inherit (cfg) extensions;
+ };
+ });
+
+ generator =
+ let
+ # NOTE: Be careful to not introduce excess newlines at the end of the
+ # injectors, as that may affect the pipes and redirects.
+
+ # Inject toplevel and init into the bootspec.
+ # This can only be done here because we *cannot* depend on $out
+ # referring to the toplevel, except by living in the toplevel itself.
+ toplevelInjector = lib.escapeShellArgs [
+ "${pkgs.jq}/bin/jq"
+ ''
+ .v1.toplevel = $toplevel |
+ .v1.init = $init
+ ''
+ "--sort-keys"
+ "--arg" "toplevel" "${placeholder "out"}"
+ "--arg" "init" "${placeholder "out"}/init"
+ ] + " < ${json}";
+
+ # We slurp all specialisations and inject them as values, such that
+ # `.specialisations.${name}` embeds the specialisation's bootspec
+ # document.
+ specialisationInjector =
+ let
+ specialisationLoader = (lib.mapAttrsToList
+ (childName: childToplevel: lib.escapeShellArgs [ "--slurpfile" childName "${childToplevel}/bootspec/${filename}" ])
+ children);
+ in
+ lib.escapeShellArgs [
+ "${pkgs.jq}/bin/jq"
+ "--sort-keys"
+ ".v1.specialisation = ($ARGS.named | map_values(. | first | .v1))"
+ ] + " ${lib.concatStringsSep " " specialisationLoader}";
+ in
+ ''
+ mkdir -p $out/bootspec
+
+ ${toplevelInjector} | ${specialisationInjector} > $out/bootspec/${filename}
+ '';
+
+ validator = pkgs.writeCueValidator ./bootspec.cue {
+ document = "Document"; # Universal validator for any version as long the schema is correctly set.
+ };
+ };
+ };
+in
+{
+ options.boot.bootspec = {
+ enable = lib.mkEnableOption (lib.mdDoc "Enable generation of RFC-0125 bootspec in $system/bootspec, e.g. /run/current-system/bootspec");
+
+ extensions = lib.mkOption {
+ type = lib.types.attrs;
+ default = { };
+ description = lib.mdDoc ''
+ User-defined data that extends the bootspec document.
+
+ To reduce incompatibility and prevent names from clashing
+ between applications, it is **highly recommended** to use a
+ unique namespace for your extensions.
+ '';
+ };
+
+ # This will be run as a part of the `systemBuilder` in ./top-level.nix. This
+ # means `$out` points to the output of `config.system.build.toplevel` and can
+ # be used for a variety of things (though, for now, it's only used to report
+ # the path of the `toplevel` itself and the `init` executable).
+ writer = lib.mkOption {
+ internal = true;
+ default = schemas.v1.generator;
+ };
+
+ validator = lib.mkOption {
+ internal = true;
+ default = schemas.v1.validator;
+ };
+
+ filename = lib.mkOption {
+ internal = true;
+ default = schemas.v1.filename;
+ };
+ };
+
+ config = lib.mkIf (cfg.enable) {
+ warnings = [
+ ''RFC-0125 is not merged yet, this is a feature preview of bootspec.
+ The schema is not definitive and features are not guaranteed to be stable until RFC-0125 is merged.
+ See:
+ - https://github.com/NixOS/nixpkgs/pull/172237 to track merge status in nixpkgs.
+ - https://github.com/NixOS/rfcs/pull/125 to track RFC status.
+ ''
+ ];
+ };
+}
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 55ff98db53829..a12a565af1f75 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -79,6 +79,11 @@ let
echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies
+ ${optionalString (!config.boot.isContainer && config.boot.bootspec.enable) ''
+ ${config.boot.bootspec.writer}
+ ${config.boot.bootspec.validator} "$out/bootspec/${config.boot.bootspec.filename}"
+ ''}
+
${config.system.extraSystemBuilderCmds}
'';
diff --git a/nixos/modules/system/boot/loader/external/external.md b/nixos/modules/system/boot/loader/external/external.md
new file mode 100644
index 0000000000000..ba1dfd4d9b9af
--- /dev/null
+++ b/nixos/modules/system/boot/loader/external/external.md
@@ -0,0 +1,26 @@
+# External Bootloader Backends {#sec-bootloader-external}
+
+NixOS has support for several bootloader backends by default: systemd-boot, grub, uboot, etc.
+The built-in bootloader backend support is generic and supports most use cases.
+Some users may prefer to create advanced workflows around managing the bootloader and bootable entries.
+
+You can replace the built-in bootloader support with your own tooling using the "external" bootloader option.
+
+Imagine you have created a new package called FooBoot.
+FooBoot provides a program at `${pkgs.fooboot}/bin/fooboot-install` which takes the system closure's path as its only argument and configures the system's bootloader.
+
+You can enable FooBoot like this:
+
+```nix
+{ pkgs, ... }: {
+ boot.loader.external = {
+ enable = true;
+ installHook = "${pkgs.fooboot}/bin/fooboot-install";
+ };
+}
+```
+
+## Developing Custom Bootloader Backends
+
+Bootloaders should use [RFC-0125](https://github.com/NixOS/rfcs/pull/125)'s Bootspec format and synthesis tools to identify the key properties for bootable system generations.
+
diff --git a/nixos/modules/system/boot/loader/external/external.nix b/nixos/modules/system/boot/loader/external/external.nix
new file mode 100644
index 0000000000000..5cf478e6c83cd
--- /dev/null
+++ b/nixos/modules/system/boot/loader/external/external.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.boot.loader.external;
+in
+{
+ meta = {
+ maintainers = with maintainers; [ cole-h grahamc raitobezarius ];
+ # Don't edit the docbook xml directly, edit the md and generate it:
+ # `pandoc external.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > external.xml`
+ doc = ./external.xml;
+ };
+
+ options.boot.loader.external = {
+ enable = mkEnableOption (lib.mdDoc "use an external tool to install your bootloader");
+
+ installHook = mkOption {
+ type = with types; path;
+ description = lib.mdDoc ''
+ The full path to a program of your choosing which performs the bootloader installation process.
+
+ The program will be called with an argument pointing to the output of the system's toplevel.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ boot.loader = {
+ grub.enable = mkDefault false;
+ systemd-boot.enable = mkDefault false;
+ supportsInitrdSecrets = mkDefault false;
+ };
+
+ system.build.installBootLoader = cfg.installHook;
+ };
+}
diff --git a/nixos/modules/system/boot/loader/external/external.xml b/nixos/modules/system/boot/loader/external/external.xml
new file mode 100644
index 0000000000000..39ab2156bc8c6
--- /dev/null
+++ b/nixos/modules/system/boot/loader/external/external.xml
@@ -0,0 +1,41 @@
+
+ External Bootloader Backends
+
+ NixOS has support for several bootloader backends by default:
+ systemd-boot, grub, uboot, etc. The built-in bootloader backend
+ support is generic and supports most use cases. Some users may
+ prefer to create advanced workflows around managing the bootloader
+ and bootable entries.
+
+
+ You can replace the built-in bootloader support with your own
+ tooling using the external bootloader option.
+
+
+ Imagine you have created a new package called FooBoot. FooBoot
+ provides a program at
+ ${pkgs.fooboot}/bin/fooboot-install which takes
+ the system closure’s path as its only argument and configures the
+ system’s bootloader.
+
+
+ You can enable FooBoot like this:
+
+
+{ pkgs, ... }: {
+ boot.loader.external = {
+ enable = true;
+ installHook = "${pkgs.fooboot}/bin/fooboot-install";
+ };
+}
+
+
+ Developing Custom Bootloader Backends
+
+ Bootloaders should use
+ RFC-0125’s
+ Bootspec format and synthesis tools to identify the key properties
+ for bootable system generations.
+
+
+
diff --git a/nixos/tests/bootspec.nix b/nixos/tests/bootspec.nix
new file mode 100644
index 0000000000000..13360bb1eaa2e
--- /dev/null
+++ b/nixos/tests/bootspec.nix
@@ -0,0 +1,144 @@
+{ system ? builtins.currentSystem,
+ config ? {},
+ pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+ baseline = {
+ virtualisation.useBootLoader = true;
+ };
+ grub = {
+ boot.loader.grub.enable = true;
+ };
+ systemd-boot = {
+ boot.loader.systemd-boot.enable = true;
+ };
+ uefi = {
+ virtualisation.useEFIBoot = true;
+ boot.loader.efi.canTouchEfiVariables = true;
+ boot.loader.grub.efiSupport = true;
+ environment.systemPackages = [ pkgs.efibootmgr ];
+ };
+ standard = {
+ boot.bootspec.enable = true;
+
+ imports = [
+ baseline
+ systemd-boot
+ uefi
+ ];
+ };
+in
+{
+ basic = makeTest {
+ name = "systemd-boot-with-bootspec";
+ meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+ nodes.machine = standard;
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed("test -e /run/current-system/bootspec/boot.json")
+ '';
+ };
+
+ grub = makeTest {
+ name = "grub-with-bootspec";
+ meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+ nodes.machine = {
+ boot.bootspec.enable = true;
+
+ imports = [
+ baseline
+ grub
+ uefi
+ ];
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed("test -e /run/current-system/bootspec/boot.json")
+ '';
+ };
+
+ legacy-boot = makeTest {
+ name = "legacy-boot-with-bootspec";
+ meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+ nodes.machine = {
+ boot.bootspec.enable = true;
+
+ imports = [
+ baseline
+ grub
+ ];
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed("test -e /run/current-system/bootspec/boot.json")
+ '';
+ };
+
+ # Check that specialisations create corresponding entries in bootspec.
+ specialisation = makeTest {
+ name = "bootspec-with-specialisation";
+ meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+ nodes.machine = {
+ imports = [ standard ];
+ environment.systemPackages = [ pkgs.jq ];
+ specialisation.something.configuration = {};
+ };
+
+ testScript = ''
+ import json
+
+ machine.start()
+ machine.wait_for_unit("multi-user.target")
+
+ machine.succeed("test -e /run/current-system/bootspec/boot.json")
+ machine.succeed("test -e /run/current-system/specialisation/something/bootspec/boot.json")
+
+ sp_in_parent = json.loads(machine.succeed("jq -r '.v1.specialisation.something' /run/current-system/bootspec/boot.json"))
+ sp_in_fs = json.loads(machine.succeed("cat /run/current-system/specialisation/something/bootspec/boot.json"))
+
+ assert sp_in_parent == sp_in_fs['v1'], "Bootspecs of the same specialisation are different!"
+ '';
+ };
+
+ # Check that extensions are propagated.
+ extensions = makeTest {
+ name = "bootspec-with-extensions";
+ meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+ nodes.machine = { config, ... }: {
+ imports = [ standard ];
+ environment.systemPackages = [ pkgs.jq ];
+ boot.bootspec.extensions = {
+ osRelease = config.environment.etc."os-release".source;
+ };
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("multi-user.target")
+
+ current_os_release = machine.succeed("cat /etc/os-release")
+ bootspec_os_release = machine.succeed("cat $(jq -r '.v1.extensions.osRelease' /run/current-system/bootspec/boot.json)")
+
+ assert current_os_release == bootspec_os_release, "Filename referenced by extension has unexpected contents"
+ '';
+ };
+
+}
diff --git a/pkgs/tools/misc/bootspec/default.nix b/pkgs/tools/misc/bootspec/default.nix
new file mode 100644
index 0000000000000..789f438de50eb
--- /dev/null
+++ b/pkgs/tools/misc/bootspec/default.nix
@@ -0,0 +1,25 @@
+{ lib
+, rustPlatform
+, fetchFromGitHub
+}:
+rustPlatform.buildRustPackage rec {
+ pname = "bootspec";
+ version = "unstable-2022-12-05";
+
+ src = fetchFromGitHub {
+ owner = "DeterminateSystems";
+ repo = pname;
+ rev = "67a617ab6b99211daa92e748d27ead3f78127cf8";
+ hash = "sha256-GX6Tzs/ClTUV9OXLvPFw6uBhrpCWSMI+PfrViyFEIxs=";
+ };
+
+ cargoHash = "sha256-N/hbfjsuvwCc0mxOpeVVcTxb5cA024lyLSEpVcrS7kA=";
+
+ meta = with lib; {
+ description = "Implementation of RFC-0125's datatype and synthesis tooling";
+ homepage = "https://github.com/DeterminateSystems/bootspec";
+ license = licenses.mit;
+ maintainers = teams.determinatesystems.members;
+ platforms = platforms.unix;
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 888ab409ad537..4f0310d612178 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -2694,6 +2694,8 @@ with pkgs;
brewtarget = libsForQt5.callPackage ../applications/misc/brewtarget { } ;
+ bootspec = callPackage ../tools/misc/bootspec { };
+
# Derivation's result is not used by nixpkgs. Useful for validation for
# regressions of bootstrapTools on hydra and on ofborg. Example:
# pkgsCross.aarch64-multiplatform.freshBootstrapTools.build