Files
lix/tests/functional2/cli/test_profile.py
T
Raito Bezarius e8015836e3 libcmd/profiles: keep derivations if keep-env-derivations = true
nix3 CLI never honored that setting.

When we update a nix3 profile element with new built store paths for
which we know the deriver (the derivation path which built it), we add
the derivation paths to the profile element to prevent garbage
collection the same way the nix2 CLI logic does.

Fixes #1095.

Change-Id: Icc236d174c5ce5ffe042d1c8e1968f6cb5b50359
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-03-03 22:13:02 +00:00

319 lines
13 KiB
Python

import re
import json
import pytest
from textwrap import dedent
from pathlib import Path
from testlib.fixtures.file_helper import File, with_files
from testlib.fixtures.nix import Nix
from testlib.utils import get_global_asset, get_global_asset_pack
from testlib.fixtures.env import ManagedEnv
from testlib.fixtures.command import Command
from testlib.environ import environ
system = environ.get("system")
flake = {
"flake.nix": File("""
{
description = "Bla bla";
outputs = { self }: with import ./config.nix; rec {
packages.${system}.default = mkDerivation {
name = "profile-test-${builtins.readFile ./version}";
outputs = [ "out" "man" "dev" ];
builder = builtins.toFile "builder.sh"
''
mkdir -p $out/bin
cat > $out/bin/hello <<EOF
#! ${shell}
echo Hello ${builtins.readFile ./who}
EOF
chmod +x $out/bin/hello
echo DONE
mkdir -p $man/share/man
mkdir -p $dev/include
'';
__contentAddressed = import ./ca.nix;
outputHashMode = "recursive";
outputHashAlgo = "sha256";
meta.outputsToInstall = [ "out" "man" ];
};
};
}
"""),
"who": File("World"),
"version": File("1.0"),
"ca.nix": File("false"),
"config.nix": get_global_asset("config.nix"),
}
@pytest.fixture(autouse=True)
def configure_system(nix: Nix):
nix.settings.add_xp_feature("nix-command", "flakes")
def run_hello(env: ManagedEnv) -> str:
return Command([str(env.dirs.home / ".nix-profile/bin/hello")], _env=env).run().ok().stdout_s
def build(nix: Nix, thing: str) -> str:
return nix.nix(["build", "--no-link", "--print-out-paths", thing]).run().ok().stdout_s.strip()
class TestUpgradeProfile:
@with_files({"flake1": flake} | get_global_asset_pack("user-envs"))
def test_nix_env_upgrade(self, nix: Nix, env: ManagedEnv):
nix.nix_env(["-f", "./user-envs.nix", "-i", "foo-1.0"]).run().ok()
result = nix.nix(["profile", "list"]).run().ok().stdout_s
assert re.match(r"Name:.*foo\nStore paths:.*foo-1.0", result)
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
result = nix.nix(["profile", "list"]).run().ok().stdout_s
assert re.search(r"Name:.*flake1\n.*?\n.*?\nLocked flake URL:.*narHash", result)
assert run_hello(env) == "Hello World\n"
assert (env.dirs.home / ".nix-profile/share/man").exists()
assert not (env.dirs.home / ".nix-profile/include").exists()
result = nix.nix(["profile", "history"]).run().ok().stdout_s
assert f"packages.{system}.default: ∅ -> 1.0" in result
result = nix.nix(["profile", "diff-closures"]).run().ok().stdout_s
assert "env-manifest.nix: ε → ∅" in result
@with_files({"flake1": flake})
def test_upgrade_from_v2(self, nix: Nix, env: ManagedEnv):
import_profile = env.dirs.home / "import-profile"
import_profile.mkdir()
path = build(nix, "./flake1^out")
(import_profile / "manifest.json").write_text(
json.dumps(
{
"version": 2,
"elements": [
{
"active": True,
"attrPath": "legacyPackages.x86_64-linux.hello",
"originalUrl": "flake:nixpkgs",
"outputs": None,
"priority": 5,
"storePaths": [path],
"url": "github:NixOS/nixpkgs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
],
}
)
)
added_path = nix.nix(["store", "add-path", str(import_profile)]).run().ok().stdout_s.strip()
nix.nix(
["build", "--profile", str(env.dirs.home / ".nix-profile"), str(added_path)]
).run().ok()
result = nix.nix(["profile", "list"]).run().ok().stdout_s
assert re.match("Name:.*hello\n.*\nStore paths:.*" + path, result, re.S)
assert (
"removed 1 packages, kept 0 packages"
in nix.nix(["profile", "remove", "hello"]).run().ok().stderr_s
)
class ProfileTestBase:
@pytest.fixture(autouse=True)
def setup(self, nix: Nix, configure_system, files): # noqa: ANN001, ARG002
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
class TestBasicOperations(ProfileTestBase):
@with_files({"flake1": flake})
def test_xdg_base_dir_support(self, nix: Nix, env: ManagedEnv):
nix.settings.use_xdg_base_directories = True
result = nix.nix(["profile", "remove", "flake1", "-L"]).run().ok()
assert "removed 1 packages" in result.stderr_s
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
assert run_hello(env) == "Hello World\n"
@with_files({"flake1": flake})
def test_wipe_history(self, nix: Nix):
nix.nix(["profile", "wipe-history"]).run().ok()
history = nix.nix(["profile", "history"]).run().ok().stdout_s
assert len(re.findall(r"^Version ", history, re.M)) == 1
@with_files({"flake1": flake} | get_global_asset_pack("simple-drv"))
def test_install_non_flake(self, nix: Nix, env: ManagedEnv):
nix.nix(["profile", "install", "--file", "./simple.nix", ""]).run().ok()
assert (env.dirs.home / ".nix-profile/hello").read_text() == "Hello World!\n"
assert "removed 1 packages" in nix.nix(["profile", "remove", "simple"]).run().ok().stderr_s
assert not (env.dirs.home / ".nix-profile/hello").exists()
output = nix.nix_build(["--no-out-link", "./simple.nix"]).run().ok().stdout_s.strip()
nix.nix(["profile", "install", output]).run().ok()
assert (env.dirs.home / ".nix-profile/hello").read_text() == "Hello World!\n"
@with_files(
{
"flake1": flake,
"simple1": get_global_asset_pack("simple-drv"),
"simple2": get_global_asset_pack("simple-drv"),
}
)
def test_install_different_sources(self, nix: Nix):
nix.nix(["profile", "install", "--file", "./simple1/simple.nix", ""]).run().ok()
nix.nix(["profile", "install", "--file", "./simple2/simple.nix", ""]).run().ok()
result = nix.nix(["profile", "list"]).run().ok().stdout_s
assert re.search(r"Name:.*simple", result)
assert re.search(r"Name:.*simple-1", result)
assert "removed 1 packages" in nix.nix(["profile", "remove", "simple"]).run().ok().stderr_s
assert (
"removed 1 packages" in nix.nix(["profile", "remove", "simple-1"]).run().ok().stderr_s
)
@with_files({"flake1": flake, "flake2": flake | {"who": File("World2")}})
def test_priority(self, nix: Nix, env: ManagedEnv):
hello1 = build(nix, "./flake1#default.out")
hello2 = build(nix, "./flake2#default.out")
result = nix.nix(["--offline", "profile", "install", "./flake2"]).run().expect(1)
result = [
line
for line in result.stderr_s.splitlines()
if not line.startswith("warning: ")
and not line.startswith("error (ignored): ")
and not re.match(r"^fetching .+ input", line)
and line
]
assert result == [
"error: An existing package already provides the following file:",
f" {hello1}/bin/hello",
" This is the conflicting file from the new package:",
f" {hello2}/bin/hello",
" To remove the existing package:",
f" nix profile remove path:{env.dirs.home}/flake1#packages.{system}.default",
" The new package can also be installed next to the existing one by assigning a different priority.",
" The conflicting packages have a priority of 5.",
" To prioritise the new package:",
f" nix profile install path:{env.dirs.home}/flake2#packages.{system}.default --priority 4",
" To prioritise the existing package:",
f" nix profile install path:{env.dirs.home}/flake2#packages.{system}.default --priority 6",
]
assert run_hello(env) == "Hello World\n"
nix.nix(["profile", "install", "./flake2", "--priority", "100"]).run().ok()
assert run_hello(env) == "Hello World\n"
nix.nix(["profile", "install", "./flake2", "--priority", "0"]).run().ok()
assert run_hello(env) == "Hello World2\n"
nix.nix(["profile", "install", "./flake1", "--priority", "100"]).run().ok()
assert run_hello(env) == "Hello World2\n"
@with_files({"flake1": flake})
class TestProfileContentUpgrade(ProfileTestBase):
@pytest.fixture(autouse=True)
def upgrade(self, nix: Nix, env: ManagedEnv, setup): # noqa: ANN001, ARG002
(env.dirs.home / "flake1/who").write_text("NixOS")
(env.dirs.home / "flake1/version").write_text("2.0")
nix.nix(["profile", "upgrade", "flake1"]).run().ok()
def test_upgrade_succeeded(self, nix: Nix, env: ManagedEnv):
assert run_hello(env) == "Hello NixOS\n"
result = nix.nix(["profile", "history"]).run().ok().stdout_s
assert f"packages.{system}.default: 1.0, 1.0-man -> 2.0, 2.0-man" in result
def test_diff_closures_works(self, nix: Nix):
assert nix.nix(["profile", "diff-closures"]).run().stdout_s == dedent("""\
Version 1 -> 2:
profile-test: 1.0 → 2.0
""")
def test_rollback_works(self, nix: Nix, env: ManagedEnv):
nix.nix(["profile", "rollback"]).run().ok()
assert run_hello(env) == "Hello World\n"
@with_files({"flake1": flake} | get_global_asset_pack("user-envs"))
def test_uninstall(nix: Nix, env: ManagedEnv):
nix.nix_env(["-f", "./user-envs.nix", "-i", "foo-1.0"]).run().ok()
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
assert (env.dirs.home / ".nix-profile/bin/foo").exists()
assert "removed 1 packages" in nix.nix(["profile", "remove", "foo"]).run().ok().stderr_s
assert not (env.dirs.home / ".nix-profile/bin/foo").exists()
assert "foo: 1.0 -> ∅" in nix.nix(["profile", "history"]).run().ok().stdout_s
assert "Version 1 -> 2" in nix.nix(["profile", "diff-closures"]).run().ok().stdout_s
@with_files({"flake1": flake})
def test_output_override(nix: Nix, env: ManagedEnv):
profile = env.dirs.home / ".nix-profile"
nix.nix(["profile", "install", "./flake1^*"]).run().ok()
assert run_hello(env) == "Hello World\n"
assert (profile / "share/man").exists()
assert (profile / "include").exists()
assert (env.dirs.home / "flake1/who").write_text("Lix")
nix.nix(["profile", "upgrade", "flake1"]).run().ok()
assert run_hello(env) == "Hello Lix\n"
assert (profile / "share/man").exists()
assert (profile / "include").exists()
assert "removed 1 packages" in nix.nix(["profile", "remove", "flake1"]).run().ok().stderr_s
nix.nix(["profile", "install", "./flake1^man"]).run().ok()
assert not (profile / "bin/hello").exists()
assert (profile / "share/man").exists()
assert not (profile / "include").exists()
@with_files({"flake1": flake, "flake2": flake})
def test_conflict_resolution_cppnix_8284(nix: Nix):
path = build(nix, "./flake1^out")
nix.nix(["profile", "install", path]).run().ok()
expr = f'(builtins.getFlake "./flake2").packages.{system}.default'
nix.nix(["profile", "install", "--impure", "--expr", expr]).run().expect(1)
class TestKeepEnvDerivations:
@with_files({"flake1": flake})
def test_keep_derivations_enabled(self, nix: Nix, env: ManagedEnv):
nix.settings["keep-env-derivations"] = "true"
nix.settings["keep-derivations"] = "false" # NOTE: This defaults to `true`.
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
assert run_hello(env) == "Hello World\n"
res = nix.nix(["eval", "--raw", "./flake1#default.drvPath"]).run().ok()
drv_path = Path(res.stdout_s.strip())
assert drv_path.exists()
# The derivation should NOT be collected because `keep-env-derivations` is true
nix.nix(["store", "gc"]).run().ok()
assert drv_path.exists()
@with_files({"flake1": flake})
def test_keep_derivations_disabled(self, nix: Nix, env: ManagedEnv):
nix.settings["keep-derivations"] = "false" # NOTE: This defaults to `true`
nix.nix(["profile", "install", "./flake1", "-L"]).run().ok()
assert run_hello(env) == "Hello World\n"
res = nix.nix(["eval", "--raw", "./flake1#default.drvPath"]).run().ok()
drv_path = Path(res.stdout_s.strip())
assert drv_path.exists()
# The derivation should be collected because `keep-env-derivations` is false AND `keep-derivations` is false.
res = nix.nix(["store", "gc"]).run().ok()
assert not drv_path.exists()