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}