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