1{
2 runTest,
3 runTestOn,
4 ...
5}:
6
7let
8 common =
9 { pkgs, ... }:
10 {
11 virtualisation.useBootLoader = true;
12 virtualisation.useEFIBoot = true;
13 boot.loader.systemd-boot.enable = true;
14 boot.loader.efi.canTouchEfiVariables = true;
15 environment.systemPackages = [ pkgs.efibootmgr ];
16 system.switch.enable = true;
17 };
18
19 commonXbootldr =
20 {
21 config,
22 lib,
23 pkgs,
24 ...
25 }:
26 let
27 diskImage = import ../lib/make-disk-image.nix {
28 inherit config lib pkgs;
29 label = "nixos";
30 format = "qcow2";
31 partitionTableType = "efixbootldr";
32 touchEFIVars = true;
33 installBootLoader = true;
34 # Don't copy the channel to avoid rebuilding this image, and all tests
35 # that use it, every time that nixpkgs changes
36 copyChannel = false;
37 };
38 in
39 {
40 imports = [ common ];
41 virtualisation.useBootLoader = lib.mkForce false; # Only way to tell qemu-vm not to create the default system image
42 virtualisation.directBoot.enable = false; # But don't direct boot either because we're testing systemd-boot
43
44 system.build.diskImage = diskImage; # Use custom disk image with an XBOOTLDR partition
45 virtualisation.efi.variables = "${diskImage}/efi-vars.fd";
46
47 virtualisation.useDefaultFilesystems = false; # Needs custom setup for `diskImage`
48 virtualisation.bootPartition = null;
49 virtualisation.fileSystems = {
50 "/" = {
51 device = "/dev/vda3";
52 fsType = "ext4";
53 };
54 "/boot" = {
55 device = "/dev/vda2";
56 fsType = "vfat";
57 noCheck = true;
58 };
59 "/efi" = {
60 device = "/dev/vda1";
61 fsType = "vfat";
62 noCheck = true;
63 };
64 };
65
66 boot.loader.systemd-boot.enable = true;
67 boot.loader.efi.efiSysMountPoint = "/efi";
68 boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
69 };
70
71 customDiskImage = nodes: ''
72 import os
73 import subprocess
74 import tempfile
75
76 tmp_disk_image = tempfile.NamedTemporaryFile()
77
78 subprocess.run([
79 "${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
80 "create",
81 "-f",
82 "qcow2",
83 "-b",
84 "${nodes.machine.system.build.diskImage}/nixos.qcow2",
85 "-F",
86 "qcow2",
87 tmp_disk_image.name,
88 ])
89
90 # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
91 os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
92 '';
93in
94{
95 basic = runTest (
96 { lib, ... }:
97 {
98 name = "systemd-boot";
99 meta.maintainers = with lib.maintainers; [
100 danielfullmer
101 julienmalka
102 ];
103
104 nodes.machine = common;
105
106 testScript = ''
107 machine.start()
108 machine.wait_for_unit("multi-user.target")
109
110 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
111 machine.succeed("grep 'sort-key nixos' /boot/loader/entries/nixos-generation-1.conf")
112
113 # Ensure we actually booted using systemd-boot
114 # Magic number is the vendor UUID used by systemd-boot.
115 machine.succeed(
116 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
117 )
118
119 # "bootctl install" should have created an EFI entry
120 machine.succeed('efibootmgr | grep "Linux Boot Manager"')
121 '';
122 }
123 );
124
125 # Test that systemd-boot works with secure boot
126 secureBoot = runTest (
127 { pkgs, lib, ... }:
128 {
129 name = "systemd-boot-secure-boot";
130
131 nodes.machine =
132 { pkgs, ... }:
133 {
134 imports = [ common ];
135 environment.systemPackages = [ pkgs.sbctl ];
136 virtualisation.useSecureBoot = true;
137 };
138
139 testScript =
140 { nodes, ... }:
141 let
142 efiArch = pkgs.stdenv.hostPlatform.efiArch;
143 in
144 ''
145 machine.start(allow_reboot=True)
146 machine.wait_for_unit("multi-user.target")
147
148 machine.succeed("sbctl create-keys")
149 machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine")
150 machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi')
151 machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI')
152 machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi')
153
154 machine.reboot()
155
156 assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
157 '';
158 }
159 );
160
161 basicXbootldr = runTest (
162 { lib, ... }:
163 {
164 name = "systemd-boot-xbootldr";
165 meta.maintainers = with lib.maintainers; [ sdht0 ];
166
167 nodes.machine = commonXbootldr;
168
169 testScript =
170 { nodes, ... }:
171 ''
172 ${customDiskImage nodes}
173
174 machine.start()
175 machine.wait_for_unit("multi-user.target")
176
177 machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
178 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
179
180 # Ensure we actually booted using systemd-boot
181 # Magic number is the vendor UUID used by systemd-boot.
182 machine.succeed(
183 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
184 )
185
186 # "bootctl install" should have created an EFI entry
187 machine.succeed('efibootmgr | grep "Linux Boot Manager"')
188 '';
189 }
190 );
191
192 # Check that specialisations create corresponding boot entries.
193 specialisation = runTest (
194 { pkgs, lib, ... }:
195 {
196 name = "systemd-boot-specialisation";
197 meta.maintainers = with lib.maintainers; [
198 lukegb
199 julienmalka
200 ];
201
202 nodes.machine =
203 { pkgs, lib, ... }:
204 {
205 imports = [ common ];
206 specialisation.something.configuration = {
207 boot.loader.systemd-boot.sortKey = "something";
208
209 # Since qemu will dynamically create a devicetree blob when starting
210 # up, it is not straight forward to create an export of that devicetree
211 # blob without knowing before-hand all the flags we would pass to qemu
212 # (we would then be able to use `dumpdtb`). Thus, the following config
213 # will not boot, but it does allow us to assert that the boot entry has
214 # the correct contents.
215 boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64;
216 hardware.deviceTree.name = "dummy.dtb";
217 hardware.deviceTree.package = lib.mkForce (
218 pkgs.runCommand "dummy-devicetree-package" { } ''
219 mkdir -p $out
220 cp ${pkgs.emptyFile} $out/dummy.dtb
221 ''
222 );
223 };
224 };
225
226 testScript =
227 { nodes, ... }:
228 ''
229 machine.start()
230 machine.wait_for_unit("multi-user.target")
231
232 machine.succeed(
233 "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
234 )
235 machine.succeed(
236 "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
237 )
238 machine.succeed(
239 "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
240 )
241 ''
242 + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
243 machine.succeed(
244 r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
245 )
246 '';
247 }
248 );
249
250 # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
251 fallback = runTest (
252 { pkgs, lib, ... }:
253 {
254 name = "systemd-boot-fallback";
255 meta.maintainers = with lib.maintainers; [
256 danielfullmer
257 julienmalka
258 ];
259
260 nodes.machine =
261 { pkgs, lib, ... }:
262 {
263 imports = [ common ];
264 boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
265 };
266
267 testScript = ''
268 machine.start()
269 machine.wait_for_unit("multi-user.target")
270
271 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
272
273 # Ensure we actually booted using systemd-boot
274 # Magic number is the vendor UUID used by systemd-boot.
275 machine.succeed(
276 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
277 )
278
279 # "bootctl install" should _not_ have created an EFI entry
280 machine.fail('efibootmgr | grep "Linux Boot Manager"')
281 '';
282 }
283 );
284
285 update = runTest (
286 { lib, ... }:
287 {
288 name = "systemd-boot-update";
289 meta.maintainers = with lib.maintainers; [
290 danielfullmer
291 julienmalka
292 ];
293
294 nodes.machine = common;
295
296 testScript = ''
297 machine.succeed("mount -o remount,rw /boot")
298
299 def switch():
300 # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
301 machine.succeed(
302 """
303 find /boot -iname '*boot*.efi' -print0 | \
304 xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 000.0-1-notnixos ####/' '{}'
305 """
306 )
307 return machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
308
309 output = switch()
310 assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
311 assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
312 assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
313
314 with subtest("Test that updating works with lowercase bootx64.efi"):
315 machine.succeed(
316 # Move to tmp file name first, otherwise mv complains the new location is the same
317 "mv /boot/EFI/BOOT/BOOTX64.EFI /boot/EFI/BOOT/bootx64.efi.new",
318 "mv /boot/EFI/BOOT/bootx64.efi.new /boot/EFI/BOOT/bootx64.efi",
319 )
320 output = switch()
321 assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
322 assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
323 assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
324 '';
325 }
326 );
327
328 memtest86 = runTestOn [ "x86_64-linux" ] (
329 { lib, ... }:
330 {
331 name = "systemd-boot-memtest86";
332 meta.maintainers = with lib.maintainers; [ julienmalka ];
333
334 nodes.machine =
335 { pkgs, lib, ... }:
336 {
337 imports = [ common ];
338 boot.loader.systemd-boot.memtest86.enable = true;
339 };
340
341 testScript = ''
342 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
343 machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
344 '';
345 }
346 );
347
348 netbootxyz = runTest (
349 { lib, ... }:
350 {
351 name = "systemd-boot-netbootxyz";
352 meta.maintainers = with lib.maintainers; [ julienmalka ];
353
354 nodes.machine =
355 { pkgs, lib, ... }:
356 {
357 imports = [ common ];
358 boot.loader.systemd-boot.netbootxyz.enable = true;
359 };
360
361 testScript = ''
362 machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
363 machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
364 '';
365 }
366 );
367
368 edk2-uefi-shell = runTest (
369 { lib, ... }:
370 {
371 name = "systemd-boot-edk2-uefi-shell";
372 meta.maintainers = with lib.maintainers; [ iFreilicht ];
373
374 nodes.machine =
375 { ... }:
376 {
377 imports = [ common ];
378 boot.loader.systemd-boot.edk2-uefi-shell.enable = true;
379 };
380
381 testScript = ''
382 machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
383 machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
384 '';
385 }
386 );
387
388 windows = runTest (
389 { lib, ... }:
390 {
391 name = "systemd-boot-windows";
392 meta.maintainers = with lib.maintainers; [ iFreilicht ];
393
394 nodes.machine =
395 { ... }:
396 {
397 imports = [ common ];
398 boot.loader.systemd-boot.windows = {
399 "7" = {
400 efiDeviceHandle = "HD0c1";
401 sortKey = "before_all_others";
402 };
403 "Ten".efiDeviceHandle = "FS0";
404 "11" = {
405 title = "Title with-_-punctuation ...?!";
406 efiDeviceHandle = "HD0d4";
407 sortKey = "zzz";
408 };
409 };
410 };
411
412 testScript = ''
413 machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
414
415 machine.succeed("test -e /boot/loader/entries/windows_7.conf")
416 machine.succeed("test -e /boot/loader/entries/windows_Ten.conf")
417 machine.succeed("test -e /boot/loader/entries/windows_11.conf")
418
419 machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf")
420 machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf")
421 machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf")
422
423 machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf")
424 machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf")
425 machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf")
426
427 machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf")
428 machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf")
429 machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf")
430
431 machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf")
432 machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf")
433 machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf')
434 '';
435 }
436 );
437
438 memtestSortKey = runTest (
439 { lib, ... }:
440 {
441 name = "systemd-boot-memtest-sortkey";
442 meta.maintainers = with lib.maintainers; [ julienmalka ];
443
444 nodes.machine =
445 { pkgs, lib, ... }:
446 {
447 imports = [ common ];
448 boot.loader.systemd-boot.memtest86.enable = true;
449 boot.loader.systemd-boot.memtest86.sortKey = "apple";
450 };
451
452 testScript = ''
453 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
454 machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
455 machine.succeed("grep 'sort-key apple' /boot/loader/entries/memtest86.conf")
456 '';
457 }
458 );
459
460 entryFilenameXbootldr = runTest (
461 { lib, ... }:
462 {
463 name = "systemd-boot-entry-filename-xbootldr";
464 meta.maintainers = with lib.maintainers; [ sdht0 ];
465
466 nodes.machine =
467 { pkgs, lib, ... }:
468 {
469 imports = [ commonXbootldr ];
470 boot.loader.systemd-boot.memtest86.enable = true;
471 };
472
473 testScript =
474 { nodes, ... }:
475 ''
476 ${customDiskImage nodes}
477
478 machine.start()
479 machine.wait_for_unit("multi-user.target")
480
481 machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
482 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
483 machine.succeed("test -e /boot/EFI/memtest86/memtest.efi")
484 '';
485 }
486 );
487
488 extraEntries = runTest (
489 { lib, ... }:
490 {
491 name = "systemd-boot-extra-entries";
492 meta.maintainers = with lib.maintainers; [ julienmalka ];
493
494 nodes.machine =
495 { pkgs, lib, ... }:
496 {
497 imports = [ common ];
498 boot.loader.systemd-boot.extraEntries = {
499 "banana.conf" = ''
500 title banana
501 '';
502 };
503 };
504
505 testScript = ''
506 machine.succeed("test -e /boot/loader/entries/banana.conf")
507 machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
508 '';
509 }
510 );
511
512 extraFiles = runTest (
513 { lib, ... }:
514 {
515 name = "systemd-boot-extra-files";
516 meta.maintainers = with lib.maintainers; [ julienmalka ];
517
518 nodes.machine =
519 { pkgs, lib, ... }:
520 {
521 imports = [ common ];
522 boot.loader.systemd-boot.extraFiles = {
523 "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
524 };
525 };
526
527 testScript = ''
528 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
529 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
530 '';
531 }
532 );
533
534 switch-test = runTest (
535 { lib, ... }:
536 {
537 name = "systemd-boot-switch-test";
538 meta.maintainers = with lib.maintainers; [ julienmalka ];
539
540 nodes = {
541 inherit common;
542
543 machine =
544 { pkgs, nodes, ... }:
545 {
546 imports = [ common ];
547 boot.loader.systemd-boot.extraFiles = {
548 "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
549 };
550
551 # These are configs for different nodes, but we'll use them here in `machine`
552 system.extraDependencies = [
553 nodes.common.system.build.toplevel
554 nodes.with_netbootxyz.system.build.toplevel
555 ];
556 };
557
558 with_netbootxyz =
559 { pkgs, ... }:
560 {
561 imports = [ common ];
562 boot.loader.systemd-boot.netbootxyz.enable = true;
563 };
564 };
565
566 testScript =
567 { nodes, ... }:
568 let
569 originalSystem = nodes.machine.system.build.toplevel;
570 baseSystem = nodes.common.system.build.toplevel;
571 finalSystem = nodes.with_netbootxyz.system.build.toplevel;
572 in
573 ''
574 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
575 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
576
577 with subtest("remove files when no longer needed"):
578 machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
579 machine.fail("test -e /boot/efi/fruits/tomato.efi")
580 machine.fail("test -d /boot/efi/fruits")
581 machine.succeed("test -d /boot/efi/nixos/.extra-files")
582 machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
583 machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
584
585 with subtest("files are added back when needed again"):
586 machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
587 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
588 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
589
590 with subtest("simultaneously removing and adding files works"):
591 machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
592 machine.fail("test -e /boot/efi/fruits/tomato.efi")
593 machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
594 machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
595 machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
596 machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf")
597 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
598 '';
599 }
600 );
601
602 garbage-collect-entry = runTest (
603 { lib, ... }:
604 {
605 name = "systemd-boot-garbage-collect-entry";
606 meta.maintainers = with lib.maintainers; [ julienmalka ];
607
608 nodes = {
609 inherit common;
610 machine =
611 { pkgs, nodes, ... }:
612 {
613 imports = [ common ];
614
615 # These are configs for different nodes, but we'll use them here in `machine`
616 system.extraDependencies = [
617 nodes.common.system.build.toplevel
618 ];
619 };
620 };
621
622 testScript =
623 { nodes, ... }:
624 let
625 baseSystem = nodes.common.system.build.toplevel;
626 in
627 ''
628 machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${baseSystem}")
629 machine.succeed("nix-env -p /nix/var/nix/profiles/system --delete-generations 1")
630 machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
631 machine.fail("test -e /boot/loader/entries/nixos-generation-1.conf")
632 machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
633 '';
634 }
635 );
636
637 no-bootspec = runTest (
638 { lib, ... }:
639 {
640 name = "systemd-boot-no-bootspec";
641 meta.maintainers = with lib.maintainers; [ julienmalka ];
642
643 nodes.machine = {
644 imports = [ common ];
645 boot.bootspec.enable = false;
646 };
647
648 testScript = ''
649 machine.start()
650 machine.wait_for_unit("multi-user.target")
651 '';
652 }
653 );
654}