139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .models import CMakeConfig, HarnessConfig, MSBuildConfig
|
|
|
|
|
|
class ConfigError(ValueError):
|
|
pass
|
|
|
|
|
|
def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None:
|
|
unknown = sorted(set(data) - allowed)
|
|
if unknown:
|
|
raise ConfigError(f"{label} contains unexpected field: {unknown[0]}")
|
|
|
|
|
|
def _repo_path(root: Path, raw: str, label: str) -> Path:
|
|
candidate = Path(raw)
|
|
if candidate.is_absolute():
|
|
raise ConfigError(f"{label} must be repository-relative")
|
|
resolved = (root / candidate).resolve()
|
|
try:
|
|
resolved.relative_to(root)
|
|
except ValueError as exc:
|
|
raise ConfigError(f"{label} resolves outside the repository") from exc
|
|
return resolved
|
|
|
|
|
|
def _mapping(value: Any, label: str) -> dict[str, Any]:
|
|
if not isinstance(value, dict):
|
|
raise ConfigError(f"{label} must be an object")
|
|
return value
|
|
|
|
|
|
def _nonempty_string(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or not value:
|
|
raise ConfigError(f"{label} must be a non-empty string")
|
|
if any(ord(character) < 32 or ord(character) == 127 for character in value):
|
|
raise ConfigError(f"{label} must not contain control characters")
|
|
return value
|
|
|
|
|
|
def _string_array(value: Any, label: str) -> tuple[str, ...]:
|
|
if not isinstance(value, list):
|
|
raise ConfigError(f"{label} must be an array")
|
|
if not value:
|
|
raise ConfigError(f"{label} must be a non-empty string array")
|
|
return tuple(
|
|
_nonempty_string(item, f"{label}[{index}]")
|
|
for index, item in enumerate(value)
|
|
)
|
|
|
|
|
|
def load_config(root: Path) -> HarnessConfig:
|
|
root = root.resolve()
|
|
config_path = root / ".harness" / "config.json"
|
|
if config_path.is_file():
|
|
try:
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
except UnicodeDecodeError as exc:
|
|
raise ConfigError(
|
|
f"{config_path}: configuration must be valid UTF-8: {exc}"
|
|
) from exc
|
|
except json.JSONDecodeError as exc:
|
|
raise ConfigError(f"{config_path}: {exc}") from exc
|
|
else:
|
|
data = {"version": 1}
|
|
|
|
data = _mapping(data, "config")
|
|
_reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config")
|
|
|
|
if data.get("version") != 1 or isinstance(data.get("version"), bool):
|
|
raise ConfigError("version must be 1")
|
|
project_type = data.get("projectType", "auto")
|
|
if project_type not in {"auto", "cmake", "msbuild"}:
|
|
raise ConfigError("projectType must be auto, cmake, or msbuild")
|
|
|
|
cmake_data = _mapping(data.get("cmake", {}), "cmake")
|
|
_reject_unknown(
|
|
cmake_data,
|
|
{"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"},
|
|
"cmake",
|
|
)
|
|
source_dir = _repo_path(
|
|
root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir"
|
|
)
|
|
binary_dir = None
|
|
if "binaryDir" in cmake_data:
|
|
binary_dir = _repo_path(
|
|
root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir"
|
|
)
|
|
preset_keys = ("configurePreset", "buildPreset", "testPreset")
|
|
if any(key in cmake_data for key in preset_keys) and not (
|
|
all(key in cmake_data for key in preset_keys) and binary_dir is not None
|
|
):
|
|
raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together")
|
|
configure_preset = (
|
|
_nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset")
|
|
if "configurePreset" in cmake_data
|
|
else None
|
|
)
|
|
build_preset = (
|
|
_nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset")
|
|
if "buildPreset" in cmake_data
|
|
else None
|
|
)
|
|
test_preset = (
|
|
_nonempty_string(cmake_data["testPreset"], "cmake.testPreset")
|
|
if "testPreset" in cmake_data
|
|
else None
|
|
)
|
|
|
|
msbuild_data = _mapping(data.get("msbuild", {}), "msbuild")
|
|
_reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild")
|
|
solution = None
|
|
if "solution" in msbuild_data:
|
|
solution = _repo_path(
|
|
root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution"
|
|
)
|
|
configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration")
|
|
platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform")
|
|
test_command = (
|
|
_string_array(msbuild_data["testCommand"], "msbuild.testCommand")
|
|
if "testCommand" in msbuild_data
|
|
else None
|
|
)
|
|
|
|
if "tdd" in data:
|
|
print("Deprecated config field 'tdd' is ignored; it may be removed.", file=sys.stderr)
|
|
|
|
return HarnessConfig(
|
|
version=1,
|
|
project_type=project_type,
|
|
cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset),
|
|
msbuild=MSBuildConfig(solution, configuration, platform, test_command),
|
|
)
|