{ 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: 1 for GRUB
Legacy (versions 0.9x), or 2 (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 nodev means that a GRUB
boot menu will be generated, but GRUB itself will not
actually be installed. To install GRUB on multiple devices,
use boot.loader.grub.devices.
'';
};
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 device 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 /boot.
Each attribute name denotes the destination file name in
/boot, 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
gzip or bzip2. Set to
null 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 df or
mount. 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 ./winkler-gnu-blue-640x480.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}";
}));
})
];
}