mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-22 09:20:58 +03:00

Before this commit there was no way to access (boot into) specialisation of previous generations from grub,even tho they are there. This commit will add grub submenu for each generation if the generation has any specialisation. Which will allow you to boot into them. Co-authored-by: Samuel Dionne-Riel <samuel@dionne-riel.com>
801 lines
28 KiB
Perl
801 lines
28 KiB
Perl
use strict;
|
||
use warnings;
|
||
use Class::Struct;
|
||
use XML::LibXML;
|
||
use File::Basename;
|
||
use File::Path;
|
||
use File::stat;
|
||
use File::Copy;
|
||
use File::Copy::Recursive qw(rcopy pathrm);
|
||
use File::Slurp;
|
||
use File::Temp;
|
||
use JSON;
|
||
use File::Find;
|
||
require List::Compare;
|
||
use POSIX;
|
||
use Cwd;
|
||
|
||
# system.build.toplevel path
|
||
my $defaultConfig = $ARGV[1] or die;
|
||
|
||
# Grub config XML generated by grubConfig function in grub.nix
|
||
my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
|
||
|
||
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 {
|
||
my ($fn) = @_;
|
||
# enable slurp mode: read entire file in one go
|
||
local $/ = undef;
|
||
open my $fh, "<$fn" or return undef;
|
||
my $s = <$fh>;
|
||
close $fh;
|
||
# disable slurp mode
|
||
local $/ = "\n";
|
||
chomp $s;
|
||
return $s;
|
||
}
|
||
|
||
sub writeFile {
|
||
my ($fn, $s) = @_;
|
||
open my $fh, ">$fn" or die "cannot create $fn: $!\n";
|
||
print $fh $s or die "cannot write to $fn: $!\n";
|
||
close $fh or die "cannot close $fn: $!\n";
|
||
}
|
||
|
||
sub runCommand {
|
||
open(my $fh, "-|", @_) or die "Failed to execute: $@_\n";
|
||
my @ret = $fh->getlines();
|
||
close $fh;
|
||
return ($?, @ret);
|
||
}
|
||
|
||
my $grub = get("grub");
|
||
my $grubTarget = get("grubTarget");
|
||
my $extraConfig = get("extraConfig");
|
||
my $extraPrepareConfig = get("extraPrepareConfig");
|
||
my $extraPerEntryConfig = get("extraPerEntryConfig");
|
||
my $extraEntries = get("extraEntries");
|
||
my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true";
|
||
my $splashImage = get("splashImage");
|
||
my $splashMode = get("splashMode");
|
||
my $entryOptions = get("entryOptions");
|
||
my $subEntryOptions = get("subEntryOptions");
|
||
my $backgroundColor = get("backgroundColor");
|
||
my $configurationLimit = int(get("configurationLimit"));
|
||
my $copyKernels = get("copyKernels") eq "true";
|
||
my $timeout = int(get("timeout"));
|
||
my $timeoutStyle = get("timeoutStyle");
|
||
my $defaultEntry = get("default");
|
||
my $fsIdentifier = get("fsIdentifier");
|
||
my $grubEfi = get("grubEfi");
|
||
my $grubTargetEfi = get("grubTargetEfi");
|
||
my $bootPath = get("bootPath");
|
||
my $storePath = get("storePath");
|
||
my $canTouchEfiVariables = get("canTouchEfiVariables");
|
||
my $efiInstallAsRemovable = get("efiInstallAsRemovable");
|
||
my $efiSysMountPoint = get("efiSysMountPoint");
|
||
my $gfxmodeEfi = get("gfxmodeEfi");
|
||
my $gfxmodeBios = get("gfxmodeBios");
|
||
my $gfxpayloadEfi = get("gfxpayloadEfi");
|
||
my $gfxpayloadBios = get("gfxpayloadBios");
|
||
my $bootloaderId = get("bootloaderId");
|
||
my $forceInstall = get("forceInstall");
|
||
my $font = get("font");
|
||
my $theme = get("theme");
|
||
my $saveDefault = $defaultEntry eq "saved";
|
||
$ENV{'PATH'} = get("path");
|
||
|
||
print STDERR "updating GRUB 2 menu...\n";
|
||
|
||
mkpath("$bootPath/grub", 0, 0700);
|
||
|
||
# Discover whether the bootPath is on the same filesystem as / and
|
||
# /nix/store. If not, then all kernels and initrds must be copied to
|
||
# the bootPath.
|
||
if (stat($bootPath)->dev != stat("/nix/store")->dev) {
|
||
$copyKernels = 1;
|
||
}
|
||
|
||
# Discover information about the location of the bootPath
|
||
struct(Fs => {
|
||
device => '$',
|
||
type => '$',
|
||
mount => '$',
|
||
});
|
||
sub PathInMount {
|
||
my ($path, $mount) = @_;
|
||
my @splitMount = split /\//, $mount;
|
||
my @splitPath = split /\//, $path;
|
||
if ($#splitPath < $#splitMount) {
|
||
return 0;
|
||
}
|
||
for (my $i = 0; $i <= $#splitMount; $i++) {
|
||
if ($splitMount[$i] ne $splitPath[$i]) {
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
# Figure out what filesystem is used for the directory with init/initrd/kernel files
|
||
sub GetFs {
|
||
my ($dir) = @_;
|
||
my $bestFs = Fs->new(device => "", type => "", mount => "");
|
||
foreach my $fs (read_file("/proc/self/mountinfo")) {
|
||
chomp $fs;
|
||
my @fields = split / /, $fs;
|
||
my $mountPoint = $fields[4];
|
||
next unless -d $mountPoint;
|
||
my @mountOptions = split /,/, $fields[5];
|
||
|
||
# Skip the optional fields.
|
||
my $n = 6; $n++ while $fields[$n] ne "-"; $n++;
|
||
my $fsType = $fields[$n];
|
||
my $device = $fields[$n + 1];
|
||
my @superOptions = split /,/, $fields[$n + 2];
|
||
|
||
# Skip the bind-mount on /nix/store.
|
||
next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions);
|
||
# 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);
|
||
|
||
# Is it better than our current match?
|
||
if (length($mountPoint) > length($bestFs->mount)) {
|
||
$bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint);
|
||
}
|
||
}
|
||
return $bestFs;
|
||
}
|
||
struct (Grub => {
|
||
path => '$',
|
||
search => '$',
|
||
});
|
||
my $driveid = 1;
|
||
sub GrubFs {
|
||
my ($dir) = @_;
|
||
my $fs = GetFs($dir);
|
||
my $path = substr($dir, length($fs->mount));
|
||
if (substr($path, 0, 1) ne "/") {
|
||
$path = "/$path";
|
||
}
|
||
my $search = "";
|
||
|
||
# ZFS is completely separate logic as zpools are always identified by a label
|
||
# or custom UUID
|
||
if ($fs->type eq 'zfs') {
|
||
my $sid = index($fs->device, '/');
|
||
|
||
if ($sid < 0) {
|
||
$search = '--label ' . $fs->device;
|
||
$path = '/@' . $path;
|
||
} else {
|
||
$search = '--label ' . substr($fs->device, 0, $sid);
|
||
$path = '/' . substr($fs->device, $sid) . '/@' . $path;
|
||
}
|
||
} else {
|
||
my %types = ('uuid' => '--fs-uuid', 'label' => '--label');
|
||
|
||
if ($fsIdentifier eq 'provided') {
|
||
# If the provided dev is identifying the partition using a label or uuid,
|
||
# we should get the label / uuid and do a proper search
|
||
my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/;
|
||
if ($#matches > 1) {
|
||
die "Too many matched devices"
|
||
} elsif ($#matches == 1) {
|
||
$search = "$types{$matches[0]} $matches[1]"
|
||
}
|
||
} else {
|
||
# Determine the identifying type
|
||
$search = $types{$fsIdentifier} . ' ';
|
||
|
||
# Based on the type pull in the identifier from the system
|
||
my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid", "-o", "export", @{[$fs->device]});
|
||
if ($status != 0) {
|
||
die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
|
||
}
|
||
my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/;
|
||
if ($#matches != 0) {
|
||
die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n"
|
||
}
|
||
$search .= $matches[0];
|
||
}
|
||
|
||
# BTRFS is a special case in that we need to fix the referenced path based on subvolumes
|
||
if ($fs->type eq 'btrfs') {
|
||
my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "show", @{[$fs->mount]});
|
||
if ($status != 0) {
|
||
die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
|
||
}
|
||
my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
|
||
if ($#ids > 0) {
|
||
die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n"
|
||
} elsif ($#ids == 0) {
|
||
my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "list", @{[$fs->mount]});
|
||
if ($status != 0) {
|
||
die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
|
||
}
|
||
my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/;
|
||
if ($#paths > 0) {
|
||
die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n";
|
||
} elsif ($#paths != 0) {
|
||
die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n";
|
||
}
|
||
$path = "/$paths[0]$path";
|
||
}
|
||
}
|
||
}
|
||
if (not $search eq "") {
|
||
$search = "search --set=drive$driveid " . $search;
|
||
$path = "(\$drive$driveid)$path";
|
||
$driveid += 1;
|
||
}
|
||
return Grub->new(path => $path, search => $search);
|
||
}
|
||
my $grubBoot = GrubFs($bootPath);
|
||
my $grubStore;
|
||
if ($copyKernels == 0) {
|
||
$grubStore = GrubFs($storePath);
|
||
}
|
||
|
||
# Generate the header.
|
||
my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n";
|
||
|
||
my @users = ();
|
||
foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
|
||
my $name = $user->findvalue('@name') or die;
|
||
my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
|
||
my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
|
||
my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
|
||
my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
|
||
|
||
if ($hashedPasswordFile) {
|
||
open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
|
||
$hashedPassword = <$f>;
|
||
chomp $hashedPassword;
|
||
}
|
||
if ($passwordFile) {
|
||
open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
|
||
$password = <$f>;
|
||
chomp $password;
|
||
}
|
||
|
||
if ($hashedPassword) {
|
||
if (index($hashedPassword, "grub.pbkdf2.") == 0) {
|
||
$conf .= "\npassword_pbkdf2 $name $hashedPassword";
|
||
}
|
||
else {
|
||
die "Password hash for GRUB user '$name' is not valid!";
|
||
}
|
||
}
|
||
elsif ($password) {
|
||
$conf .= "\npassword $name $password";
|
||
}
|
||
else {
|
||
die "GRUB user '$name' has no password!";
|
||
}
|
||
push(@users, $name);
|
||
}
|
||
if (@users) {
|
||
$conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
|
||
}
|
||
|
||
if ($copyKernels == 0) {
|
||
$conf .= "
|
||
" . $grubStore->search;
|
||
}
|
||
# FIXME: should use grub-mkconfig.
|
||
my $defaultEntryText = $defaultEntry;
|
||
if ($saveDefault) {
|
||
$defaultEntryText = "\"\${saved_entry}\"";
|
||
}
|
||
$conf .= "
|
||
" . $grubBoot->search . "
|
||
if [ -s \$prefix/grubenv ]; then
|
||
load_env
|
||
fi
|
||
|
||
# ‘grub-reboot’ sets a one-time saved entry, which we process here and
|
||
# then delete.
|
||
if [ \"\${next_entry}\" ]; then
|
||
set default=\"\${next_entry}\"
|
||
set next_entry=
|
||
save_env next_entry
|
||
set timeout=1
|
||
set boot_once=true
|
||
else
|
||
set default=$defaultEntryText
|
||
set timeout=$timeout
|
||
fi
|
||
set timeout_style=$timeoutStyle
|
||
|
||
function savedefault {
|
||
if [ -z \"\${boot_once}\"]; then
|
||
saved_entry=\"\${chosen}\"
|
||
save_env saved_entry
|
||
fi
|
||
}
|
||
|
||
# Setup the graphics stack for bios and efi systems
|
||
if [ \"\${grub_platform}\" = \"efi\" ]; then
|
||
insmod efi_gop
|
||
insmod efi_uga
|
||
else
|
||
insmod vbe
|
||
fi
|
||
";
|
||
|
||
if ($font) {
|
||
copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
|
||
$conf .= "
|
||
insmod font
|
||
if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
|
||
insmod gfxterm
|
||
if [ \"\${grub_platform}\" = \"efi\" ]; then
|
||
set gfxmode=$gfxmodeEfi
|
||
set gfxpayload=$gfxpayloadEfi
|
||
else
|
||
set gfxmode=$gfxmodeBios
|
||
set gfxpayload=$gfxpayloadBios
|
||
fi
|
||
terminal_output gfxterm
|
||
fi
|
||
";
|
||
}
|
||
if ($splashImage) {
|
||
# Keeps the image's extension.
|
||
my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$");
|
||
# The module for jpg is jpeg.
|
||
if ($suffix eq ".jpg") {
|
||
$suffix = ".jpeg";
|
||
}
|
||
if ($backgroundColor) {
|
||
$conf .= "
|
||
background_color '$backgroundColor'
|
||
";
|
||
}
|
||
copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
|
||
$conf .= "
|
||
insmod " . substr($suffix, 1) . "
|
||
if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
|
||
set color_normal=white/black
|
||
set color_highlight=black/white
|
||
else
|
||
set menu_color_normal=cyan/blue
|
||
set menu_color_highlight=white/blue
|
||
fi
|
||
";
|
||
}
|
||
|
||
rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
|
||
|
||
if ($theme) {
|
||
# Copy theme
|
||
rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
|
||
|
||
# Detect which modules will need to be loaded
|
||
my $with_png = 0;
|
||
my $with_jpeg = 0;
|
||
|
||
find({ wanted => sub {
|
||
if ($_ =~ /\.png$/i) {
|
||
$with_png = 1;
|
||
}
|
||
elsif ($_ =~ /\.jpe?g$/i) {
|
||
$with_jpeg = 1;
|
||
}
|
||
}, no_chdir => 1 }, $theme);
|
||
|
||
if ($with_png) {
|
||
$conf .= "
|
||
insmod png
|
||
"
|
||
}
|
||
if ($with_jpeg) {
|
||
$conf .= "
|
||
insmod jpeg
|
||
"
|
||
}
|
||
|
||
$conf .= "
|
||
# Sets theme.
|
||
set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
|
||
export theme
|
||
# Load theme fonts, if any
|
||
";
|
||
|
||
find( { wanted => sub {
|
||
if ($_ =~ /\.pf2$/i) {
|
||
$font = File::Spec->abs2rel($File::Find::name, $theme);
|
||
$conf .= "
|
||
loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
|
||
";
|
||
}
|
||
}, no_chdir => 1 }, $theme );
|
||
}
|
||
|
||
$conf .= "$extraConfig\n";
|
||
|
||
|
||
# Generate the menu entries.
|
||
$conf .= "\n";
|
||
|
||
my %copied;
|
||
mkpath("$bootPath/kernels", 0, 0755) if $copyKernels;
|
||
|
||
sub copyToKernelsDir {
|
||
my ($path) = @_;
|
||
return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels;
|
||
$path =~ /\/nix\/store\/(.*)/ or die;
|
||
my $name = $1; $name =~ s/\//-/g;
|
||
my $dst = "$bootPath/kernels/$name";
|
||
# Don't copy the file if $dst already exists. This means that we
|
||
# have to create $dst atomically to prevent partially copied
|
||
# kernels or initrd if this script is ever interrupted.
|
||
if (! -e $dst) {
|
||
my $tmp = "$dst.tmp";
|
||
copy $path, $tmp or die "cannot copy $path to $tmp: $!\n";
|
||
rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n";
|
||
}
|
||
$copied{$dst} = 1;
|
||
return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name";
|
||
}
|
||
|
||
sub addEntry {
|
||
my ($name, $path, $options, $current) = @_;
|
||
return unless -e "$path/kernel" && -e "$path/initrd";
|
||
|
||
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
|
||
my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd"));
|
||
|
||
# Include second initrd with secrets
|
||
if (-e -x "$path/append-initrd-secrets") {
|
||
# Name the initrd secrets after the system from which they're derived.
|
||
my $systemName = basename(Cwd::abs_path("$path"));
|
||
my $initrdSecretsPath = "$bootPath/kernels/$systemName-secrets";
|
||
|
||
mkpath(dirname($initrdSecretsPath), 0, 0755);
|
||
my $oldUmask = umask;
|
||
# Make sure initrd is not world readable (won't work if /boot is FAT)
|
||
umask 0137;
|
||
my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
|
||
if (system("$path/append-initrd-secrets", $initrdSecretsPathTemp) != 0) {
|
||
if ($current) {
|
||
die "failed to create initrd secrets $!\n";
|
||
} else {
|
||
say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
|
||
say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
|
||
}
|
||
}
|
||
# Check whether any secrets were actually added
|
||
if (-e $initrdSecretsPathTemp && ! -z _) {
|
||
rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
|
||
$copied{$initrdSecretsPath} = 1;
|
||
$initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$systemName-secrets";
|
||
} else {
|
||
unlink $initrdSecretsPathTemp;
|
||
rmdir dirname($initrdSecretsPathTemp);
|
||
}
|
||
umask $oldUmask;
|
||
}
|
||
|
||
my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef;
|
||
|
||
# FIXME: $confName
|
||
|
||
my $kernelParams =
|
||
"init=" . Cwd::abs_path("$path/init") . " " .
|
||
readFile("$path/kernel-params");
|
||
my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
|
||
|
||
$conf .= "menuentry \"$name\" " . $options . " {\n";
|
||
if ($saveDefault) {
|
||
$conf .= " savedefault\n";
|
||
}
|
||
$conf .= $grubBoot->search . "\n";
|
||
if ($copyKernels == 0) {
|
||
$conf .= $grubStore->search . "\n";
|
||
}
|
||
$conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig;
|
||
$conf .= " multiboot $xen $xenParams\n" if $xen;
|
||
$conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
|
||
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n";
|
||
$conf .= "}\n\n";
|
||
}
|
||
|
||
sub addGeneration {
|
||
my ($name, $nameSuffix, $path, $options, $current) = @_;
|
||
|
||
# Do not search for grand children
|
||
my @links = sort (glob "$path/specialisation/*");
|
||
|
||
if ($current != 1 && scalar(@links) != 0) {
|
||
$conf .= "submenu \"> $name$nameSuffix\" --class submenu {\n";
|
||
}
|
||
|
||
addEntry("$name" . (scalar(@links) == 0 ? "" : " - Default") . $nameSuffix, $path, $options, $current);
|
||
|
||
# Find all the children of the current default configuration
|
||
# Do not search for grand children
|
||
foreach my $link (@links) {
|
||
|
||
my $entryName = "";
|
||
|
||
my $cfgName = readFile("$link/configuration-name");
|
||
|
||
my $date = strftime("%F", localtime(lstat($link)->mtime));
|
||
my $version =
|
||
-e "$link/nixos-version"
|
||
? readFile("$link/nixos-version")
|
||
: basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
|
||
|
||
if ($cfgName) {
|
||
$entryName = $cfgName;
|
||
} else {
|
||
my $linkname = basename($link);
|
||
$entryName = "($linkname - $date - $version)";
|
||
}
|
||
addEntry("$name - $entryName", $link, "", 1);
|
||
}
|
||
|
||
if ($current != 1 && scalar(@links) != 0) {
|
||
$conf .= "}\n";
|
||
}
|
||
}
|
||
|
||
# Add default entries.
|
||
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
|
||
|
||
addGeneration("@distroName@", "", $defaultConfig, $entryOptions, 1);
|
||
|
||
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
|
||
|
||
my $grubBootPath = $grubBoot->path;
|
||
# extraEntries could refer to @bootRoot@, which we have to substitute
|
||
$conf =~ s/\@bootRoot\@/$grubBootPath/g;
|
||
|
||
# Emit submenus for all system profiles.
|
||
sub addProfile {
|
||
my ($profile, $description) = @_;
|
||
|
||
# Add entries for all generations of this profile.
|
||
$conf .= "submenu \"$description\" --class submenu {\n";
|
||
|
||
sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
|
||
|
||
my @links = sort
|
||
{ nrFromGen($b) <=> nrFromGen($a) }
|
||
(glob "$profile-*-link");
|
||
|
||
my $curEntry = 0;
|
||
foreach my $link (@links) {
|
||
last if $curEntry++ >= $configurationLimit;
|
||
if (! -e "$link/nixos-version") {
|
||
warn "skipping corrupt system profile entry ‘$link’\n";
|
||
next;
|
||
}
|
||
my $date = strftime("%F", localtime(lstat($link)->mtime));
|
||
my $version =
|
||
-e "$link/nixos-version"
|
||
? readFile("$link/nixos-version")
|
||
: basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
|
||
addGeneration("@distroName@ - Configuration " . nrFromGen($link), " ($date - $version)", $link, $subEntryOptions, 0);
|
||
}
|
||
|
||
$conf .= "}\n";
|
||
}
|
||
|
||
addProfile "/nix/var/nix/profiles/system", "@distroName@ - All configurations";
|
||
|
||
for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
|
||
my $name = basename($profile);
|
||
next unless $name =~ /^\w+$/;
|
||
addProfile $profile, "@distroName@ - Profile '$name'";
|
||
}
|
||
|
||
# extraPrepareConfig could refer to @bootPath@, which we have to substitute
|
||
$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g;
|
||
|
||
# Run extraPrepareConfig in sh
|
||
if ($extraPrepareConfig ne "") {
|
||
system((get("shell"), "-c", $extraPrepareConfig));
|
||
}
|
||
|
||
# write the GRUB config.
|
||
my $confFile = "$bootPath/grub/grub.cfg";
|
||
my $tmpFile = $confFile . ".tmp";
|
||
writeFile($tmpFile, $conf);
|
||
|
||
|
||
# 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 target 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 {
|
||
# prevent an installation if neither grub nor grubEfi is given
|
||
return "neither"
|
||
}
|
||
}
|
||
|
||
my $efiTarget = getEfiTarget();
|
||
|
||
# Append entries detected by os-prober
|
||
if (get("useOSProber") eq "true") {
|
||
if ($saveDefault) {
|
||
# os-prober will read this to determine if "savedefault" should be added to generated entries
|
||
$ENV{'GRUB_SAVEDEFAULT'} = "true";
|
||
}
|
||
|
||
my $targetpackage = ($efiTarget eq "no") ? $grub : $grubEfi;
|
||
system(get("shell"), "-c", "pkgdatadir=$targetpackage/share/grub $targetpackage/etc/grub.d/30_os-prober >> $tmpFile");
|
||
}
|
||
|
||
# Atomically switch to the new config
|
||
rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n";
|
||
|
||
|
||
# Remove obsolete files from $bootPath/kernels.
|
||
foreach my $fn (glob "$bootPath/kernels/*") {
|
||
next if defined $copied{$fn};
|
||
print STDERR "removing obsolete file $fn\n";
|
||
unlink $fn;
|
||
}
|
||
|
||
|
||
#
|
||
# Install GRUB if the parameters changed from the last time we installed it.
|
||
#
|
||
|
||
struct(GrubState => {
|
||
name => '$',
|
||
version => '$',
|
||
efi => '$',
|
||
devices => '$',
|
||
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 {
|
||
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
|
||
open FILE, "<$bootPath/grub/state" or return $defaultGrubState;
|
||
local $/ = "\n";
|
||
my $name = <FILE>;
|
||
chomp($name);
|
||
my $version = <FILE>;
|
||
chomp($version);
|
||
my $efi = <FILE>;
|
||
chomp($efi);
|
||
my $devices = <FILE>;
|
||
chomp($devices);
|
||
my $efiMountPoint = <FILE>;
|
||
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);
|
||
if ($jsonStateLine eq "") {
|
||
$jsonStateLine = '{}'; # empty JSON object
|
||
}
|
||
my %jsonState = %{decode_json($jsonStateLine)};
|
||
my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
|
||
close FILE;
|
||
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
|
||
return $grubState
|
||
}
|
||
|
||
my @deviceTargets = getList('devices');
|
||
my $prevGrubState = readGrubState();
|
||
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 $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
|
||
my $nameDiffer = get("fullName") ne $prevGrubState->name;
|
||
my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
|
||
my $efiDiffer = $efiTarget ne $prevGrubState->efi;
|
||
my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint;
|
||
if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
|
||
warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
|
||
$ENV{'NIXOS_INSTALL_BOOTLOADER'} = "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
|
||
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: $!";
|
||
|
||
# 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 2 boot loader on $dev...\n";
|
||
my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
|
||
if ($forceInstall eq "true") {
|
||
push @command, "--force";
|
||
}
|
||
if ($grubTarget ne "") {
|
||
push @command, "--target=$grubTarget";
|
||
}
|
||
(system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n";
|
||
}
|
||
}
|
||
|
||
|
||
# install EFI GRUB
|
||
if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
|
||
print STDERR "installing the GRUB 2 boot loader into $efiSysMountPoint...\n";
|
||
my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
|
||
if ($forceInstall eq "true") {
|
||
push @command, "--force";
|
||
}
|
||
push @command, "--bootloader-id=$bootloaderId";
|
||
if ($canTouchEfiVariables ne "true") {
|
||
push @command, "--no-nvram";
|
||
push @command, "--removable" if $efiInstallAsRemovable eq "true";
|
||
}
|
||
|
||
(system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n";
|
||
}
|
||
|
||
|
||
# update GRUB state file
|
||
if ($requireNewInstall != 0) {
|
||
# 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("fullVersion"), "\n" or die;
|
||
print FILE $efiTarget, "\n" or die;
|
||
print FILE join( ",", @deviceTargets ), "\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;
|
||
|
||
# Atomically switch to the new state file
|
||
rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n";
|
||
}
|