diff --git a/pkgs/clan-cli/clan_lib/flake/__init__.py b/pkgs/clan-cli/clan_lib/flake/__init__.py index 25c428fa2..ed265fee9 100644 --- a/pkgs/clan-cli/clan_lib/flake/__init__.py +++ b/pkgs/clan-cli/clan_lib/flake/__init__.py @@ -1 +1 @@ -from .flake import Flake, require_flake # noqa +from .flake import Flake, require_flake, ClanSelectError # noqa diff --git a/pkgs/clan-cli/clan_lib/flake/flake.py b/pkgs/clan-cli/clan_lib/flake/flake.py index 1eb7f2911..bdb9ec22c 100644 --- a/pkgs/clan-cli/clan_lib/flake/flake.py +++ b/pkgs/clan-cli/clan_lib/flake/flake.py @@ -11,7 +11,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Any -from clan_lib.errors import ClanError +from clan_lib.errors import ClanCmdError, ClanError log = logging.getLogger(__name__) @@ -143,6 +143,42 @@ class Selector: raise ValueError(msg) +class ClanSelectError(ClanError): + selectors: list[str] + failed_attr: str | None = None + flake_identifier: str + + def __init__( + self, + flake_identifier: str, + selectors: list[str], + cmd_error: ClanCmdError | None = None, + ) -> None: + attribute = None + if cmd_error: + attribute_match = re.search(r"error: attribute '([^']+)'", str(cmd_error)) + attribute = attribute_match.group(1) if attribute_match else None + if selectors == []: + msg = "failed to select []\n" + if len(selectors) == 1: + msg = f"failed to select {selectors[0]}\n" + else: + msg = f"failed to select: {'\n'.join(selectors)}\n" + msg += f" from {flake_identifier}\n" + if attribute: + msg += f" '{attribute}' is missing\n" + self.selectors = selectors + self.failed_attr = attribute + self.flake_identifier = flake_identifier + super().__init__(msg) + + def __str__(self) -> str: + return self.msg + + def __repr__(self) -> str: + return f"ClanSelectError({self.failed_attr})" + + def selectors_as_dict(selectors: list[Selector]) -> list[dict[str, Any]]: return [selector.as_dict() for selector in selectors] @@ -180,7 +216,7 @@ def parse_selector(selector: str) -> list[Selector]: if c == ".": stack.pop() if stack != []: - msg = "expected empy stack, but got {stack}" + msg = "expected empty stack, but got {stack}" raise ValueError(msg) else: msg = "expected ., but got {c}" @@ -863,12 +899,15 @@ class Flake: }} """ trace = os.environ.get("CLAN_DEBUG_NIX_SELECTORS", False) == "1" - build_output = Path( - run( - nix_build(["--expr", nix_code, *nix_options]), - RunOpts(log=Log.NONE, trace=trace), - ).stdout.strip() - ) + try: + build_output = Path( + run( + nix_build(["--expr", nix_code, *nix_options]), + RunOpts(log=Log.NONE, trace=trace), + ).stdout.strip() + ) + except ClanCmdError as e: + raise ClanSelectError(flake_identifier=self.identifier, selectors=selectors, cmd_error=e) from e if tmp_store := nix_test_store(): build_output = tmp_store.joinpath(*build_output.parts[1:]) diff --git a/pkgs/clan-cli/clan_lib/machines/machines.py b/pkgs/clan-cli/clan_lib/machines/machines.py index 96bf57cc4..21cb0e861 100644 --- a/pkgs/clan-cli/clan_lib/machines/machines.py +++ b/pkgs/clan-cli/clan_lib/machines/machines.py @@ -1,6 +1,5 @@ import importlib import logging -import re from dataclasses import dataclass from functools import cached_property from pathlib import Path @@ -11,8 +10,8 @@ from clan_cli.facts import secret_modules as facts_secret_modules from clan_cli.vars._types import StoreBase from clan_lib.api import API -from clan_lib.errors import ClanCmdError, ClanError -from clan_lib.flake import Flake +from clan_lib.errors import ClanError +from clan_lib.flake import ClanSelectError, Flake from clan_lib.nix_models.clan import InventoryMachine from clan_lib.ssh.remote import Remote @@ -77,10 +76,8 @@ class Machine: return self.flake.select( f'clanInternals.inventoryClass.inventory.machines."{self.name}".machineClass' ) - except ClanCmdError as e: - if re.search(f"error: attribute '{self.name}' missing", e.cmd.stderr): - return "nixos" - raise + except ClanSelectError: + return "nixos" @property def system(self) -> str: diff --git a/pkgs/clan-cli/clan_lib/tests/test_create.py b/pkgs/clan-cli/clan_lib/tests/test_create.py index f0d4df399..b34ab4ac8 100644 --- a/pkgs/clan-cli/clan_lib/tests/test_create.py +++ b/pkgs/clan-cli/clan_lib/tests/test_create.py @@ -19,7 +19,7 @@ from clan_cli.vars.generate import get_generators, run_generators from clan_lib.cmd import RunOpts, run from clan_lib.dirs import specific_machine_dir from clan_lib.errors import ClanError -from clan_lib.flake import Flake +from clan_lib.flake import ClanSelectError, Flake from clan_lib.machines.machines import Machine from clan_lib.nix import nix_command from clan_lib.nix_models.clan import ( @@ -278,10 +278,10 @@ def test_clan_create_api( if in_sandbox: # In sandbox: expect build to fail due to network restrictions - with pytest.raises(ClanError) as exc_info: + with pytest.raises(ClanSelectError) as select_error: Path(machine.select("config.system.build.toplevel")) - # The error should mention the system derivation name - assert "nixos-system-test-clan" in str(exc_info.value) + # The error should be a select_error without a failed_attr + assert select_error.value.failed_attr is None else: # Outside sandbox: build should succeed toplevel_path = Path(machine.select("config.system.build.toplevel"))