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