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