at 23.11-beta 28 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::Copy::Recursive qw(rcopy pathrm); 10use File::Slurp; 11use File::Temp; 12use JSON; 13use File::Find; 14require List::Compare; 15use POSIX; 16use Cwd; 17 18# system.build.toplevel path 19my $defaultConfig = $ARGV[1] or die; 20 21# Grub config XML generated by grubConfig function in grub.nix 22my $dom = XML::LibXML->load_xml(location => $ARGV[0]); 23 24sub get { my ($name) = @_; return $dom->findvalue("/expr/attrs/attr[\@name = '$name']/*/\@value"); } 25 26sub getList { 27 my ($name) = @_; 28 my @list = (); 29 foreach my $entry ($dom->findnodes("/expr/attrs/attr[\@name = '$name']/list/string/\@value")) { 30 $entry = $entry->findvalue(".") or die; 31 push(@list, $entry); 32 } 33 return @list; 34} 35 36sub readFile { 37 my ($fn) = @_; 38 # enable slurp mode: read entire file in one go 39 local $/ = undef; 40 open my $fh, "<$fn" or return undef; 41 my $s = <$fh>; 42 close $fh; 43 # disable slurp mode 44 local $/ = "\n"; 45 chomp $s; 46 return $s; 47} 48 49sub writeFile { 50 my ($fn, $s) = @_; 51 open my $fh, ">$fn" or die "cannot create $fn: $!\n"; 52 print $fh $s or die "cannot write to $fn: $!\n"; 53 close $fh or die "cannot close $fn: $!\n"; 54} 55 56sub runCommand { 57 open(my $fh, "-|", @_) or die "Failed to execute: $@_\n"; 58 my @ret = $fh->getlines(); 59 close $fh; 60 return ($?, @ret); 61} 62 63my $grub = get("grub"); 64my $grubTarget = get("grubTarget"); 65my $extraConfig = get("extraConfig"); 66my $extraPrepareConfig = get("extraPrepareConfig"); 67my $extraPerEntryConfig = get("extraPerEntryConfig"); 68my $extraEntries = get("extraEntries"); 69my $extraEntriesBeforeNixOS = get("extraEntriesBeforeNixOS") eq "true"; 70my $splashImage = get("splashImage"); 71my $splashMode = get("splashMode"); 72my $entryOptions = get("entryOptions"); 73my $subEntryOptions = get("subEntryOptions"); 74my $backgroundColor = get("backgroundColor"); 75my $configurationLimit = int(get("configurationLimit")); 76my $copyKernels = get("copyKernels") eq "true"; 77my $timeout = int(get("timeout")); 78my $timeoutStyle = get("timeoutStyle"); 79my $defaultEntry = get("default"); 80my $fsIdentifier = get("fsIdentifier"); 81my $grubEfi = get("grubEfi"); 82my $grubTargetEfi = get("grubTargetEfi"); 83my $bootPath = get("bootPath"); 84my $storePath = get("storePath"); 85my $canTouchEfiVariables = get("canTouchEfiVariables"); 86my $efiInstallAsRemovable = get("efiInstallAsRemovable"); 87my $efiSysMountPoint = get("efiSysMountPoint"); 88my $gfxmodeEfi = get("gfxmodeEfi"); 89my $gfxmodeBios = get("gfxmodeBios"); 90my $gfxpayloadEfi = get("gfxpayloadEfi"); 91my $gfxpayloadBios = get("gfxpayloadBios"); 92my $bootloaderId = get("bootloaderId"); 93my $forceInstall = get("forceInstall"); 94my $font = get("font"); 95my $theme = get("theme"); 96my $saveDefault = $defaultEntry eq "saved"; 97$ENV{'PATH'} = get("path"); 98 99print STDERR "updating GRUB 2 menu...\n"; 100 101mkpath("$bootPath/grub", 0, 0700); 102 103# Discover whether the bootPath is on the same filesystem as / and 104# /nix/store. If not, then all kernels and initrds must be copied to 105# the bootPath. 106if (stat($bootPath)->dev != stat("/nix/store")->dev) { 107 $copyKernels = 1; 108} 109 110# Discover information about the location of the bootPath 111struct(Fs => { 112 device => '$', 113 type => '$', 114 mount => '$', 115}); 116sub PathInMount { 117 my ($path, $mount) = @_; 118 my @splitMount = split /\//, $mount; 119 my @splitPath = split /\//, $path; 120 if ($#splitPath < $#splitMount) { 121 return 0; 122 } 123 for (my $i = 0; $i <= $#splitMount; $i++) { 124 if ($splitMount[$i] ne $splitPath[$i]) { 125 return 0; 126 } 127 } 128 return 1; 129} 130 131# Figure out what filesystem is used for the directory with init/initrd/kernel files 132sub GetFs { 133 my ($dir) = @_; 134 my $bestFs = Fs->new(device => "", type => "", mount => ""); 135 foreach my $fs (read_file("/proc/self/mountinfo")) { 136 chomp $fs; 137 my @fields = split / /, $fs; 138 my $mountPoint = $fields[4]; 139 next unless -d $mountPoint; 140 my @mountOptions = split /,/, $fields[5]; 141 142 # Skip the optional fields. 143 my $n = 6; $n++ while $fields[$n] ne "-"; $n++; 144 my $fsType = $fields[$n]; 145 my $device = $fields[$n + 1]; 146 my @superOptions = split /,/, $fields[$n + 2]; 147 148 # Skip the bind-mount on /nix/store. 149 next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions); 150 # Skip mount point generated by systemd-efi-boot-generator? 151 next if $fsType eq "autofs"; 152 153 # Ensure this matches the intended directory 154 next unless PathInMount($dir, $mountPoint); 155 156 # Is it better than our current match? 157 if (length($mountPoint) > length($bestFs->mount)) { 158 $bestFs = Fs->new(device => $device, type => $fsType, mount => $mountPoint); 159 } 160 } 161 return $bestFs; 162} 163struct (Grub => { 164 path => '$', 165 search => '$', 166}); 167my $driveid = 1; 168sub GrubFs { 169 my ($dir) = @_; 170 my $fs = GetFs($dir); 171 my $path = substr($dir, length($fs->mount)); 172 if (substr($path, 0, 1) ne "/") { 173 $path = "/$path"; 174 } 175 my $search = ""; 176 177 # ZFS is completely separate logic as zpools are always identified by a label 178 # or custom UUID 179 if ($fs->type eq 'zfs') { 180 my $sid = index($fs->device, '/'); 181 182 if ($sid < 0) { 183 $search = '--label ' . $fs->device; 184 $path = '/@' . $path; 185 } else { 186 $search = '--label ' . substr($fs->device, 0, $sid); 187 $path = '/' . substr($fs->device, $sid) . '/@' . $path; 188 } 189 } else { 190 my %types = ('uuid' => '--fs-uuid', 'label' => '--label'); 191 192 if ($fsIdentifier eq 'provided') { 193 # If the provided dev is identifying the partition using a label or uuid, 194 # we should get the label / uuid and do a proper search 195 my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/; 196 if ($#matches > 1) { 197 die "Too many matched devices" 198 } elsif ($#matches == 1) { 199 $search = "$types{$matches[0]} $matches[1]" 200 } 201 } else { 202 # Determine the identifying type 203 $search = $types{$fsIdentifier} . ' '; 204 205 # Based on the type pull in the identifier from the system 206 my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid", "-o", "export", @{[$fs->device]}); 207 if ($status != 0) { 208 die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}"; 209 } 210 my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/; 211 if ($#matches != 0) { 212 die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n" 213 } 214 $search .= $matches[0]; 215 } 216 217 # BTRFS is a special case in that we need to fix the referenced path based on subvolumes 218 if ($fs->type eq 'btrfs') { 219 my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "show", @{[$fs->mount]}); 220 if ($status != 0) { 221 die "Failed to retrieve subvolume info for @{[$fs->mount]}\n"; 222 } 223 my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s; 224 if ($#ids > 0) { 225 die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n" 226 } elsif ($#ids == 0) { 227 my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "list", @{[$fs->mount]}); 228 if ($status != 0) { 229 die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n"; 230 } 231 my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/; 232 if ($#paths > 0) { 233 die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n"; 234 } elsif ($#paths != 0) { 235 die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n"; 236 } 237 $path = "/$paths[0]$path"; 238 } 239 } 240 } 241 if (not $search eq "") { 242 $search = "search --set=drive$driveid " . $search; 243 $path = "(\$drive$driveid)$path"; 244 $driveid += 1; 245 } 246 return Grub->new(path => $path, search => $search); 247} 248my $grubBoot = GrubFs($bootPath); 249my $grubStore; 250if ($copyKernels == 0) { 251 $grubStore = GrubFs($storePath); 252} 253 254# Generate the header. 255my $conf .= "# Automatically generated. DO NOT EDIT THIS FILE!\n"; 256 257my @users = (); 258foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) { 259 my $name = $user->findvalue('@name') or die; 260 my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value'); 261 my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value'); 262 my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value'); 263 my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value'); 264 265 if ($hashedPasswordFile) { 266 open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!"; 267 $hashedPassword = <$f>; 268 chomp $hashedPassword; 269 } 270 if ($passwordFile) { 271 open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!"; 272 $password = <$f>; 273 chomp $password; 274 } 275 276 if ($hashedPassword) { 277 if (index($hashedPassword, "grub.pbkdf2.") == 0) { 278 $conf .= "\npassword_pbkdf2 $name $hashedPassword"; 279 } 280 else { 281 die "Password hash for GRUB user '$name' is not valid!"; 282 } 283 } 284 elsif ($password) { 285 $conf .= "\npassword $name $password"; 286 } 287 else { 288 die "GRUB user '$name' has no password!"; 289 } 290 push(@users, $name); 291} 292if (@users) { 293 $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n"; 294} 295 296if ($copyKernels == 0) { 297 $conf .= " 298 " . $grubStore->search; 299} 300# FIXME: should use grub-mkconfig. 301my $defaultEntryText = $defaultEntry; 302if ($saveDefault) { 303 $defaultEntryText = "\"\${saved_entry}\""; 304} 305$conf .= " 306 " . $grubBoot->search . " 307 if [ -s \$prefix/grubenv ]; then 308 load_env 309 fi 310 311 # ‘grub-reboot’ sets a one-time saved entry, which we process here and 312 # then delete. 313 if [ \"\${next_entry}\" ]; then 314 set default=\"\${next_entry}\" 315 set next_entry= 316 save_env next_entry 317 set timeout=1 318 set boot_once=true 319 else 320 set default=$defaultEntryText 321 set timeout=$timeout 322 fi 323 set timeout_style=$timeoutStyle 324 325 function savedefault { 326 if [ -z \"\${boot_once}\"]; then 327 saved_entry=\"\${chosen}\" 328 save_env saved_entry 329 fi 330 } 331 332 # Setup the graphics stack for bios and efi systems 333 if [ \"\${grub_platform}\" = \"efi\" ]; then 334 insmod efi_gop 335 insmod efi_uga 336 else 337 insmod vbe 338 fi 339"; 340 341if ($font) { 342 copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n"; 343 $conf .= " 344 insmod font 345 if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then 346 insmod gfxterm 347 if [ \"\${grub_platform}\" = \"efi\" ]; then 348 set gfxmode=$gfxmodeEfi 349 set gfxpayload=$gfxpayloadEfi 350 else 351 set gfxmode=$gfxmodeBios 352 set gfxpayload=$gfxpayloadBios 353 fi 354 terminal_output gfxterm 355 fi 356 "; 357} 358if ($splashImage) { 359 # Keeps the image's extension. 360 my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$"); 361 # The module for jpg is jpeg. 362 if ($suffix eq ".jpg") { 363 $suffix = ".jpeg"; 364 } 365 if ($backgroundColor) { 366 $conf .= " 367 background_color '$backgroundColor' 368 "; 369 } 370 copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n"; 371 $conf .= " 372 insmod " . substr($suffix, 1) . " 373 if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then 374 set color_normal=white/black 375 set color_highlight=black/white 376 else 377 set menu_color_normal=cyan/blue 378 set menu_color_highlight=white/blue 379 fi 380 "; 381} 382 383rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme"; 384 385if ($theme) { 386 # Copy theme 387 rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n"; 388 389 # Detect which modules will need to be loaded 390 my $with_png = 0; 391 my $with_jpeg = 0; 392 393 find({ wanted => sub { 394 if ($_ =~ /\.png$/i) { 395 $with_png = 1; 396 } 397 elsif ($_ =~ /\.jpe?g$/i) { 398 $with_jpeg = 1; 399 } 400 }, no_chdir => 1 }, $theme); 401 402 if ($with_png) { 403 $conf .= " 404 insmod png 405 " 406 } 407 if ($with_jpeg) { 408 $conf .= " 409 insmod jpeg 410 " 411 } 412 413 $conf .= " 414 # Sets theme. 415 set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt 416 export theme 417 # Load theme fonts, if any 418 "; 419 420 find( { wanted => sub { 421 if ($_ =~ /\.pf2$/i) { 422 $font = File::Spec->abs2rel($File::Find::name, $theme); 423 $conf .= " 424 loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font 425 "; 426 } 427 }, no_chdir => 1 }, $theme ); 428} 429 430$conf .= "$extraConfig\n"; 431 432 433# Generate the menu entries. 434$conf .= "\n"; 435 436my %copied; 437mkpath("$bootPath/kernels", 0, 0755) if $copyKernels; 438 439sub copyToKernelsDir { 440 my ($path) = @_; 441 return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels; 442 $path =~ /\/nix\/store\/(.*)/ or die; 443 my $name = $1; $name =~ s/\//-/g; 444 my $dst = "$bootPath/kernels/$name"; 445 # Don't copy the file if $dst already exists. This means that we 446 # have to create $dst atomically to prevent partially copied 447 # kernels or initrd if this script is ever interrupted. 448 if (! -e $dst) { 449 my $tmp = "$dst.tmp"; 450 copy $path, $tmp or die "cannot copy $path to $tmp: $!\n"; 451 rename $tmp, $dst or die "cannot rename $tmp to $dst: $!\n"; 452 } 453 $copied{$dst} = 1; 454 return ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$name"; 455} 456 457sub addEntry { 458 my ($name, $path, $options, $current) = @_; 459 return unless -e "$path/kernel" && -e "$path/initrd"; 460 461 my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel")); 462 my $initrd = copyToKernelsDir(Cwd::abs_path("$path/initrd")); 463 464 # Include second initrd with secrets 465 if (-e -x "$path/append-initrd-secrets") { 466 # Name the initrd secrets after the system from which they're derived. 467 my $systemName = basename(Cwd::abs_path("$path")); 468 my $initrdSecretsPath = "$bootPath/kernels/$systemName-secrets"; 469 470 mkpath(dirname($initrdSecretsPath), 0, 0755); 471 my $oldUmask = umask; 472 # Make sure initrd is not world readable (won't work if /boot is FAT) 473 umask 0137; 474 my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX"); 475 if (system("$path/append-initrd-secrets", $initrdSecretsPathTemp) != 0) { 476 if ($current) { 477 die "failed to create initrd secrets $!\n"; 478 } else { 479 say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation"; 480 say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`"; 481 } 482 } 483 # Check whether any secrets were actually added 484 if (-e $initrdSecretsPathTemp && ! -z _) { 485 rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n"; 486 $copied{$initrdSecretsPath} = 1; 487 $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$systemName-secrets"; 488 } else { 489 unlink $initrdSecretsPathTemp; 490 rmdir dirname($initrdSecretsPathTemp); 491 } 492 umask $oldUmask; 493 } 494 495 my $xen = -e "$path/xen.gz" ? copyToKernelsDir(Cwd::abs_path("$path/xen.gz")) : undef; 496 497 # FIXME: $confName 498 499 my $kernelParams = 500 "init=" . Cwd::abs_path("$path/init") . " " . 501 readFile("$path/kernel-params"); 502 my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : ""; 503 504 $conf .= "menuentry \"$name\" " . $options . " {\n"; 505 if ($saveDefault) { 506 $conf .= " savedefault\n"; 507 } 508 $conf .= $grubBoot->search . "\n"; 509 if ($copyKernels == 0) { 510 $conf .= $grubStore->search . "\n"; 511 } 512 $conf .= " $extraPerEntryConfig\n" if $extraPerEntryConfig; 513 $conf .= " multiboot $xen $xenParams\n" if $xen; 514 $conf .= " " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n"; 515 $conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n"; 516 $conf .= "}\n\n"; 517} 518 519sub addGeneration { 520 my ($name, $nameSuffix, $path, $options, $current) = @_; 521 522 # Do not search for grand children 523 my @links = sort (glob "$path/specialisation/*"); 524 525 if ($current != 1 && scalar(@links) != 0) { 526 $conf .= "submenu \"> $name$nameSuffix\" --class submenu {\n"; 527 } 528 529 addEntry("$name" . (scalar(@links) == 0 ? "" : " - Default") . $nameSuffix, $path, $options, $current); 530 531 # Find all the children of the current default configuration 532 # Do not search for grand children 533 foreach my $link (@links) { 534 535 my $entryName = ""; 536 537 my $cfgName = readFile("$link/configuration-name"); 538 539 my $date = strftime("%F", localtime(lstat($link)->mtime)); 540 my $version = 541 -e "$link/nixos-version" 542 ? readFile("$link/nixos-version") 543 : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); 544 545 if ($cfgName) { 546 $entryName = $cfgName; 547 } else { 548 my $linkname = basename($link); 549 $entryName = "($linkname - $date - $version)"; 550 } 551 addEntry("$name - $entryName", $link, "", 1); 552 } 553 554 if ($current != 1 && scalar(@links) != 0) { 555 $conf .= "}\n"; 556 } 557} 558 559# Add default entries. 560$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS; 561 562addGeneration("@distroName@", "", $defaultConfig, $entryOptions, 1); 563 564$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS; 565 566my $grubBootPath = $grubBoot->path; 567# extraEntries could refer to @bootRoot@, which we have to substitute 568$conf =~ s/\@bootRoot\@/$grubBootPath/g; 569 570# Emit submenus for all system profiles. 571sub addProfile { 572 my ($profile, $description) = @_; 573 574 # Add entries for all generations of this profile. 575 $conf .= "submenu \"$description\" --class submenu {\n"; 576 577 sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; } 578 579 my @links = sort 580 { nrFromGen($b) <=> nrFromGen($a) } 581 (glob "$profile-*-link"); 582 583 my $curEntry = 0; 584 foreach my $link (@links) { 585 last if $curEntry++ >= $configurationLimit; 586 if (! -e "$link/nixos-version") { 587 warn "skipping corrupt system profile entry ‘$link’\n"; 588 next; 589 } 590 my $date = strftime("%F", localtime(lstat($link)->mtime)); 591 my $version = 592 -e "$link/nixos-version" 593 ? readFile("$link/nixos-version") 594 : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); 595 addGeneration("@distroName@ - Configuration " . nrFromGen($link), " ($date - $version)", $link, $subEntryOptions, 0); 596 } 597 598 $conf .= "}\n"; 599} 600 601addProfile "/nix/var/nix/profiles/system", "@distroName@ - All configurations"; 602 603for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") { 604 my $name = basename($profile); 605 next unless $name =~ /^\w+$/; 606 addProfile $profile, "@distroName@ - Profile '$name'"; 607} 608 609# extraPrepareConfig could refer to @bootPath@, which we have to substitute 610$extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g; 611 612# Run extraPrepareConfig in sh 613if ($extraPrepareConfig ne "") { 614 system((get("shell"), "-c", $extraPrepareConfig)); 615} 616 617# write the GRUB config. 618my $confFile = "$bootPath/grub/grub.cfg"; 619my $tmpFile = $confFile . ".tmp"; 620writeFile($tmpFile, $conf); 621 622 623# check whether to install GRUB EFI or not 624sub getEfiTarget { 625 if (($grub ne "") && ($grubEfi ne "")) { 626 # EFI can only be installed when target is set; 627 # A target is also required then for non-EFI grub 628 if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die } 629 else { return "both" } 630 } elsif (($grub ne "") && ($grubEfi eq "")) { 631 # TODO: It would be safer to disallow non-EFI grub installation if no target is given. 632 # If no target is given, then grub auto-detects the target which can lead to errors. 633 # E.g. it seems as if grub would auto-detect a EFI target based on the availability 634 # of a EFI partition. 635 # However, it seems as auto-detection is currently relied on for non-x86_64 and non-i386 636 # architectures in NixOS. That would have to be fixed in the nixos modules first. 637 return "no" 638 } elsif (($grub eq "") && ($grubEfi ne "")) { 639 # EFI can only be installed when target is set; 640 if ($grubTargetEfi eq "") { die } 641 else {return "only" } 642 } else { 643 # prevent an installation if neither grub nor grubEfi is given 644 return "neither" 645 } 646} 647 648my $efiTarget = getEfiTarget(); 649 650# Append entries detected by os-prober 651if (get("useOSProber") eq "true") { 652 if ($saveDefault) { 653 # os-prober will read this to determine if "savedefault" should be added to generated entries 654 $ENV{'GRUB_SAVEDEFAULT'} = "true"; 655 } 656 657 my $targetpackage = ($efiTarget eq "no") ? $grub : $grubEfi; 658 system(get("shell"), "-c", "pkgdatadir=$targetpackage/share/grub $targetpackage/etc/grub.d/30_os-prober >> $tmpFile"); 659} 660 661# Atomically switch to the new config 662rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile: $!\n"; 663 664 665# Remove obsolete files from $bootPath/kernels. 666foreach my $fn (glob "$bootPath/kernels/*") { 667 next if defined $copied{$fn}; 668 print STDERR "removing obsolete file $fn\n"; 669 unlink $fn; 670} 671 672 673# 674# Install GRUB if the parameters changed from the last time we installed it. 675# 676 677struct(GrubState => { 678 name => '$', 679 version => '$', 680 efi => '$', 681 devices => '$', 682 efiMountPoint => '$', 683 extraGrubInstallArgs => '@', 684}); 685# If you add something to the state file, only add it to the end 686# because it is read line-by-line. 687sub readGrubState { 688 my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () ); 689 open FILE, "<$bootPath/grub/state" or return $defaultGrubState; 690 local $/ = "\n"; 691 my $name = <FILE>; 692 chomp($name); 693 my $version = <FILE>; 694 chomp($version); 695 my $efi = <FILE>; 696 chomp($efi); 697 my $devices = <FILE>; 698 chomp($devices); 699 my $efiMountPoint = <FILE>; 700 chomp($efiMountPoint); 701 # Historically, arguments in the state file were one per each line, but that 702 # gets really messy when newlines are involved, structured arguments 703 # like lists are needed (they have to have a separator encoding), or even worse, 704 # when we need to remove a setting in the future. Thus, the 6th line is a JSON 705 # object that can store structured data, with named keys, and all new state 706 # should go in there. 707 my $jsonStateLine = <FILE>; 708 # For historical reasons we do not check the values above for un-definedness 709 # (that is, when the state file has too few lines and EOF is reached), 710 # because the above come from the first version of this logic and are thus 711 # guaranteed to be present. 712 $jsonStateLine = defined $jsonStateLine ? $jsonStateLine : '{}'; # empty JSON object 713 chomp($jsonStateLine); 714 if ($jsonStateLine eq "") { 715 $jsonStateLine = '{}'; # empty JSON object 716 } 717 my %jsonState = %{decode_json($jsonStateLine)}; 718 my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : (); 719 close FILE; 720 my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs ); 721 return $grubState 722} 723 724my @deviceTargets = getList('devices'); 725my $prevGrubState = readGrubState(); 726my @prevDeviceTargets = split/,/, $prevGrubState->devices; 727my @extraGrubInstallArgs = getList('extraGrubInstallArgs'); 728my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs}; 729 730my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference()); 731my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference()); 732my $nameDiffer = get("fullName") ne $prevGrubState->name; 733my $versionDiffer = get("fullVersion") ne $prevGrubState->version; 734my $efiDiffer = $efiTarget ne $prevGrubState->efi; 735my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint; 736if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") { 737 warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER"; 738 $ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1"; 739} 740my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1"); 741 742# install a symlink so that grub can detect the boot drive 743my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!"; 744symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!"; 745 746# install non-EFI GRUB 747if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { 748 foreach my $dev (@deviceTargets) { 749 next if $dev eq "nodev"; 750 print STDERR "installing the GRUB 2 boot loader on $dev...\n"; 751 my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs); 752 if ($forceInstall eq "true") { 753 push @command, "--force"; 754 } 755 if ($grubTarget ne "") { 756 push @command, "--target=$grubTarget"; 757 } 758 (system @command) == 0 or die "$0: installation of GRUB on $dev failed: $!\n"; 759 } 760} 761 762 763# install EFI GRUB 764if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { 765 print STDERR "installing the GRUB 2 boot loader into $efiSysMountPoint...\n"; 766 my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs); 767 if ($forceInstall eq "true") { 768 push @command, "--force"; 769 } 770 push @command, "--bootloader-id=$bootloaderId"; 771 if ($canTouchEfiVariables ne "true") { 772 push @command, "--no-nvram"; 773 push @command, "--removable" if $efiInstallAsRemovable eq "true"; 774 } 775 776 (system @command) == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed: $!\n"; 777} 778 779 780# update GRUB state file 781if ($requireNewInstall != 0) { 782 # Temp file for atomic rename. 783 my $stateFile = "$bootPath/grub/state"; 784 my $stateFileTmp = $stateFile . ".tmp"; 785 786 open FILE, ">$stateFileTmp" or die "cannot create $stateFileTmp: $!\n"; 787 print FILE get("fullName"), "\n" or die; 788 print FILE get("fullVersion"), "\n" or die; 789 print FILE $efiTarget, "\n" or die; 790 print FILE join( ",", @deviceTargets ), "\n" or die; 791 print FILE $efiSysMountPoint, "\n" or die; 792 my %jsonState = ( 793 extraGrubInstallArgs => \@extraGrubInstallArgs 794 ); 795 my $jsonStateLine = encode_json(\%jsonState); 796 print FILE $jsonStateLine, "\n" or die; 797 close FILE or die; 798 799 # Atomically switch to the new state file 800 rename $stateFileTmp, $stateFile or die "cannot rename $stateFileTmp to $stateFile: $!\n"; 801}