Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions aoc-main/src/aoc_main/_cmake_presets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Minimal reader for CMake preset files.

CMake's preset schema (``CMakePresets.json`` / ``CMakeUserPresets.json``) is
large; this module implements only the slice the C++ solver build needs:

* looking up a preset by *type* and *name*, with user presets taking precedence
over project presets (mirroring CMake's own override order);
* finding the configure step of a workflow preset;
* resolving the ``binaryDir`` of a configure preset, walking ``inherits`` chains
when the field is not set on the preset directly.

Features such as ``include`` and macro expansion (e.g. ``${sourceDir}``) are
intentionally unsupported.
"""

import json
import pathlib
from functools import cached_property
from typing import Any, Literal

from aoc_main import _logging

_logger = _logging.logger

PresetType = Literal["build", "configure", "workflow"]


class CMakePresetError(Exception):
"""Raised when the preset files cannot satisfy a requested lookup."""


class CMakePresets:
"""Reads CMake preset files from a directory and answers queries about them.

Pass the directory that contains ``CMakePresets.json`` (and optionally
``CMakeUserPresets.json``). Files are read lazily and cached on first
access, so an instance reflects their contents at that point. A missing
file is treated as containing no presets.
"""

def __init__(self, root_dir: pathlib.Path) -> None:
self._root_dir = root_dir

@cached_property
def _user_presets(self) -> dict[str, Any]:
return self._load(self._root_dir / "CMakeUserPresets.json")

@cached_property
def _project_presets(self) -> dict[str, Any]:
return self._load(self._root_dir / "CMakePresets.json")

@staticmethod
def _load(preset_file: pathlib.Path) -> dict[str, Any]:
if not preset_file.exists():
return {}
return json.loads(preset_file.read_text())
Comment thread
nikobockerman marked this conversation as resolved.

def _get_preset(self, preset_type: PresetType, name: str) -> dict[str, Any] | None:
for presets in (self._user_presets, self._project_presets):
preset = next(
(
preset
for preset in presets.get(f"{preset_type}Presets", [])
if preset["name"] == name
),
None,
)
if preset is not None:
return preset
return None
Comment thread
nikobockerman marked this conversation as resolved.

def workflow_configure_preset_name(self, workflow_name: str) -> str:
"""Return the configure step's preset name for a workflow preset."""
workflow_preset = self._get_preset("workflow", workflow_name)
if workflow_preset is None:
msg = f"Requested workflow preset not found: {workflow_name}"
raise CMakePresetError(msg)

try:
return next(
step["name"]
for step in workflow_preset["steps"]
if step["type"] == "configure"
)
except KeyError as e:
msg = f"Invalid CMake Preset json for workflow preset: {workflow_name}"
raise CMakePresetError(msg) from e
Comment thread
nikobockerman marked this conversation as resolved.
except StopIteration:
msg = f"No configure step found for workflow: {workflow_name}"
raise CMakePresetError(msg) from None

def binary_dir(self, configure_preset_name: str) -> pathlib.Path:
"""Resolve the ``binaryDir`` of a configure preset.

If the named preset does not set ``binaryDir`` itself, its ``inherits``
parents are searched breadth-first and the first one that sets the field
wins.
"""
pending = [configure_preset_name]
while pending:
name = pending.pop(0)
preset = self._get_preset("configure", name)
if preset is None:
msg = f"Configure preset not found: {name}"
raise CMakePresetError(msg)

binary_dir = preset.get("binaryDir")
if binary_dir is not None:
_logger.debug("Found binaryDir from preset %s: %s", name, binary_dir)
return pathlib.Path(binary_dir)

_logger.debug(
"binaryDir not set on preset %s; checking inherited presets", name
)
pending.extend(preset.get("inherits", []))

Comment thread
nikobockerman marked this conversation as resolved.
msg = f"binaryDir not found for configure preset {configure_preset_name}"
raise CMakePresetError(msg)
106 changes: 22 additions & 84 deletions aoc-main/src/aoc_main/_solver_cpp.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import asyncio
import json
import os
import pathlib
from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING

from aoc_main import _logging, _solvers, _types
from aoc_main import _cmake_presets, _logging, _solvers, _types

if TYPE_CHECKING:
import pathlib
from collections.abc import Iterable
Comment thread
nikobockerman marked this conversation as resolved.

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


class _CMakeConfigResolver:
@cached_property
def configure_preset_name(self) -> str:
return self._resolve_configure_preset()
"""Selects which CMake presets to use, then defers parsing to ``CMakePresets``.

The *selection* (which configure or workflow preset to use, read from
environment variables) lives here; the *parsing* of the preset files lives
in :mod:`aoc_main._cmake_presets`.
"""

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

@cached_property
def binary_dir(self) -> pathlib.Path:
return self._resolve_binary_dir()
def _presets(self) -> _cmake_presets.CMakePresets:
return _cmake_presets.CMakePresets(self.solver_root_dir)

@cached_property
def _cmake_user_presets(self) -> dict[str, Any]:
return self._load_cmake_preset_file(
self.solver_root_dir / "CMakeUserPresets.json"
)
def configure_preset_name(self) -> str:
return self._resolve_configure_preset()

@cached_property
def _cmake_presets(self) -> dict[str, Any]:
return self._load_cmake_preset_file(self.solver_root_dir / "CMakePresets.json")

@staticmethod
def _load_cmake_preset_file(preset_file: pathlib.Path) -> dict[str, Any]:
if not preset_file.exists():
return {}
return json.loads(preset_file.read_text())

def _get_preset(
self,
preset_type: Literal["build", "configure", "workflow"],
name: str,
) -> Any | None: # noqa: ANN401
for presets in (self._cmake_user_presets, self._cmake_presets):
preset = next(
(
preset
for preset in presets.get(f"{preset_type}Presets", {})
if preset["name"] == name
),
None,
)
if preset is not None:
return preset
return None
def binary_dir(self) -> pathlib.Path:
try:
return self._presets.binary_dir(self.configure_preset_name)
except _cmake_presets.CMakePresetError as e:
raise _CppSolverConfigureError(str(e)) from e

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

_logger.debug("Using workflow preset: %s", workflow_preset_name)
workflow_preset = self._get_preset("workflow", workflow_preset_name)
if workflow_preset is None:
msg = f"Requested workflow preset not found: {workflow_preset_name}"
raise _CppSolverConfigureError(msg)

try:
configure_preset_name = next(
step["name"]
for step in workflow_preset["steps"]
if step["type"] == "configure"
configure_preset_name = self._presets.workflow_configure_preset_name(
workflow_preset_name
)
except KeyError as e:
msg = (
f"Invalid CMake Preset json for workflow preset: {workflow_preset_name}"
)
raise _CppSolverConfigureError(msg) from e
except StopIteration:
msg = f"No configure step found for workflow: {workflow_preset_name}"
raise _CppSolverConfigureError(msg) from None
except _cmake_presets.CMakePresetError as e:
raise _CppSolverConfigureError(str(e)) from e

_logger.debug("Using configure preset: %s", configure_preset_name)
assert configure_preset_name is not None
return configure_preset_name

def _resolve_binary_dir(self) -> pathlib.Path:
configure_preset_names = [self.configure_preset_name]
while configure_preset_names:
configure_preset_name = configure_preset_names.pop(0)
configure_preset = self._get_preset("configure", configure_preset_name)
if configure_preset is None:
msg = f"Configure preset not found: {configure_preset_name}"
raise _CppSolverConfigureError(msg)

binary_dir = configure_preset.get("binaryDir")
if binary_dir is not None:
_logger.debug(
"Found binaryDir from preset %s: %s",
configure_preset_name,
binary_dir,
)
return pathlib.Path(binary_dir)

_logger.debug(
"Binary dir not found for preset %s. Checking inherited presets",
configure_preset_name,
)
configure_preset_names.extend(configure_preset.get("inherits", []))

error = f"binaryDir not found for configure preset {self.configure_preset_name}"
raise _CppSolverConfigureError(error)


class SolverCpp:
def __init__(self, solver_ids: Iterable[_solvers.SolverId]) -> None:
Expand Down
Loading
Loading