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
57 extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
58 default fsIdentifier efiSupport gfxmodeEfi gfxmodeBios;
59 path = (makeBinPath ([
60 pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfs-progs
61 pkgs.utillinux ] ++ (if cfg.efiSupport && (cfg.version == 2) then [pkgs.efibootmgr ] else [])
62 )) + ":" + (makeSearchPathOutput "bin" "sbin" [
63 pkgs.mdadm pkgs.utillinux
64 ]);
65 });
66
67 bootDeviceCounters = fold (device: attr: attr // { "${device}" = (attr."${device}" or 0) + 1; }) {}
68 (concatMap (args: args.devices) cfg.mirroredBoots);
69
70in
71
72{
73
74 ###### interface
75
76 options = {
77
78 boot.loader.grub = {
79
80 enable = mkOption {
81 default = !config.boot.isContainer;
82 type = types.bool;
83 description = ''
84 Whether to enable the GNU GRUB boot loader.
85 '';
86 };
87
88 version = mkOption {
89 default = 2;
90 example = 1;
91 type = types.int;
92 description = ''
93 The version of GRUB to use: <literal>1</literal> for GRUB
94 Legacy (versions 0.9x), or <literal>2</literal> (the
95 default) for GRUB 2.
96 '';
97 };
98
99 device = mkOption {
100 default = "";
101 example = "/dev/hda";
102 type = types.str;
103 description = ''
104 The device on which the GRUB boot loader will be installed.
105 The special value <literal>nodev</literal> means that a GRUB
106 boot menu will be generated, but GRUB itself will not
107 actually be installed. To install GRUB on multiple devices,
108 use <literal>boot.loader.grub.devices</literal>.
109 '';
110 };
111
112 devices = mkOption {
113 default = [];
114 example = [ "/dev/hda" ];
115 type = types.listOf types.str;
116 description = ''
117 The devices on which the boot loader, GRUB, will be
118 installed. Can be used instead of <literal>device</literal> to
119 install GRUB onto multiple devices.
120 '';
121 };
122
123 mirroredBoots = mkOption {
124 default = [ ];
125 example = [
126 { path = "/boot1"; devices = [ "/dev/sda" ]; }
127 { path = "/boot2"; devices = [ "/dev/sdb" ]; }
128 ];
129 description = ''
130 Mirror the boot configuration to multiple partitions and install grub
131 to the respective devices corresponding to those partitions.
132 '';
133
134 type = types.listOf types.optionSet;
135
136 options = {
137
138 path = mkOption {
139 example = "/boot1";
140 type = types.str;
141 description = ''
142 The path to the boot directory where GRUB will be written. Generally
143 this boot path should double as an EFI path.
144 '';
145 };
146
147 efiSysMountPoint = mkOption {
148 default = null;
149 example = "/boot1/efi";
150 type = types.nullOr types.str;
151 description = ''
152 The path to the efi system mount point. Usually this is the same
153 partition as the above path and can be left as null.
154 '';
155 };
156
157 efiBootloaderId = mkOption {
158 default = null;
159 example = "NixOS-fsid";
160 type = types.nullOr types.str;
161 description = ''
162 The id of the bootloader to store in efi nvram.
163 The default is to name it NixOS and append the path or efiSysMountPoint.
164 This is only used if <literal>boot.loader.efi.canTouchEfiVariables</literal> is true.
165 '';
166 };
167
168 devices = mkOption {
169 default = [ ];
170 example = [ "/dev/sda" "/dev/sdb" ];
171 type = types.listOf types.str;
172 description = ''
173 The path to the devices which will have the GRUB MBR written.
174 Note these are typically device paths and not paths to partitions.
175 '';
176 };
177
178 };
179 };
180
181 configurationName = mkOption {
182 default = "";
183 example = "Stable 2.6.21";
184 type = types.str;
185 description = ''
186 GRUB entry name instead of default.
187 '';
188 };
189
190 storePath = mkOption {
191 default = "/nix/store";
192 type = types.str;
193 description = ''
194 Path to the Nix store when looking for kernels at boot.
195 Only makes sense when copyKernels is false.
196 '';
197 };
198
199 extraPrepareConfig = mkOption {
200 default = "";
201 type = types.lines;
202 description = ''
203 Additional bash commands to be run at the script that
204 prepares the GRUB menu entries.
205 '';
206 };
207
208 extraConfig = mkOption {
209 default = "";
210 example = "serial; terminal_output.serial";
211 type = types.lines;
212 description = ''
213 Additional GRUB commands inserted in the configuration file
214 just before the menu entries.
215 '';
216 };
217
218 extraPerEntryConfig = mkOption {
219 default = "";
220 example = "root (hd0)";
221 type = types.lines;
222 description = ''
223 Additional GRUB commands inserted in the configuration file
224 at the start of each NixOS menu entry.
225 '';
226 };
227
228 extraEntries = mkOption {
229 default = "";
230 type = types.lines;
231 example = ''
232 # GRUB 1 example (not GRUB 2 compatible)
233 title Windows
234 chainloader (hd0,1)+1
235
236 # GRUB 2 example
237 menuentry "Windows 7" {
238 chainloader (hd0,4)+1
239 }
240 '';
241 description = ''
242 Any additional entries you want added to the GRUB boot menu.
243 '';
244 };
245
246 extraEntriesBeforeNixOS = mkOption {
247 default = false;
248 type = types.bool;
249 description = ''
250 Whether extraEntries are included before the default option.
251 '';
252 };
253
254 extraFiles = mkOption {
255 type = types.attrsOf types.path;
256 default = {};
257 example = literalExample ''
258 { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; }
259 '';
260 description = ''
261 A set of files to be copied to <filename>/boot</filename>.
262 Each attribute name denotes the destination file name in
263 <filename>/boot</filename>, while the corresponding
264 attribute value specifies the source file.
265 '';
266 };
267
268 splashImage = mkOption {
269 type = types.nullOr types.path;
270 example = literalExample "./my-background.png";
271 description = ''
272 Background image used for GRUB. It must be a 640x480,
273 14-colour image in XPM format, optionally compressed with
274 <command>gzip</command> or <command>bzip2</command>. Set to
275 <literal>null</literal> to run GRUB in text mode.
276 '';
277 };
278
279 gfxmodeEfi = mkOption {
280 default = "auto";
281 example = "1024x768";
282 type = types.str;
283 description = ''
284 The gfxmode to pass to GRUB when loading a graphical boot interface under EFI.
285 '';
286 };
287
288 gfxmodeBios = mkOption {
289 default = "1024x768";
290 example = "auto";
291 type = types.str;
292 description = ''
293 The gfxmode to pass to GRUB when loading a graphical boot interface under BIOS.
294 '';
295 };
296
297 configurationLimit = mkOption {
298 default = 100;
299 example = 120;
300 type = types.int;
301 description = ''
302 Maximum of configurations in boot menu. GRUB has problems when
303 there are too many entries.
304 '';
305 };
306
307 copyKernels = mkOption {
308 default = false;
309 type = types.bool;
310 description = ''
311 Whether the GRUB menu builder should copy kernels and initial
312 ramdisks to /boot. This is done automatically if /boot is
313 on a different partition than /.
314 '';
315 };
316
317 default = mkOption {
318 default = 0;
319 type = types.int;
320 description = ''
321 Index of the default menu item to be booted.
322 '';
323 };
324
325 fsIdentifier = mkOption {
326 default = "uuid";
327 type = types.addCheck types.str
328 (type: type == "uuid" || type == "label" || type == "provided");
329 description = ''
330 Determines how GRUB will identify devices when generating the
331 configuration file. A value of uuid / label signifies that grub
332 will always resolve the uuid or label of the device before using
333 it in the configuration. A value of provided means that GRUB will
334 use the device name as show in <command>df</command> or
335 <command>mount</command>. Note, zfs zpools / datasets are ignored
336 and will always be mounted using their labels.
337 '';
338 };
339
340 zfsSupport = mkOption {
341 default = false;
342 type = types.bool;
343 description = ''
344 Whether GRUB should be built against libzfs.
345 ZFS support is only available for GRUB v2.
346 This option is ignored for GRUB v1.
347 '';
348 };
349
350 efiSupport = mkOption {
351 default = false;
352 type = types.bool;
353 description = ''
354 Whether GRUB should be built with EFI support.
355 EFI support is only available for GRUB v2.
356 This option is ignored for GRUB v1.
357 '';
358 };
359
360 enableCryptodisk = mkOption {
361 default = false;
362 type = types.bool;
363 description = ''
364 Enable support for encrypted partitions. GRUB should automatically
365 unlock the correct encrypted partition and look for filesystems.
366 '';
367 };
368
369 trustedBoot = {
370
371 enable = mkOption {
372 default = false;
373 type = types.bool;
374 description = ''
375 Enable trusted boot. GRUB will measure all critical components during
376 the boot process to offer TCG (TPM) support.
377 '';
378 };
379
380 systemHasTPM = mkOption {
381 default = "";
382 example = "YES_TPM_is_activated";
383 type = types.string;
384 description = ''
385 Assertion that the target system has an activated TPM. It is a safety
386 check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
387 WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
388 '';
389 };
390
391 isHPLaptop = mkOption {
392 default = false;
393 type = types.bool;
394 description = ''
395 Use a special version of TrustedGRUB that is needed by some HP laptops
396 and works only for the HP laptops.
397 '';
398 };
399
400 };
401
402 };
403
404 };
405
406
407 ###### implementation
408
409 config = mkMerge [
410
411 { boot.loader.grub.splashImage = mkDefault (
412 if cfg.version == 1 then pkgs.fetchurl {
413 url = http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz;
414 sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
415 }
416 # GRUB 1.97 doesn't support gzipped XPMs.
417 else "${pkgs.nixos-artwork}/share/artwork/gnome/Gnome_Dark.png");
418 }
419
420 (mkIf cfg.enable {
421
422 boot.loader.grub.devices = optional (cfg.device != "") cfg.device;
423
424 boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [
425 { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; }
426 ];
427
428 system.build.installBootLoader =
429 let
430 install-grub-pl = pkgs.substituteAll {
431 src = ./install-grub.pl;
432 inherit (pkgs) utillinux;
433 btrfsprogs = pkgs.btrfs-progs;
434 };
435 in 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 ${device} 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}