
I'm not any good at perl, and I only came up with this after many slow attempts. Any review welcome. But until this, memtest was broken, and extraPrepareConfig as well, in grub.
250 lines
8.0 KiB
Perl
250 lines
8.0 KiB
Perl
use strict;
|
||
use warnings;
|
||
use XML::LibXML;
|
||
use File::Basename;
|
||
use File::Path;
|
||
use File::stat;
|
||
use File::Copy;
|
||
use POSIX;
|
||
use Cwd;
|
||
|
||
my $defaultConfig = $ARGV[1] or die;
|
||
|
||
my $dom = XML::LibXML->load_xml(location => $ARGV[0]);
|
||
|
||
sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); }
|
||
|
||
sub readFile {
|
||
my ($fn) = @_; local $/ = undef;
|
||
open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
|
||
local $/ = "\n"; chomp $s; return $s;
|
||
}
|
||
|
||
sub writeFile {
|
||
my ($fn, $s) = @_;
|
||
open FILE, ">$fn" or die "cannot create $fn: $!\n";
|
||
print FILE $s or die;
|
||
close FILE or die;
|
||
}
|
||
|
||
my $grub = get("grub");
|
||
my $grubVersion = int(get("version"));
|
||
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 $configurationLimit = int(get("configurationLimit"));
|
||
my $copyKernels = get("copyKernels") eq "true";
|
||
my $timeout = int(get("timeout"));
|
||
my $defaultEntry = int(get("default"));
|
||
|
||
die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2;
|
||
|
||
print STDERR "updating GRUB $grubVersion menu...\n";
|
||
|
||
mkpath("/boot/grub", 0, 0700);
|
||
|
||
|
||
# Discover whether /boot is on the same filesystem as / and
|
||
# /nix/store. If not, then all kernels and initrds must be copied to
|
||
# /boot, and all paths in the GRUB config file must be relative to the
|
||
# root of the /boot filesystem. `$bootRoot' is the path to be
|
||
# prepended to paths under /boot.
|
||
my $bootRoot = "/boot";
|
||
if (stat("/")->dev != stat("/boot")->dev) {
|
||
$bootRoot = "";
|
||
$copyKernels = 1;
|
||
} elsif (stat("/boot")->dev != stat("/nix/store")->dev) {
|
||
$copyKernels = 1;
|
||
}
|
||
|
||
|
||
# Generate the header.
|
||
my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n";
|
||
|
||
if ($grubVersion == 1) {
|
||
$conf .= "
|
||
default $defaultEntry
|
||
timeout $timeout
|
||
";
|
||
if ($splashImage) {
|
||
copy $splashImage, "/boot/background.xpm.gz" or die "cannot copy $splashImage to /boot\n";
|
||
$conf .= "splashimage $bootRoot/background.xpm.gz\n";
|
||
}
|
||
}
|
||
|
||
else {
|
||
$conf .= "
|
||
if [ -s \$prefix/grubenv ]; then
|
||
load_env
|
||
fi
|
||
|
||
# ‘grub-reboot’ sets a one-time saved entry, which we process here and
|
||
# then delete.
|
||
if [ \"\${saved_entry}\" ]; then
|
||
# The next line *has* to look exactly like this, otherwise KDM's
|
||
# reboot feature won't work properly with GRUB 2.
|
||
set default=\"\${saved_entry}\"
|
||
set saved_entry=
|
||
set prev_saved_entry=
|
||
save_env saved_entry
|
||
save_env prev_saved_entry
|
||
set timeout=1
|
||
else
|
||
set default=$defaultEntry
|
||
set timeout=$timeout
|
||
fi
|
||
|
||
if loadfont $bootRoot/grub/fonts/unicode.pf2; then
|
||
set gfxmode=640x480
|
||
insmod gfxterm
|
||
insmod vbe
|
||
terminal_output gfxterm
|
||
fi
|
||
";
|
||
|
||
if ($splashImage) {
|
||
# FIXME: GRUB 1.97 doesn't resize the background image if it
|
||
# doesn't match the video resolution.
|
||
copy $splashImage, "/boot/background.png" or die "cannot copy $splashImage to /boot\n";
|
||
$conf .= "
|
||
insmod png
|
||
if background_image $bootRoot/background.png; 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
|
||
";
|
||
}
|
||
}
|
||
|
||
$conf .= "$extraConfig\n";
|
||
|
||
|
||
# Generate the menu entries.
|
||
$conf .= "\n";
|
||
|
||
my %copied;
|
||
mkpath("/boot/kernels", 0, 0755) if $copyKernels;
|
||
|
||
sub copyToKernelsDir {
|
||
my ($path) = @_;
|
||
return $path unless $copyKernels;
|
||
$path =~ /\/nix\/store\/(.*)/ or die;
|
||
my $name = $1; $name =~ s/\//-/g;
|
||
my $dst = "/boot/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 "$bootRoot/kernels/$name";
|
||
}
|
||
|
||
sub addEntry {
|
||
my ($name, $path) = @_;
|
||
return unless -e "$path/kernel" && -e "$path/initrd";
|
||
|
||
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
|
||
my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd"));
|
||
my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef;
|
||
|
||
# FIXME: $confName
|
||
|
||
my $kernelParams =
|
||
"systemConfig=" . Cwd::abs_path($path) . " " .
|
||
"init=" . Cwd::abs_path("$path/init") . " " .
|
||
readFile("$path/kernel-params");
|
||
my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
|
||
|
||
if ($grubVersion == 1) {
|
||
$conf .= "title $name\n";
|
||
$conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig;
|
||
$conf .= " kernel $xen $xenParams\n" if $xen;
|
||
$conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
|
||
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n";
|
||
} else {
|
||
$conf .= "menuentry \"$name\" {\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";
|
||
}
|
||
}
|
||
|
||
|
||
# Add default entries.
|
||
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
|
||
|
||
addEntry("NixOS - Default", $defaultConfig);
|
||
|
||
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
|
||
|
||
# extraEntries could refer to @bootRoot@, which we have to substitute
|
||
$conf =~ s/\@bootRoot\@/$bootRoot/g;
|
||
|
||
# Add entries for all previous generations of the system profile.
|
||
$conf .= "submenu \"NixOS - Old configurations\" {\n" if $grubVersion == 2;
|
||
|
||
sub nrFromGen { my ($x) = @_; $x =~ /system-(.*)-link/; return $1; }
|
||
|
||
my @links = sort
|
||
{ nrFromGen($b) <=> nrFromGen($a) }
|
||
(glob "/nix/var/nix/profiles/system-*-link");
|
||
|
||
my $curEntry = 0;
|
||
foreach my $link (@links) {
|
||
last if $curEntry++ >= $configurationLimit;
|
||
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]);
|
||
addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link);
|
||
}
|
||
|
||
$conf .= "}\n" if $grubVersion == 2;
|
||
|
||
# Run extraPrepareConfig in sh
|
||
if ($extraPrepareConfig ne "") {
|
||
system((get("shell"), "-c", $extraPrepareConfig));
|
||
}
|
||
|
||
# Atomically update the GRUB config.
|
||
my $confFile = $grubVersion == 1 ? "/boot/grub/menu.lst" : "/boot/grub/grub.cfg";
|
||
my $tmpFile = $confFile . ".tmp";
|
||
writeFile($tmpFile, $conf);
|
||
rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n";
|
||
|
||
|
||
# Remove obsolete files from /boot/kernels.
|
||
foreach my $fn (glob "/boot/kernels/*") {
|
||
next if defined $copied{$fn};
|
||
print STDERR "removing obsolete file $fn\n";
|
||
unlink $fn;
|
||
}
|
||
|
||
|
||
# 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) {
|
||
foreach my $dev ($dom->findnodes('/expr/attrs/attr[@name = "devices"]/list/string/@value')) {
|
||
$dev = $dev->findvalue(".") or die;
|
||
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";
|
||
}
|
||
writeFile("/boot/grub/version", get("fullVersion"));
|
||
}
|