Unverified Commit 12ef18d2 authored by Atemu's avatar Atemu Committed by GitHub
Browse files

nixos/systemd-boot: Simpler windows dual booting (#344327)

parents 8d4e0861 73011ba9
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -631,6 +631,8 @@
  The derivation now installs "impl" headers selectively instead of by a wildcard.
  Use `imgui.src` if you just want to access the unpacked sources.

- The new `boot.loader.systemd-boot.windows` option makes setting up dual-booting with Windows on a different drive easier

- Linux 4.19 has been removed because it will reach its end of life within the lifespan of 24.11

- Unprivileged access to the kernel syslog via `dmesg` is now restricted by default. Users wanting to keep an
+293 −92
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:
{
  config,
  lib,
  pkgs,
  ...
}:

with lib;

@@ -10,9 +15,12 @@ let
  # We check the source code in a derivation that does not depend on the
  # system configuration so that most users don't have to redo the check and require
  # the necessary dependencies.
  checkedSource = pkgs.runCommand "systemd-boot" {
  checkedSource =
    pkgs.runCommand "systemd-boot"
      {
        preferLocalBuild = true;
  } ''
      }
      ''
        install -m755 -D ${./systemd-boot-builder.py} $out
        ${lib.getExe pkgs.buildPackages.mypy} \
          --no-implicit-optional \
@@ -21,6 +29,8 @@ let
          $out
      '';

  edk2ShellEspPath = "efi/edk2-uefi-shell/shell.efi";

  systemdBootBuilder = pkgs.substituteAll rec {
    name = "systemd-boot";

@@ -44,13 +54,17 @@ let

    configurationLimit = if cfg.configurationLimit == null then 0 else cfg.configurationLimit;

    inherit (cfg) consoleMode graceful editor rebootForBitlocker;
    inherit (cfg)
      consoleMode
      graceful
      editor
      rebootForBitlocker
      ;

    inherit (efi) efiSysMountPoint canTouchEfiVariables;

    bootMountPoint = if cfg.xbootldrMountPoint != null
      then cfg.xbootldrMountPoint
      else efi.efiSysMountPoint;
    bootMountPoint =
      if cfg.xbootldrMountPoint != null then cfg.xbootldrMountPoint else efi.efiSysMountPoint;

    nixosDir = "/EFI/nixos";

@@ -60,29 +74,35 @@ let

    netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;

    edk2-uefi-shell = optionalString cfg.edk2-uefi-shell.enable pkgs.edk2-uefi-shell;

    checkMountpoints = pkgs.writeShellScript "check-mountpoints" ''
      fail() {
        echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2
        exit 1
      }
      ${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint}
      ${lib.optionalString
        (cfg.xbootldrMountPoint != null)
        "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"}
      ${lib.optionalString (cfg.xbootldrMountPoint != null)
        "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"
      }
    '';

    copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
      empty_file=$(${pkgs.coreutils}/bin/mktemp)

      ${concatStrings (mapAttrsToList (n: v: ''
      ${concatStrings (
        mapAttrsToList (n: v: ''
          ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n}
          ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n}
      '') cfg.extraFiles)}
        '') cfg.extraFiles
      )}

      ${concatStrings (mapAttrsToList (n: v: ''
      ${concatStrings (
        mapAttrsToList (n: v: ''
          ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n}
          ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n}
      '') cfg.extraEntries)}
        '') cfg.extraEntries
      )}
    '';
  };

@@ -91,20 +111,58 @@ let
    ${systemdBootBuilder}/bin/systemd-boot "$@"
    ${cfg.extraInstallCommands}
  '';
in {
in
{

  meta.maintainers = with lib.maintainers; [ julienmalka ];

  imports =
    [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ])
  imports = [
    (mkRenamedOptionModule
      [
        "boot"
        "loader"
        "gummiboot"
        "enable"
      ]
      [
        "boot"
        "loader"
        "systemd-boot"
        "enable"
      ]
    )
    (lib.mkChangedOptionModule
        [ "boot" "loader" "systemd-boot" "memtest86" "entryFilename" ]
        [ "boot" "loader" "systemd-boot" "memtest86" "sortKey" ]
      [
        "boot"
        "loader"
        "systemd-boot"
        "memtest86"
        "entryFilename"
      ]
      [
        "boot"
        "loader"
        "systemd-boot"
        "memtest86"
        "sortKey"
      ]
      (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.memtest86.entryFilename)
    )
    (lib.mkChangedOptionModule
        [ "boot" "loader" "systemd-boot" "netbootxyz" "entryFilename" ]
        [ "boot" "loader" "systemd-boot" "netbootxyz" "sortKey" ]
      [
        "boot"
        "loader"
        "systemd-boot"
        "netbootxyz"
        "entryFilename"
      ]
      [
        "boot"
        "loader"
        "systemd-boot"
        "netbootxyz"
        "sortKey"
      ]
      (config: lib.strings.removeSuffix ".conf" config.boot.loader.systemd-boot.netbootxyz.entryFilename)
    )
  ];
@@ -124,7 +182,7 @@ in {

    sortKey = mkOption {
      default = "nixos";
      type = lib.types.str;
      type = types.str;
      description = ''
        The sort key used for the NixOS bootloader entries.
        This key determines sorting relative to non-NixOS entries.
@@ -218,7 +276,15 @@ in {
    consoleMode = mkOption {
      default = "keep";

      type = types.enum [ "0" "1" "2" "5" "auto" "max" "keep" ];
      type = types.enum [
        "0"
        "1"
        "2"
        "5"
        "auto"
        "max"
        "keep"
      ];

      description = ''
        The resolution of the console. The following values are valid:
@@ -281,6 +347,29 @@ in {
      };
    };

    edk2-uefi-shell = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Make the EDK2 UEFI Shell available from the systemd-boot menu.
          It can be used to manually boot other operating systems or for debugging.
        '';
      };

      sortKey = mkOption {
        type = types.str;
        default = "o_edk2-uefi-shell";
        description = ''
          `systemd-boot` orders the menu entries by their sort keys,
          so if you want something to appear after all the NixOS entries,
          it should start with {file}`o` or onwards.

          See also {option}`boot.loader.systemd-boot.sortKey`..
        '';
      };
    };

    extraEntries = mkOption {
      type = types.attrsOf types.lines;
      default = { };
@@ -349,10 +438,92 @@ in {
        Windows can unseal the encryption key.
      '';
    };

    windows = mkOption {
      default = { };
      description = ''
        Make Windows bootable from systemd-boot. This option is not necessary when Windows and
        NixOS use the same EFI System Partition (ESP). In that case, Windows will automatically be
        detected by systemd-boot.

        However, if Windows is installed on a separate drive or ESP, you can use this option to add
        a menu entry for each installation manually.

        The attribute name is used for the title of the menu entry and internal file names.
      '';
      example = literalExpression ''
        {
          "10".efiDeviceHandle = "HD0c3";
          "11-ame" = {
            title = "Windows 11 Ameliorated Edition";
            efiDeviceHandle = "HD0b1";
          };
          "11-home" = {
            title = "Windows 11 Home";
            efiDeviceHandle = "FS1";
            sortKey = "z_windows";
          };
        }
      '';
      type = types.attrsOf (
        types.submodule (
          { name, ... }:
          {
            options = {
              efiDeviceHandle = mkOption {
                type = types.str;
                example = "HD1b3";
                description = ''
                  The device handle of the EFI System Partition (ESP) where the Windows bootloader is
                  located. This is the device handle that the EDK2 UEFI Shell uses to load the
                  bootloader.

                  To find this handle, follow these steps:
                  1. Set {option}`boot.loader.systemd-boot.edk2-uefi-shell.enable` to `true`
                  2. Run `nixos-rebuild boot`
                  3. Reboot and select "EDK2 UEFI Shell" from the systemd-boot menu
                  4. Run `map -c` to list all consistent device handles
                  5. For each device handle (for example, `HD0c1`), run `ls HD0c1:\EFI`
                  6. If the output contains the directory `Microsoft`, you might have found the correct device handle
                  7. Run `HD0c1:\EFI\Microsoft\Boot\Bootmgfw.efi` to check if Windows boots correctly
                  8. If it does, this device handle is the one you need (in this example, `HD0c1`)

                  This option is required, there is no useful default.
                '';
              };

              title = mkOption {
                type = types.str;
                example = "Michaelsoft Binbows";
                default = "Windows ${name}";
                defaultText = ''attribute name of this entry, prefixed with "Windows "'';
                description = ''
                  The title of the boot menu entry.
                '';
              };

              sortKey = mkOption {
                type = types.str;
                default = "o_windows_${name}";
                defaultText = ''attribute name of this entry, prefixed with "o_windows_"'';
                description = ''
                  `systemd-boot` orders the menu entries by their sort keys,
                  so if you want something to appear after all the NixOS entries,
                  it should start with {file}`o` or onwards.

                  See also {option}`boot.loader.systemd-boot.sortKey`..
                '';
              };
            };
          }
        )
      );
    };
  };

  config = mkIf cfg.enable {
    assertions = [
    assertions =
      [
        {
          assertion = (hasPrefix "/" efi.efiSysMountPoint);
          message = "The ESP mount point '${toString efi.efiSysMountPoint}' must be an absolute path";
@@ -370,10 +541,14 @@ in {
          message = "This kernel does not support the EFI boot stub";
        }
        {
        assertion = cfg.installDeviceTree -> config.hardware.deviceTree.enable -> config.hardware.deviceTree.name != null;
          assertion =
            cfg.installDeviceTree
            -> config.hardware.deviceTree.enable
            -> config.hardware.deviceTree.name != null;
          message = "Cannot install devicetree without 'config.hardware.deviceTree.enable' enabled and 'config.hardware.deviceTree.name' set";
        }
    ] ++ concatMap (filename: [
      ]
      ++ concatMap (filename: [
        {
          assertion = !(hasInfix "/" filename);
          message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported";
@@ -396,7 +571,13 @@ in {
          assertion = !(hasInfix "nixos/.extra-files" (toLower filename));
          message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: files cannot be placed in the nixos/.extra-files directory";
        }
      ]) (builtins.attrNames cfg.extraFiles);
      ]) (builtins.attrNames cfg.extraFiles)
      ++ concatMap (winVersion: [
        {
          assertion = lib.match "^[-_0-9A-Za-z]+$" winVersion != null;
          message = "boot.loader.systemd-boot.windows.${winVersion} is invalid: key must only contain alphanumeric characters, hyphens, and underscores";
        }
      ]) (builtins.attrNames cfg.windows);

    boot.loader.grub.enable = mkDefault false;

@@ -409,9 +590,13 @@ in {
      (mkIf cfg.netbootxyz.enable {
        "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}";
      })
      (mkIf (cfg.edk2-uefi-shell.enable || cfg.windows != { }) {
        ${edk2ShellEspPath} = "${pkgs.edk2-uefi-shell}/shell.efi";
      })
    ];

    boot.loader.systemd-boot.extraEntries = mkMerge [
    boot.loader.systemd-boot.extraEntries = mkMerge (
      [
        (mkIf cfg.memtest86.enable {
          "memtest86.conf" = ''
            title  Memtest86+
@@ -426,7 +611,23 @@ in {
            sort-key ${cfg.netbootxyz.sortKey}
          '';
        })
    ];
        (mkIf cfg.edk2-uefi-shell.enable {
          "edk2-uefi-shell.conf" = ''
            title  EDK2 UEFI Shell
            efi    /${edk2ShellEspPath}
            sort-key ${cfg.edk2-uefi-shell.sortKey}
          '';
        })
      ]
      ++ (mapAttrsToList (winVersion: cfg: {
        "windows_${winVersion}.conf" = ''
          title ${cfg.title}
          efi /${edk2ShellEspPath}
          options -nointerrupt -nomap -noversion ${cfg.efiDeviceHandle}:EFI\Microsoft\Boot\Bootmgfw.efi
          sort-key ${cfg.sortKey}
        '';
      }) cfg.windows)
    );

    boot.bootspec.extensions."org.nixos.systemd-boot" = {
      inherit (config.boot.loader.systemd-boot) sortKey;
+305 −187
Original line number Diff line number Diff line
{ system ? builtins.currentSystem,
{
  system ? builtins.currentSystem,
  config ? { },
  pkgs ? import ../.. { inherit system config; }
  pkgs ? import ../.. { inherit system config; },
}:

with import ../lib/testing-python.nix { inherit system pkgs; };
@@ -16,7 +17,13 @@ let
    system.switch.enable = true;
  };

  commonXbootldr = { config, lib, pkgs, ... }:
  commonXbootldr =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    let
      diskImage = import ../lib/make-disk-image.nix {
        inherit config lib pkgs;
@@ -85,7 +92,10 @@ in
{
  basic = makeTest {
    name = "systemd-boot";
    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
    meta.maintainers = with pkgs.lib.maintainers; [
      danielfullmer
      julienmalka
    ];

    nodes.machine = common;

@@ -117,9 +127,12 @@ in
      virtualisation.useSecureBoot = true;
    };

    testScript = let
    testScript =
      let
        efiArch = pkgs.stdenv.hostPlatform.efiArch;
    in { nodes, ... }: ''
      in
      { nodes, ... }:
      ''
        machine.start(allow_reboot=True)
        machine.wait_for_unit("multi-user.target")

@@ -141,7 +154,9 @@ in

    nodes.machine = commonXbootldr;

    testScript = { nodes, ... }: ''
    testScript =
      { nodes, ... }:
      ''
        ${customDiskImage nodes}

        machine.start()
@@ -164,9 +179,14 @@ in
  # Check that specialisations create corresponding boot entries.
  specialisation = makeTest {
    name = "systemd-boot-specialisation";
    meta.maintainers = with pkgs.lib.maintainers; [ lukegb julienmalka ];
    meta.maintainers = with pkgs.lib.maintainers; [
      lukegb
      julienmalka
    ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        specialisation.something.configuration = {
          boot.loader.systemd-boot.sortKey = "something";
@@ -179,14 +199,18 @@ in
          # the correct contents.
          boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64;
          hardware.deviceTree.name = "dummy.dtb";
        hardware.deviceTree.package = lib.mkForce (pkgs.runCommand "dummy-devicetree-package" { } ''
          hardware.deviceTree.package = lib.mkForce (
            pkgs.runCommand "dummy-devicetree-package" { } ''
              mkdir -p $out
              cp ${pkgs.emptyFile} $out/dummy.dtb
        '');
            ''
          );
        };
      };

    testScript = { nodes, ... }: ''
    testScript =
      { nodes, ... }:
      ''
        machine.start()
        machine.wait_for_unit("multi-user.target")

@@ -199,7 +223,8 @@ in
        machine.succeed(
            "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
        )
    '' + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
      ''
      + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
        machine.succeed(
            r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
        )
@@ -209,9 +234,14 @@ in
  # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
  fallback = makeTest {
    name = "systemd-boot-fallback";
    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
    meta.maintainers = with pkgs.lib.maintainers; [
      danielfullmer
      julienmalka
    ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        boot.loader.efi.canTouchEfiVariables = mkForce false;
      };
@@ -235,7 +265,10 @@ in

  update = makeTest {
    name = "systemd-boot-update";
    meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ];
    meta.maintainers = with pkgs.lib.maintainers; [
      danielfullmer
      julienmalka
    ];

    nodes.machine = common;

@@ -270,11 +303,15 @@ in
    '';
  };

  memtest86 = with pkgs.lib; optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
  memtest86 =
    with pkgs.lib;
    optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
      name = "systemd-boot-memtest86";
      meta.maintainers = with maintainers; [ julienmalka ];

    nodes.machine = { pkgs, lib, ... }: {
      nodes.machine =
        { pkgs, lib, ... }:
        {
          imports = [ common ];
          boot.loader.systemd-boot.memtest86.enable = true;
        };
@@ -289,7 +326,9 @@ in
    name = "systemd-boot-netbootxyz";
    meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        boot.loader.systemd-boot.netbootxyz.enable = true;
      };
@@ -300,11 +339,73 @@ in
    '';
  };

  edk2-uefi-shell = makeTest {
    name = "systemd-boot-edk2-uefi-shell";
    meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];

    nodes.machine = { ... }: {
      imports = [ common ];
      boot.loader.systemd-boot.edk2-uefi-shell.enable = true;
    };

    testScript = ''
      machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
      machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
    '';
  };

  windows = makeTest {
    name = "systemd-boot-windows";
    meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];

    nodes.machine = { ... }: {
      imports = [ common ];
      boot.loader.systemd-boot.windows = {
        "7" = {
          efiDeviceHandle = "HD0c1";
          sortKey = "before_all_others";
        };
        "Ten".efiDeviceHandle = "FS0";
        "11" = {
          title = "Title with-_-punctuation ...?!";
          efiDeviceHandle = "HD0d4";
          sortKey = "zzz";
        };
      };
    };

    testScript = ''
      machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")

      machine.succeed("test -e /boot/loader/entries/windows_7.conf")
      machine.succeed("test -e /boot/loader/entries/windows_Ten.conf")
      machine.succeed("test -e /boot/loader/entries/windows_11.conf")

      machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf")
      machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf")
      machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf")

      machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf")
      machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf")
      machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf")

      machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf")
      machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf")
      machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf")

      machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf")
      machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf")
      machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf')
    '';
  };

  memtestSortKey = makeTest {
    name = "systemd-boot-memtest-sortkey";
    meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        boot.loader.systemd-boot.memtest86.enable = true;
        boot.loader.systemd-boot.memtest86.sortKey = "apple";
@@ -321,12 +422,16 @@ in
    name = "systemd-boot-entry-filename-xbootldr";
    meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ commonXbootldr ];
        boot.loader.systemd-boot.memtest86.enable = true;
      };

    testScript = { nodes, ... }: ''
    testScript =
      { nodes, ... }:
      ''
        ${customDiskImage nodes}

        machine.start()
@@ -342,7 +447,9 @@ in
    name = "systemd-boot-extra-entries";
    meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        boot.loader.systemd-boot.extraEntries = {
          "banana.conf" = ''
@@ -361,7 +468,9 @@ in
    name = "systemd-boot-extra-files";
    meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];

    nodes.machine = { pkgs, lib, ... }: {
    nodes.machine =
      { pkgs, lib, ... }:
      {
        imports = [ common ];
        boot.loader.systemd-boot.extraFiles = {
          "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
@@ -381,7 +490,9 @@ in
    nodes = {
      inherit common;

      machine = { pkgs, nodes, ... }: {
      machine =
        { pkgs, nodes, ... }:
        {
          imports = [ common ];
          boot.loader.systemd-boot.extraFiles = {
            "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
@@ -394,17 +505,22 @@ in
          ];
        };

      with_netbootxyz = { pkgs, ... }: {
      with_netbootxyz =
        { pkgs, ... }:
        {
          imports = [ common ];
          boot.loader.systemd-boot.netbootxyz.enable = true;
        };
    };

    testScript = { nodes, ... }: let
    testScript =
      { nodes, ... }:
      let
        originalSystem = nodes.machine.system.build.toplevel;
        baseSystem = nodes.common.system.build.toplevel;
        finalSystem = nodes.with_netbootxyz.system.build.toplevel;
    in ''
      in
      ''
        machine.succeed("test -e /boot/efi/fruits/tomato.efi")
        machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")

@@ -438,7 +554,9 @@ in

    nodes = {
      inherit common;
      machine = { pkgs, nodes, ... }: {
      machine =
        { pkgs, nodes, ... }:
        {
          imports = [ common ];

          # These are configs for different nodes, but we'll use them here in `machine`
@@ -448,7 +566,8 @@ in
        };
    };

    testScript = { nodes, ... }:
    testScript =
      { nodes, ... }:
      let
        baseSystem = nodes.common.system.build.toplevel;
      in
@@ -461,8 +580,7 @@ in
      '';
  };

  no-bootspec = makeTest
    {
  no-bootspec = makeTest {
    name = "systemd-boot-no-bootspec";
    meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];