#!/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") ABAQUS_VALIDATION_MODES = {"off", "detect", "run"} class ValidationConfigError(RuntimeError): pass class AbaqusValidation: def __init__(self, *, mode: str, executable: str | None, commands: list[str], required: bool): self.mode = mode self.executable = executable self.commands = commands self.required = required 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 _load_multiline_env(name: str) -> list[str]: raw = os.environ.get(name, "") return [line.strip() for line in raw.splitlines() if line.strip()] def python_script_command(script_name: str) -> str: return subprocess.list2cmdline([sys.executable, str(Path("scripts") / script_name)]) 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 load_harness_commands(root: Path) -> list[str]: return [ python_script_command("validate_reference_artifacts.py"), python_script_command("validate_fortran.py"), ] def _quote(value: str | Path) -> str: return subprocess.list2cmdline([str(value)]) def _resolve_executable(command: str, which=shutil.which) -> str | None: path = Path(command) if path.exists(): return str(path) return which(command) def _discover_oneapi_env_script() -> Path | None: configured = os.environ.get("HARNESS_ONEAPI_VARS_BAT") if configured: path = Path(configured) return path if path.exists() else None for candidate in ( Path(r"C:\Program Files (x86)\Intel\oneAPI\compiler\latest\env\vars.bat"), Path(r"C:\Program Files\Intel\oneAPI\compiler\latest\env\vars.bat"), Path(r"C:\Program Files (x86)\Intel\oneAPI\setvars.bat"), Path(r"C:\Program Files\Intel\oneAPI\setvars.bat"), ): if candidate.exists(): return candidate return None def _maybe_wrap_oneapi(command: str) -> str: mode = os.environ.get("HARNESS_ABAQUS_USE_ONEAPI_ENV", "auto").lower() if mode not in {"auto", "on", "off"}: raise ValidationConfigError(f"Unsupported HARNESS_ABAQUS_USE_ONEAPI_ENV: {mode}") if mode == "off": return command env_script = _discover_oneapi_env_script() if env_script is None: if mode == "on": raise ValidationConfigError("HARNESS_ABAQUS_USE_ONEAPI_ENV=on but no oneAPI vars.bat was found.") return command return f'cmd /d /s /c "call {_quote(env_script)} intel64 >nul && {command}"' def discover_abaqus_validation(which=shutil.which) -> AbaqusValidation: mode = os.environ.get("HARNESS_ABAQUS_VALIDATION", "off").lower() if mode not in ABAQUS_VALIDATION_MODES: raise ValidationConfigError(f"Unsupported HARNESS_ABAQUS_VALIDATION: {mode}") if mode == "off": return AbaqusValidation(mode=mode, executable=None, commands=[], required=False) command_name = os.environ.get("HARNESS_ABAQUS_COMMAND", "abaqus") executable = _resolve_executable(command_name, which=which) if mode == "detect": return AbaqusValidation(mode=mode, executable=executable, commands=[], required=False) if executable is None: raise ValidationConfigError(f"Abaqus executable not found: {command_name}") commands = _load_multiline_env("HARNESS_ABAQUS_VALIDATION_COMMANDS") if not commands: raise ValidationConfigError("HARNESS_ABAQUS_VALIDATION=run requires HARNESS_ABAQUS_VALIDATION_COMMANDS.") quoted_executable = _quote(executable) resolved_commands = [_maybe_wrap_oneapi(command.replace("{abaqus}", quoted_executable)) for command in commands] return AbaqusValidation(mode=mode, executable=executable, commands=resolved_commands, required=True) def discover_commands(root: Path) -> list[str]: env_commands = load_env_commands() if env_commands: return env_commands commands = load_harness_commands(root) preset_commands = load_preset_commands(root) if preset_commands: commands.extend(preset_commands) else: commands.extend(load_cmake_commands(root)) commands.extend(discover_abaqus_validation().commands) return commands 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 try: commands = discover_commands(root) abaqus_validation = ( AbaqusValidation(mode="off", executable=None, commands=[], required=False) if load_env_commands() else discover_abaqus_validation() ) except ValidationConfigError as exc: print(f"Validation configuration failed: {exc}", file=sys.stderr) return 2 if abaqus_validation.mode == "detect": if abaqus_validation.executable: print(f"Abaqus executable detected: {abaqus_validation.executable}") else: print("Abaqus executable not detected.") if not commands: print("No 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())