From f07f221f0ee46e64bb8a51e04dec6bade8645cfa Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 24 Jul 2012 19:16:27 -0400 Subject: [PATCH] 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". --- modules/installer/grub/grub-menu-builder.pl | 217 +++++++++++++ modules/installer/grub/grub-menu-builder.sh | 305 ------------------ modules/installer/grub/grub.nix | 22 +- .../activation/switch-to-configuration.sh | 3 +- 4 files changed, 229 insertions(+), 318 deletions(-) create mode 100644 modules/installer/grub/grub-menu-builder.pl delete mode 100644 modules/installer/grub/grub-menu-builder.sh diff --git a/modules/installer/grub/grub-menu-builder.pl b/modules/installer/grub/grub-menu-builder.pl new file mode 100644 index 00000000000..7f0dc15836b --- /dev/null +++ b/modules/installer/grub/grub-menu-builder.pl @@ -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; +} diff --git a/modules/installer/grub/grub-menu-builder.sh b/modules/installer/grub/grub-menu-builder.sh deleted file mode 100644 index f0e9fdb288c..00000000000 --- a/modules/installer/grub/grub-menu-builder.sh +++ /dev/null @@ -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" <> "$1" <-/file to --. -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 < /boot/nixos-grub-config </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 <> $tmp <> $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 <> $tmp <