1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.boot.loader.systemd-boot;
7
8 efi = config.boot.loader.efi;
9
10 systemdBootBuilder = pkgs.substituteAll {
11 src = ./systemd-boot-builder.py;
12
13 isExecutable = true;
14
15 inherit (pkgs) python3;
16
17 systemd = config.systemd.package;
18
19 bootspecTools = pkgs.bootspec;
20
21 nix = config.nix.package.out;
22
23 timeout = optionalString (config.boot.loader.timeout != null) config.boot.loader.timeout;
24
25 editor = if cfg.editor then "True" else "False";
26
27 configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;
28
29 inherit (cfg) consoleMode graceful;
30
31 inherit (efi) efiSysMountPoint canTouchEfiVariables;
32
33 inherit (config.system.nixos) distroName;
34
35 memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86plus;
36
37 netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
38
39 copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
40 empty_file=$(${pkgs.coreutils}/bin/mktemp)
41
42 ${concatStrings (mapAttrsToList (n: v: ''
43 ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
44 ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
45 '') cfg.extraFiles)}
46
47 ${concatStrings (mapAttrsToList (n: v: ''
48 ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
49 ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
50 '') cfg.extraEntries)}
51 '';
52 };
53
54 checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
55 nativeBuildInputs = [ pkgs.mypy ];
56 } ''
57 install -m755 ${systemdBootBuilder} $out
58 mypy \
59 --no-implicit-optional \
60 --disallow-untyped-calls \
61 --disallow-untyped-defs \
62 $out
63 '';
64
65 finalSystemdBootBuilder = pkgs.writeScript "install-systemd-boot.sh" ''
66 #!${pkgs.runtimeShell}
67 ${checkedSystemdBootBuilder} "$@"
68 ${cfg.extraInstallCommands}
69 '';
70in {
71
72 meta.maintainers = with lib.maintainers; [ julienmalka ];
73
74 imports =
75 [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
76 ];
77
78 options.boot.loader.systemd-boot = {
79 enable = mkOption {
80 default = false;
81
82 type = types.bool;
83
84 description = lib.mdDoc "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager";
85 };
86
87 editor = mkOption {
88 default = true;
89
90 type = types.bool;
91
92 description = lib.mdDoc ''
93 Whether to allow editing the kernel command-line before
94 boot. It is recommended to set this to false, as it allows
95 gaining root access by passing init=/bin/sh as a kernel
96 parameter. However, it is enabled by default for backwards
97 compatibility.
98 '';
99 };
100
101 configurationLimit = mkOption {
102 default = null;
103 example = 120;
104 type = types.nullOr types.int;
105 description = lib.mdDoc ''
106 Maximum number of latest generations in the boot menu.
107 Useful to prevent boot partition running out of disk space.
108
109 `null` means no limit i.e. all generations
110 that were not garbage collected yet.
111 '';
112 };
113
114 extraInstallCommands = mkOption {
115 default = "";
116 example = ''
117 default_cfg=$(cat /boot/loader/loader.conf | grep default | awk '{print $2}')
118 init_value=$(cat /boot/loader/entries/$default_cfg | grep init= | awk '{print $2}')
119 sed -i "s|@INIT@|$init_value|g" /boot/custom/config_with_placeholder.conf
120 '';
121 type = types.lines;
122 description = lib.mdDoc ''
123 Additional shell commands inserted in the bootloader installer
124 script after generating menu entries. It can be used to expand
125 on extra boot entries that cannot incorporate certain pieces of
126 information (such as the resulting `init=` kernel parameter).
127 '';
128 };
129
130 consoleMode = mkOption {
131 default = "keep";
132
133 type = types.enum [ "0" "1" "2" "auto" "max" "keep" ];
134
135 description = lib.mdDoc ''
136 The resolution of the console. The following values are valid:
137
138 - `"0"`: Standard UEFI 80x25 mode
139 - `"1"`: 80x50 mode, not supported by all devices
140 - `"2"`: The first non-standard mode provided by the device firmware, if any
141 - `"auto"`: Pick a suitable mode automatically using heuristics
142 - `"max"`: Pick the highest-numbered available mode
143 - `"keep"`: Keep the mode selected by firmware (the default)
144 '';
145 };
146
147 memtest86 = {
148 enable = mkOption {
149 default = false;
150 type = types.bool;
151 description = lib.mdDoc ''
152 Make Memtest86+ available from the systemd-boot menu. Memtest86+ is a
153 program for testing memory.
154 '';
155 };
156
157 entryFilename = mkOption {
158 default = "memtest86.conf";
159 type = types.str;
160 description = lib.mdDoc ''
161 `systemd-boot` orders the menu entries by the config file names,
162 so if you want something to appear after all the NixOS entries,
163 it should start with {file}`o` or onwards.
164 '';
165 };
166 };
167
168 netbootxyz = {
169 enable = mkOption {
170 default = false;
171 type = types.bool;
172 description = lib.mdDoc ''
173 Make `netboot.xyz` available from the
174 `systemd-boot` menu. `netboot.xyz`
175 is a menu system that allows you to boot OS installers and
176 utilities over the network.
177 '';
178 };
179
180 entryFilename = mkOption {
181 default = "o_netbootxyz.conf";
182 type = types.str;
183 description = lib.mdDoc ''
184 `systemd-boot` orders the menu entries by the config file names,
185 so if you want something to appear after all the NixOS entries,
186 it should start with {file}`o` or onwards.
187 '';
188 };
189 };
190
191 extraEntries = mkOption {
192 type = types.attrsOf types.lines;
193 default = {};
194 example = literalExpression ''
195 { "memtest86.conf" = '''
196 title Memtest86+
197 efi /efi/memtest86/memtest.efi
198 '''; }
199 '';
200 description = lib.mdDoc ''
201 Any additional entries you want added to the `systemd-boot` menu.
202 These entries will be copied to {file}`/boot/loader/entries`.
203 Each attribute name denotes the destination file name,
204 and the corresponding attribute value is the contents of the entry.
205
206 `systemd-boot` orders the menu entries by the config file names,
207 so if you want something to appear after all the NixOS entries,
208 it should start with {file}`o` or onwards.
209 '';
210 };
211
212 extraFiles = mkOption {
213 type = types.attrsOf types.path;
214 default = {};
215 example = literalExpression ''
216 { "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; }
217 '';
218 description = lib.mdDoc ''
219 A set of files to be copied to {file}`/boot`.
220 Each attribute name denotes the destination file name in
221 {file}`/boot`, while the corresponding
222 attribute value specifies the source file.
223 '';
224 };
225
226 graceful = mkOption {
227 default = false;
228
229 type = types.bool;
230
231 description = lib.mdDoc ''
232 Invoke `bootctl install` with the `--graceful` option,
233 which ignores errors when EFI variables cannot be written or when the EFI System Partition
234 cannot be found. Currently only applies to random seed operations.
235
236 Only enable this option if `systemd-boot` otherwise fails to install, as the
237 scope or implication of the `--graceful` option may change in the future.
238 '';
239 };
240
241 };
242
243 config = mkIf cfg.enable {
244 assertions = [
245 {
246 assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub;
247 message = "This kernel does not support the EFI boot stub";
248 }
249 ] ++ concatMap (filename: [
250 {
251 assertion = !(hasInfix "/" filename);
252 message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
253 }
254 {
255 assertion = hasSuffix ".conf" filename;
256 message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension";
257 }
258 ]) (builtins.attrNames cfg.extraEntries)
259 ++ concatMap (filename: [
260 {
261 assertion = !(hasPrefix "/" filename);
262 message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash";
263 }
264 {
265 assertion = !(hasInfix ".." filename);
266 message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory";
267 }
268 {
269 assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
270 message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
271 }
272 ]) (builtins.attrNames cfg.extraFiles);
273
274 boot.loader.grub.enable = mkDefault false;
275
276 boot.loader.supportsInitrdSecrets = true;
277
278 boot.loader.systemd-boot.extraFiles = mkMerge [
279 (mkIf cfg.memtest86.enable {
280 "efi/memtest86/memtest.efi" = "${pkgs.memtest86plus.efi}";
281 })
282 (mkIf cfg.netbootxyz.enable {
283 "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
284 })
285 ];
286
287 boot.loader.systemd-boot.extraEntries = mkMerge [
288 (mkIf cfg.memtest86.enable {
289 "${cfg.memtest86.entryFilename}" = ''
290 title Memtest86+
291 efi /efi/memtest86/memtest.efi
292 '';
293 })
294 (mkIf cfg.netbootxyz.enable {
295 "${cfg.netbootxyz.entryFilename}" = ''
296 title netboot.xyz
297 efi /efi/netbootxyz/netboot.xyz.efi
298 '';
299 })
300 ];
301
302 system = {
303 build.installBootLoader = finalSystemdBootBuilder;
304
305 boot.loader.id = "systemd-boot";
306
307 requiredKernelConfig = with config.lib.kernelConfig; [
308 (isYes "EFI_STUB")
309 ];
310 };
311 };
312}