1# Note that these schemas are defined by RFC-0125.
2# This document is considered a stable API, and is depended upon by external tooling.
3# Changes to the structure of the document, or the semantics of the values should go through an RFC.
4#
5# See: https://github.com/NixOS/rfcs/pull/125
6{ config
7, pkgs
8, lib
9, ...
10}:
11let
12 cfg = config.boot.bootspec;
13 children = lib.mapAttrs (childName: childConfig: childConfig.configuration.system.build.toplevel) config.specialisation;
14 schemas = {
15 v1 = rec {
16 filename = "boot.json";
17 json =
18 pkgs.writeText filename
19 (builtins.toJSON
20 # Merge extensions first to not let them shadow NixOS bootspec data.
21 (cfg.extensions //
22 {
23 "org.nixos.bootspec.v1" = {
24 system = config.boot.kernelPackages.stdenv.hostPlatform.system;
25 kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
26 kernelParams = config.boot.kernelParams;
27 label = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
28 } // lib.optionalAttrs config.boot.initrd.enable {
29 initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
30 initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
31 };
32 }));
33
34 generator =
35 let
36 # NOTE: Be careful to not introduce excess newlines at the end of the
37 # injectors, as that may affect the pipes and redirects.
38
39 # Inject toplevel and init into the bootspec.
40 # This can only be done here because we *cannot* depend on $out
41 # referring to the toplevel, except by living in the toplevel itself.
42 toplevelInjector = lib.escapeShellArgs [
43 "${pkgs.buildPackages.jq}/bin/jq"
44 ''
45 ."org.nixos.bootspec.v1".toplevel = $toplevel |
46 ."org.nixos.bootspec.v1".init = $init
47 ''
48 "--sort-keys"
49 "--arg" "toplevel" "${placeholder "out"}"
50 "--arg" "init" "${placeholder "out"}/init"
51 ] + " < ${json}";
52
53 # We slurp all specialisations and inject them as values, such that
54 # `.specialisations.${name}` embeds the specialisation's bootspec
55 # document.
56 specialisationInjector =
57 let
58 specialisationLoader = (lib.mapAttrsToList
59 (childName: childToplevel: lib.escapeShellArgs [ "--slurpfile" childName "${childToplevel}/${filename}" ])
60 children);
61 in
62 lib.escapeShellArgs [
63 "${pkgs.buildPackages.jq}/bin/jq"
64 "--sort-keys"
65 ''."org.nixos.specialisation.v1" = ($ARGS.named | map_values(. | first))''
66 ] + " ${lib.concatStringsSep " " specialisationLoader}";
67 in
68 "${toplevelInjector} | ${specialisationInjector} > $out/${filename}";
69
70 validator = pkgs.writeCueValidator ./bootspec.cue {
71 document = "Document"; # Universal validator for any version as long the schema is correctly set.
72 };
73 };
74 };
75in
76{
77 options.boot.bootspec = {
78 enable = lib.mkEnableOption (lib.mdDoc "the generation of RFC-0125 bootspec in $system/boot.json, e.g. /run/current-system/boot.json")
79 // { default = true; internal = true; };
80 enableValidation = lib.mkEnableOption (lib.mdDoc ''the validation of bootspec documents for each build.
81 This will introduce Go in the build-time closure as we are relying on [Cuelang](https://cuelang.org/) for schema validation.
82 Enable this option if you want to ascertain that your documents are correct.
83 ''
84 );
85
86 extensions = lib.mkOption {
87 # NOTE(RaitoBezarius): this is not enough to validate: extensions."osRelease" = drv; those are picked up by cue validation.
88 type = lib.types.attrsOf lib.types.anything; # <namespace>: { ...namespace-specific fields }
89 default = { };
90 description = lib.mdDoc ''
91 User-defined data that extends the bootspec document.
92
93 To reduce incompatibility and prevent names from clashing
94 between applications, it is **highly recommended** to use a
95 unique namespace for your extensions.
96 '';
97 };
98
99 # This will be run as a part of the `systemBuilder` in ./top-level.nix. This
100 # means `$out` points to the output of `config.system.build.toplevel` and can
101 # be used for a variety of things (though, for now, it's only used to report
102 # the path of the `toplevel` itself and the `init` executable).
103 writer = lib.mkOption {
104 internal = true;
105 default = schemas.v1.generator;
106 };
107
108 validator = lib.mkOption {
109 internal = true;
110 default = schemas.v1.validator;
111 };
112
113 filename = lib.mkOption {
114 internal = true;
115 default = schemas.v1.filename;
116 };
117 };
118}