Merge pull request 'clan_lib flake: fix handling garbage collected store paths as cached values' (#3699) from select-path-fix into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3699
This commit is contained in:
lassulus
2025-05-19 16:01:31 +00:00
2 changed files with 75 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
import json import json
import logging import logging
import os
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import Enum from enum import Enum
from hashlib import sha1 from hashlib import sha1
@@ -318,9 +319,10 @@ class FlakeCacheEntry:
# strings need to be checked if they are store paths # strings need to be checked if they are store paths
# if they are, we store them as a dict with the outPath key # if they are, we store them as a dict with the outPath key
# this is to mirror nix behavior, where the outPath of an attrset is used if no further key is specified # this is to mirror nix behavior, where the outPath of an attrset is used if no further key is specified
elif isinstance(value, str) and value.startswith("/nix/store/"): elif isinstance(value, str) and value.startswith(
os.environ.get("NIX_STORE_DIR", "/nix/store")
):
assert selectors == [] assert selectors == []
if value.startswith("/nix/store/"):
self.value = {"outPath": FlakeCacheEntry(value)} self.value = {"outPath": FlakeCacheEntry(value)}
# if we have a normal scalar, we check if it conflicts with a maybe already store value # if we have a normal scalar, we check if it conflicts with a maybe already store value
@@ -337,7 +339,9 @@ class FlakeCacheEntry:
selector: Selector selector: Selector
# for store paths we have to check if they still exist, otherwise they have to be rebuild and are thus not cached # for store paths we have to check if they still exist, otherwise they have to be rebuild and are thus not cached
if isinstance(self.value, str) and self.value.startswith("/nix/store/"): if isinstance(self.value, str) and self.value.startswith(
os.environ.get("NIX_STORE_DIR", "/nix/store")
):
return Path(self.value).exists() return Path(self.value).exists()
# if self.value is not dict but we request more selectors, we assume we are cached and an error will be thrown in the select function # if self.value is not dict but we request more selectors, we assume we are cached and an error will be thrown in the select function
@@ -345,7 +349,8 @@ class FlakeCacheEntry:
return True return True
if selectors == []: if selectors == []:
return self.fetched_all selector = Selector(type=SelectorType.ALL)
else:
selector = selectors[0] selector = selectors[0]
# we just fetch all subkeys, so we need to check of we inserted all keys at this level before # we just fetch all subkeys, so we need to check of we inserted all keys at this level before
@@ -739,18 +744,36 @@ class Flake:
) )
select_hash = select_flake.hash select_hash = select_flake.hash
# fmt: off
nix_code = f""" nix_code = f"""
let let
flake = builtins.getFlake "path:{self.store_path}?narHash={self.hash}"; flake = builtins.getFlake "path:{self.store_path}?narHash={self.hash}";
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib; selectLib = (
nixpkgs = flake.inputs.nixpkgs or (builtins.getFlake "path:{nixpkgs_source()}?narHash={fallback_nixpkgs_hash}"); builtins.getFlake
"path:{select_source()}?narHash={select_hash}"
).lib;
in in
nixpkgs.legacyPackages.{config["system"]}.writeText "clan-flake-select" ( derivation {{
builtins.toJSON [ {" ".join([f"(selectLib.applySelectors (builtins.fromJSON ''{attr}'') flake)" for attr in str_selectors])} ] name = "clan-flake-select";
) system = "{config["system"]}";
builder = "/bin/sh";
args = [
"-c"
''
printf %s '${{builtins.toJSON [
{" ".join(
[
f"(selectLib.applySelectors (builtins.fromJSON ''{attr}'') flake)"
for attr in str_selectors
]
)}
]}}' > $out
''
];
}}
""" """
# fmt: on
if tmp_store := nix_test_store(): if tmp_store := nix_test_store():
nix_options += ["--store", str(tmp_store)]
nix_options.append("--impure") nix_options.append("--impure")
build_output = Path( build_output = Path(

View File

@@ -1,4 +1,8 @@
import logging import logging
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
import pytest import pytest
from clan_cli.tests.fixtures_flakes import ClanFlake from clan_cli.tests.fixtures_flakes import ClanFlake
@@ -347,10 +351,6 @@ def test_conditional_all_selector(flake: ClanFlake) -> None:
# Test that the caching works # Test that the caching works
@pytest.mark.with_core @pytest.mark.with_core
def test_caching_works(flake: ClanFlake) -> None: def test_caching_works(flake: ClanFlake) -> None:
from unittest.mock import patch
from clan_lib.flake import Flake
my_flake = Flake(str(flake.path)) my_flake = Flake(str(flake.path))
with patch.object( with patch.object(
@@ -363,6 +363,42 @@ def test_caching_works(flake: ClanFlake) -> None:
assert tracked_build.call_count == 1 assert tracked_build.call_count == 1
@pytest.mark.with_core
def test_cache_gc(monkeypatch: pytest.MonkeyPatch) -> None:
with TemporaryDirectory() as tempdir_:
tempdir = Path(tempdir_)
monkeypatch.setenv("NIX_STATE_DIR", str(tempdir / "var"))
monkeypatch.setenv("NIX_LOG_DIR", str(tempdir / "var" / "log"))
monkeypatch.setenv("NIX_STORE_DIR", str(tempdir / "store"))
monkeypatch.setenv("NIX_CACHE_HOME", str(tempdir / "cache"))
monkeypatch.setenv("HOME", str(tempdir / "home"))
monkeypatch.delenv("CLAN_TEST_STORE")
monkeypatch.setenv("NIX_BUILD_TOP", str(tempdir / "build"))
test_file = tempdir / "flake" / "testfile"
test_file.parent.mkdir(parents=True, exist_ok=True)
test_file.write_text("test")
test_flake = tempdir / "flake" / "flake.nix"
test_flake.write_text("""
{
outputs = _: {
testfile = ./testfile;
};
}
""")
my_flake = Flake(str(tempdir / "flake"))
my_flake.select(
"testfile", nix_options=["--sandbox-build-dir", str(tempdir / "build")]
)
assert my_flake._cache is not None # noqa: SLF001
assert my_flake._cache.is_cached("testfile") # noqa: SLF001
subprocess.run(["nix-collect-garbage"], check=True)
assert not my_flake._cache.is_cached("testfile") # noqa: SLF001
# This test fails because the CI sandbox does not have the required packages to run the generators # This test fails because the CI sandbox does not have the required packages to run the generators
# maybe @DavHau or @Qubasa can fix this at some point :) # maybe @DavHau or @Qubasa can fix this at some point :)
# @pytest.mark.with_core # @pytest.mark.with_core