summaryrefslogtreecommitdiffstats
path: root/nixos/modules/system/activation/bootspec.nix
blob: 9f8c1da7359f23fadd10f40d1b523781e52688dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 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;
  hasAtLeastOneInitrdSecret = lib.length (lib.attrNames config.boot.initrd.secrets) > 0;
  schemas = {
    v1 = rec {
      filename = "boot.json";
      json = pkgs.writeText filename (
        builtins.toJSON
          # Merge extensions first to not let them shadow NixOS bootspec data.
          (
            cfg.extensions
            // {
              "org.nixos.bootspec.v1" = {
                system = config.boot.kernelPackages.stdenv.hostPlatform.system;
                label = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
              }
              // lib.optionalAttrs config.boot.kernel.enable {
                kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
                kernelParams = config.boot.kernelParams;
              }
              // lib.optionalAttrs config.boot.initrd.enable {
                initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
              }
              // lib.optionalAttrs hasAtLeastOneInitrdSecret {
                initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
              };
            }
          )
      );

      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.buildPackages.jq}/bin/jq"
              ''
                ."org.nixos.bootspec.v1".toplevel = $toplevel |
                ."org.nixos.bootspec.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}/${filename}"
                  ]
                ) children
              );
            in
            lib.escapeShellArgs [
              "${pkgs.buildPackages.jq}/bin/jq"
              "--sort-keys"
              ''."org.nixos.specialisation.v1" = ($ARGS.named | map_values(. | first))''
            ]
            + " ${lib.concatStringsSep " " specialisationLoader}";
        in
        "${toplevelInjector} | ${specialisationInjector} > $out/${filename}";

      validator = pkgs.writeCueValidator ./bootspec.cue {
        document = "Document"; # Universal validator for any version as long the schema is correctly set.
      };
    };
  };
in
{
  imports = [
    (lib.mkRemovedOptionModule [ "boot" "bootspec" "enable" ] ''
      Bootspec is now always generated and can no longer be disabled.
    '')
  ];

  options.boot.bootspec = {
    enableValidation = lib.mkEnableOption ''
      the validation of bootspec documents for each build.
            This will introduce Go in the build-time closure as we are relying on [Cuelang](https://cuelang.org/) for schema validation.
            Enable this option if you want to ascertain that your documents are correct
    '';

    package = lib.mkPackageOption pkgs "bootspec" { };

    extensions = lib.mkOption {
      # NOTE(RaitoBezarius): this is not enough to validate: extensions."osRelease" = drv; those are picked up by cue validation.
      type = lib.types.attrsOf lib.types.anything; # <namespace>: { ...namespace-specific fields }
      default = { };
      description = ''
        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;
    };
  };
}