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