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".
This commit is contained in:
Eelco Dolstra 2012-07-24 19:16:27 -04:00
parent b3b6b8ad60
commit f07f221f0e
4 changed files with 229 additions and 318 deletions

View File

@ -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;
}

View File

@ -1,305 +0,0 @@
#! @bash@/bin/sh -e
shopt -s nullglob
export PATH=/empty
for i in @path@; do PATH=$PATH:$i/bin; done
if test $# -ne 1; then
echo "Usage: grub-menu-builder.sh DEFAULT-CONFIG"
exit 1
fi
grubVersion="@version@"
defaultConfig="$1"
case "$grubVersion" in
1|2)
echo "updating GRUB $grubVersion menu..."
;;
*)
echo "Unsupported GRUB version \`$grubVersion'" >&2
echo "Supported versions are \`1' (GRUB Legacy) and \`2' (GRUB 1.9x)." >&2
exit 1
;;
esac
# 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.
if [ "$(stat -c '%D' /.)" != "$(stat -c '%D' /boot/.)" ]; then
bootRoot=
copyKernels=1
elif [ "$(stat -c '%D' /boot/.)" != "$(stat -c '%D' /nix/store/.)" ]; then
bootRoot=/boot
copyKernels=1
else
bootRoot=/boot
copyKernels="@copyKernels@" # user can override in the NixOS config
fi
prologue() {
case "$grubVersion" in
1)
cat > "$1" << GRUBEND
# Automatically generated. DO NOT EDIT THIS FILE!
default @default@
timeout @timeout@
GRUBEND
if [ -n "@splashImage@" ]; then
cp -f "@splashImage@" /boot/background.xpm.gz
echo "splashimage $bootRoot/background.xpm.gz" >> "$1"
fi
;;
2)
cp -f @grub@/share/grub/unicode.pf2 /boot/grub/unicode.pf2
cat > "$1" <<EOF
# Automatically generated. DO NOT EDIT THIS FILE!
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=@default@
set timeout=@timeout@
fi
if loadfont $bootRoot/grub/unicode.pf2; then
set gfxmode=640x480
insmod gfxterm
insmod vbe
terminal_output gfxterm
fi
EOF
if test -n "@splashImage@"; then
cp -f "@splashImage@" /boot/background.png
# FIXME: GRUB 1.97 doesn't resize the background image
# if it doesn't match the video resolution.
cat >> "$1" <<EOF
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
EOF
fi
;;
esac
}
case "$grubVersion" in
1) target="/boot/grub/menu.lst";;
2) target="/boot/grub/grub.cfg";;
esac
tmp="$target.tmp"
prologue "$tmp"
configurationCounter=0
configurationLimit="@configurationLimit@"
numAlienEntries=`cat <<EOF | egrep '^[[:space:]]*title' | wc -l
@extraEntries@
EOF`
if test $((configurationLimit+numAlienEntries)) -gt 190; then
configurationLimit=$((190-numAlienEntries));
fi
# Convert a path to a file in the Nix store such as
# /nix/store/<hash>-<name>/file to <hash>-<name>-<file>.
cleanName() {
local path="$1"
echo "$path" | sed 's|^/nix/store/||' | sed 's|/|-|g'
}
# Copy a file from the Nix store to /boot/kernels.
declare -A filesCopied
copyToKernelsDir() {
local src="$1"
local p="kernels/$(cleanName $src)"
local dst="/boot/$p"
# 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 ! test -e $dst; then
local dstTmp=$dst.tmp.$$
cp "$src" "$dstTmp"
mv $dstTmp $dst
fi
filesCopied[$dst]=1
result="$bootRoot/$p"
}
# Add an entry for a configuration to the Grub menu, and if
# appropriate, copy its kernel and initrd to /boot/kernels.
addEntry() {
local name="$1"
local path="$2"
local shortSuffix="$3"
configurationCounter=$((configurationCounter + 1))
if test $configurationCounter -gt @configurationLimit@; then
return
fi
if ! test -e $path/kernel -a -e $path/initrd; then
return
fi
local kernel=$(readlink -f $path/kernel)
local initrd=$(readlink -f $path/initrd)
local xen=$([ -f $path/xen.gz ] && readlink -f $path/xen.gz)
if test "$path" = "$defaultConfig"; then
cp "$kernel" /boot/nixos-kernel
cp "$initrd" /boot/nixos-initrd
cp "$(readlink -f "$path/init")" /boot/nixos-init
case "$grubVersion" in
1)
cat > /boot/nixos-grub-config <<EOF
title Emergency boot
kernel $bootRoot/nixos-kernel systemConfig=$(readlink -f "$path") init=/boot/nixos-init $(cat "$path/kernel-params")
initrd $bootRoot/nixos-initrd
EOF
;;
2)
cat > /boot/nixos-grub-config <<EOF
menuentry "Emergency boot" {
linux $bootRoot/nixos-kernel systemConfig=$(readlink -f "$path") init=/boot/nixos-init $(cat "$path/kernel-params")
initrd $bootRoot/nixos-initrd
}
EOF
;;
esac
fi
if test -n "$copyKernels"; then
copyToKernelsDir $kernel; kernel=$result
copyToKernelsDir $initrd; initrd=$result
if [ -n "$xen" ]; then copyToKernelsDir $xen; xen=$result; fi
fi
local confName=$(cat $path/configuration-name 2>/dev/null || true)
if test -n "$confName"; then
name="$confName $3"
fi
local kernelParams="systemConfig=$(readlink -f $path) init=$(readlink -f $path/init) $(cat $path/kernel-params)"
local xenParams="$([ -n "$xen" ] && cat $path/xen-params)"
case "$grubVersion" in
1)
cat >> "$tmp" << GRUBEND
title $name
@extraPerEntryConfig@
${xen:+kernel $xen $xenParams}
$(if [ -z "$xen" ]; then echo kernel; else echo module; fi) $kernel $kernelParams
$(if [ -z "$xen" ]; then echo initrd; else echo module; fi) $initrd
GRUBEND
;;
2)
cat >> "$tmp" << GRUBEND
menuentry "$name" {
@extraPerEntryConfig@
${xen:+multiboot $xen $xenParams}
$(if [ -z "$xen" ]; then echo linux; else echo module; fi) $kernel $kernelParams
$(if [ -z "$xen" ]; then echo initrd; else echo module; fi) $initrd
}
GRUBEND
;;
esac
}
if test -n "$copyKernels"; then
mkdir -p /boot/kernels
fi
@extraPrepareConfig@
# Additional entries specified verbatim by the configuration.
extraEntries=`cat <<EOF
@extraEntries@
EOF`
cat >> $tmp <<EOF
@extraConfig@
EOF
if test -n "@extraEntriesBeforeNixOS@"; then
echo "$extraEntries" >> $tmp
fi
addEntry "NixOS - Default" $defaultConfig ""
if test -z "@extraEntriesBeforeNixOS@"; then
echo "$extraEntries" >> $tmp
fi
# Add all generations of the system profile to the menu, in reverse
# (most recent to least recent) order.
for link in $((ls -d $defaultConfig/fine-tune/* ) | sort -n); do
date=$(stat --printf="%y\n" $link | sed 's/\..*//')
addEntry "NixOS - variation" $link ""
done
if [ "$grubVersion" = 2 ]; then
cat >> $tmp <<EOF
submenu "NixOS - Old configurations" {
EOF
fi
for generation in $(
(cd /nix/var/nix/profiles && for i in system-*-link; do echo $i; done) \
| sed 's/system-\([0-9]\+\)-link/\1/' \
| sort -n -r); do
link=/nix/var/nix/profiles/system-$generation-link
date=$(stat --printf="%y\n" $link | sed 's/\..*//' | sed 's/ .*//')
kernelVersion=$(cd $(dirname $(readlink -f $link/kernel))/lib/modules && echo *)
nixosVersion=$(if [ -e $link/nixos-version ]; then cat $link/nixos-version; fi)
addEntry "NixOS - Configuration $generation ($date - ${nixosVersion:-$kernelVersion})" $link "$generation ($date)"
done
if [ "$grubVersion" = 2 ]; then
cat >> $tmp <<EOF
}
EOF
fi
# Atomically update the GRUB configuration file.
mv $tmp $target
# Remove obsolete files from /boot/kernels.
for fn in /boot/kernels/*; do
if ! test "${filesCopied[$fn]}" = 1; then
rm -vf -- "$fn"
fi
done

View File

@ -8,16 +8,14 @@ let
grub = if cfg.version == 1 then pkgs.grub else pkgs.grub2;
grubMenuBuilder = pkgs.substituteAll {
src = ./grub-menu-builder.sh;
isExecutable = true;
inherit grub;
inherit (pkgs) bash;
path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
inherit (config.boot.loader.grub) copyKernels extraPrepareConfig
extraConfig extraEntries extraEntriesBeforeNixOS extraPerEntryConfig
splashImage configurationLimit version default timeout;
};
grubConfig = pkgs.writeText "grub-config.xml" (builtins.toXML
{ splashImage = "" + config.boot.loader.grub.splashImage;
grub = "" + grub;
inherit (config.boot.loader.grub)
version extraConfig extraPerEntryConfig extraEntries
extraEntriesBeforeNixOS configurationLimit copyKernels timeout
default;
});
in
@ -199,7 +197,9 @@ in
system.build = mkAssert (cfg.devices != [])
"You must set the boot.loader.grub.device option to make the system bootable."
{ menuBuilder = grubMenuBuilder;
{ menuBuilder =
"PERL5LIB=${makePerlPath [ pkgs.perlPackages.XMLLibXML pkgs.perlPackages.XMLSAX ]} " +
"${pkgs.perl}/bin/perl ${./grub-menu-builder.pl} ${grubConfig}";
inherit grub;
};

View File

@ -23,10 +23,9 @@ fi
# Install or update the bootloader.
if [ "$action" = "switch" -o "$action" = "boot" ]; then
if [ "@bootLoader@" = "grub" ]; then
mkdir -m 0700 -p /boot/grub
@menuBuilder@ @out@
# If the GRUB version has changed, then force a reinstall.