Merge pull request #85895 from nh2/extra-grub-install-flags

grub: Add `boot.loader.grub.extraGrubInstallArgs` option
This commit is contained in:
Niklas Hambüchen 2020-07-06 22:08:31 +02:00 committed by GitHub
commit d676d5d119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 28 deletions

View File

@ -61,6 +61,7 @@ let
inherit (efi) canTouchEfiVariables; inherit (efi) canTouchEfiVariables;
inherit (cfg) inherit (cfg)
version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
extraGrubInstallArgs
extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios; default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios;
path = with pkgs; makeBinPath ( path = with pkgs; makeBinPath (
@ -298,6 +299,33 @@ in
''; '';
}; };
extraGrubInstallArgs = mkOption {
default = [ ];
example = [ "--modules=nativedisk ahci pata part_gpt part_msdos diskfilter mdraid1x lvm ext2" ];
type = types.listOf types.str;
description = ''
Additional arguments passed to <literal>grub-install</literal>.
A use case for this is to build specific GRUB2 modules
directly into the GRUB2 kernel image, so that they are available
and activated even in the <literal>grub rescue</literal> shell.
They are also necessary when the BIOS/UEFI is bugged and cannot
correctly read large disks (e.g. above 2 TB), so GRUB2's own
<literal>nativedisk</literal> and related modules can be used
to use its own disk drivers. The example shows one such case.
This is also useful for booting from USB.
See the
<link xlink:href="http://git.savannah.gnu.org/cgit/grub.git/tree/grub-core/commands/nativedisk.c?h=grub-2.04#n326">
GRUB source code
</link>
for which disk modules are available.
The list elements are passed directly as <literal>argv</literal>
arguments to the <literal>grub-install</literal> program, in order.
'';
};
extraPerEntryConfig = mkOption { extraPerEntryConfig = mkOption {
default = ""; default = "";
example = "root (hd0)"; example = "root (hd0)";
@ -669,7 +697,7 @@ in
in pkgs.writeScript "install-grub.sh" ('' in pkgs.writeScript "install-grub.sh" (''
#!${pkgs.runtimeShell} #!${pkgs.runtimeShell}
set -e set -e
export PERL5LIB=${with pkgs.perlPackages; makePerlPath [ FileSlurp XMLLibXML XMLSAX XMLSAXBase ListCompare ]} export PERL5LIB=${with pkgs.perlPackages; makePerlPath [ FileSlurp XMLLibXML XMLSAX XMLSAXBase ListCompare JSON ]}
${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"} ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
'' + flip concatMapStrings cfg.mirroredBoots (args: '' '' + flip concatMapStrings cfg.mirroredBoots (args: ''
${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@ ${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@

View File

@ -8,6 +8,7 @@ use File::stat;
use File::Copy; use File::Copy;
use File::Slurp; use File::Slurp;
use File::Temp; use File::Temp;
use JSON;
require List::Compare; require List::Compare;
use POSIX; use POSIX;
use Cwd; use Cwd;
@ -20,6 +21,16 @@ my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); } sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); }
sub getList {
my ($name) = @_;
my @list = ();
foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) {
$entry = $entry->findvalue(".") or die;
push(@list, $entry);
}
return @list;
}
sub readFile { sub readFile {
my ($fn) = @_; local $/ = undef; my ($fn) = @_; local $/ = undef;
open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE; open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
@ -241,7 +252,7 @@ if ($grubVersion == 1) {
timeout $timeout timeout $timeout
"; ";
if ($splashImage) { if ($splashImage) {
copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath\n"; copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
$conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n"; $conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n";
} }
} }
@ -319,7 +330,7 @@ else {
"; ";
if ($font) { if ($font) {
copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath\n"; copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
$conf .= " $conf .= "
insmod font insmod font
if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
@ -347,7 +358,7 @@ else {
background_color '$backgroundColor' background_color '$backgroundColor'
"; ";
} }
copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath\n"; copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
$conf .= " $conf .= "
insmod " . substr($suffix, 1) . " insmod " . substr($suffix, 1) . "
if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
@ -381,8 +392,8 @@ sub copyToKernelsDir {
# kernels or initrd if this script is ever interrupted. # kernels or initrd if this script is ever interrupted.
if (! -e $dst) { if (! -e $dst) {
my $tmp = "$dst.tmp"; my $tmp = "$dst.tmp";
copy $path, $tmp or die "cannot copy $path to $tmp\n"; copy $path, $tmp or die "cannot copy $path to $tmp: $!\n";
rename $tmp, $dst or die "cannot rename $tmp to $dst\n"; rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n";
} }
$copied{$dst} = 1; $copied{$dst} = 1;
return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name"; return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name";
@ -405,10 +416,10 @@ sub addEntry {
# Make sure initrd is not world readable (won't work if /boot is FAT) # Make sure initrd is not world readable (won't work if /boot is FAT)
umask 0137; umask 0137;
my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX"); my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets\n"; system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
# Check whether any secrets were actually added # Check whether any secrets were actually added
if (-e $initrdSecretsPathTemp && ! -z _) { if (-e $initrdSecretsPathTemp && ! -z _) {
rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place\n"; rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
$copied{$initrdSecretsPath} = 1; $copied{$initrdSecretsPath} = 1;
$initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets"; $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets";
} else { } else {
@ -575,7 +586,7 @@ if (get("useOSProber") eq "true") {
} }
# Atomically switch to the new config # Atomically switch to the new config
rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n"; rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n";
# Remove obsolete files from $bootPath/kernels. # Remove obsolete files from $bootPath/kernels.
@ -596,9 +607,12 @@ struct(GrubState => {
efi => '$', efi => '$',
devices => '$', devices => '$',
efiMountPoint => '$', efiMountPoint => '$',
extraGrubInstallArgs => '@',
}); });
# If you add something to the state file, only add it to the end
# because it is read line-by-line.
sub readGrubState { sub readGrubState {
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "" ); my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
open FILE, "<$bootPath/grub/state" or return $defaultGrubState; open FILE, "<$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n"; local $/ = "\n";
my $name = <FILE>; my $name = <FILE>;
@ -611,24 +625,34 @@ sub readGrubState {
chomp($devices); chomp($devices);
my $efiMountPoint = <FILE>; my $efiMountPoint = <FILE>;
chomp($efiMountPoint); chomp($efiMountPoint);
# Historically, arguments in the state file were one per each line, but that
# gets really messy when newlines are involved, structured arguments
# like lists are needed (they have to have a separator encoding), or even worse,
# when we need to remove a setting in the future. Thus, the 6th line is a JSON
# object that can store structured data, with named keys, and all new state
# should go in there.
my $jsonStateLine = <FILE>;
# For historical reasons we do not check the values above for un-definedness
# (that is, when the state file has too few lines and EOF is reached),
# because the above come from the first version of this logic and are thus
# guaranteed to be present.
$jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object
chomp($jsonStateLine);
my %jsonState = %{decode_json($jsonStateLine)};
my @extraGrubInstallArgs = @{$jsonState{'extraGrubInstallArgs'}};
close FILE; close FILE;
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint ); my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
return $grubState return $grubState
} }
sub getDeviceTargets { my @deviceTargets = getList('devices');
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;
}
my @deviceTargets = getDeviceTargets();
my $prevGrubState = readGrubState(); my $prevGrubState = readGrubState();
my @prevDeviceTargets = split/,/, $prevGrubState->devices; my @prevDeviceTargets = split/,/, $prevGrubState->devices;
my @extraGrubInstallArgs = getList('extraGrubInstallArgs');
my @prevExtraGrubInstallArgs = $prevGrubState->extraGrubInstallArgs;
my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference()); my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
my $nameDiffer = get("fullName") ne $prevGrubState->name; my $nameDiffer = get("fullName") ne $prevGrubState->name;
my $versionDiffer = get("fullVersion") ne $prevGrubState->version; my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
my $efiDiffer = $efiTarget ne $prevGrubState->efi; my $efiDiffer = $efiTarget ne $prevGrubState->efi;
@ -637,25 +661,25 @@ if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER"; warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
$ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1"; $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
} }
my $requireNewInstall = $devicesDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1"); my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
# install a symlink so that grub can detect the boot drive # install a symlink so that grub can detect the boot drive
my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space"; my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot"; symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
# install non-EFI GRUB # install non-EFI GRUB
if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
foreach my $dev (@deviceTargets) { foreach my $dev (@deviceTargets) {
next if $dev eq "nodev"; next if $dev eq "nodev";
print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n";
my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev)); my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
if ($forceInstall eq "true") { if ($forceInstall eq "true") {
push @command, "--force"; push @command, "--force";
} }
if ($grubTarget ne "") { if ($grubTarget ne "") {
push @command, "--target=$grubTarget"; push @command, "--target=$grubTarget";
} }
(system @command) == 0 or die "$0: installation of GRUB on $dev failed\n"; (system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n";
} }
} }
@ -663,7 +687,7 @@ if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
# install EFI GRUB # install EFI GRUB
if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n"; print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n";
my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint"); my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
if ($forceInstall eq "true") { if ($forceInstall eq "true") {
push @command, "--force"; push @command, "--force";
} }
@ -674,17 +698,29 @@ if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both"))
push @command, "--removable" if $efiInstallAsRemovable eq "true"; push @command, "--removable" if $efiInstallAsRemovable eq "true";
} }
(system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n";
} }
# update GRUB state file # update GRUB state file
if ($requireNewInstall != 0) { if ($requireNewInstall != 0) {
open FILE, ">$bootPath/grub/state" or die "cannot create $bootPath/grub/state: $!\n"; # Temp file for atomic rename.
my $stateFile = "$bootPath/grub/state";
my $stateFileTmp = $stateFile . ".tmp";
open FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n";
print FILE get("fullName"), "\n" or die; print FILE get("fullName"), "\n" or die;
print FILE get("fullVersion"), "\n" or die; print FILE get("fullVersion"), "\n" or die;
print FILE $efiTarget, "\n" or die; print FILE $efiTarget, "\n" or die;
print FILE join( ",", @deviceTargets ), "\n" or die; print FILE join( ",", @deviceTargets ), "\n" or die;
print FILE $efiSysMountPoint, "\n" or die; print FILE $efiSysMountPoint, "\n" or die;
my %jsonState = (
extraGrubInstallArgs => \@extraGrubInstallArgs
);
my $jsonStateLine = encode_json(\%jsonState);
print FILE $jsonStateLine, "\n" or die;
close FILE or die; close FILE or die;
# Atomically switch to the new state file
rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n";
} }