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