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 python3 = pkgs.python3.withPackages (ps: [ ps.packaging ]);
11
12 systemdBootBuilder = pkgs.substituteAll {
13 src = ./systemd-boot-builder.py;
14
15 isExecutable = true;
16
17 inherit python3;
18
19 systemd = config.systemd.package;
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.memtest86-efi;
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 python3 ];
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 imports =
73 [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
74 ];
75
76 options.boot.loader.systemd-boot = {
77 enable = mkOption {
78 default = false;
79
80 type = types.bool;
81
82 description = lib.mdDoc "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager";
83 };
84
85 editor = mkOption {
86 default = true;
87
88 type = types.bool;
89
90 description = lib.mdDoc ''
91 Whether to allow editing the kernel command-line before
92 boot. It is recommended to set this to false, as it allows
93 gaining root access by passing init=/bin/sh as a kernel
94 parameter. However, it is enabled by default for backwards
95 compatibility.
96 '';
97 };
98
99 configurationLimit = mkOption {
100 default = null;
101 example = 120;
102 type = types.nullOr types.int;
103 description = lib.mdDoc ''
104 Maximum number of latest generations in the boot menu.
105 Useful to prevent boot partition running out of disk space.
106
107 `null` means no limit i.e. all generations
108 that were not garbage collected yet.
109 '';
110 };
111
112 extraInstallCommands = mkOption {
113 default = "";
114 example = ''
115 default_cfg=$(cat /boot/loader/loader.conf | grep default | awk '{print $2}')
116 init_value=$(cat /boot/loader/entries/$default_cfg | grep init= | awk '{print $2}')
117 sed -i "s|@INIT@|$init_value|g" /boot/custom/config_with_placeholder.conf
118 '';
119 type = types.lines;
120 description = lib.mdDoc ''
121 Additional shell commands inserted in the bootloader installer
122 script after generating menu entries. It can be used to expand
123 on extra boot entries that cannot incorporate certain pieces of
124 information (such as the resulting `init=` kernel parameter).
125 '';
126 };
127
128 consoleMode = mkOption {
129 default = "keep";
130
131 type = types.enum [ "0" "1" "2" "auto" "max" "keep" ];
132
133 description = lib.mdDoc ''
134 The resolution of the console. The following values are valid:
135
136 - `"0"`: Standard UEFI 80x25 mode
137 - `"1"`: 80x50 mode, not supported by all devices
138 - `"2"`: The first non-standard mode provided by the device firmware, if any
139 - `"auto"`: Pick a suitable mode automatically using heuristics
140 - `"max"`: Pick the highest-numbered available mode
141 - `"keep"`: Keep the mode selected by firmware (the default)
142 '';
143 };
144
145 memtest86 = {
146 enable = mkOption {
147 default = false;
148 type = types.bool;
149 description = lib.mdDoc ''
150 Make MemTest86 available from the systemd-boot menu. MemTest86 is a
151 program for testing memory. MemTest86 is an unfree program, so
152 this requires `allowUnfree` to be set to
153 `true`.
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/memtest86.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/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.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 # TODO: This is hard-coded to use the 64-bit EFI app, but it could probably
280 # be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI
281 # app filename is BOOTIA32.efi.
282 (mkIf cfg.memtest86.enable {
283 "efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi";
284 })
285 (mkIf cfg.netbootxyz.enable {
286 "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
287 })
288 ];
289
290 boot.loader.systemd-boot.extraEntries = mkMerge [
291 (mkIf cfg.memtest86.enable {
292 "${cfg.memtest86.entryFilename}" = ''
293 title MemTest86
294 efi /efi/memtest86/BOOTX64.efi
295 '';
296 })
297 (mkIf cfg.netbootxyz.enable {
298 "${cfg.netbootxyz.entryFilename}" = ''
299 title netboot.xyz
300 efi /efi/netbootxyz/netboot.xyz.efi
301 '';
302 })
303 ];
304
305 system = {
306 build.installBootLoader = finalSystemdBootBuilder;
307
308 boot.loader.id = "systemd-boot";
309
310 requiredKernelConfig = with config.lib.kernelConfig; [
311 (isYes "EFI_STUB")
312 ];
313 };
314 };
315}