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