"""Validate the SDK-evaluated project-reference graph before restore.""" import json import subprocess import time from collections import deque from pathlib import Path from .discovery import DiscoveryError, validate_csproj from .models import CommandSpec, ResultCheck, ResultCheckKind, ValidationPlan, ValidationStep from .process import ValidationFailure, execute_plan def _project_path(root, path): try: path = path.resolve() if not path.is_relative_to(root): raise ValidationFailure(f"ProjectReference resolves outside the repository: {path}") validate_csproj(path) except (DiscoveryError, OSError, ValueError) as exc: raise ValidationFailure(f"invalid ProjectReference {path}: {exc}") from exc return path def validate_project_graph( root, selection, config, tools, *, deadline, clock=time.monotonic, run=subprocess.run, ): root = root.resolve() pending = deque((path, None) for path in (*selection.projects, *config.dotnet.test_projects)) visited = set() while pending: project, framework = pending.popleft() project = _project_path(root, project) key = (project, framework) if key in visited: continue visited.add(key) argv = ( str(tools.dotnet), "msbuild", str(project), "-getProperty:TargetFramework,TargetFrameworks", "-getItem:ProjectReference", f"-p:Configuration={config.dotnet.configuration}", ) if framework is not None: argv += (f"-p:TargetFramework={framework}",) plan = ValidationPlan(selection.kind, (ValidationStep( CommandSpec(argv, root, f"metadata: {project.name}"), (ResultCheck(ResultCheckKind.PROJECT_IS_NET8),), ),)) result = execute_plan(plan, root, deadline=deadline, clock=clock, run=run)[0] try: data = json.loads(result.stdout) references = data["Items"]["ProjectReference"] if not isinstance(references, list): raise ValueError("expected an array") for reference in references: fields = {key.casefold(): value for key, value in reference.items()} if any(fields.get(name) for name in ( "additionalproperties", "globalpropertiestoremove", "setconfiguration", "setplatform", "settargetframework", )): raise ValueError("custom reference properties are unsupported; use the shared Configuration and project TFM") raw = fields.get("fullpath") or fields.get("identity") if not isinstance(raw, str) or not raw.strip(): raise ValueError("reference must have FullPath or Identity") child = _project_path(root, project.parent / Path(raw)) pending.append((child, None)) # Outer builds omit references conditioned on an individual TFM. if framework is None: targets = data["Properties"].get("TargetFrameworks", "") for target in targets.split(";"): if target.strip(): pending.append((project, target.strip())) except (ValueError, KeyError, TypeError, AttributeError) as exc: raise ValidationFailure( f"invalid evaluated ProjectReference metadata for {project}: {exc}" ) from exc