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