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 splashMode = f cfg.splashMode;
42 backgroundColor = f cfg.backgroundColor;
43 grub = f grub;
44 grubTarget = f (grub.grubTarget or "");
45 shell = "${pkgs.runtimeShell}";
46 fullName = (builtins.parseDrvName realGrub.name).name;
47 fullVersion = (builtins.parseDrvName realGrub.name).version;
48 grubEfi = f grubEfi;
49 grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
50 bootPath = args.path;
51 storePath = config.boot.loader.grub.storePath;
52 bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
53 timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
54 inherit efiSysMountPoint;
55 inherit (args) devices;
56 inherit (efi) canTouchEfiVariables;
57 inherit (cfg)
58 version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
59 extraEntriesBeforeNixOS extraPrepareConfig extraInitrd configurationLimit copyKernels
60 default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios;
61 path = (makeBinPath ([
62 pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfs-progs
63 pkgs.utillinux ]
64 ++ (optional (cfg.efiSupport && (cfg.version == 2)) pkgs.efibootmgr)
65 ++ (optionals cfg.useOSProber [pkgs.busybox pkgs.os-prober])
66 )) + ":" + (makeSearchPathOutput "bin" "sbin" [
67 pkgs.mdadm pkgs.utillinux
68 ]);
69 font = if cfg.font == null then ""
70 else (if lib.last (lib.splitString "." cfg.font) == "pf2"
71 then cfg.font
72 else "${convertedFont}");
73 });
74
75 bootDeviceCounters = fold (device: attr: attr // { "${device}" = (attr."${device}" or 0) + 1; }) {}
76 (concatMap (args: args.devices) cfg.mirroredBoots);
77
78 convertedFont = (pkgs.runCommand "grub-font-converted.pf2" {}
79 (builtins.concatStringsSep " "
80 ([ "${realGrub}/bin/grub-mkfont"
81 cfg.font
82 "--output" "$out"
83 ] ++ (optional (cfg.fontSize!=null) "--size ${toString cfg.fontSize}")))
84 );
85
86 defaultSplash = "${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader}/share/artwork/gnome/nix-wallpaper-simple-dark-gray_bootloader.png";
87in
88
89{
90
91 ###### interface
92
93 options = {
94
95 boot.loader.grub = {
96
97 enable = mkOption {
98 default = !config.boot.isContainer;
99 type = types.bool;
100 description = ''
101 Whether to enable the GNU GRUB boot loader.
102 '';
103 };
104
105 version = mkOption {
106 default = 2;
107 example = 1;
108 type = types.int;
109 description = ''
110 The version of GRUB to use: <literal>1</literal> for GRUB
111 Legacy (versions 0.9x), or <literal>2</literal> (the
112 default) for GRUB 2.
113 '';
114 };
115
116 device = mkOption {
117 default = "";
118 example = "/dev/disk/by-id/wwn-0x500001234567890a";
119 type = types.str;
120 description = ''
121 The device on which the GRUB boot loader will be installed.
122 The special value <literal>nodev</literal> means that a GRUB
123 boot menu will be generated, but GRUB itself will not
124 actually be installed. To install GRUB on multiple devices,
125 use <literal>boot.loader.grub.devices</literal>.
126 '';
127 };
128
129 devices = mkOption {
130 default = [];
131 example = [ "/dev/disk/by-id/wwn-0x500001234567890a" ];
132 type = types.listOf types.str;
133 description = ''
134 The devices on which the boot loader, GRUB, will be
135 installed. Can be used instead of <literal>device</literal> to
136 install GRUB onto multiple devices.
137 '';
138 };
139
140 mirroredBoots = mkOption {
141 default = [ ];
142 example = [
143 { path = "/boot1"; devices = [ "/dev/disk/by-id/wwn-0x500001234567890a" ]; }
144 { path = "/boot2"; devices = [ "/dev/disk/by-id/wwn-0x500009876543210a" ]; }
145 ];
146 description = ''
147 Mirror the boot configuration to multiple partitions and install grub
148 to the respective devices corresponding to those partitions.
149 '';
150
151 type = with types; listOf (submodule {
152 options = {
153
154 path = mkOption {
155 example = "/boot1";
156 type = types.str;
157 description = ''
158 The path to the boot directory where GRUB will be written. Generally
159 this boot path should double as an EFI path.
160 '';
161 };
162
163 efiSysMountPoint = mkOption {
164 default = null;
165 example = "/boot1/efi";
166 type = types.nullOr types.str;
167 description = ''
168 The path to the efi system mount point. Usually this is the same
169 partition as the above path and can be left as null.
170 '';
171 };
172
173 efiBootloaderId = mkOption {
174 default = null;
175 example = "NixOS-fsid";
176 type = types.nullOr types.str;
177 description = ''
178 The id of the bootloader to store in efi nvram.
179 The default is to name it NixOS and append the path or efiSysMountPoint.
180 This is only used if <literal>boot.loader.efi.canTouchEfiVariables</literal> is true.
181 '';
182 };
183
184 devices = mkOption {
185 default = [ ];
186 example = [ "/dev/disk/by-id/wwn-0x500001234567890a" "/dev/disk/by-id/wwn-0x500009876543210a" ];
187 type = types.listOf types.str;
188 description = ''
189 The path to the devices which will have the GRUB MBR written.
190 Note these are typically device paths and not paths to partitions.
191 '';
192 };
193
194 };
195 });
196 };
197
198 configurationName = mkOption {
199 default = "";
200 example = "Stable 2.6.21";
201 type = types.str;
202 description = ''
203 GRUB entry name instead of default.
204 '';
205 };
206
207 storePath = mkOption {
208 default = "/nix/store";
209 type = types.str;
210 description = ''
211 Path to the Nix store when looking for kernels at boot.
212 Only makes sense when copyKernels is false.
213 '';
214 };
215
216 extraPrepareConfig = mkOption {
217 default = "";
218 type = types.lines;
219 description = ''
220 Additional bash commands to be run at the script that
221 prepares the GRUB menu entries.
222 '';
223 };
224
225 extraConfig = mkOption {
226 default = "";
227 example = "serial; terminal_output.serial";
228 type = types.lines;
229 description = ''
230 Additional GRUB commands inserted in the configuration file
231 just before the menu entries.
232 '';
233 };
234
235 extraPerEntryConfig = mkOption {
236 default = "";
237 example = "root (hd0)";
238 type = types.lines;
239 description = ''
240 Additional GRUB commands inserted in the configuration file
241 at the start of each NixOS menu entry.
242 '';
243 };
244
245 extraEntries = mkOption {
246 default = "";
247 type = types.lines;
248 example = ''
249 # GRUB 1 example (not GRUB 2 compatible)
250 title Windows
251 chainloader (hd0,1)+1
252
253 # GRUB 2 example
254 menuentry "Windows 7" {
255 chainloader (hd0,4)+1
256 }
257
258 # GRUB 2 with UEFI example, chainloading another distro
259 menuentry "Fedora" {
260 set root=(hd1,1)
261 chainloader /efi/fedora/grubx64.efi
262 }
263 '';
264 description = ''
265 Any additional entries you want added to the GRUB boot menu.
266 '';
267 };
268
269 extraEntriesBeforeNixOS = mkOption {
270 default = false;
271 type = types.bool;
272 description = ''
273 Whether extraEntries are included before the default option.
274 '';
275 };
276
277 extraFiles = mkOption {
278 type = types.attrsOf types.path;
279 default = {};
280 example = literalExample ''
281 { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; }
282 '';
283 description = ''
284 A set of files to be copied to <filename>/boot</filename>.
285 Each attribute name denotes the destination file name in
286 <filename>/boot</filename>, while the corresponding
287 attribute value specifies the source file.
288 '';
289 };
290
291 extraInitrd = mkOption {
292 type = types.nullOr types.path;
293 default = null;
294 example = "/boot/extra_initramfs.gz";
295 description = ''
296 The path to a second initramfs to be supplied to the kernel.
297 This ramfs will not be copied to the store, so that it can
298 contain secrets such as LUKS keyfiles or ssh keys.
299 This implies that rolling back to a previous configuration
300 won't rollback the state of this file.
301 '';
302 };
303
304 useOSProber = mkOption {
305 default = false;
306 type = types.bool;
307 description = ''
308 If set to true, append entries for other OSs detected by os-prober.
309 '';
310 };
311
312 splashImage = mkOption {
313 type = types.nullOr types.path;
314 example = literalExample "./my-background.png";
315 description = ''
316 Background image used for GRUB.
317 Set to <literal>null</literal> to run GRUB in text mode.
318
319 <note><para>
320 For grub 1:
321 It must be a 640x480,
322 14-colour image in XPM format, optionally compressed with
323 <command>gzip</command> or <command>bzip2</command>.
324 </para></note>
325
326 <note><para>
327 For grub 2:
328 File must be one of .png, .tga, .jpg, or .jpeg. JPEG images must
329 not be progressive.
330 The image will be scaled if necessary to fit the screen.
331 </para></note>
332 '';
333 };
334
335 backgroundColor = mkOption {
336 type = types.nullOr types.string;
337 example = "#7EBAE4";
338 default = null;
339 description = ''
340 Background color to be used for GRUB to fill the areas the image isn't filling.
341
342 <note><para>
343 This options has no effect for GRUB 1.
344 </para></note>
345 '';
346 };
347
348 splashMode = mkOption {
349 type = types.enum [ "normal" "stretch" ];
350 default = "stretch";
351 description = ''
352 Whether to stretch the image or show the image in the top-left corner unstretched.
353
354 <note><para>
355 This options has no effect for GRUB 1.
356 </para></note>
357 '';
358 };
359
360 font = mkOption {
361 type = types.nullOr types.path;
362 default = "${realGrub}/share/grub/unicode.pf2";
363 description = ''
364 Path to a TrueType, OpenType, or pf2 font to be used by Grub.
365 '';
366 };
367
368 fontSize = mkOption {
369 type = types.nullOr types.int;
370 example = literalExample 16;
371 default = null;
372 description = ''
373 Font size for the grub menu. Ignored unless <literal>font</literal>
374 is set to a ttf or otf font.
375 '';
376 };
377
378 gfxmodeEfi = mkOption {
379 default = "auto";
380 example = "1024x768";
381 type = types.str;
382 description = ''
383 The gfxmode to pass to GRUB when loading a graphical boot interface under EFI.
384 '';
385 };
386
387 gfxmodeBios = mkOption {
388 default = "1024x768";
389 example = "auto";
390 type = types.str;
391 description = ''
392 The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS.
393 '';
394 };
395
396 configurationLimit = mkOption {
397 default = 100;
398 example = 120;
399 type = types.int;
400 description = ''
401 Maximum of configurations in boot menu. GRUB has problems when
402 there are too many entries.
403 '';
404 };
405
406 copyKernels = mkOption {
407 default = false;
408 type = types.bool;
409 description = ''
410 Whether the GRUB menu builder should copy kernels and initial
411 ramdisks to /boot. This is done automatically if /boot is
412 on a different partition than /.
413 '';
414 };
415
416 default = mkOption {
417 default = "0";
418 type = types.either types.int types.str;
419 apply = toString;
420 description = ''
421 Index of the default menu item to be booted.
422 '';
423 };
424
425 fsIdentifier = mkOption {
426 default = "uuid";
427 type = types.enum [ "uuid" "label" "provided" ];
428 description = ''
429 Determines how GRUB will identify devices when generating the
430 configuration file. A value of uuid / label signifies that grub
431 will always resolve the uuid or label of the device before using
432 it in the configuration. A value of provided means that GRUB will
433 use the device name as show in <command>df</command> or
434 <command>mount</command>. Note, zfs zpools / datasets are ignored
435 and will always be mounted using their labels.
436 '';
437 };
438
439 zfsSupport = mkOption {
440 default = false;
441 type = types.bool;
442 description = ''
443 Whether GRUB should be built against libzfs.
444 ZFS support is only available for GRUB v2.
445 This option is ignored for GRUB v1.
446 '';
447 };
448
449 efiSupport = mkOption {
450 default = false;
451 type = types.bool;
452 description = ''
453 Whether GRUB should be built with EFI support.
454 EFI support is only available for GRUB v2.
455 This option is ignored for GRUB v1.
456 '';
457 };
458
459 efiInstallAsRemovable = mkOption {
460 default = false;
461 type = types.bool;
462 description = ''
463 Whether to invoke <literal>grub-install</literal> with
464 <literal>--removable</literal>.</para>
465
466 <para>Unless you turn this on, GRUB will install itself somewhere in
467 <literal>boot.loader.efi.efiSysMountPoint</literal> (exactly where
468 depends on other config variables). If you've set
469 <literal>boot.loader.efi.canTouchEfiVariables</literal> *AND* you
470 are currently booted in UEFI mode, then GRUB will use
471 <literal>efibootmgr</literal> to modify the boot order in the
472 EFI variables of your firmware to include this location. If you are
473 *not* booted in UEFI mode at the time GRUB is being installed, the
474 NVRAM will not be modified, and your system will not find GRUB at
475 boot time. However, GRUB will still return success so you may miss
476 the warning that gets printed ("<literal>efibootmgr: EFI variables
477 are not supported on this system.</literal>").</para>
478
479 <para>If you turn this feature on, GRUB will install itself in a
480 special location within <literal>efiSysMountPoint</literal> (namely
481 <literal>EFI/boot/boot$arch.efi</literal>) which the firmwares
482 are hardcoded to try first, regardless of NVRAM EFI variables.</para>
483
484 <para>To summarize, turn this on if:
485 <itemizedlist>
486 <listitem><para>You are installing NixOS and want it to boot in UEFI mode,
487 but you are currently booted in legacy mode</para></listitem>
488 <listitem><para>You want to make a drive that will boot regardless of
489 the NVRAM state of the computer (like a USB "removable" drive)</para></listitem>
490 <listitem><para>You simply dislike the idea of depending on NVRAM
491 state to make your drive bootable</para></listitem>
492 </itemizedlist>
493 '';
494 };
495
496 enableCryptodisk = mkOption {
497 default = false;
498 type = types.bool;
499 description = ''
500 Enable support for encrypted partitions. GRUB should automatically
501 unlock the correct encrypted partition and look for filesystems.
502 '';
503 };
504
505 forceInstall = mkOption {
506 default = false;
507 type = types.bool;
508 description = ''
509 Whether to try and forcibly install GRUB even if problems are
510 detected. It is not recommended to enable this unless you know what
511 you are doing.
512 '';
513 };
514
515 trustedBoot = {
516
517 enable = mkOption {
518 default = false;
519 type = types.bool;
520 description = ''
521 Enable trusted boot. GRUB will measure all critical components during
522 the boot process to offer TCG (TPM) support.
523 '';
524 };
525
526 systemHasTPM = mkOption {
527 default = "";
528 example = "YES_TPM_is_activated";
529 type = types.string;
530 description = ''
531 Assertion that the target system has an activated TPM. It is a safety
532 check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
533 WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
534 '';
535 };
536
537 isHPLaptop = mkOption {
538 default = false;
539 type = types.bool;
540 description = ''
541 Use a special version of TrustedGRUB that is needed by some HP laptops
542 and works only for the HP laptops.
543 '';
544 };
545
546 };
547
548 };
549
550 };
551
552
553 ###### implementation
554
555 config = mkMerge [
556
557 { boot.loader.grub.splashImage = mkDefault (
558 if cfg.version == 1 then pkgs.fetchurl {
559 url = http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz;
560 sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
561 }
562 # GRUB 1.97 doesn't support gzipped XPMs.
563 else defaultSplash);
564 }
565
566 (mkIf (cfg.splashImage == defaultSplash) {
567 boot.loader.grub.backgroundColor = mkDefault "#2F302F";
568 boot.loader.grub.splashMode = mkDefault "normal";
569 })
570
571 (mkIf cfg.enable {
572
573 boot.loader.grub.devices = optional (cfg.device != "") cfg.device;
574
575 boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [
576 { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
577 ];
578
579 system.build.installBootLoader =
580 let
581 install-grub-pl = pkgs.substituteAll {
582 src = ./install-grub.pl;
583 inherit (pkgs) utillinux;
584 btrfsprogs = pkgs.btrfs-progs;
585 };
586 in pkgs.writeScript "install-grub.sh" (''
587 #!${pkgs.runtimeShell}
588 set -e
589 export PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX XMLSAXBase ListCompare ])}
590 ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
591 '' + flip concatMapStrings cfg.mirroredBoots (args: ''
592 ${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
593 ''));
594
595 system.build.grub = grub;
596
597 # Common attribute for boot loaders so only one of them can be
598 # set at once.
599 system.boot.loader.id = "grub";
600
601 environment.systemPackages = optional (grub != null) grub;
602
603 boot.loader.grub.extraPrepareConfig =
604 concatStrings (mapAttrsToList (n: v: ''
605 ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}"
606 '') config.boot.loader.grub.extraFiles);
607
608 assertions = [
609 {
610 assertion = !cfg.zfsSupport || cfg.version == 2;
611 message = "Only GRUB version 2 provides ZFS support";
612 }
613 {
614 assertion = cfg.mirroredBoots != [ ];
615 message = "You must set the option ‘boot.loader.grub.devices’ or "
616 + "'boot.loader.grub.mirroredBoots' to make the system bootable.";
617 }
618 {
619 assertion = cfg.efiSupport || all (c: c < 2) (mapAttrsToList (_: c: c) bootDeviceCounters);
620 message = "You cannot have duplicated devices in mirroredBoots";
621 }
622 {
623 assertion = !cfg.trustedBoot.enable || cfg.version == 2;
624 message = "Trusted GRUB is only available for GRUB 2";
625 }
626 {
627 assertion = !cfg.efiSupport || !cfg.trustedBoot.enable;
628 message = "Trusted GRUB does not have EFI support";
629 }
630 {
631 assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable;
632 message = "Trusted GRUB does not have ZFS support";
633 }
634 {
635 assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated";
636 message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'.";
637 }
638 {
639 assertion = cfg.efiInstallAsRemovable -> cfg.efiSupport;
640 message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn on boot.loader.grub.efiSupport";
641 }
642 {
643 assertion = cfg.efiInstallAsRemovable -> !config.boot.loader.efi.canTouchEfiVariables;
644 message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn off boot.loader.efi.canTouchEfiVariables";
645 }
646 ] ++ flip concatMap cfg.mirroredBoots (args: [
647 {
648 assertion = args.devices != [ ];
649 message = "A boot path cannot have an empty devices string in ${args.path}";
650 }
651 {
652 assertion = hasPrefix "/" args.path;
653 message = "Boot paths must be absolute, not ${args.path}";
654 }
655 {
656 assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint;
657 message = "EFI paths must be absolute, not ${args.efiSysMountPoint}";
658 }
659 ] ++ flip map args.devices (device: {
660 assertion = device == "nodev" || hasPrefix "/" device;
661 message = "GRUB devices must be absolute paths, not ${device} in ${args.path}";
662 }));
663 })
664
665 ];
666
667
668 imports =
669 [ (mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "")
670 (mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ])
671 (mkRenamedOptionModule [ "boot" "extraGrubEntries" ] [ "boot" "loader" "grub" "extraEntries" ])
672 (mkRenamedOptionModule [ "boot" "extraGrubEntriesBeforeNixos" ] [ "boot" "loader" "grub" "extraEntriesBeforeNixOS" ])
673 (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
674 (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
675 (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
676 ];
677
678}