import os import shutil import subprocess import time from pathlib import Path from .models import ProjectKind, Toolchain VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" VS_CMAKE_RELATIVE = Path( "Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin" ) class ToolchainError(RuntimeError): pass def _decode(value): if isinstance(value, str): return value return (value or b"").decode("utf-8", errors="replace") def _first_path(result, label, argv, cwd): text = _decode(result.stdout).strip() if result.returncode != 0: diagnostic = ( _decode(result.stdout) + "\n" + _decode(result.stderr) ).rstrip()[-8000:] diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") message = ( f"{label} probe failed with exit code {result.returncode}; " f"argv={argv!r}; cwd={str(cwd)!r}" ) if diagnostic: message = f"{message}; output tail: {diagnostic}" raise ToolchainError( f"{message}; install Visual Studio Desktop development with C++" ) if not text: raise ToolchainError( f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; " "install Visual Studio Desktop development with C++" ) path = Path(text.splitlines()[0]).resolve() if not path.exists(): diagnostic = ( _decode(result.stdout) + "\n" + _decode(result.stderr) ).rstrip()[-8000:] diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ") message = ( f"{label} probe returned nonexistent path with exit code 0; " f"argv={argv!r}; cwd={str(cwd)!r}" ) if diagnostic: message = f"{message}; output tail: {diagnostic}" raise ToolchainError( f"{message}; install or repair Visual Studio Desktop development with C++" ) return path def _require_path(path, label): if path is None or not path.exists(): raise ToolchainError(f"{label} not found") return path.resolve() def _remaining(deadline, clock, label): if deadline is None: return None remaining = deadline - clock() if remaining <= 0: raise ToolchainError(f"{label} probe timed out before it could start") return remaining def _probe(run, argv, label, *, deadline, clock): cwd = Path.cwd().resolve() kwargs = { "cwd": cwd, "capture_output": True, "check": False, "shell": False, } remaining = _remaining(deadline, clock, label) if remaining is not None: kwargs["timeout"] = remaining try: return run(argv, **kwargs) except subprocess.TimeoutExpired as exc: raise ToolchainError( f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; " "install or repair Visual Studio Desktop development with C++" ) from exc def discover_toolchain( kind: ProjectKind, *, env=None, which=shutil.which, run=subprocess.run, deadline=None, clock=time.monotonic, ) -> Toolchain: environment = os.environ if env is None else env vswhere_path = which("vswhere.exe") if vswhere_path is None: program_files = environment.get("ProgramFiles(x86)") if program_files: candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe" vswhere_path = str(candidate) if candidate.exists() else None vswhere = _require_path( Path(vswhere_path) if vswhere_path else None, "vswhere.exe; install Visual Studio Desktop development with C++", ) install_argv = [ str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, "-property", "installationPath", ] install_result = _probe( run, install_argv, "Visual Studio installation", deadline=deadline, clock=clock, ) probe_cwd = Path.cwd().resolve() installation = _require_path( _first_path( install_result, "Visual Studio installation", install_argv, probe_cwd, ), "Visual Studio installation", ) msbuild_argv = [ str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD, "-find", r"MSBuild\**\Bin\MSBuild.exe", ] msbuild_result = _probe( run, msbuild_argv, "MSBuild", deadline=deadline, clock=clock, ) msbuild = _require_path( _first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd), "MSBuild.exe", ) if kind is not ProjectKind.CMAKE: return Toolchain(installation=installation, msbuild=msbuild) bundled_cmake_dir = installation / VS_CMAKE_RELATIVE cmake = _require_path( Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe", "cmake.exe", ) ctest = _require_path( Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe", "ctest.exe", ) return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)