Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace grub-menu-builder with a much faster version
The old GRUB menu builder script is quite slow, typically taking several seconds. This is a real annoyance since it's run every time you switch to a new configuration. Therefore this patch replaces the Bash script with a much faster Perl script. In a VirtualBox test, the execution time went from 2.7s to 0.1s. The Perl version is also more correct because it uses XML to get the GRUB configuration (through builtins.toXML), so there are no shell escaping issues. The new script currently lacks support for subconfigurations defined through "nesting.children".
- Loading branch information
Showing
4 changed files
with
229 additions
and
318 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
use strict; | ||
use warnings; | ||
use XML::LibXML; | ||
use File::Basename; | ||
use File::Path; | ||
use File::stat; | ||
use File::Copy; | ||
use IO::File; | ||
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"); } | ||
|
||
my $grub = get("grub"); | ||
my $grubVersion = int(get("version")); | ||
my $extraConfig = get("extraConfig"); | ||
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; | ||
|
||
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 { | ||
copy "$grub/share/grub/unicode.pf2", "/boot/grub/unicode.pf2" or die "cannot copy unicode.pf2 to /boot/grub: $!\n"; | ||
|
||
$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/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. | ||
my $curEntry = 0; | ||
$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 if $curEntry++ > $configurationLimit; | ||
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")) : undef; | ||
|
||
# FIXME: $confName | ||
|
||
my $kernelParams = | ||
"systemConfig=" . Cwd::abs_path($path) . " " . | ||
"init=" . Cwd::abs_path("$path/init") . " " . | ||
join " ", IO::File->new("$path/kernel-params")->getlines; | ||
my $xenParams = $xen && -e "$path/xen-params" ? join " ", IO::File->new("$path/xen-params")->getlines : ""; | ||
|
||
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; | ||
|
||
|
||
# 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"); | ||
|
||
foreach my $link (@links) { | ||
my $date = strftime("%F", localtime(lstat($link)->mtime)); | ||
my $version = | ||
-e "$link/nixos-version" | ||
? IO::File->new("$link/nixos-version")->getline | ||
: basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); | ||
addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link); | ||
} | ||
|
||
$conf .= "}\n" if $grubVersion == 2; | ||
|
||
|
||
# Atomically update the GRUB config. | ||
my $confFile = $grubVersion == 1 ? "/boot/grub/menu.lst" : "/boot/grub/grub.cfg"; | ||
my $tmpFile = $confFile . ".tmp"; | ||
open CONF, ">$tmpFile" or die "cannot open $tmpFile for writing\n"; | ||
print CONF $conf or die; | ||
close 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; | ||
} |
Oops, something went wrong.