{ config, lib, pkgs, ... }: with lib; let cfg = config.boot.loader.grub; efi = config.boot.loader.efi; realGrub = if cfg.version == 1 then pkgs.grub else if cfg.zfsSupport then pkgs.grub2.override { zfsSupport = true; } else pkgs.grub2; grub = # Don't include GRUB if we're only generating a GRUB menu (e.g., # in EC2 instances). if cfg.devices == ["nodev"] then null else realGrub; grubEfi = # EFI version of Grub v2 if cfg.efiSupport && (cfg.version == 2) then realGrub.override { efiSupport = cfg.efiSupport; } else null; f = x: if x == null then "" else "" + x; grubConfig = args: pkgs.writeText "grub-config.xml" (builtins.toXML { splashImage = f config.boot.loader.grub.splashImage; grub = f grub; grubTarget = f (grub.grubTarget or ""); shell = "${pkgs.stdenv.shell}"; fullVersion = (builtins.parseDrvName realGrub.name).version; grubEfi = f grubEfi; grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else ""; bootPath = args.path; efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint; inherit (args) devices; inherit (efi) canTouchEfiVariables; inherit (cfg) version extraConfig extraPerEntryConfig extraEntries extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels timeout default fsIdentifier efiSupport; path = (makeSearchPath "bin" ([ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfsProgs pkgs.utillinux ] ++ (if cfg.efiSupport && (cfg.version == 2) then [pkgs.efibootmgr ] else []) )) + ":" + (makeSearchPath "sbin" [ pkgs.mdadm pkgs.utillinux ]); }); bootDeviceCounters = fold (device: attr: attr // { "${device}" = (attr."${device}" or 0) + 1; }) {} (concatMap (args: args.devices) cfg.mirroredBoots); in { ###### interface options = { boot.loader.grub = { enable = mkOption { default = !config.boot.isContainer; type = types.bool; description = '' Whether to enable the GNU GRUB boot loader. ''; }; version = mkOption { default = 2; example = 1; type = types.int; description = '' The version of GRUB to use: <literal>1</literal> for GRUB Legacy (versions 0.9x), or <literal>2</literal> (the default) for GRUB 2. ''; }; device = mkOption { default = ""; example = "/dev/hda"; type = types.str; description = '' The device on which the GRUB boot loader will be installed. The special value <literal>nodev</literal> means that a GRUB boot menu will be generated, but GRUB itself will not actually be installed. To install GRUB on multiple devices, use <literal>boot.loader.grub.devices</literal>. ''; }; devices = mkOption { default = []; example = [ "/dev/hda" ]; type = types.listOf types.str; description = '' The devices on which the boot loader, GRUB, will be installed. Can be used instead of <literal>device</literal> to install grub into multiple devices (e.g., if as softraid arrays holding /boot). ''; }; mirroredBoots = mkOption { default = [ ]; example = [ { path = "/boot1"; devices = [ "/dev/sda" ]; } { path = "/boot2"; devices = [ "/dev/sdb" ]; } ]; description = '' Mirror the boot configuration to multiple partitions and install grub to the respective devices corresponding to those partitions. ''; type = types.listOf types.optionSet; options = { path = mkOption { example = "/boot1"; type = types.str; description = '' The path to the boot directory where grub will be written. Generally this boot parth should double as an efi path. ''; }; efiSysMountPoint = mkOption { default = null; example = "/boot1/efi"; type = types.nullOr types.str; description = '' The path to the efi system mount point. Usually this is the same partition as the above path and can be left as null. ''; }; devices = mkOption { default = [ ]; example = [ "/dev/sda" "/dev/sdb" ]; type = types.listOf types.str; description = '' The path to the devices which will have the grub mbr written. Note these are typically device paths and not paths to partitions. ''; }; }; }; configurationName = mkOption { default = ""; example = "Stable 2.6.21"; type = types.str; description = '' GRUB entry name instead of default. ''; }; extraPrepareConfig = mkOption { default = ""; type = types.lines; description = '' Additional bash commands to be run at the script that prepares the grub menu entries. ''; }; extraConfig = mkOption { default = ""; example = "serial; terminal_output.serial"; type = types.lines; description = '' Additional GRUB commands inserted in the configuration file just before the menu entries. ''; }; extraPerEntryConfig = mkOption { default = ""; example = "root (hd0)"; type = types.lines; description = '' Additional GRUB commands inserted in the configuration file at the start of each NixOS menu entry. ''; }; extraEntries = mkOption { default = ""; type = types.lines; example = '' # GRUB 1 example (not GRUB 2 compatible) title Windows chainloader (hd0,1)+1 # GRUB 2 example menuentry "Windows 7" { chainloader (hd0,4)+1 } ''; description = '' Any additional entries you want added to the GRUB boot menu. ''; }; extraEntriesBeforeNixOS = mkOption { default = false; type = types.bool; description = '' Whether extraEntries are included before the default option. ''; }; extraFiles = mkOption { default = {}; example = literalExample '' { "memtest.bin" = "''${pkgs.memtest86plus}/memtest.bin"; } ''; description = '' A set of files to be copied to <filename>/boot</filename>. Each attribute name denotes the destination file name in <filename>/boot</filename>, while the corresponding attribute value specifies the source file. ''; }; splashImage = mkOption { type = types.nullOr types.path; example = literalExample "./my-background.png"; description = '' Background image used for GRUB. It must be a 640x480, 14-colour image in XPM format, optionally compressed with <command>gzip</command> or <command>bzip2</command>. Set to <literal>null</literal> to run GRUB in text mode. ''; }; configurationLimit = mkOption { default = 100; example = 120; type = types.int; description = '' Maximum of configurations in boot menu. GRUB has problems when there are too many entries. ''; }; copyKernels = mkOption { default = false; type = types.bool; description = '' Whether the GRUB menu builder should copy kernels and initial ramdisks to /boot. This is done automatically if /boot is on a different partition than /. ''; }; timeout = mkOption { default = if (config.boot.loader.timeout != null) then config.boot.loader.timeout else -1; type = types.int; description = '' Timeout (in seconds) until GRUB boots the default menu item. ''; }; default = mkOption { default = 0; type = types.int; description = '' Index of the default menu item to be booted. ''; }; fsIdentifier = mkOption { default = "uuid"; type = types.addCheck types.str (type: type == "uuid" || type == "label" || type == "provided"); description = '' Determines how grub will identify devices when generating the configuration file. A value of uuid / label signifies that grub will always resolve the uuid or label of the device before using it in the configuration. A value of provided means that grub will use the device name as show in <command>df</command> or <command>mount</command>. Note, zfs zpools / datasets are ignored and will always be mounted using their labels. ''; }; zfsSupport = mkOption { default = false; type = types.bool; description = '' Whether grub should be build against libzfs. ZFS support is only available for GRUB v2. This option is ignored for GRUB v1. ''; }; efiSupport = mkOption { default = false; type = types.bool; description = '' Whether grub should be build with EFI support. EFI support is only available for GRUB v2. This option is ignored for GRUB v1. ''; }; enableCryptodisk = mkOption { default = false; type = types.bool; description = '' Enable support for encrypted partitions. Grub should automatically unlock the correct encrypted partition and look for filesystems. ''; }; }; }; ###### implementation config = mkMerge [ { boot.loader.grub.splashImage = mkDefault ( if cfg.version == 1 then pkgs.fetchurl { url = http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz; sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59"; } # GRUB 1.97 doesn't support gzipped XPMs. else "${pkgs.nixos-artwork}/share/artwork/gnome/Gnome_Dark.png"); } (mkIf cfg.enable { boot.loader.grub.devices = optional (cfg.device != "") cfg.device; boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [ { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; } ]; system.build.installBootLoader = pkgs.writeScript "install-grub.sh" ('' #!${pkgs.stdenv.shell} set -e export PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ListCompare ])} ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"} '' + flip concatMapStrings cfg.mirroredBoots (args: '' ${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig args} $@ '')); system.build.grub = grub; # Common attribute for boot loaders so only one of them can be # set at once. system.boot.loader.id = "grub"; environment.systemPackages = optional (grub != null) grub; boot.loader.grub.extraPrepareConfig = concatStrings (mapAttrsToList (n: v: '' ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}" '') config.boot.loader.grub.extraFiles); assertions = [ { assertion = !cfg.zfsSupport || cfg.version == 2; message = "Only grub version 2 provides zfs support"; } { assertion = cfg.mirroredBoots != [ ]; message = "You must set the option ‘boot.loader.grub.devices’ or " + "'boot.loader.grub.mirroredBoots' to make the system bootable."; } { assertion = all (c: c < 2) (mapAttrsToList (_: c: c) bootDeviceCounters); message = "You cannot have duplicated devices in mirroredBoots"; } ] ++ flip concatMap cfg.mirroredBoots (args: [ { assertion = args.devices != [ ]; message = "A boot path cannot have an empty devices string in ${arg.path}"; } { assertion = hasPrefix "/" args.path; message = "Boot paths must be absolute, not ${args.path}"; } { assertion = if args.efiSysMountPoint == null then true else hasPrefix "/" args.efiSysMountPoint; message = "Efi paths must be absolute, not ${args.efiSysMountPoint}"; } ] ++ flip map args.devices (device: { assertion = device == "nodev" || hasPrefix "/" device; message = "Grub devices must be absolute paths, not ${dev} in ${args.path}"; })); }) ]; }