import re import shutil import subprocess import time from pathlib import Path from .models import Toolchain class ToolchainError(RuntimeError): pass def _decode(value): return value if isinstance(value, str) else (value or b"").decode("utf-8", errors="replace") def discover_toolchain(root: Path, *, which=shutil.which, run=subprocess.run, deadline=None, clock=time.monotonic) -> Toolchain: root = root.resolve() executable = which("dotnet") if executable is None or not Path(executable).is_file(): raise ToolchainError("dotnet not found; install .NET SDK 8 and check PATH and global.json") executable = Path(executable).resolve() argv = [str(executable), "--version"] context = f"argv={argv!r}; cwd={str(root)!r}; check .NET SDK 8 and global.json" timeout = 1800 if deadline is None else min(1800, deadline - clock()) if timeout <= 0: raise ToolchainError(f"SDK probe timed out before it could start; {context}") try: result = run(argv, cwd=root, capture_output=True, check=False, shell=False, timeout=timeout) except subprocess.TimeoutExpired as exc: raise ToolchainError(f"SDK probe timed out; {context}") from exc except OSError as exc: raise ToolchainError(f"SDK probe could not start: {exc}; {context}") from exc if result.returncode != 0: diagnostic = (_decode(result.stdout) + "\n" + _decode(result.stderr)).strip()[-8000:] raise ToolchainError(f"SDK probe failed with exit code {result.returncode}; {context}; output tail: {diagnostic}") version = _decode(result.stdout).strip() if not re.fullmatch(r"8\.0\.\d+", version): raise ToolchainError(f"expected stable .NET SDK 8, got {version!r}; {context}") return Toolchain(executable, version)