about summary refs log tree commit diff
path: root/nixos/modules/system/activation/bootspec.nix
blob: da76bf9084af8ffa94ec366e0da2db8978b56c93 (plain) (blame)
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
# 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.
      ''
    ];
  };
}