Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions nix/lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
16 changes: 16 additions & 0 deletions nix/modules/upstream/nixpkgs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"/services/web-servers/nginx/"
# nix settings
"/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"
];
Expand All @@ -51,6 +55,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;
Expand Down
19 changes: 19 additions & 0 deletions nix/modules/upstream/nixpkgs/nix.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@
{
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;
}

(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
# it. The original file is backed up and restored on deactivation.
environment.etc."nix/registry.json" = {
enable = config.nix.registry != { };
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"
Expand Down
23 changes: 22 additions & 1 deletion testFlake/container-tests/nix-enabled.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -37,9 +41,26 @@ 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("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 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")
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}"
)
'';
}
52 changes: 52 additions & 0 deletions testFlake/container-tests/nix-flakes.nix
Original file line number Diff line number Diff line change
@@ -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}"
'';
}
62 changes: 62 additions & 0 deletions testFlake/container-tests/nix-remote-build.nix
Original file line number Diff line number Diff line change
@@ -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}"
)
'';
}
47 changes: 47 additions & 0 deletions testFlake/container-tests/nixpkgs-flake.nix
Original file line number Diff line number Diff line change
@@ -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}"
)
'';
}
Loading