at 16.09-beta 20 kB view raw
1use strict; 2use warnings; 3use Class::Struct; 4use XML::LibXML; 5use File::Basename; 6use File::Path; 7use File::stat; 8use File::Copy; 9use File::Slurp; 10use File::Temp; 11require List::Compare; 12use POSIX; 13use Cwd; 14 15# system.build.toplevel path 16my $defaultConfig = $ARGV[1] or die; 17 18# Grub config XML generated by grubConfig function in grub.nix 19my $dom = XML::LibXML->load_xml(location => $ARGV[0]); 20 21sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); } 22 23sub readFile { 24 my ($fn) = @_; local $/ = undef; 25 open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE; 26 local $/ = "\n"; chomp $s; return $s; 27} 28 29sub writeFile { 30 my ($fn, $s) = @_; 31 open FILE, ">$fn" or die "cannot create $fn: $!\n"; 32 print FILE $s or die; 33 close FILE or die; 34} 35 36sub runCommand { 37 my ($cmd) = @_; 38 open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n"; 39 my @ret = <FILE>; 40 close FILE; 41 return ($?, @ret); 42} 43 44my $grub = get("grub"); 45my $grubVersion = int(get("version")); 46my $grubTarget = get("grubTarget"); 47my $extraConfig = get("extraConfig"); 48my $extraPrepareConfig = get("extraPrepareConfig"); 49my $extraPerEntryConfig = get("extraPerEntryConfig"); 50my $extraEntries = get("extraEntries"); 51my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true"; 52my $splashImage = get("splashImage"); 53my $configurationLimit = int(get("configurationLimit")); 54my $copyKernels = get("copyKernels") eq "true"; 55my $timeout = int(get("timeout")); 56my $defaultEntry = int(get("default")); 57my $fsIdentifier = get("fsIdentifier"); 58my $grubEfi = get("grubEfi"); 59my $grubTargetEfi = get("grubTargetEfi"); 60my $bootPath = get("bootPath"); 61my $storePath = get("storePath"); 62my $canTouchEfiVariables = get("canTouchEfiVariables"); 63my $efiSysMountPoint = get("efiSysMountPoint"); 64my $gfxmodeEfi = get("gfxmodeEfi"); 65my $gfxmodeBios = get("gfxmodeBios"); 66my $bootloaderId = get("bootloaderId"); 67$ENV{'PATH'} = get("path"); 68 69die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2; 70 71print STDERR "updating GRUB $grubVersion menu...\n"; 72 73mkpath("$bootPath/grub", 0, 0700); 74 75# Discover whether the bootPath is on the same filesystem as / and 76# /nix/store. If not, then all kernels and initrds must be copied to 77# the bootPath. 78if (stat($bootPath)->dev != stat("/nix/store")->dev) { 79 $copyKernels = 1; 80} 81 82# Discover information about the location of the bootPath 83struct(Fs => { 84 device => '$', 85 type => '$', 86 mount => '$', 87}); 88sub PathInMount { 89 my ($path, $mount) = @_; 90 my @splitMount = split /\//, $mount; 91 my @splitPath = split /\//, $path; 92 if ($#splitPath < $#splitMount) { 93 return 0; 94 } 95 for (my $i = 0; $i <= $#splitMount; $i++) { 96 if ($splitMount[$i] ne $splitPath[$i]) { 97 return 0; 98 } 99 } 100 return 1; 101} 102 103# Figure out what filesystem is used for the directory with init/initrd/kernel files 104sub GetFs { 105 my ($dir) = @_; 106 my $bestFs = Fs->new(device => "", type => "", mount => ""); 107 foreach my $fs (read_file("/proc/self/mountinfo")) { 108 chomp $fs; 109 my @fields = split / /, $fs; 110 my $mountPoint = $fields[4]; 111 next unless -d $mountPoint; 112 my @mountOptions = split /,/, $fields[5]; 113 114 # Skip the optional fields. 115 my $n = 6; $n++ while $fields[$n] ne "-"; $n++; 116 my $fsType = $fields[$n]; 117 my $device = $fields[$n + 1]; 118 my @superOptions = split /,/, $fields[$n + 2]; 119 120 # Skip the read-only bind-mount on /nix/store. 121 next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions); 122 # Skip mount point generated by systemd-efi-boot-generator? 123 next if $fsType eq "autofs"; 124 125 # Ensure this matches the intended directory 126 next unless PathInMount($dir, $mountPoint); 127 128 # Is it better than our current match? 129 if (length($mountPoint) > length($bestFs->mount)) { 130 $bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint); 131 } 132 } 133 return $bestFs; 134} 135struct (Grub => { 136 path => '$', 137 search => '$', 138}); 139my $driveid = 1; 140sub GrubFs { 141 my ($dir) = @_; 142 my $fs = GetFs($dir); 143 my $path = substr($dir, length($fs->mount)); 144 if (substr($path, 0, 1) ne "/") { 145 $path = "/$path"; 146 } 147 my $search = ""; 148 149 if ($grubVersion > 1) { 150 # ZFS is completely separate logic as zpools are always identified by a label 151 # or custom UUID 152 if ($fs->type eq 'zfs') { 153 my $sid = index($fs->device, '/'); 154 155 if ($sid < 0) { 156 $search = '--label ' . $fs->device; 157 $path = '/@' . $path; 158 } else { 159 $search = '--label ' . substr($fs->device, 0, $sid); 160 $path = '/' . substr($fs->device, $sid) . '/@' . $path; 161 } 162 } else { 163 my %types = ('uuid' => '--fs-uuid', 'label' => '--label'); 164 165 if ($fsIdentifier eq 'provided') { 166 # If the provided dev is identifying the partition using a label or uuid, 167 # we should get the label / uuid and do a proper search 168 my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/; 169 if ($#matches > 1) { 170 die "Too many matched devices" 171 } elsif ($#matches == 1) { 172 $search = "$types{$matches[0]} $matches[1]" 173 } 174 } else { 175 # Determine the identifying type 176 $search = $types{$fsIdentifier} . ' '; 177 178 # Based on the type pull in the identifier from the system 179 my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid -o export @{[$fs->device]}"); 180 if ($status != 0) { 181 die "Failed to get blkid info for @{[$fs->mount]} on @{[$fs->device]}"; 182 } 183 my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/; 184 if ($#matches != 0) { 185 die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n" 186 } 187 $search .= $matches[0]; 188 } 189 190 # BTRFS is a special case in that we need to fix the referrenced path based on subvolumes 191 if ($fs->type eq 'btrfs') { 192 my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs subvol show @{[$fs->mount]}"); 193 if ($status != 0) { 194 die "Failed to retrieve subvolume info for @{[$fs->mount]}\n"; 195 } 196 my @ids = join("", @id_info) =~ m/Subvolume ID:[ \t\n]*([^ \t\n]*)/; 197 if ($#ids > 0) { 198 die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n" 199 } elsif ($#ids == 0) { 200 my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs subvol list @{[$fs->mount]}"); 201 if ($status != 0) { 202 die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n"; 203 } 204 my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/; 205 if ($#paths > 0) { 206 die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n"; 207 } elsif ($#paths != 0) { 208 die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n"; 209 } 210 $path = "/$paths[0]$path"; 211 } 212 } 213 } 214 if (not $search eq "") { 215 $search = "search --set=drive$driveid " . $search; 216 $path = "(\$drive$driveid)$path"; 217 $driveid += 1; 218 } 219 } 220 return Grub->new(path => $path, search => $search); 221} 222my $grubBoot = GrubFs($bootPath); 223my $grubStore; 224if ($copyKernels == 0) { 225 $grubStore = GrubFs($storePath); 226} 227 228# Generate the header. 229my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n"; 230 231if ($grubVersion == 1) { 232 $conf .= " 233 default $defaultEntry 234 timeout $timeout 235 "; 236 if ($splashImage) { 237 copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath\n"; 238 $conf .= "splashimage " . $grubBoot->path . "/background.xpm.gz\n"; 239 } 240} 241 242else { 243 if ($copyKernels == 0) { 244 $conf .= " 245 " . $grubStore->search; 246 } 247 # FIXME: should use grub-mkconfig. 248 $conf .= " 249 " . $grubBoot->search . " 250 if [ -s \$prefix/grubenv ]; then 251 load_env 252 fi 253 254 # ‘grub-reboot’ sets a one-time saved entry, which we process here and 255 # then delete. 256 if [ \"\${next_entry}\" ]; then 257 # FIXME: KDM expects the next line to be present. 258 set default=\"\${saved_entry}\" 259 set default=\"\${next_entry}\" 260 set next_entry= 261 save_env next_entry 262 set timeout=1 263 else 264 set default=$defaultEntry 265 set timeout=$timeout 266 fi 267 268 # Setup the graphics stack for bios and efi systems 269 if [ \"\${grub_platform}\" = \"efi\" ]; then 270 insmod efi_gop 271 insmod efi_uga 272 else 273 insmod vbe 274 fi 275 insmod font 276 if loadfont " . $grubBoot->path . "/grub/fonts/unicode.pf2; then 277 insmod gfxterm 278 if [ \"\${grub_platform}\" = \"efi\" ]; then 279 set gfxmode=$gfxmodeEfi 280 set gfxpayload=keep 281 else 282 set gfxmode=$gfxmodeBios 283 set gfxpayload=text 284 fi 285 terminal_output gfxterm 286 fi 287 "; 288 289 if ($splashImage) { 290 # FIXME: GRUB 1.97 doesn't resize the background image if it 291 # doesn't match the video resolution. 292 copy $splashImage, "$bootPath/background.png" or die "cannot copy $splashImage to $bootPath\n"; 293 $conf .= " 294 insmod png 295 if background_image " . $grubBoot->path . "/background.png; then 296 set color_normal=white/black 297 set color_highlight=black/white 298 else 299 set menu_color_normal=cyan/blue 300 set menu_color_highlight=white/blue 301 fi 302 "; 303 } 304} 305 306$conf .= "$extraConfig\n"; 307 308 309# Generate the menu entries. 310$conf .= "\n"; 311 312my %copied; 313mkpath("$bootPath/kernels", 0, 0755) if $copyKernels; 314 315sub copyToKernelsDir { 316 my ($path) = @_; 317 return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels; 318 $path =~ /\/nix\/store\/(.*)/ or die; 319 my $name = $1; $name =~ s/\//-/g; 320 my $dst = "$bootPath/kernels/$name"; 321 # Don't copy the file if $dst already exists. This means that we 322 # have to create $dst atomically to prevent partially copied 323 # kernels or initrd if this script is ever interrupted. 324 if (! -e $dst) { 325 my $tmp = "$dst.tmp"; 326 copy $path, $tmp or die "cannot copy $path to $tmp\n"; 327 rename $tmp, $dst or die "cannot rename $tmp to $dst\n"; 328 } 329 $copied{$dst} = 1; 330 return $grubBoot->path . "/kernels/$name"; 331} 332 333sub addEntry { 334 my ($name, $path) = @_; 335 return unless -e "$path/kernel" && -e "$path/initrd"; 336 337 my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel")); 338 my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd")); 339 my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef; 340 341 # FIXME: $confName 342 343 my $kernelParams = 344 "systemConfig=" . Cwd::abs_path($path) . " " . 345 "init=" . Cwd::abs_path("$path/init") . " " . 346 readFile("$path/kernel-params"); 347 my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : ""; 348 349 if ($grubVersion == 1) { 350 $conf .= "title $name\n"; 351 $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; 352 $conf .= " kernel $xen $xenParams\n" if $xen; 353 $conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n"; 354 $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n"; 355 } else { 356 $conf .= "menuentry \"$name\" {\n"; 357 $conf .= $grubBoot->search . "\n"; 358 if ($copyKernels == 0) { 359 $conf .= $grubStore->search . "\n"; 360 } 361 $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; 362 $conf .= " multiboot $xen $xenParams\n" if $xen; 363 $conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n"; 364 $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n"; 365 $conf .= "}\n\n"; 366 } 367} 368 369 370# Add default entries. 371$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS; 372 373addEntry("NixOS - Default", $defaultConfig); 374 375$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS; 376 377my $grubBootPath = $grubBoot->path; 378# extraEntries could refer to @bootRoot@, which we have to substitute 379$conf =~ s/\@bootRoot\@/$grubBootPath/g; 380 381# Emit submenus for all system profiles. 382sub addProfile { 383 my ($profile, $description) = @_; 384 385 # Add entries for all generations of this profile. 386 $conf .= "submenu \"$description\" {\n" if $grubVersion == 2; 387 388 sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; } 389 390 my @links = sort 391 { nrFromGen($b) <=> nrFromGen($a) } 392 (glob "$profile-*-link"); 393 394 my $curEntry = 0; 395 foreach my $link (@links) { 396 last if $curEntry++ >= $configurationLimit; 397 if (! -e "$link/nixos-version") { 398 warn "skipping corrupt system profile entry ‘$link’\n"; 399 next; 400 } 401 my $date = strftime("%F", localtime(lstat($link)->mtime)); 402 my $version = 403 -e "$link/nixos-version" 404 ? readFile("$link/nixos-version") 405 : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); 406 addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link); 407 } 408 409 $conf .= "}\n" if $grubVersion == 2; 410} 411 412addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations"; 413 414if ($grubVersion == 2) { 415 for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") { 416 my $name = basename($profile); 417 next unless $name =~ /^\w+$/; 418 addProfile $profile, "NixOS - Profile '$name'"; 419 } 420} 421 422# Run extraPrepareConfig in sh 423if ($extraPrepareConfig ne "") { 424 system((get("shell"), "-c", $extraPrepareConfig)); 425} 426 427# Atomically update the GRUB config. 428my $confFile = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg"; 429my $tmpFile = $confFile . ".tmp"; 430writeFile($tmpFile, $conf); 431rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n"; 432 433 434# Remove obsolete files from $bootPath/kernels. 435foreach my $fn (glob "$bootPath/kernels/*") { 436 next if defined $copied{$fn}; 437 print STDERR "removing obsolete file $fn\n"; 438 unlink $fn; 439} 440 441 442# 443# Install GRUB if the parameters changed from the last time we installed it. 444# 445 446struct(GrubState => { 447 name => '$', 448 version => '$', 449 efi => '$', 450 devices => '$', 451 efiMountPoint => '$', 452}); 453sub readGrubState { 454 my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "" ); 455 open FILE, "<$bootPath/grub/state" or return $defaultGrubState; 456 local $/ = "\n"; 457 my $name = <FILE>; 458 chomp($name); 459 my $version = <FILE>; 460 chomp($version); 461 my $efi = <FILE>; 462 chomp($efi); 463 my $devices = <FILE>; 464 chomp($devices); 465 my $efiMountPoint = <FILE>; 466 chomp($efiMountPoint); 467 close FILE; 468 my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint ); 469 return $grubState 470} 471 472sub getDeviceTargets { 473 my @devices = (); 474 foreach my $dev ($dom->findnodes('/expr/attrs/attr[@name = "devices"]/list/string/@value')) { 475 $dev = $dev->findvalue(".") or die; 476 push(@devices, $dev); 477 } 478 return @devices; 479} 480 481# check whether to install GRUB EFI or not 482sub getEfiTarget { 483 if ($grubVersion == 1) { 484 return "no" 485 } elsif (($grub ne "") && ($grubEfi ne "")) { 486 # EFI can only be installed when target is set; 487 # A target is also required then for non-EFI grub 488 if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die } 489 else { return "both" } 490 } elsif (($grub ne "") && ($grubEfi eq "")) { 491 # TODO: It would be safer to disallow non-EFI grub installation if no taget is given. 492 # If no target is given, then grub auto-detects the target which can lead to errors. 493 # E.g. it seems as if grub would auto-detect a EFI target based on the availability 494 # of a EFI partition. 495 # However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386 496 # architectures in NixOS. That would have to be fixed in the nixos modules first. 497 return "no" 498 } elsif (($grub eq "") && ($grubEfi ne "")) { 499 # EFI can only be installed when target is set; 500 if ($grubTargetEfi eq "") { die } 501 else {return "only" } 502 } else { 503 # prevent an installation if neither grub nor grubEfi is given 504 return "neither" 505 } 506} 507 508my @deviceTargets = getDeviceTargets(); 509my $efiTarget = getEfiTarget(); 510my $prevGrubState = readGrubState(); 511my @prevDeviceTargets = split/,/, $prevGrubState->devices; 512 513my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference()); 514my $nameDiffer = get("fullName") ne $prevGrubState->name; 515my $versionDiffer = get("fullVersion") ne $prevGrubState->version; 516my $efiDiffer = $efiTarget ne $prevGrubState->efi; 517my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint; 518if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") { 519 warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER"; 520 $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1"; 521} 522my $requireNewInstall = $devicesDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1"); 523 524# install a symlink so that grub can detect the boot drive 525my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space"; 526symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot"; 527 528# install non-EFI GRUB 529if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { 530 foreach my $dev (@deviceTargets) { 531 next if $dev eq "nodev"; 532 print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; 533 if ($grubTarget eq "") { 534 system("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev)) == 0 535 or die "$0: installation of GRUB on $dev failed\n"; 536 } else { 537 system("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", "--target=$grubTarget", Cwd::abs_path($dev)) == 0 538 or die "$0: installation of GRUB on $dev failed\n"; 539 } 540 } 541} 542 543 544# install EFI GRUB 545if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { 546 print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n"; 547 if ($canTouchEfiVariables eq "true") { 548 system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", "--bootloader-id=$bootloaderId") == 0 549 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; 550 } else { 551 system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", "--no-nvram") == 0 552 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; 553 } 554} 555 556 557# update GRUB state file 558if ($requireNewInstall != 0) { 559 open FILE, ">$bootPath/grub/state" or die "cannot create $bootPath/grub/state: $!\n"; 560 print FILE get("fullName"), "\n" or die; 561 print FILE get("fullVersion"), "\n" or die; 562 print FILE $efiTarget, "\n" or die; 563 print FILE join( ",", @deviceTargets ), "\n" or die; 564 print FILE $efiSysMountPoint, "\n" or die; 565 close FILE or die; 566}