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}