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