grub: Add boot.loader.grub.extraGrubInstallArgs option.

Useful for when you need to build grub modules into your grub kernel
to get a working boot, as shown in the added example.

To store this new value, we switch to more structural JSON approach.

Using one line per value to store in `/boot/grub/state` gets really messy when
the values are arrays, or even worse, can contain newlines (escaping would be
needed). Further, removing a value from the file would get extra messy
(empty lines we'd have to keep for backwards compatibility).

Thus, from now on we use JSON to store all values we'll need in the future.
This commit is contained in:
Niklas Hambüchen 2020-04-23 22:44:21 +02:00
parent 81c15742ce
commit 8665b5ab91
2 changed files with 61 additions and 6 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;
@ -606,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>;
@ -621,16 +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
} }
my @deviceTargets = getList('devices'); my @deviceTargets = getList('devices');
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;
@ -639,7 +661,7 @@ 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";
@ -650,7 +672,7 @@ 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";
} }
@ -665,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";
} }
@ -692,6 +714,11 @@ if ($requireNewInstall != 0) {
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 # Atomically switch to the new state file