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}