Files
Agentic-AI-Template/Codex/Coding/harness framework cs/scripts/dotnet_harness/discovery.py
T
2026-09-08 10:30:07 +09:00

107 lines
5.1 KiB
Python

import re
import xml.etree.ElementTree as ET
from pathlib import Path, PureWindowsPath
from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection
from .paths import iter_source_files
class DiscoveryError(RuntimeError):
pass
def validate_csproj(path: Path) -> None:
if path.suffix.lower() != ".csproj" or not path.is_file():
raise DiscoveryError(f"expected an existing SDK-style .csproj: {path}")
try:
project = ET.parse(path).getroot()
except (ET.ParseError, OSError) as exc:
raise DiscoveryError(f"cannot read project {path}: {exc}") from exc
name = lambda element: element.tag.rsplit("}", 1)[-1]
if name(project) != "Project" or not (
project.get("Sdk", "").strip()
or any(name(child) == "Sdk" and child.get("Name", "").strip() for child in project)
):
raise DiscoveryError(f"project must use SDK style; .NET Framework is unsupported: {path}")
for element in project.iter():
tag = name(element)
value = (element.text or "").strip()
if tag == "TargetFrameworkVersion" and value:
raise DiscoveryError(f".NET Framework metadata is unsupported: {path}")
if tag in {"TargetFramework", "TargetFrameworks"}:
# Build evaluates inherited properties and expressions. This preflight
# rejects explicit incompatible values rather than guessing expansion.
for target in value.split(";"):
target = target.strip()
if target and "$(" not in target and not re.fullmatch(r"net8\.0(?:-[A-Za-z0-9.]+)?", target):
raise DiscoveryError(f"only net8.0 targets are supported; found {target!r}: {path}")
def _validate_solution(path: Path, root: Path) -> tuple[Path, ...]:
try:
content = path.read_text(encoding="utf-8-sig")
except (OSError, UnicodeError) as exc:
raise DiscoveryError(f"cannot read solution {path}: {exc}") from exc
if "Microsoft Visual Studio Solution File" not in content:
raise DiscoveryError(f"invalid .sln file: {path}")
projects = []
folder_types = {"66A26720-8FB5-11D2-AA7E-00C04F688DDE", "2150E333-8FDC-42A3-9474-1A3956D46DE8"}
csharp_types = {"FAE04EC0-301F-11D3-BF4B-00C04F79EFBC", "9A19103F-16F7-4668-BE54-9A1E7A4F7556"}
for line in content.splitlines():
if not line.lstrip().startswith("Project("):
continue
match = re.match(r'\s*Project\("\{([^}]+)\}"\)\s*=\s*"[^"]*",\s*"([^"]+)"', line)
if not match:
raise DiscoveryError(f"invalid project entry in solution: {path}")
kind, reference = match.groups()
if kind.upper() in folder_types:
continue
if kind.upper() not in csharp_types:
raise DiscoveryError(f"unsupported project type in solution: {reference}")
windows_path = PureWindowsPath(reference)
if windows_path.drive or windows_path.root or Path(reference).is_absolute():
raise DiscoveryError(f"solution project must be repository-relative: {reference}")
candidate = (path.parent / reference.replace("\\", "/")).resolve()
if not candidate.is_relative_to(root):
raise DiscoveryError(f"solution project resolves outside repository: {reference}")
validate_csproj(candidate)
projects.append(candidate)
if not projects:
raise DiscoveryError(f"solution has no supported C# projects: {path}")
return tuple(projects)
def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult:
root = root.resolve()
target = config.dotnet.target
if target is None:
files = tuple(iter_source_files(root))
solutions = [path for path in files if path.parent == root and path.suffix.lower() == ".sln"]
candidates = solutions or [path for path in files if path.suffix.lower() == ".csproj"]
if len(candidates) > 1:
raise DiscoveryError("multiple project candidates; set dotnet.target")
if candidates:
target = candidates[0]
elif config.project_type == "dotnet" or any(
path.suffix.lower() in {".cs", ".sln", ".slnx", ".vcxproj", ".fsproj", ".vbproj"}
for path in files
):
raise DiscoveryError("no supported C# metadata found; set dotnet.target to a .sln or SDK-style .csproj")
else:
return DiscoveryResult(None)
target = target.resolve()
if not target.is_relative_to(root) or not target.is_file():
raise DiscoveryError(f"project target must be an existing repository file: {target}")
if target.suffix.lower() == ".sln":
projects = _validate_solution(target, root)
else:
validate_csproj(target)
projects = (target,)
if not config.dotnet.test_projects:
raise DiscoveryError("product validation requires an explicit non-empty dotnet.testProjects list")
for test_project in config.dotnet.test_projects:
if not test_project.resolve().is_relative_to(root):
raise DiscoveryError(f"test project resolves outside repository: {test_project}")
validate_csproj(test_project)
return DiscoveryResult(ProjectSelection(ProjectKind.DOTNET, target, projects))