Skip to content

Commit a2b7a1e

Browse files
committed
Refactor CMake preset parser to own module
1 parent 36e39a6 commit a2b7a1e

3 files changed

Lines changed: 325 additions & 84 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Minimal reader for CMake preset files.
2+
3+
CMake's preset schema (``CMakePresets.json`` / ``CMakeUserPresets.json``) is
4+
large; this module implements only the slice the C++ solver build needs:
5+
6+
* looking up a preset by *type* and *name*, with user presets taking precedence
7+
over project presets (mirroring CMake's own override order);
8+
* finding the configure step of a workflow preset;
9+
* resolving the ``binaryDir`` of a configure preset, walking ``inherits`` chains
10+
when the field is not set on the preset directly.
11+
12+
Features such as ``include`` and macro expansion (e.g. ``${sourceDir}``) are
13+
intentionally unsupported.
14+
"""
15+
16+
import json
17+
import pathlib
18+
from functools import cached_property
19+
from typing import Any, Literal
20+
21+
from aoc_main import _logging
22+
23+
_logger = _logging.logger
24+
25+
PresetType = Literal["build", "configure", "workflow"]
26+
27+
28+
class CMakePresetError(Exception):
29+
"""Raised when the preset files cannot satisfy a requested lookup."""
30+
31+
32+
class CMakePresets:
33+
"""Reads CMake preset files from a directory and answers queries about them.
34+
35+
Pass the directory that contains ``CMakePresets.json`` (and optionally
36+
``CMakeUserPresets.json``). Files are read lazily and cached on first
37+
access, so an instance reflects their contents at that point. A missing
38+
file is treated as containing no presets.
39+
"""
40+
41+
def __init__(self, root_dir: pathlib.Path) -> None:
42+
self._root_dir = root_dir
43+
44+
@cached_property
45+
def _user_presets(self) -> dict[str, Any]:
46+
return self._load(self._root_dir / "CMakeUserPresets.json")
47+
48+
@cached_property
49+
def _project_presets(self) -> dict[str, Any]:
50+
return self._load(self._root_dir / "CMakePresets.json")
51+
52+
@staticmethod
53+
def _load(preset_file: pathlib.Path) -> dict[str, Any]:
54+
if not preset_file.exists():
55+
return {}
56+
return json.loads(preset_file.read_text())
57+
58+
def _get_preset(self, preset_type: PresetType, name: str) -> dict[str, Any] | None:
59+
for presets in (self._user_presets, self._project_presets):
60+
preset = next(
61+
(
62+
preset
63+
for preset in presets.get(f"{preset_type}Presets", [])
64+
if preset["name"] == name
65+
),
66+
None,
67+
)
68+
if preset is not None:
69+
return preset
70+
return None
71+
72+
def workflow_configure_preset_name(self, workflow_name: str) -> str:
73+
"""Return the configure step's preset name for a workflow preset."""
74+
workflow_preset = self._get_preset("workflow", workflow_name)
75+
if workflow_preset is None:
76+
msg = f"Requested workflow preset not found: {workflow_name}"
77+
raise CMakePresetError(msg)
78+
79+
try:
80+
return next(
81+
step["name"]
82+
for step in workflow_preset["steps"]
83+
if step["type"] == "configure"
84+
)
85+
except KeyError as e:
86+
msg = f"Invalid CMake Preset json for workflow preset: {workflow_name}"
87+
raise CMakePresetError(msg) from e
88+
except StopIteration:
89+
msg = f"No configure step found for workflow: {workflow_name}"
90+
raise CMakePresetError(msg) from None
91+
92+
def binary_dir(self, configure_preset_name: str) -> pathlib.Path:
93+
"""Resolve the ``binaryDir`` of a configure preset.
94+
95+
If the named preset does not set ``binaryDir`` itself, its ``inherits``
96+
parents are searched breadth-first and the first one that sets the field
97+
wins.
98+
"""
99+
pending = [configure_preset_name]
100+
while pending:
101+
name = pending.pop(0)
102+
preset = self._get_preset("configure", name)
103+
if preset is None:
104+
msg = f"Configure preset not found: {name}"
105+
raise CMakePresetError(msg)
106+
107+
binary_dir = preset.get("binaryDir")
108+
if binary_dir is not None:
109+
_logger.debug("Found binaryDir from preset %s: %s", name, binary_dir)
110+
return pathlib.Path(binary_dir)
111+
112+
_logger.debug(
113+
"binaryDir not set on preset %s; checking inherited presets", name
114+
)
115+
pending.extend(preset.get("inherits", []))
116+
117+
msg = f"binaryDir not found for configure preset {configure_preset_name}"
118+
raise CMakePresetError(msg)

aoc-main/src/aoc_main/_solver_cpp.py

Lines changed: 22 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import asyncio
2-
import json
32
import os
4-
import pathlib
53
from dataclasses import dataclass
64
from functools import cached_property
7-
from typing import TYPE_CHECKING, Any, Literal
5+
from typing import TYPE_CHECKING
86

9-
from aoc_main import _logging, _solvers, _types
7+
from aoc_main import _cmake_presets, _logging, _solvers, _types
108

119
if TYPE_CHECKING:
10+
import pathlib
1211
from collections.abc import Iterable
1312

1413
_logger = _logging.logger
@@ -57,51 +56,31 @@ def __init__(self, target: str, returncode: int) -> None:
5756

5857

5958
class _CMakeConfigResolver:
60-
@cached_property
61-
def configure_preset_name(self) -> str:
62-
return self._resolve_configure_preset()
59+
"""Selects which CMake presets to use, then defers parsing to ``CMakePresets``.
60+
61+
The *selection* (which configure or workflow preset to use, read from
62+
environment variables) lives here; the *parsing* of the preset files lives
63+
in :mod:`aoc_main._cmake_presets`.
64+
"""
6365

6466
@cached_property
6567
def solver_root_dir(self) -> pathlib.Path:
6668
return _solvers.get_solver_root_dir(_solvers.Solver.Cpp)
6769

6870
@cached_property
69-
def binary_dir(self) -> pathlib.Path:
70-
return self._resolve_binary_dir()
71+
def _presets(self) -> _cmake_presets.CMakePresets:
72+
return _cmake_presets.CMakePresets(self.solver_root_dir)
7173

7274
@cached_property
73-
def _cmake_user_presets(self) -> dict[str, Any]:
74-
return self._load_cmake_preset_file(
75-
self.solver_root_dir / "CMakeUserPresets.json"
76-
)
75+
def configure_preset_name(self) -> str:
76+
return self._resolve_configure_preset()
7777

7878
@cached_property
79-
def _cmake_presets(self) -> dict[str, Any]:
80-
return self._load_cmake_preset_file(self.solver_root_dir / "CMakePresets.json")
81-
82-
@staticmethod
83-
def _load_cmake_preset_file(preset_file: pathlib.Path) -> dict[str, Any]:
84-
if not preset_file.exists():
85-
return {}
86-
return json.loads(preset_file.read_text())
87-
88-
def _get_preset(
89-
self,
90-
preset_type: Literal["build", "configure", "workflow"],
91-
name: str,
92-
) -> Any | None: # noqa: ANN401
93-
for presets in (self._cmake_user_presets, self._cmake_presets):
94-
preset = next(
95-
(
96-
preset
97-
for preset in presets.get(f"{preset_type}Presets", {})
98-
if preset["name"] == name
99-
),
100-
None,
101-
)
102-
if preset is not None:
103-
return preset
104-
return None
79+
def binary_dir(self) -> pathlib.Path:
80+
try:
81+
return self._presets.binary_dir(self.configure_preset_name)
82+
except _cmake_presets.CMakePresetError as e:
83+
raise _CppSolverConfigureError(str(e)) from e
10584

10685
def _resolve_configure_preset(self) -> str:
10786
configure_preset_name = os.environ.get(_ENV_PRESET_CONFIGURE)
@@ -117,57 +96,16 @@ def _resolve_configure_preset(self) -> str:
11796
return _DEFAULT_CONFIGURE_PRESET
11897

11998
_logger.debug("Using workflow preset: %s", workflow_preset_name)
120-
workflow_preset = self._get_preset("workflow", workflow_preset_name)
121-
if workflow_preset is None:
122-
msg = f"Requested workflow preset not found: {workflow_preset_name}"
123-
raise _CppSolverConfigureError(msg)
124-
12599
try:
126-
configure_preset_name = next(
127-
step["name"]
128-
for step in workflow_preset["steps"]
129-
if step["type"] == "configure"
100+
configure_preset_name = self._presets.workflow_configure_preset_name(
101+
workflow_preset_name
130102
)
131-
except KeyError as e:
132-
msg = (
133-
f"Invalid CMake Preset json for workflow preset: {workflow_preset_name}"
134-
)
135-
raise _CppSolverConfigureError(msg) from e
136-
except StopIteration:
137-
msg = f"No configure step found for workflow: {workflow_preset_name}"
138-
raise _CppSolverConfigureError(msg) from None
103+
except _cmake_presets.CMakePresetError as e:
104+
raise _CppSolverConfigureError(str(e)) from e
139105

140106
_logger.debug("Using configure preset: %s", configure_preset_name)
141-
assert configure_preset_name is not None
142107
return configure_preset_name
143108

144-
def _resolve_binary_dir(self) -> pathlib.Path:
145-
configure_preset_names = [self.configure_preset_name]
146-
while configure_preset_names:
147-
configure_preset_name = configure_preset_names.pop(0)
148-
configure_preset = self._get_preset("configure", configure_preset_name)
149-
if configure_preset is None:
150-
msg = f"Configure preset not found: {configure_preset_name}"
151-
raise _CppSolverConfigureError(msg)
152-
153-
binary_dir = configure_preset.get("binaryDir")
154-
if binary_dir is not None:
155-
_logger.debug(
156-
"Found binaryDir from preset %s: %s",
157-
configure_preset_name,
158-
binary_dir,
159-
)
160-
return pathlib.Path(binary_dir)
161-
162-
_logger.debug(
163-
"Binary dir not found for preset %s. Checking inherited presets",
164-
configure_preset_name,
165-
)
166-
configure_preset_names.extend(configure_preset.get("inherits", []))
167-
168-
error = f"binaryDir not found for configure preset {self.configure_preset_name}"
169-
raise _CppSolverConfigureError(error)
170-
171109

172110
class SolverCpp:
173111
def __init__(self, solver_ids: Iterable[_solvers.SolverId]) -> None:

0 commit comments

Comments
 (0)