#!/usr/bin/env python3 """Run C++/MSVC Harness validation commands.""" from __future__ import annotations import json import os import shutil import subprocess import sys from pathlib import Path DEFAULT_GENERATOR = "Visual Studio 17 2022" DEFAULT_PLATFORM = "x64" DEFAULT_CONFIG = "Debug" DEFAULT_BUILD_DIR = "build/msvc-debug" PRESET_NAME = "msvc-debug" COMMON_CMAKE_BIN = Path(r"C:\Program Files\CMake\bin") def load_env_commands() -> list[str]: raw = os.environ.get("HARNESS_VALIDATION_COMMANDS", "") return [line.strip() for line in raw.splitlines() if line.strip()] def _cmake_config() -> tuple[str, str, str, Path]: generator = os.environ.get("HARNESS_CMAKE_GENERATOR", DEFAULT_GENERATOR) platform = os.environ.get("HARNESS_CMAKE_PLATFORM", DEFAULT_PLATFORM) config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG) build_dir = Path(os.environ.get("HARNESS_BUILD_DIR", DEFAULT_BUILD_DIR)) return generator, platform, config, build_dir def _read_presets(root: Path) -> dict: presets_file = root / "CMakePresets.json" if not presets_file.exists(): return {} try: return json.loads(presets_file.read_text(encoding="utf-8")) except json.JSONDecodeError: return {} def _preset_binary_dir(root: Path, preset: dict) -> Path: binary_dir = str(preset.get("binaryDir") or DEFAULT_BUILD_DIR) binary_dir = binary_dir.replace("${sourceDir}", str(root)) binary_dir = binary_dir.replace("$sourceDir", str(root)) path = Path(binary_dir) return path if path.is_absolute() else root / path def load_preset_commands(root: Path) -> list[str]: payload = _read_presets(root) config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG) for preset in payload.get("configurePresets", []): if isinstance(preset, dict) and preset.get("name") == PRESET_NAME: build_dir = _preset_binary_dir(root, preset) return [ f"cmake --preset {PRESET_NAME}", f'cmake --build "{build_dir}" --config {config}', f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}', ] return [] def load_cmake_commands(root: Path) -> list[str]: if not (root / "CMakeLists.txt").exists(): return [] generator, platform, config, build_dir = _cmake_config() if not build_dir.is_absolute(): build_dir = root / build_dir return [ f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}', f'cmake --build "{build_dir}" --config {config}', f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}', ] def discover_commands(root: Path) -> list[str]: env_commands = load_env_commands() if env_commands: return env_commands preset_commands = load_preset_commands(root) if preset_commands: return preset_commands return load_cmake_commands(root) def run_command(command: str, root: Path) -> subprocess.CompletedProcess: return subprocess.run( command, cwd=root, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", env=validation_environment(os.environ), ) def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]: env = dict(base_env) if shutil.which("cmake") is not None: return env cmake_exe = COMMON_CMAKE_BIN / "cmake.exe" if not cmake_exe.exists(): return env current_path = env.get("PATH", "") paths = [part for part in current_path.split(os.pathsep) if part] common_bin_text = str(COMMON_CMAKE_BIN) if not any(part.lower() == common_bin_text.lower() for part in paths): env["PATH"] = common_bin_text + (os.pathsep + current_path if current_path else "") return env def emit_stream(prefix: str, content: str, *, stream) -> None: text = (content or "").strip() if not text: return print(prefix, file=stream) print(text, file=stream) def main() -> int: root = Path(__file__).resolve().parent.parent commands = discover_commands(root) if not commands: print("No C++ validation commands configured.") print("Add CMakeLists.txt or set HARNESS_VALIDATION_COMMANDS.") return 0 for command in commands: print(f"$ {command}") result = run_command(command, root) emit_stream("[stdout]", result.stdout, stream=sys.stdout) emit_stream("[stderr]", result.stderr, stream=sys.stderr) if result.returncode != 0: print(f"Validation failed: {command}", file=sys.stderr) return result.returncode print("Validation succeeded.") return 0 if __name__ == "__main__": raise SystemExit(main())