149 lines
3.9 KiB
Python
149 lines
3.9 KiB
Python
#!/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())
|