diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix index 3ea00e40c3b..a149d642abf 100644 --- a/nixos/modules/system/boot/loader/grub/grub.nix +++ b/nixos/modules/system/boot/loader/grub/grub.nix @@ -6,6 +6,8 @@ let cfg = config.boot.loader.grub; + efi = config.boot.loader.efi; + realGrub = if cfg.version == 1 then pkgs.grub else pkgs.grub2.override { zfsSupport = cfg.zfsSupport; }; @@ -16,21 +18,31 @@ let then null else realGrub; + grubEfi = + # EFI version of Grub v2 + if (cfg.devices != ["nodev"]) && cfg.efiSupport && (cfg.version == 2) + then pkgs.grub2.override { zfsSupport = cfg.zfsSupport; efiSupport = cfg.efiSupport; } + else null; + f = x: if x == null then "" else "" + x; grubConfig = pkgs.writeText "grub-config.xml" (builtins.toXML { splashImage = f config.boot.loader.grub.splashImage; grub = f grub; + grubTarget = f grub.grubTarget; shell = "${pkgs.stdenv.shell}"; fullVersion = (builtins.parseDrvName realGrub.name).version; + grubEfi = f grubEfi; + grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f grubEfi.grubTarget else ""; + inherit (efi) efiSysMountPoint canTouchEfiVariables; inherit (cfg) version extraConfig extraPerEntryConfig extraEntries extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels timeout - default devices fsIdentifier; - path = (makeSearchPath "bin" [ + default devices fsIdentifier efiSupport; + path = (makeSearchPath "bin" ([ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfsProgs - pkgs.utillinux - ]) + ":" + (makeSearchPath "sbin" [ + pkgs.utillinux ] ++ (if cfg.efiSupport && (cfg.version == 2) then [pkgs.efibootmgr ] else []) + )) + ":" + (makeSearchPath "sbin" [ pkgs.mdadm pkgs.utillinux ]); }); @@ -231,6 +243,18 @@ in 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. ''; }; @@ -269,7 +293,7 @@ in if cfg.devices == [] then throw "You must set the option ‘boot.loader.grub.device’ to make the system bootable." else - "PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ])} " + + "PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ListCompare ])} " + (if cfg.enableCryptodisk then "GRUB_ENABLE_CRYPTODISK=y " else "") + "${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig}"; diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl index ffee0271e93..74a934c6793 100644 --- a/nixos/modules/system/boot/loader/grub/install-grub.pl +++ b/nixos/modules/system/boot/loader/grub/install-grub.pl @@ -7,6 +7,7 @@ use File::Path; use File::stat; use File::Copy; use File::Slurp; +require List::Compare; use POSIX; use Cwd; @@ -39,6 +40,7 @@ sub runCommand { my $grub = get("grub"); my $grubVersion = int(get("version")); +my $grubTarget = get("grubTarget"); my $extraConfig = get("extraConfig"); my $extraPrepareConfig = get("extraPrepareConfig"); my $extraPerEntryConfig = get("extraPerEntryConfig"); @@ -50,6 +52,10 @@ my $copyKernels = get("copyKernels") eq "true"; my $timeout = int(get("timeout")); my $defaultEntry = int(get("default")); my $fsIdentifier = get("fsIdentifier"); +my $grubEfi = get("grubEfi"); +my $grubTargetEfi = get("grubTargetEfi"); +my $canTouchEfiVariables = get("canTouchEfiVariables"); +my $efiSysMountPoint = get("efiSysMountPoint"); $ENV{'PATH'} = get("path"); die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2; @@ -103,6 +109,8 @@ sub GetFs { # Skip the read-only bind-mount on /nix/store. next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions); + # Skip mount point generated by systemd-efi-boot-generator? + next if $fsType eq "autofs"; # Ensure this matches the intended directory next unless PathInMount($dir, $mountPoint); @@ -402,16 +410,114 @@ foreach my $fn (glob "/boot/kernels/*") { } -# Install GRUB if the version changed from the last time we installed -# it. FIXME: shouldn't we reinstall if ‘devices’ changed? -my $prevVersion = readFile("/boot/grub/version") // ""; -if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1" || get("fullVersion") ne $prevVersion) { +# +# Install GRUB if the parameters changed from the last time we installed it. +# + +struct(GrubState => { + version => '$', + efi => '$', + devices => '$', + efiMountPoint => '$', +}); +sub readGrubState { + my $defaultGrubState = GrubState->new(version => "", efi => "", devices => "", efiMountPoint => "" ); + open FILE, "; + chomp($version); + my $efi = ; + chomp($efi); + my $devices = ; + chomp($devices); + my $efiMountPoint = ; + chomp($efiMountPoint); + close FILE; + my $grubState = GrubState->new(version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint ); + return $grubState +} + +sub getDeviceTargets { + my @devices = (); foreach my $dev ($dom->findnodes('/expr/attrs/attr[@name = "devices"]/list/string/@value')) { $dev = $dev->findvalue(".") or die; + push(@devices, $dev); + } + return @devices; +} + +# check whether to install GRUB EFI or not +sub getEfiTarget { + if (($grub ne "") && ($grubEfi ne "")) { + # EFI can only be installed when target is set; + # A target is also required then for non-EFI grub + if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die } + else { return "both" } + } elsif (($grub ne "") && ($grubEfi eq "")) { + # TODO: It would be safer to disallow non-EFI grub installation if no taget is given. + # If no target is given, then grub auto-detects the target which can lead to errors. + # E.g. it seems as if grub would auto-detect a EFI target based on the availability + # of a EFI partition. + # However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386 + # architectures in NixOS. That would have to be fixed in the nixos modules first. + return "no" + } elsif (($grub eq "") && ($grubEfi ne "")) { + # EFI can only be installed when target is set; + if ($grubTargetEfi eq "") { die } + else {return "only" } + } else { + # at least one grub target has to be given + die + } +} + +my @deviceTargets = getDeviceTargets(); +my $efiTarget = getEfiTarget(); +my $prevGrubState = readGrubState(); +my @prevDeviceTargets = split/:/, $prevGrubState->devices; + +my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference() ); +my $versionDiffer = (get("fullVersion") eq \$prevGrubState->version); +my $efiDiffer = ($efiTarget eq \$prevGrubState->efi); +my $efiMountPointDiffer = ($efiSysMountPoint eq \$prevGrubState->efiMountPoint); +my $requireNewInstall = $devicesDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1"); + + +# install non-EFI GRUB +if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { + foreach my $dev (@deviceTargets) { next if $dev eq "nodev"; print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; - system("$grub/sbin/grub-install", "--recheck", Cwd::abs_path($dev)) == 0 - or die "$0: installation of GRUB on $dev failed\n"; + if ($grubTarget eq "") { + system("$grub/sbin/grub-install", "--recheck", Cwd::abs_path($dev)) == 0 + or die "$0: installation of GRUB on $dev failed\n"; + } else { + system("$grub/sbin/grub-install", "--recheck", "--target=$grubTarget", Cwd::abs_path($dev)) == 0 + or die "$0: installation of GRUB on $dev failed\n"; + } } - writeFile("/boot/grub/version", get("fullVersion")); +} + + +# install EFI GRUB +if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { + print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n"; + if ($canTouchEfiVariables eq "true") { + system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--efi-directory=$efiSysMountPoint") == 0 + or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; + } else { + system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--efi-directory=$efiSysMountPoint", "--no-nvram") == 0 + or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; + } +} + + +# update GRUB state file +if ($requireNewInstall != 0) { + open FILE, ">/boot/grub/state" or die "cannot create /boot/grub/state: $!\n"; + print FILE get("fullVersion"), "\n" or die; + print FILE $efiTarget, "\n" or die; + print FILE join( ":", @deviceTargets ), "\n" or die; + print FILE $efiSysMountPoint, "\n" or die; + close FILE or die; } diff --git a/pkgs/tools/misc/grub/2.0x.nix b/pkgs/tools/misc/grub/2.0x.nix index 1dbdfff7448..0f2f9fc7bf3 100644 --- a/pkgs/tools/misc/grub/2.0x.nix +++ b/pkgs/tools/misc/grub/2.0x.nix @@ -7,12 +7,18 @@ with stdenv.lib; let + pcSystems = { + "i686-linux".target = "i386"; + "x86_64-linux".target = "i386"; + }; + efiSystems = { "i686-linux".target = "i386"; "x86_64-linux".target = "x86_64"; }; canEfi = any (system: stdenv.system == system) (mapAttrsToList (name: _: name) efiSystems); + inPCSystems = any (system: stdenv.system == system) (mapAttrsToList (name: _: name) pcSystems); prefix = "grub${if efiSupport then "-efi" else ""}${optionalString zfsSupport "-zfs"}"; @@ -82,6 +88,13 @@ stdenv.mkDerivation rec { configureFlags = optional zfsSupport "--enable-libzfs" ++ optionals efiSupport [ "--with-platform=efi" "--target=${efiSystems.${stdenv.system}.target}" "--program-prefix=" ]; + # save target that grub is compiled for + grubTarget = if efiSupport + then "${efiSystems.${stdenv.system}.target}-efi" + else if inPCSystems + then "${pcSystems.${stdenv.system}.target}-pc" + else ""; + doCheck = false; enableParallelBuilding = true; diff --git a/pkgs/top-level/perl-packages.nix b/pkgs/top-level/perl-packages.nix index 2c7a8e0f530..7b37743358a 100644 --- a/pkgs/top-level/perl-packages.nix +++ b/pkgs/top-level/perl-packages.nix @@ -227,6 +227,14 @@ let self = _self // overrides; _self = with self; { }; }; + ListCompare = buildPerlPackage { + name = "List-Compare-1.18"; + src = fetchurl { + url = mirror://cpan/authors/id/J/JK/JKEENAN/List-Compare-0.39.tar.gz; + sha256 = "1v4gn176faanzf1kr9axdp1220da7nkvz0d66mnk34nd0skjjxcl"; + }; + }; + ArchiveCpio = buildPerlPackage { name = "Archive-Cpio-0.09"; src = fetchurl {