#!/usr/bin/env python3 """Codex Stop hook: validate discovered C/C++ projects with MSVC.""" import json import os import subprocess import sys import time from pathlib import Path SCRIPTS = Path(__file__).resolve().parents[1] sys.path.insert(0, str(SCRIPTS)) from msvc_harness.adapters.base import AdapterError from msvc_harness.adapters.cmake import CMakeAdapter from msvc_harness.adapters.msbuild import MSBuildAdapter from msvc_harness.config import ConfigError, load_config from msvc_harness.discovery import DiscoveryError, discover_project from msvc_harness.models import ProjectKind from msvc_harness.process import ValidationFailure, execute_plan from msvc_harness.toolchain import ToolchainError, discover_toolchain REENTRY_ENV = "CODEX_STOP_VALIDATION_ACTIVE" TOTAL_TIMEOUT_SECONDS = 1800 def _remaining(deadline, clock, stage): remaining = deadline - clock() if remaining <= 0: raise ValidationFailure(f"{stage} timed out before it could start") return remaining def _project_root( cwd: Path, *, deadline: float, run=subprocess.run, clock=time.monotonic, ) -> Path: argv = ["git", "rev-parse", "--show-toplevel"] resolved_cwd = cwd.resolve() try: result = run( argv, cwd=resolved_cwd, capture_output=True, text=True, shell=False, timeout=_remaining(deadline, clock, "repository discovery"), ) except subprocess.TimeoutExpired as exc: raise ValidationFailure( f"repository discovery timed out; argv={argv!r}; " f"cwd={str(resolved_cwd)!r}" ) from exc if result.returncode == 0 and result.stdout.strip(): return Path(result.stdout.strip()).resolve() return resolved_cwd def run_validations( root: Path, *, deadline: float | None = None, clock=time.monotonic, ) -> tuple[bool, str]: """Build and test a discovered C/C++ project, when one exists.""" if deadline is None: deadline = clock() + TOTAL_TIMEOUT_SECONDS try: config = load_config(root) discovery = discover_project(root, config) if discovery.selection is None: return True, "" selection = discovery.selection tools = discover_toolchain( selection.kind, deadline=deadline, clock=clock, ) adapter = ( CMakeAdapter() if selection.kind is ProjectKind.CMAKE else MSBuildAdapter() ) plan = adapter.create_plan(root, selection, config, tools) child_env = os.environ.copy() child_env[REENTRY_ENV] = "1" execute_plan( plan, root, env=child_env, deadline=deadline, clock=clock, ) return True, "" except ( ConfigError, DiscoveryError, ToolchainError, AdapterError, ValidationFailure, OSError, ) as exc: return False, str(exc) def _emit_stop_response(message: str) -> None: print( json.dumps( { "continue": False, "stopReason": message, "systemMessage": message, }, ensure_ascii=False, ) ) def main(*, run=subprocess.run, clock=time.monotonic) -> int: if os.environ.get(REENTRY_ENV) == "1": return 0 deadline = clock() + TOTAL_TIMEOUT_SECONDS try: root = _project_root( Path.cwd(), deadline=deadline, run=run, clock=clock, ) ok, message = run_validations(root, deadline=deadline, clock=clock) except (OSError, ValidationFailure) as exc: _emit_stop_response(f"validation hook failed: {exc}") return 0 if ok: return 0 _emit_stop_response(message) return 0 if __name__ == "__main__": raise SystemExit(main())