diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh index 6469d9faa038..115b3d7a7c5e 100644 --- a/nixos/modules/installer/tools/nixos-enter.sh +++ b/nixos/modules/installer/tools/nixos-enter.sh @@ -104,4 +104,6 @@ chroot_add_resolv_conf "$mountPoint" || print "ERROR: failed to set up resolv.co chroot "$mountPoint" systemd-tmpfiles --create --remove --exclude-prefix=/dev 1>&2 || true ) +unset TMPDIR + exec chroot "$mountPoint" "${command[@]}" diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index e9697b5f0e64..2980b4ba6857 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -45,16 +45,6 @@ initrd {initrd} options {kernel_params} """ -# The boot loader entry for memtest86. -# -# TODO: This is hard-coded to use the 64-bit EFI app, but it could probably -# be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI -# app filename is BOOTIA32.efi. -MEMTEST_BOOT_ENTRY = """title MemTest86 -efi /efi/memtest86/BOOTX64.efi -""" - - def generation_conf_filename(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str: pieces = [ "nixos", @@ -283,23 +273,24 @@ def main() -> None: except OSError as e: print("ignoring profile '{}' in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) - memtest_entry_file = "@efiSysMountPoint@/loader/entries/memtest86.conf" - if os.path.exists(memtest_entry_file): - os.unlink(memtest_entry_file) - shutil.rmtree("@efiSysMountPoint@/efi/memtest86", ignore_errors=True) - if "@memtest86@" != "": - mkdir_p("@efiSysMountPoint@/efi/memtest86") - for path in glob.iglob("@memtest86@/*"): - if os.path.isdir(path): - shutil.copytree(path, os.path.join("@efiSysMountPoint@/efi/memtest86", os.path.basename(path))) - else: - shutil.copy(path, "@efiSysMountPoint@/efi/memtest86/") + for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False): + relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/") + actual_root = os.path.join("@efiSysMountPoint@", relative_root) - memtest_entry_file = "@efiSysMountPoint@/loader/entries/memtest86.conf" - memtest_entry_file_tmp_path = "%s.tmp" % memtest_entry_file - with open(memtest_entry_file_tmp_path, 'w') as f: - f.write(MEMTEST_BOOT_ENTRY) - os.rename(memtest_entry_file_tmp_path, memtest_entry_file) + for file in files: + actual_file = os.path.join(actual_root, file) + + if os.path.exists(actual_file): + os.unlink(actual_file) + os.unlink(os.path.join(root, file)) + + if not len(os.listdir(actual_root)): + os.rmdir(actual_root) + os.rmdir(root) + + mkdir_p("@efiSysMountPoint@/efi/nixos/.extra-files") + + subprocess.check_call("@copyExtraFiles@") # Since fat32 provides little recovery facilities after a crash, # it can leave the system in an unbootable state, when a crash/outage diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index 0f76d7d6b24a..c07567ec82ea 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -29,6 +29,22 @@ let inherit (efi) efiSysMountPoint canTouchEfiVariables; memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else ""; + + netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else ""; + + copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' + empty_file=$(mktemp) + + ${concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n} + '') cfg.extraFiles)} + + ${concatStrings (mapAttrsToList (n: v: '' + ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n} + '') cfg.extraEntries)} + ''; }; checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" { @@ -125,6 +141,74 @@ in { true. ''; }; + + entryFilename = mkOption { + default = "memtest86.conf"; + type = types.str; + description = '' + systemd-boot orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with o or onwards. + ''; + }; + }; + + netbootxyz = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Make netboot.xyz available from the + systemd-boot menu. netboot.xyz + is a menu system that allows you to boot OS installers and + utilities over the network. + ''; + }; + + entryFilename = mkOption { + default = "o_netbootxyz.conf"; + type = types.str; + description = '' + systemd-boot orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with o or onwards. + ''; + }; + }; + + extraEntries = mkOption { + type = types.attrsOf types.lines; + default = {}; + example = literalExpression '' + { "memtest86.conf" = ''' + title MemTest86 + efi /efi/memtest86/memtest86.efi + '''; } + ''; + description = '' + Any additional entries you want added to the systemd-boot menu. + These entries will be copied to /boot/loader/entries. + Each attribute name denotes the destination file name, + and the corresponding attribute value is the contents of the entry. + + systemd-boot orders the menu entries by the config file names, + so if you want something to appear after all the NixOS entries, + it should start with o or onwards. + ''; + }; + + extraFiles = mkOption { + type = types.attrsOf types.path; + default = {}; + example = literalExpression '' + { "efi/memtest86/memtest86.efi" = "''${pkgs.memtest86-efi}/BOOTX64.efi"; } + ''; + description = '' + A set of files to be copied to /boot. + Each attribute name denotes the destination file name in + /boot, while the corresponding + attribute value specifies the source file. + ''; }; graceful = mkOption { @@ -148,15 +232,64 @@ in { assertions = [ { assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; - message = "This kernel does not support the EFI boot stub"; } - ]; + ] ++ concatMap (filename: [ + { + assertion = !(hasInfix "/" filename); + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries within folders are not supported"; + } + { + assertion = hasSuffix ".conf" filename; + message = "boot.loader.systemd-boot.extraEntries.${lib.strings.escapeNixIdentifier filename} is invalid: entries must have a .conf file extension"; + } + ]) (builtins.attrNames cfg.extraEntries) + ++ concatMap (filename: [ + { + assertion = !(hasPrefix "/" filename); + message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not begin with a slash"; + } + { + assertion = !(hasInfix ".." filename); + message = "boot.loader.systemd-boot.extraFiles.${lib.strings.escapeNixIdentifier filename} is invalid: paths must not reference the parent directory"; + } + { + 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); boot.loader.grub.enable = mkDefault false; boot.loader.supportsInitrdSecrets = true; + boot.loader.systemd-boot.extraFiles = mkMerge [ + # TODO: This is hard-coded to use the 64-bit EFI app, but it could probably + # be updated to use the 32-bit EFI app on 32-bit systems. The 32-bit EFI + # app filename is BOOTIA32.efi. + (mkIf cfg.memtest86.enable { + "efi/memtest86/BOOTX64.efi" = "${pkgs.memtest86-efi}/BOOTX64.efi"; + }) + (mkIf cfg.netbootxyz.enable { + "efi/netbootxyz/netboot.xyz.efi" = "${pkgs.netbootxyz-efi}"; + }) + ]; + + boot.loader.systemd-boot.extraEntries = mkMerge [ + (mkIf cfg.memtest86.enable { + "${cfg.memtest86.entryFilename}" = '' + title MemTest86 + efi /efi/memtest86/BOOTX64.efi + ''; + }) + (mkIf cfg.netbootxyz.enable { + "${cfg.netbootxyz.entryFilename}" = '' + title netboot.xyz + efi /efi/netbootxyz/netboot.xyz.efi + ''; + }) + ]; + system = { build.installBootLoader = checkedSystemdBootBuilder; diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix index c3899b58d6b3..51cfd82e6c4b 100644 --- a/nixos/tests/systemd-boot.nix +++ b/nixos/tests/systemd-boot.nix @@ -110,4 +110,145 @@ in assert "updating systemd-boot from (000.0-1-notnixos) to " in output ''; }; + + memtest86 = makeTest { + name = "systemd-boot-memtest86"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.memtest86.enable = true; + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "memtest86-efi" + ]; + }; + + testScript = '' + machine.succeed("test -e /boot/loader/entries/memtest86.conf") + machine.succeed("test -e /boot/efi/memtest86/BOOTX64.efi") + ''; + }; + + netbootxyz = makeTest { + name = "systemd-boot-netbootxyz"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.netbootxyz.enable = true; + }; + + testScript = '' + machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf") + machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") + ''; + }; + + entryFilename = makeTest { + name = "systemd-boot-entry-filename"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.memtest86.enable = true; + boot.loader.systemd-boot.memtest86.entryFilename = "apple.conf"; + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "memtest86-efi" + ]; + }; + + testScript = '' + machine.fail("test -e /boot/loader/entries/memtest86.conf") + machine.succeed("test -e /boot/loader/entries/apple.conf") + machine.succeed("test -e /boot/efi/memtest86/BOOTX64.efi") + ''; + }; + + extraEntries = makeTest { + name = "systemd-boot-extra-entries"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.extraEntries = { + "banana.conf" = '' + title banana + ''; + }; + }; + + testScript = '' + machine.succeed("test -e /boot/loader/entries/banana.conf") + machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf") + ''; + }; + + extraFiles = makeTest { + name = "systemd-boot-extra-files"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + machine = { pkgs, lib, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.extraFiles = { + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + }; + }; + + testScript = '' + machine.succeed("test -e /boot/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + ''; + }; + + switch-test = makeTest { + name = "systemd-boot-switch-test"; + meta.maintainers = with pkgs.lib.maintainers; [ Enzime ]; + + nodes = { + inherit common; + + machine = { pkgs, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.extraFiles = { + "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi; + }; + }; + + with_netbootxyz = { pkgs, ... }: { + imports = [ common ]; + boot.loader.systemd-boot.netbootxyz.enable = true; + }; + }; + + testScript = { nodes, ... }: let + originalSystem = nodes.machine.config.system.build.toplevel; + baseSystem = nodes.common.config.system.build.toplevel; + finalSystem = nodes.with_netbootxyz.config.system.build.toplevel; + in '' + machine.succeed("test -e /boot/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + + with subtest("remove files when no longer needed"): + machine.succeed("${baseSystem}/bin/switch-to-configuration boot") + machine.fail("test -e /boot/efi/fruits/tomato.efi") + machine.fail("test -d /boot/efi/fruits") + machine.succeed("test -d /boot/efi/nixos/.extra-files") + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits") + + with subtest("files are added back when needed again"): + machine.succeed("${originalSystem}/bin/switch-to-configuration boot") + machine.succeed("test -e /boot/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + + with subtest("simultaneously removing and adding files works"): + machine.succeed("${finalSystem}/bin/switch-to-configuration boot") + machine.fail("test -e /boot/efi/fruits/tomato.efi") + machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi") + machine.succeed("test -e /boot/loader/entries/o_netbootxyz.conf") + machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi") + machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/o_netbootxyz.conf") + machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi") + ''; + }; } diff --git a/pkgs/tools/misc/netbootxyz-efi/default.nix b/pkgs/tools/misc/netbootxyz-efi/default.nix new file mode 100644 index 000000000000..1394a8e745cb --- /dev/null +++ b/pkgs/tools/misc/netbootxyz-efi/default.nix @@ -0,0 +1,21 @@ +{ lib +, fetchurl +}: + +let + pname = "netboot.xyz-efi"; + version = "2.0.53"; +in fetchurl { + name = "${pname}-${version}"; + + url = "https://github.com/netbootxyz/netboot.xyz/releases/download/${version}/netboot.xyz.efi"; + sha256 = "sha256-v7XqrhG94BLTpDHDazTiowQUXu/ITEcgVMmhlqgmSQE="; + + meta = with lib; { + homepage = "https://netboot.xyz/"; + description = "A tool to boot OS installers and utilities over the network, to be run from a bootloader"; + license = licenses.asl20; + maintainers = with maintainers; [ Enzime ]; + platforms = platforms.linux; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index e6355f8f2f1b..6e6b6586bfc3 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -7961,6 +7961,8 @@ with pkgs; netboot = callPackage ../tools/networking/netboot {}; + netbootxyz-efi = callPackage ../tools/misc/netbootxyz-efi { }; + netcat = libressl.nc; netcat-gnu = callPackage ../tools/networking/netcat { };