From ad0b86a2aba0a3b7a70487df75097a27e99cac32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 21 Apr 2026 14:35:55 +0200 Subject: [PATCH 1/5] feat: import nixos/config/nix-flakes.nix --- nix/modules/upstream/nixpkgs/default.nix | 1 + nix/modules/upstream/nixpkgs/nix.nix | 8 ++++ testFlake/container-tests/nix-enabled.nix | 11 ++++- testFlake/container-tests/nix-flakes.nix | 52 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 testFlake/container-tests/nix-flakes.nix diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 5456a42a..c52a91a6 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -29,6 +29,7 @@ "/services/web-servers/nginx/" # nix settings "/config/nix.nix" + "/config/nix-flakes.nix" "/services/system/userborn.nix" "/system/build.nix" ]; diff --git a/nix/modules/upstream/nixpkgs/nix.nix b/nix/modules/upstream/nixpkgs/nix.nix index 92b441f7..eead344a 100644 --- a/nix/modules/upstream/nixpkgs/nix.nix +++ b/nix/modules/upstream/nixpkgs/nix.nix @@ -15,6 +15,14 @@ (lib.mkIf config.nix.enable { environment.etc."nix/nix.conf".replaceExisting = true; + + # nix-flakes.nix always renders /etc/nix/registry.json, but we only want + # to take over a pre-existing registry when we have something to put in + # it. The original file is backed up and restored on deactivation. + environment.etc."nix/registry.json" = { + enable = config.nix.registry != { }; + replaceExisting = true; + }; nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" diff --git a/testFlake/container-tests/nix-enabled.nix b/testFlake/container-tests/nix-enabled.nix index 31e3957e..663d36ec 100644 --- a/testFlake/container-tests/nix-enabled.nix +++ b/testFlake/container-tests/nix-enabled.nix @@ -20,6 +20,10 @@ forEachDistro "nix-enabled" { assert machine.file("/etc/nix/nix.conf").exists, "/etc/nix/nix.conf should exist before activation" original_nix_conf = machine.succeed("cat /etc/nix/nix.conf") + with subtest("Pre-existing registry.json before activation"): + original_registry = '{"flakes":[],"version":2}' + machine.succeed(f"printf %s '{original_registry}' > /etc/nix/registry.json") + machine.activate() machine.wait_for_unit("system-manager.target") @@ -37,9 +41,14 @@ forEachDistro "nix-enabled" { assert nix_conf.exists, "/etc/nix/nix.conf should still exist after re-activation" assert nix_conf.contains("flakes"), "nix.conf should still contain flakes" - with subtest("Deactivation restores original nix.conf"): + with subtest("Deactivation restores original nix.conf and registry.json"): machine.succeed("${toplevel}/bin/deactivate") restored_nix_conf = machine.succeed("cat /etc/nix/nix.conf") assert restored_nix_conf == original_nix_conf, f"nix.conf content differs after deactivation:\n original: {original_nix_conf!r}\n restored: {restored_nix_conf!r}" + restored_registry = machine.succeed("cat /etc/nix/registry.json") + assert restored_registry == original_registry, ( + f"registry.json content differs after deactivation:\n" + f" original: {original_registry!r}\n restored: {restored_registry!r}" + ) ''; } diff --git a/testFlake/container-tests/nix-flakes.nix b/testFlake/container-tests/nix-flakes.nix new file mode 100644 index 00000000..53b7187a --- /dev/null +++ b/testFlake/container-tests/nix-flakes.nix @@ -0,0 +1,52 @@ +{ forEachDistro, ... }: + +forEachDistro "nix-flakes" { + modules = [ + ( + { ... }: + { + nix.enable = true; + nix.registry.example = { + from = { + type = "indirect"; + id = "example"; + }; + to = { + type = "github"; + owner = "foo"; + repo = "bar"; + }; + }; + } + ) + ]; + testScriptFunction = + { toplevel, hostPkgs, ... }: + '' + start_all() + + machine.wait_for_unit("multi-user.target") + + activation_logs = machine.activate() + for line in activation_logs.split("\n"): + assert not "ERROR" in line, line + machine.wait_for_unit("system-manager.target") + + registry_file = machine.file("/etc/nix/registry.json") + + with subtest("/etc/nix/registry.json exists and is valid JSON"): + assert registry_file.exists, "/etc/nix/registry.json should exist" + import json + data = json.loads(registry_file.content) + + with subtest("registry.json contains the example entry"): + assert data["version"] == 2, f"Expected version 2, got: {data}" + flakes = data["flakes"] + example_entries = [f for f in flakes if f["from"].get("id") == "example"] + assert len(example_entries) == 1, f"Expected one 'example' entry, got: {example_entries}" + entry = example_entries[0] + assert entry["to"]["type"] == "github", f"Expected github to, got: {entry}" + assert entry["to"]["owner"] == "foo", f"Expected owner foo, got: {entry}" + assert entry["to"]["repo"] == "bar", f"Expected repo bar, got: {entry}" + ''; +} From 34f51ff8fe92b987d476651692ea6dd7a72f5322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 21 Apr 2026 15:14:07 +0200 Subject: [PATCH 2/5] feat: add nix package to systemPackages when nix.enable --- nix/modules/upstream/nixpkgs/nix.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nix/modules/upstream/nixpkgs/nix.nix b/nix/modules/upstream/nixpkgs/nix.nix index eead344a..0c9c6ded 100644 --- a/nix/modules/upstream/nixpkgs/nix.nix +++ b/nix/modules/upstream/nixpkgs/nix.nix @@ -15,6 +15,7 @@ (lib.mkIf config.nix.enable { environment.etc."nix/nix.conf".replaceExisting = true; + environment.systemPackages = [ config.nix.package ]; # nix-flakes.nix always renders /etc/nix/registry.json, but we only want # to take over a pre-existing registry when we have something to put in @@ -23,6 +24,7 @@ enable = config.nix.registry != { }; replaceExisting = true; }; + nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" From 7d5f9c27270b2eb1b27f307dbb01e161f4b627b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Sun, 20 Sep 2026 23:20:27 +0200 Subject: [PATCH 3/5] feat: import nixos/config/nix-channel.nix --- nix/modules/upstream/nixpkgs/default.nix | 13 +++++++++++++ nix/modules/upstream/nixpkgs/nix.nix | 9 +++++++++ testFlake/container-tests/nix-enabled.nix | 10 ++++++++++ 3 files changed, 32 insertions(+) diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index c52a91a6..293a8c27 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -29,6 +29,7 @@ "/services/web-servers/nginx/" # nix settings "/config/nix.nix" + "/config/nix-channel.nix" "/config/nix-flakes.nix" "/services/system/userborn.nix" "/system/build.nix" @@ -52,6 +53,18 @@ default = ""; }; + # nix-channel.nix registers a pre-switch check warning about leftover + # channel state. We don't run NixOS pre-switch checks, so ignore them. + system.preSwitchChecks = lib.mkOption { + type = lib.types.attrsOf lib.types.raw; + default = { }; + }; + + # Channels are a NixOS-only concept here, see nix.nix for the rationale. + nix.channel.enable = lib.mkOption { + internal = true; + }; + # Stubs for home-manager system.userActivationScripts = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; diff --git a/nix/modules/upstream/nixpkgs/nix.nix b/nix/modules/upstream/nixpkgs/nix.nix index 0c9c6ded..cce99ef8 100644 --- a/nix/modules/upstream/nixpkgs/nix.nix +++ b/nix/modules/upstream/nixpkgs/nix.nix @@ -8,6 +8,11 @@ { nix.enable = lib.mkDefault false; + # nix-channel is a NixOS-ism: on foreign distros the channels profile of + # root is not managed by us. Flake-based configs are the norm here, so we + # keep channels off unless the user explicitly asks for them. + nix.channel.enable = lib.mkDefault false; + # Nix already owns its build users on the hosts we manage. # Priority 900 overrides the upstream mkDefault, users can still set it. nix.nrBuildUsers = lib.mkOverride 900 0; @@ -25,6 +30,10 @@ replaceExisting = true; }; + # nix-channel.nix sets NIX_PATH unconditionally. Exporting an empty value + # would shadow the default search path Nix uses, so unset it instead. + environment.sessionVariables.NIX_PATH = lib.mkIf (config.nix.nixPath == [ ]) (lib.mkForce null); + nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" diff --git a/testFlake/container-tests/nix-enabled.nix b/testFlake/container-tests/nix-enabled.nix index 663d36ec..839f465a 100644 --- a/testFlake/container-tests/nix-enabled.nix +++ b/testFlake/container-tests/nix-enabled.nix @@ -41,6 +41,16 @@ forEachDistro "nix-enabled" { assert nix_conf.exists, "/etc/nix/nix.conf should still exist after re-activation" assert nix_conf.contains("flakes"), "nix.conf should still contain flakes" + with subtest("Channels are disabled by default"): + assert not machine.file("/root/.nix-channels").exists, ( + "/root/.nix-channels should not be created when channels are disabled" + ) + machine.fail("test -e /run/system-manager/sw/bin/nix-channel") + + with subtest("NIX_PATH is unset when no nixPath is configured"): + nix_path = machine.succeed("bash --login -c 'printf %s \"''${NIX_PATH-unset}\"'").strip() + assert nix_path == "unset", f"Expected NIX_PATH to be unset, got: {nix_path!r}" + with subtest("Deactivation restores original nix.conf and registry.json"): machine.succeed("${toplevel}/bin/deactivate") restored_nix_conf = machine.succeed("cat /etc/nix/nix.conf") From dabde6883117d673ad3a43e893cf865cf81de20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 21 Apr 2026 15:27:15 +0200 Subject: [PATCH 4/5] feat: import nixos/misc/nixpkgs-flake.nix --- nix/lib.nix | 7 +++ nix/modules/upstream/nixpkgs/default.nix | 1 + testFlake/container-tests/nix-enabled.nix | 8 ++-- testFlake/container-tests/nixpkgs-flake.nix | 47 +++++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 testFlake/container-tests/nixpkgs-flake.nix diff --git a/nix/lib.nix b/nix/lib.nix index c204d962..a0566c8b 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -123,6 +123,13 @@ let _file = "${self.printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module"; build = { inherit toplevel; }; } + { + _file = "${self.printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module"; + # Pin the nixpkgs used to build this configuration, like + # nixpkgs.lib.nixosSystem does: misc/nixpkgs-flake.nix turns this + # into a flake registry entry and NIX_PATH=nixpkgs=flake:nixpkgs. + config.nixpkgs.flake.source = lib.mkDefault (toString nixpkgs); + } { config = { assertions = [ diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 293a8c27..951e9df6 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -31,6 +31,7 @@ "/config/nix.nix" "/config/nix-channel.nix" "/config/nix-flakes.nix" + "/misc/nixpkgs-flake.nix" "/services/system/userborn.nix" "/system/build.nix" ]; diff --git a/testFlake/container-tests/nix-enabled.nix b/testFlake/container-tests/nix-enabled.nix index 839f465a..c0fc4dda 100644 --- a/testFlake/container-tests/nix-enabled.nix +++ b/testFlake/container-tests/nix-enabled.nix @@ -47,9 +47,11 @@ forEachDistro "nix-enabled" { ) machine.fail("test -e /run/system-manager/sw/bin/nix-channel") - with subtest("NIX_PATH is unset when no nixPath is configured"): - nix_path = machine.succeed("bash --login -c 'printf %s \"''${NIX_PATH-unset}\"'").strip() - assert nix_path == "unset", f"Expected NIX_PATH to be unset, got: {nix_path!r}" + with subtest("NIX_PATH points at the pinned nixpkgs flake"): + nix_path = machine.succeed("bash --login -c 'printf %s \"$NIX_PATH\"'").strip() + assert nix_path == "nixpkgs=flake:nixpkgs", ( + f"Expected configured NIX_PATH, got: {nix_path!r}" + ) with subtest("Deactivation restores original nix.conf and registry.json"): machine.succeed("${toplevel}/bin/deactivate") diff --git a/testFlake/container-tests/nixpkgs-flake.nix b/testFlake/container-tests/nixpkgs-flake.nix new file mode 100644 index 00000000..d741eb05 --- /dev/null +++ b/testFlake/container-tests/nixpkgs-flake.nix @@ -0,0 +1,47 @@ +{ forEachDistro, ... }: + +# nixpkgs.flake.source is set automatically by makeSystemConfig, so enabling +# nix is enough to get a pinned nixpkgs registry entry and NIX_PATH. +forEachDistro "nixpkgs-flake" { + modules = [ + ( + { ... }: + { + nix.enable = true; + } + ) + ]; + testScriptFunction = + { toplevel, hostPkgs, ... }: + '' + start_all() + + machine.wait_for_unit("multi-user.target") + + activation_logs = machine.activate() + for line in activation_logs.split("\n"): + assert not "ERROR" in line, line + machine.wait_for_unit("system-manager.target") + + registry_file = machine.file("/etc/nix/registry.json") + + with subtest("/etc/nix/registry.json contains a nixpkgs entry"): + assert registry_file.exists, "/etc/nix/registry.json should exist" + import json + data = json.loads(registry_file.content) + flakes = data["flakes"] + nixpkgs_entries = [f for f in flakes if f["from"].get("id") == "nixpkgs"] + assert len(nixpkgs_entries) == 1, f"Expected one nixpkgs entry, got: {nixpkgs_entries}" + entry = nixpkgs_entries[0] + assert entry["to"] == { + "type": "path", + "path": "${toplevel.config.nixpkgs.flake.source}", + }, f"Expected the nixpkgs sources used to build the system, got: {entry}" + + with subtest("NIX_PATH contains nixpkgs=flake:nixpkgs"): + nix_path = machine.succeed("bash --login -c 'echo $NIX_PATH'").strip() + assert "nixpkgs=flake:nixpkgs" in nix_path, ( + f"Expected 'nixpkgs=flake:nixpkgs' in NIX_PATH, got: {nix_path!r}" + ) + ''; +} From c970ab567e5048ae6810d33cdfd443adce4d9cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 21 Apr 2026 16:09:16 +0200 Subject: [PATCH 5/5] feat: import nixos/config/nix-remote-build.nix --- nix/modules/upstream/nixpkgs/default.nix | 1 + .../container-tests/nix-remote-build.nix | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 testFlake/container-tests/nix-remote-build.nix diff --git a/nix/modules/upstream/nixpkgs/default.nix b/nix/modules/upstream/nixpkgs/default.nix index 951e9df6..91e48fe1 100644 --- a/nix/modules/upstream/nixpkgs/default.nix +++ b/nix/modules/upstream/nixpkgs/default.nix @@ -31,6 +31,7 @@ "/config/nix.nix" "/config/nix-channel.nix" "/config/nix-flakes.nix" + "/config/nix-remote-build.nix" "/misc/nixpkgs-flake.nix" "/services/system/userborn.nix" "/system/build.nix" diff --git a/testFlake/container-tests/nix-remote-build.nix b/testFlake/container-tests/nix-remote-build.nix new file mode 100644 index 00000000..8a1e2848 --- /dev/null +++ b/testFlake/container-tests/nix-remote-build.nix @@ -0,0 +1,62 @@ +{ forEachDistro, ... }: + +forEachDistro "nix-remote-build" { + modules = [ + ( + { ... }: + { + nix.enable = true; + nix.distributedBuilds = true; + nix.buildMachines = [ + { + hostName = "builder.example.org"; + sshUser = "builder"; + sshKey = "/root/.ssh/id_builder"; + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + maxJobs = 4; + speedFactor = 2; + supportedFeatures = [ + "kvm" + "big-parallel" + ]; + } + ]; + } + ) + ]; + testScriptFunction = + { toplevel, hostPkgs, ... }: + '' + start_all() + + machine.wait_for_unit("multi-user.target") + + activation_logs = machine.activate() + for line in activation_logs.split("\n"): + assert not "ERROR" in line, line + machine.wait_for_unit("system-manager.target") + + machines_file = machine.file("/etc/nix/machines") + + with subtest("/etc/nix/machines exists"): + assert machines_file.exists, "/etc/nix/machines should exist" + + with subtest("/etc/nix/machines contains the builder spec"): + content = machines_file.content_string + assert "ssh://builder@builder.example.org" in content, ( + f"Expected ssh://builder@builder.example.org, got: {content!r}" + ) + assert "x86_64-linux,aarch64-linux" in content, ( + f"Expected comma-joined systems, got: {content!r}" + ) + assert "/root/.ssh/id_builder" in content, ( + f"Expected ssh key path, got: {content!r}" + ) + assert "4 2" in content, ( + f"Expected '4 2' for maxJobs and speedFactor, got: {content!r}" + ) + ''; +}