1{ system ? builtins.currentSystem,
2 config ? {},
3 pkgs ? import ../.. { inherit system config; }
4}:
5
6with import ../lib/testing-python.nix { inherit system pkgs; };
7with pkgs.lib;
8
9let
10 common = {
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 };
17
18 commonXbootldr = { config, lib, pkgs, ... }:
19 let
20 diskImage = import ../lib/make-disk-image.nix {
21 inherit config lib pkgs;
22 label = "nixos";
23 format = "qcow2";
24 partitionTableType = "efixbootldr";
25 touchEFIVars = true;
26 installBootLoader = true;
27 };
28 in
29 {
30 imports = [ common ];
31 virtualisation.useBootLoader = lib.mkForce false; # Only way to tell qemu-vm not to create the default system image
32 virtualisation.directBoot.enable = false; # But don't direct boot either because we're testing systemd-boot
33
34 system.build.diskImage = diskImage; # Use custom disk image with an XBOOTLDR partition
35 virtualisation.efi.variables = "${diskImage}/efi-vars.fd";
36
37 virtualisation.useDefaultFilesystems = false; # Needs custom setup for `diskImage`
38 virtualisation.bootPartition = null;
39 virtualisation.fileSystems = {
40 "/" = {
41 device = "/dev/vda3";
42 fsType = "ext4";
43 };
44 "/boot" = {
45 device = "/dev/vda2";
46 fsType = "vfat";
47 noCheck = true;
48 };
49 "/efi" = {
50 device = "/dev/vda1";
51 fsType = "vfat";
52 noCheck = true;
53 };
54 };
55
56 boot.loader.systemd-boot.enable = true;
57 boot.loader.efi.efiSysMountPoint = "/efi";
58 boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
59 };
60
61 customDiskImage = nodes: ''
62 import os
63 import subprocess
64 import tempfile
65
66 tmp_disk_image = tempfile.NamedTemporaryFile()
67
68 subprocess.run([
69 "${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
70 "create",
71 "-f",
72 "qcow2",
73 "-b",
74 "${nodes.machine.system.build.diskImage}/nixos.qcow2",
75 "-F",
76 "qcow2",
77 tmp_disk_image.name,
78 ])
79
80 # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
81 os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
82 '';
83in
84{
85 basic = makeTest {
86 name = "systemd-boot";
87 meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
88
89 nodes.machine = common;
90
91 testScript = ''
92 machine.start()
93 machine.wait_for_unit("multi-user.target")
94
95 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
96 machine.succeed("grep 'sort-key nixos' /boot/loader/entries/nixos-generation-1.conf")
97
98 # Ensure we actually booted using systemd-boot
99 # Magic number is the vendor UUID used by systemd-boot.
100 machine.succeed(
101 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
102 )
103
104 # "bootctl install" should have created an EFI entry
105 machine.succeed('efibootmgr | grep "Linux Boot Manager"')
106 '';
107 };
108
109 # Test that systemd-boot works with secure boot
110 secureBoot = makeTest {
111 name = "systemd-boot-secure-boot";
112
113 nodes.machine = {
114 imports = [ common ];
115 environment.systemPackages = [ pkgs.sbctl ];
116 virtualisation.useSecureBoot = true;
117 };
118
119 testScript = let
120 efiArch = pkgs.stdenv.hostPlatform.efiArch;
121 in { nodes, ... }: ''
122 machine.start(allow_reboot=True)
123 machine.wait_for_unit("multi-user.target")
124
125 machine.succeed("sbctl create-keys")
126 machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine")
127 machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi')
128 machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI')
129 machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi')
130
131 machine.reboot()
132
133 assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
134 '';
135 };
136
137 basicXbootldr = makeTest {
138 name = "systemd-boot-xbootldr";
139 meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
140
141 nodes.machine = commonXbootldr;
142
143 testScript = { nodes, ... }: ''
144 ${customDiskImage nodes}
145
146 machine.start()
147 machine.wait_for_unit("multi-user.target")
148
149 machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
150 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
151
152 # Ensure we actually booted using systemd-boot
153 # Magic number is the vendor UUID used by systemd-boot.
154 machine.succeed(
155 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
156 )
157
158 # "bootctl install" should have created an EFI entry
159 machine.succeed('efibootmgr | grep "Linux Boot Manager"')
160 '';
161 };
162
163 # Check that specialisations create corresponding boot entries.
164 specialisation = makeTest {
165 name = "systemd-boot-specialisation";
166 meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ];
167
168 nodes.machine = { pkgs, lib, ... }: {
169 imports = [ common ];
170 specialisation.something.configuration = {
171 boot.loader.systemd-boot.sortKey = "something";
172 };
173 };
174
175 testScript = ''
176 machine.start()
177 machine.wait_for_unit("multi-user.target")
178
179 machine.succeed(
180 "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
181 )
182 machine.succeed(
183 "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
184 )
185 machine.succeed(
186 "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
187 )
188 '';
189 };
190
191 # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
192 fallback = makeTest {
193 name = "systemd-boot-fallback";
194 meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
195
196 nodes.machine = { pkgs, lib, ... }: {
197 imports = [ common ];
198 boot.loader.efi.canTouchEfiVariables = mkForce false;
199 };
200
201 testScript = ''
202 machine.start()
203 machine.wait_for_unit("multi-user.target")
204
205 machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
206
207 # Ensure we actually booted using systemd-boot
208 # Magic number is the vendor UUID used by systemd-boot.
209 machine.succeed(
210 "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
211 )
212
213 # "bootctl install" should _not_ have created an EFI entry
214 machine.fail('efibootmgr | grep "Linux Boot Manager"')
215 '';
216 };
217
218 update = makeTest {
219 name = "systemd-boot-update";
220 meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
221
222 nodes.machine = common;
223
224 testScript = ''
225 machine.succeed("mount -o remount,rw /boot")
226
227 # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
228 machine.succeed(
229 """
230 find /boot -iname '*boot*.efi' -print0 | \
231 xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 000.0-1-notnixos ####/' '{}'
232 """
233 )
234
235 output = machine.succeed("/run/current-system/bin/switch-to-configuration boot")
236 assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
237 '';
238 };
239
240 memtest86 = makeTest {
241 name = "systemd-boot-memtest86";
242 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
243
244 nodes.machine = { pkgs, lib, ... }: {
245 imports = [ common ];
246 boot.loader.systemd-boot.memtest86.enable = true;
247 };
248
249 testScript = ''
250 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
251 machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
252 '';
253 };
254
255 netbootxyz = makeTest {
256 name = "systemd-boot-netbootxyz";
257 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
258
259 nodes.machine = { pkgs, lib, ... }: {
260 imports = [ common ];
261 boot.loader.systemd-boot.netbootxyz.enable = true;
262 };
263
264 testScript = ''
265 machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
266 machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
267 '';
268 };
269
270 memtestSortKey = makeTest {
271 name = "systemd-boot-memtest-sortkey";
272 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
273
274 nodes.machine = { pkgs, lib, ... }: {
275 imports = [ common ];
276 boot.loader.systemd-boot.memtest86.enable = true;
277 boot.loader.systemd-boot.memtest86.sortKey = "apple";
278 };
279
280 testScript = ''
281 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
282 machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
283 machine.succeed("grep 'sort-key apple' /boot/loader/entries/memtest86.conf")
284 '';
285 };
286
287 entryFilenameXbootldr = makeTest {
288 name = "systemd-boot-entry-filename-xbootldr";
289 meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
290
291 nodes.machine = { pkgs, lib, ... }: {
292 imports = [ commonXbootldr ];
293 boot.loader.systemd-boot.memtest86.enable = true;
294 };
295
296 testScript = { nodes, ... }: ''
297 ${customDiskImage nodes}
298
299 machine.start()
300 machine.wait_for_unit("multi-user.target")
301
302 machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
303 machine.succeed("test -e /boot/loader/entries/memtest86.conf")
304 machine.succeed("test -e /boot/EFI/memtest86/memtest.efi")
305 '';
306 };
307
308 extraEntries = makeTest {
309 name = "systemd-boot-extra-entries";
310 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
311
312 nodes.machine = { pkgs, lib, ... }: {
313 imports = [ common ];
314 boot.loader.systemd-boot.extraEntries = {
315 "banana.conf" = ''
316 title banana
317 '';
318 };
319 };
320
321 testScript = ''
322 machine.succeed("test -e /boot/loader/entries/banana.conf")
323 machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
324 '';
325 };
326
327 extraFiles = makeTest {
328 name = "systemd-boot-extra-files";
329 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
330
331 nodes.machine = { pkgs, lib, ... }: {
332 imports = [ common ];
333 boot.loader.systemd-boot.extraFiles = {
334 "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
335 };
336 };
337
338 testScript = ''
339 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
340 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
341 '';
342 };
343
344 switch-test = makeTest {
345 name = "systemd-boot-switch-test";
346 meta.maintainers = with pkgs.lib.maintainers; [ Enzime julienmalka ];
347
348 nodes = {
349 inherit common;
350
351 machine = { pkgs, nodes, ... }: {
352 imports = [ common ];
353 boot.loader.systemd-boot.extraFiles = {
354 "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
355 };
356
357 # These are configs for different nodes, but we'll use them here in `machine`
358 system.extraDependencies = [
359 nodes.common.system.build.toplevel
360 nodes.with_netbootxyz.system.build.toplevel
361 ];
362 };
363
364 with_netbootxyz = { pkgs, ... }: {
365 imports = [ common ];
366 boot.loader.systemd-boot.netbootxyz.enable = true;
367 };
368 };
369
370 testScript = { nodes, ... }: let
371 originalSystem = nodes.machine.system.build.toplevel;
372 baseSystem = nodes.common.system.build.toplevel;
373 finalSystem = nodes.with_netbootxyz.system.build.toplevel;
374 in ''
375 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
376 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
377
378 with subtest("remove files when no longer needed"):
379 machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
380 machine.fail("test -e /boot/efi/fruits/tomato.efi")
381 machine.fail("test -d /boot/efi/fruits")
382 machine.succeed("test -d /boot/efi/nixos/.extra-files")
383 machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
384 machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
385
386 with subtest("files are added back when needed again"):
387 machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
388 machine.succeed("test -e /boot/efi/fruits/tomato.efi")
389 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
390
391 with subtest("simultaneously removing and adding files works"):
392 machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
393 machine.fail("test -e /boot/efi/fruits/tomato.efi")
394 machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
395 machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
396 machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
397 machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf")
398 machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
399 '';
400 };
401
402 garbage-collect-entry = makeTest {
403 name = "systemd-boot-garbage-collect-entry";
404 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
405
406 nodes = {
407 inherit common;
408 machine = { pkgs, nodes, ... }: {
409 imports = [ common ];
410
411 # These are configs for different nodes, but we'll use them here in `machine`
412 system.extraDependencies = [
413 nodes.common.system.build.toplevel
414 ];
415 };
416 };
417
418 testScript = { nodes, ... }:
419 let
420 baseSystem = nodes.common.system.build.toplevel;
421 in
422 ''
423 machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${baseSystem}")
424 machine.succeed("nix-env -p /nix/var/nix/profiles/system --delete-generations 1")
425 machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
426 machine.fail("test -e /boot/loader/entries/nixos-generation-1.conf")
427 machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
428 '';
429 };
430
431 # Some UEFI firmwares fail on large reads. Now that systemd-boot loads initrd
432 # itself, systems with such firmware won't boot without this fix
433 uefiLargeFileWorkaround = makeTest {
434 name = "uefi-large-file-workaround";
435 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
436 nodes.machine = { pkgs, ... }: {
437 imports = [common];
438 virtualisation.efi.OVMF = pkgs.OVMF.overrideAttrs (old: {
439 # This patch deliberately breaks the FAT driver in EDK2 to
440 # exhibit (part of) the firmware bug that we are testing
441 # for. Files greater than 10MiB will fail to be read in a
442 # single Read() call, so systemd-boot will fail to load the
443 # initrd without a workaround. The number 10MiB was chosen
444 # because if it were smaller than the kernel size, even the
445 # LoadImage call would fail, which is not the failure mode
446 # we're testing for. It needs to be between the kernel size
447 # and the initrd size.
448 patches = old.patches or [] ++ [ ./systemd-boot-ovmf-broken-fat-driver.patch ];
449 });
450 };
451
452 testScript = ''
453 machine.wait_for_unit("multi-user.target")
454 '';
455 };
456
457 no-bootspec = makeTest
458 {
459 name = "systemd-boot-no-bootspec";
460 meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
461
462 nodes.machine = {
463 imports = [ common ];
464 boot.bootspec.enable = false;
465 };
466
467 testScript = ''
468 machine.start()
469 machine.wait_for_unit("multi-user.target")
470 '';
471 };
472}