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