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