about summary refs log tree commit diff
path: root/nixos/modules/system
diff options
context:
space:
mode:
authorJörg Thalheim <Mic92@users.noreply.github.com>2022-12-17 08:35:53 +0000
committerGitHub <noreply@github.com>2022-12-17 08:35:53 +0000
commit668a2b2f3322c062d7a040cf084d763c9f2e271f (patch)
treeefde23edbce251b2a730a84220501a2945ad9e63 /nixos/modules/system
parent8392158289e7d1a801905bc4195983f1b7434770 (diff)
parentaac4134f430fb15386a174c6556ca0116616d2ea (diff)
Merge pull request #172237 from DeterminateSystems/bootspec-rfc
Support external bootloader backends (RFC-0125)
Diffstat (limited to 'nixos/modules/system')
-rw-r--r--nixos/modules/system/activation/bootspec.cue17
-rw-r--r--nixos/modules/system/activation/bootspec.nix124
-rw-r--r--nixos/modules/system/activation/top-level.nix5
-rw-r--r--nixos/modules/system/boot/loader/external/external.md26
-rw-r--r--nixos/modules/system/boot/loader/external/external.nix38
-rw-r--r--nixos/modules/system/boot/loader/external/external.xml41
6 files changed, 251 insertions, 0 deletions
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 64e97b510b067..0bb3628ceed9a 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -79,6 +79,11 @@ let
 
       echo -n "$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 @@
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-bootloader-external">
+  <title>External Bootloader Backends</title>
+  <para>
+    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.
+  </para>
+  <para>
+    You can replace the built-in bootloader support with your own
+    tooling using the <quote>external</quote> bootloader option.
+  </para>
+  <para>
+    Imagine you have created a new package called FooBoot. FooBoot
+    provides a program at
+    <literal>${pkgs.fooboot}/bin/fooboot-install</literal> which takes
+    the system closure’s path as its only argument and configures the
+    system’s bootloader.
+  </para>
+  <para>
+    You can enable FooBoot like this:
+  </para>
+  <programlisting language="nix">
+{ pkgs, ... }: {
+  boot.loader.external = {
+    enable = true;
+    installHook = &quot;${pkgs.fooboot}/bin/fooboot-install&quot;;
+  };
+}
+</programlisting>
+  <section xml:id="developing-custom-bootloader-backends">
+    <title>Developing Custom Bootloader Backends</title>
+    <para>
+      Bootloaders should use
+      <link xlink:href="https://github.com/NixOS/rfcs/pull/125">RFC-0125</link>’s
+      Bootspec format and synthesis tools to identify the key properties
+      for bootable system generations.
+    </para>
+  </section>
+</chapter>