1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.boot.loader.grub;
8
9 efi = config.boot.loader.efi;
10
11 realGrub = if cfg.version == 1 then pkgs.grub
12 else if cfg.zfsSupport then pkgs.grub2.override { zfsSupport = true; }
13 else if cfg.trustedBoot.enable
14 then if cfg.trustedBoot.isHPLaptop
15 then pkgs.trustedGrub-for-HP
16 else pkgs.trustedGrub
17 else pkgs.grub2;
18
19 grub =
20 # Don't include GRUB if we're only generating a GRUB menu (e.g.,
21 # in EC2 instances).
22 if cfg.devices == ["nodev"]
23 then null
24 else realGrub;
25
26 grubEfi =
27 # EFI version of Grub v2
28 if cfg.efiSupport && (cfg.version == 2)
29 then realGrub.override { efiSupport = cfg.efiSupport; }
30 else null;
31
32 f = x: if x == null then "" else "" + x;
33
34 grubConfig = args:
35 let
36 efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint;
37 efiSysMountPoint' = replaceChars [ "/" ] [ "-" ] efiSysMountPoint;
38 in
39 pkgs.writeText "grub-config.xml" (builtins.toXML
40 { splashImage = f cfg.splashImage;
41 grub = f grub;
42 grubTarget = f (grub.grubTarget or "");
43 shell = "${pkgs.stdenv.shell}";
44 fullName = (builtins.parseDrvName realGrub.name).name;
45 fullVersion = (builtins.parseDrvName realGrub.name).version;
46 grubEfi = f grubEfi;
47 grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
48 bootPath = args.path;
49 storePath = config.boot.loader.grub.storePath;
50 bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
51 inherit efiSysMountPoint;
52 inherit (args) devices;
53 inherit (efi) canTouchEfiVariables;
54 inherit (cfg)
55 version extraConfig extraPerEntryConfig extraEntries
56 extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels timeout
57 default fsIdentifier efiSupport gfxmodeEfi gfxmodeBios;
58 path = (makeSearchPath "bin" ([
59 pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfs-progs
60 pkgs.utillinux ] ++ (if cfg.efiSupport && (cfg.version == 2) then [pkgs.efibootmgr ] else [])
61 )) + ":" + (makeSearchPath "sbin" [
62 pkgs.mdadm pkgs.utillinux
63 ]);
64 });
65
66 bootDeviceCounters = fold (device: attr: attr // { "${device}" = (attr."${device}" or 0) + 1; }) {}
67 (concatMap (args: args.devices) cfg.mirroredBoots);
68
69in
70
71{
72
73 ###### interface
74
75 options = {
76
77 boot.loader.grub = {
78
79 enable = mkOption {
80 default = !config.boot.isContainer;
81 type = types.bool;
82 description = ''
83 Whether to enable the GNU GRUB boot loader.
84 '';
85 };
86
87 version = mkOption {
88 default = 2;
89 example = 1;
90 type = types.int;
91 description = ''
92 The version of GRUB to use: <literal>1</literal> for GRUB
93 Legacy (versions 0.9x), or <literal>2</literal> (the
94 default) for GRUB 2.
95 '';
96 };
97
98 device = mkOption {
99 default = "";
100 example = "/dev/hda";
101 type = types.str;
102 description = ''
103 The device on which the GRUB boot loader will be installed.
104 The special value <literal>nodev</literal> means that a GRUB
105 boot menu will be generated, but GRUB itself will not
106 actually be installed. To install GRUB on multiple devices,
107 use <literal>boot.loader.grub.devices</literal>.
108 '';
109 };
110
111 devices = mkOption {
112 default = [];
113 example = [ "/dev/hda" ];
114 type = types.listOf types.str;
115 description = ''
116 The devices on which the boot loader, GRUB, will be
117 installed. Can be used instead of <literal>device</literal> to
118 install GRUB onto multiple devices.
119 '';
120 };
121
122 mirroredBoots = mkOption {
123 default = [ ];
124 example = [
125 { path = "/boot1"; devices = [ "/dev/sda" ]; }
126 { path = "/boot2"; devices = [ "/dev/sdb" ]; }
127 ];
128 description = ''
129 Mirror the boot configuration to multiple partitions and install grub
130 to the respective devices corresponding to those partitions.
131 '';
132
133 type = types.listOf types.optionSet;
134
135 options = {
136
137 path = mkOption {
138 example = "/boot1";
139 type = types.str;
140 description = ''
141 The path to the boot directory where GRUB will be written. Generally
142 this boot path should double as an EFI path.
143 '';
144 };
145
146 efiSysMountPoint = mkOption {
147 default = null;
148 example = "/boot1/efi";
149 type = types.nullOr types.str;
150 description = ''
151 The path to the efi system mount point. Usually this is the same
152 partition as the above path and can be left as null.
153 '';
154 };
155
156 efiBootloaderId = mkOption {
157 default = null;
158 example = "NixOS-fsid";
159 type = types.nullOr types.str;
160 description = ''
161 The id of the bootloader to store in efi nvram.
162 The default is to name it NixOS and append the path or efiSysMountPoint.
163 This is only used if <literal>boot.loader.efi.canTouchEfiVariables</literal> is true.
164 '';
165 };
166
167 devices = mkOption {
168 default = [ ];
169 example = [ "/dev/sda" "/dev/sdb" ];
170 type = types.listOf types.str;
171 description = ''
172 The path to the devices which will have the GRUB MBR written.
173 Note these are typically device paths and not paths to partitions.
174 '';
175 };
176
177 };
178 };
179
180 configurationName = mkOption {
181 default = "";
182 example = "Stable 2.6.21";
183 type = types.str;
184 description = ''
185 GRUB entry name instead of default.
186 '';
187 };
188
189 storePath = mkOption {
190 default = "/nix/store";
191 type = types.str;
192 description = ''
193 Path to the Nix store when looking for kernels at boot.
194 Only makes sense when copyKernels is false.
195 '';
196 };
197
198 extraPrepareConfig = mkOption {
199 default = "";
200 type = types.lines;
201 description = ''
202 Additional bash commands to be run at the script that
203 prepares the GRUB menu entries.
204 '';
205 };
206
207 extraConfig = mkOption {
208 default = "";
209 example = "serial; terminal_output.serial";
210 type = types.lines;
211 description = ''
212 Additional GRUB commands inserted in the configuration file
213 just before the menu entries.
214 '';
215 };
216
217 extraPerEntryConfig = mkOption {
218 default = "";
219 example = "root (hd0)";
220 type = types.lines;
221 description = ''
222 Additional GRUB commands inserted in the configuration file
223 at the start of each NixOS menu entry.
224 '';
225 };
226
227 extraEntries = mkOption {
228 default = "";
229 type = types.lines;
230 example = ''
231 # GRUB 1 example (not GRUB 2 compatible)
232 title Windows
233 chainloader (hd0,1)+1
234
235 # GRUB 2 example
236 menuentry "Windows 7" {
237 chainloader (hd0,4)+1
238 }
239 '';
240 description = ''
241 Any additional entries you want added to the GRUB boot menu.
242 '';
243 };
244
245 extraEntriesBeforeNixOS = mkOption {
246 default = false;
247 type = types.bool;
248 description = ''
249 Whether extraEntries are included before the default option.
250 '';
251 };
252
253 extraFiles = mkOption {
254 type = types.attrsOf types.path;
255 default = {};
256 example = literalExample ''
257 { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; }
258 '';
259 description = ''
260 A set of files to be copied to <filename>/boot</filename>.
261 Each attribute name denotes the destination file name in
262 <filename>/boot</filename>, while the corresponding
263 attribute value specifies the source file.
264 '';
265 };
266
267 splashImage = mkOption {
268 type = types.nullOr types.path;
269 example = literalExample "./my-background.png";
270 description = ''
271 Background image used for GRUB. It must be a 640x480,
272 14-colour image in XPM format, optionally compressed with
273 <command>gzip</command> or <command>bzip2</command>. Set to
274 <literal>null</literal> to run GRUB in text mode.
275 '';
276 };
277
278 gfxmodeEfi = mkOption {
279 default = "auto";
280 example = "1024x768";
281 type = types.str;
282 description = ''
283 The gfxmode to pass to GRUB when loading a graphical boot interface under EFI.
284 '';
285 };
286
287 gfxmodeBios = mkOption {
288 default = "1024x768";
289 example = "auto";
290 type = types.str;
291 description = ''
292 The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS.
293 '';
294 };
295
296 configurationLimit = mkOption {
297 default = 100;
298 example = 120;
299 type = types.int;
300 description = ''
301 Maximum of configurations in boot menu. GRUB has problems when
302 there are too many entries.
303 '';
304 };
305
306 copyKernels = mkOption {
307 default = false;
308 type = types.bool;
309 description = ''
310 Whether the GRUB menu builder should copy kernels and initial
311 ramdisks to /boot. This is done automatically if /boot is
312 on a different partition than /.
313 '';
314 };
315
316 timeout = mkOption {
317 default = if (config.boot.loader.timeout != null) then config.boot.loader.timeout else -1;
318 type = types.int;
319 description = ''
320 Timeout (in seconds) until GRUB boots the default menu item.
321 '';
322 };
323
324 default = mkOption {
325 default = 0;
326 type = types.int;
327 description = ''
328 Index of the default menu item to be booted.
329 '';
330 };
331
332 fsIdentifier = mkOption {
333 default = "uuid";
334 type = types.addCheck types.str
335 (type: type == "uuid" || type == "label" || type == "provided");
336 description = ''
337 Determines how GRUB will identify devices when generating the
338 configuration file. A value of uuid / label signifies that grub
339 will always resolve the uuid or label of the device before using
340 it in the configuration. A value of provided means that GRUB will
341 use the device name as show in <command>df</command> or
342 <command>mount</command>. Note, zfs zpools / datasets are ignored
343 and will always be mounted using their labels.
344 '';
345 };
346
347 zfsSupport = mkOption {
348 default = false;
349 type = types.bool;
350 description = ''
351 Whether GRUB should be build against libzfs.
352 ZFS support is only available for GRUB v2.
353 This option is ignored for GRUB v1.
354 '';
355 };
356
357 efiSupport = mkOption {
358 default = false;
359 type = types.bool;
360 description = ''
361 Whether GRUB should be build with EFI support.
362 EFI support is only available for GRUB v2.
363 This option is ignored for GRUB v1.
364 '';
365 };
366
367 enableCryptodisk = mkOption {
368 default = false;
369 type = types.bool;
370 description = ''
371 Enable support for encrypted partitions. GRUB should automatically
372 unlock the correct encrypted partition and look for filesystems.
373 '';
374 };
375
376 trustedBoot = {
377
378 enable = mkOption {
379 default = false;
380 type = types.bool;
381 description = ''
382 Enable trusted boot. GRUB will measure all critical components during
383 the boot process to offer TCG (TPM) support.
384 '';
385 };
386
387 systemHasTPM = mkOption {
388 default = "";
389 example = "YES_TPM_is_activated";
390 type = types.string;
391 description = ''
392 Assertion that the target system has an activated TPM. It is a safety
393 check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
394 WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
395 '';
396 };
397
398 isHPLaptop = mkOption {
399 default = false;
400 type = types.bool;
401 description = ''
402 Use a special version of TrustedGRUB that is needed by some HP laptops
403 and works only for the HP laptops.
404 '';
405 };
406
407 };
408
409 };
410
411 };
412
413
414 ###### implementation
415
416 config = mkMerge [
417
418 { boot.loader.grub.splashImage = mkDefault (
419 if cfg.version == 1 then pkgs.fetchurl {
420 url = http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz;
421 sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
422 }
423 # GRUB 1.97 doesn't support gzipped XPMs.
424 else "${pkgs.nixos-artwork}/share/artwork/gnome/Gnome_Dark.png");
425 }
426
427 (mkIf cfg.enable {
428
429 boot.loader.grub.devices = optional (cfg.device != "") cfg.device;
430
431 boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [
432 { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
433 ];
434
435 system.build.installBootLoader = pkgs.writeScript "install-grub.sh" (''
436 #!${pkgs.stdenv.shell}
437 set -e
438 export PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ListCompare ])}
439 ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
440 '' + flip concatMapStrings cfg.mirroredBoots (args: ''
441 ${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig args} $@
442 ''));
443
444 system.build.grub = grub;
445
446 # Common attribute for boot loaders so only one of them can be
447 # set at once.
448 system.boot.loader.id = "grub";
449
450 environment.systemPackages = optional (grub != null) grub;
451
452 boot.loader.grub.extraPrepareConfig =
453 concatStrings (mapAttrsToList (n: v: ''
454 ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}"
455 '') config.boot.loader.grub.extraFiles);
456
457 assertions = [
458 {
459 assertion = !cfg.zfsSupport || cfg.version == 2;
460 message = "Only GRUB version 2 provides ZFS support";
461 }
462 {
463 assertion = cfg.mirroredBoots != [ ];
464 message = "You must set the option ‘boot.loader.grub.devices’ or "
465 + "'boot.loader.grub.mirroredBoots' to make the system bootable.";
466 }
467 {
468 assertion = all (c: c < 2) (mapAttrsToList (_: c: c) bootDeviceCounters);
469 message = "You cannot have duplicated devices in mirroredBoots";
470 }
471 {
472 assertion = !cfg.trustedBoot.enable || cfg.version == 2;
473 message = "Trusted GRUB is only available for GRUB 2";
474 }
475 {
476 assertion = !cfg.efiSupport || !cfg.trustedBoot.enable;
477 message = "Trusted GRUB does not have EFI support";
478 }
479 {
480 assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable;
481 message = "Trusted GRUB does not have ZFS support";
482 }
483 {
484 assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated";
485 message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'.";
486 }
487 ] ++ flip concatMap cfg.mirroredBoots (args: [
488 {
489 assertion = args.devices != [ ];
490 message = "A boot path cannot have an empty devices string in ${args.path}";
491 }
492 {
493 assertion = hasPrefix "/" args.path;
494 message = "Boot paths must be absolute, not ${args.path}";
495 }
496 {
497 assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint;
498 message = "Efi paths must be absolute, not ${args.efiSysMountPoint}";
499 }
500 ] ++ flip map args.devices (device: {
501 assertion = device == "nodev" || hasPrefix "/" device;
502 message = "GRUB devices must be absolute paths, not ${dev} in ${args.path}";
503 }));
504 })
505
506 ];
507
508
509 imports =
510 [ (mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ])
511 (mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ])
512 (mkRenamedOptionModule [ "boot" "extraGrubEntries" ] [ "boot" "loader" "grub" "extraEntries" ])
513 (mkRenamedOptionModule [ "boot" "extraGrubEntriesBeforeNixos" ] [ "boot" "loader" "grub" "extraEntriesBeforeNixOS" ])
514 (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
515 (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
516 (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
517 ];
518
519}