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}