clan-cli: add persistant flake caching

This commit is contained in:
lassulus
2025-02-10 11:31:03 +01:00
committed by clan-bot
parent 86e91c8604
commit 0872b781d7
2 changed files with 45 additions and 4 deletions

View File

@@ -1,11 +1,13 @@
import json import json
import logging import logging
import pickle
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from clan_cli.cmd import run from clan_cli.cmd import run
from clan_cli.dirs import user_cache_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.nix import nix_build, nix_command, nix_config, nix_test_store from clan_cli.nix import nix_build, nix_command, nix_config, nix_test_store
@@ -290,6 +292,16 @@ class FlakeCache:
selectors = split_selector(selector_str) selectors = split_selector(selector_str)
return self.cache.is_cached(selectors) return self.cache.is_cached(selectors)
def save_to_file(self, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("wb") as f:
pickle.dump(self.cache, f)
def load_from_file(self, path: Path) -> None:
if path.exists():
with path.open("rb") as f:
self.cache = pickle.load(f)
@dataclass @dataclass
class Flake: class Flake:
@@ -335,7 +347,6 @@ class Flake:
return self._path return self._path
def prefetch(self) -> None: def prefetch(self) -> None:
self._cache = FlakeCache()
flake_prefetch = run( flake_prefetch = run(
nix_command( nix_command(
[ [
@@ -352,6 +363,12 @@ class Flake:
flake_metadata = json.loads(flake_prefetch.stdout) flake_metadata = json.loads(flake_prefetch.stdout)
self.store_path = flake_metadata["storePath"] self.store_path = flake_metadata["storePath"]
self.hash = flake_metadata["hash"] self.hash = flake_metadata["hash"]
self._cache = FlakeCache()
self.flake_cache_path = Path(user_cache_dir()) / "clan" / "flakes" / self.hash
if self.flake_cache_path.exists():
self._cache.load_from_file(self.flake_cache_path)
if flake_metadata["original"].get("url", "").startswith("file:"): if flake_metadata["original"].get("url", "").startswith("file:"):
self._is_local = True self._is_local = True
path = flake_metadata["original"]["url"].removeprefix("file://") path = flake_metadata["original"]["url"].removeprefix("file://")
@@ -364,7 +381,7 @@ class Flake:
self._is_local = False self._is_local = False
self._path = Path(self.store_path) self._path = Path(self.store_path)
def prepare_cache(self, selectors: list[str]) -> None: def get_from_nix(self, selectors: list[str]) -> None:
if self._cache is None: if self._cache is None:
self.prefetch() self.prefetch()
assert self._cache is not None assert self._cache is not None
@@ -383,15 +400,18 @@ class Flake:
if len(outputs) != len(selectors): if len(outputs) != len(selectors):
msg = f"flake_prepare_cache: Expected {len(outputs)} outputs, got {len(outputs)}" msg = f"flake_prepare_cache: Expected {len(outputs)} outputs, got {len(outputs)}"
raise ClanError(msg) raise ClanError(msg)
self._cache.load_from_file(self.flake_cache_path)
for i, selector in enumerate(selectors): for i, selector in enumerate(selectors):
self._cache.insert(outputs[i], selector) self._cache.insert(outputs[i], selector)
self._cache.save_to_file(self.flake_cache_path)
def select(self, selector: str) -> Any: def select(self, selector: str) -> Any:
if self._cache is None: if self._cache is None:
self.prefetch() self.prefetch()
assert self._cache is not None assert self._cache is not None
self._cache.load_from_file(self.flake_cache_path)
if not self._cache.is_cached(selector): if not self._cache.is_cached(selector):
log.info(f"Cache miss for {selector}") log.info(f"Cache miss for {selector}")
self.prepare_cache([selector]) self.get_from_nix([selector])
return self._cache.select(selector) return self._cache.select(selector)

View File

@@ -1,5 +1,5 @@
import pytest import pytest
from clan_cli.flake import Flake, FlakeCacheEntry from clan_cli.flake import Flake, FlakeCache, FlakeCacheEntry
from fixtures_flakes import ClanFlake from fixtures_flakes import ClanFlake
@@ -8,6 +8,7 @@ def test_select() -> None:
test_cache = FlakeCacheEntry(testdict, []) test_cache = FlakeCacheEntry(testdict, [])
assert test_cache["x"]["z"].value == "bla" assert test_cache["x"]["z"].value == "bla"
assert test_cache.is_cached(["x", "z"]) assert test_cache.is_cached(["x", "z"])
assert not test_cache.is_cached(["x", "y", "z"])
assert test_cache.select(["x", "y", 0]) == 123 assert test_cache.select(["x", "y", 0]) == 123
assert not test_cache.is_cached(["x", "z", 1]) assert not test_cache.is_cached(["x", "z", 1])
@@ -34,3 +35,23 @@ def test_flake_caching(flake: ClanFlake) -> None:
"machine2": "machine2", "machine2": "machine2",
"machine3": "machine3", "machine3": "machine3",
} }
@pytest.mark.with_core
def test_cache_persistance(flake: ClanFlake) -> None:
m1 = flake.machines["machine1"]
m1["nixpkgs"]["hostPlatform"] = "x86_64-linux"
flake.refresh()
flake1 = Flake(str(flake.path))
flake2 = Flake(str(flake.path))
flake1.prefetch()
flake2.prefetch()
assert isinstance(flake1._cache, FlakeCache) # noqa: SLF001
assert isinstance(flake2._cache, FlakeCache) # noqa: SLF001
assert not flake1._cache.is_cached( # noqa: SLF001
"nixosConfigurations.*.config.networking.hostName"
)
flake1.select("nixosConfigurations.*.config.networking.hostName")
flake2.prefetch()
assert flake2._cache.is_cached("nixosConfigurations.*.config.networking.hostName") # noqa: SLF001