diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index c31f4db7b21..170cd10a030 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -14,6 +14,7 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; using Microsoft.Build.UnitTests; using Shouldly; using Xunit; @@ -3003,6 +3004,64 @@ public void InvalidProjectReferenceErrorIncludesMultipleReferringProjects() (mentionsProject1 || mentionsProject2).ShouldBeTrue(); } + [Fact] + public void Test1() + { + using var env = TestEnvironment.Create(); + + TransientTestFile project1 = env.CreateFile("project1.proj", """ + + + net472;net10.0 + TargetFramework + TargetFrameworks + + + """); + + TransientTestFile project2 = env.CreateFile("project2.proj", $""" + + + v4.7.2 + + + + TargetFramework=net472 + + + + """); + + var projectGraph = new ProjectGraph( + new ProjectGraphOptions + { + EntryPoints = [new ProjectGraphEntryPoint(project2.Path)], + Mode = ProjectGraphMode.Full + }); + + var sorted = projectGraph.ProjectNodesTopologicallySorted.ToList(); + + sorted[0].ProjectInstance.FullPath.ShouldBe(project1.Path); + sorted[0].ProjectInstance.GlobalProperties.Count.ShouldBe(2); // IsGraphBuild=true plus TargetFramework=net10.0 (inner build) + sorted[0].ProjectReferences.Count.ShouldBe(0); + sorted[0].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild); + + sorted[1].ProjectInstance.FullPath.ShouldBe(project1.Path); + sorted[1].ProjectInstance.GlobalProperties.Count.ShouldBe(2); // IsGraphBuild=true plus TargetFramework=net472 (inner build) + sorted[1].ProjectReferences.Count.ShouldBe(0); + sorted[1].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild); + + sorted[2].ProjectInstance.FullPath.ShouldBe(project1.Path); + sorted[2].ProjectInstance.GlobalProperties.Count.ShouldBe(1); // IsGraphBuild=true (outer build) + sorted[2].ProjectReferences.Count.ShouldBe(2); // Should have project references to the two inner builds + sorted[2].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.OuterBuild); + + sorted[3].ProjectInstance.FullPath.ShouldBe(project2.Path); + sorted[3].ProjectInstance.GlobalProperties.Count.ShouldBe(1); // IsGraphBuild=true + sorted[3].ProjectReferences.Count.ShouldBe(3); // Should have project references to the outer build of project1 and the two inner builds + sorted[3].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.NonMultitargeting); + } + public void Dispose() { _env.Dispose(); diff --git a/src/Build/Graph/GraphBuilder.cs b/src/Build/Graph/GraphBuilder.cs index fbdfb77a499..19cc92ccda4 100644 --- a/src/Build/Graph/GraphBuilder.cs +++ b/src/Build/Graph/GraphBuilder.cs @@ -50,6 +50,7 @@ internal class GraphBuilder private readonly ProjectInterpretation _projectInterpretation; private readonly ProjectGraph.ProjectInstanceFactoryFunc _projectInstanceFactory; + private readonly ProjectGraphMode _graphMode; private IReadOnlyDictionary> _solutionDependencies; private ConcurrentDictionary> _platformNegotiationInstancesCache = new(); @@ -67,6 +68,7 @@ public GraphBuilder( ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory, ProjectInterpretation projectInterpretation, int degreeOfParallelism, + ProjectGraphMode mode, CancellationToken cancellationToken) { var (actualEntryPoints, solutionDependencies) = ExpandSolutionIfPresent(entryPoints.ToImmutableArray()); @@ -85,6 +87,7 @@ public GraphBuilder( _projectCollection = projectCollection; _projectInstanceFactory = projectInstanceFactory; _projectInterpretation = projectInterpretation; + _graphMode = mode; } public void BuildGraph() @@ -617,7 +620,7 @@ private void SubmitProjectForParsing(ConfigurationMetadata projectToEvaluate) { var referenceInfos = new List(); - foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching)) + foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching, _graphMode)) { if (FileUtilities.IsSolutionFilename(referenceInfo.ReferenceConfiguration.ProjectFullPath)) { diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs index d717694d641..0a2a3094bcf 100644 --- a/src/Build/Graph/ProjectGraph.cs +++ b/src/Build/Graph/ProjectGraph.cs @@ -420,11 +420,34 @@ public ProjectGraph( ProjectInstanceFactoryFunc projectInstanceFactory, int degreeOfParallelism, CancellationToken cancellationToken) + : this(new ProjectGraphOptions + { + EntryPoints = entryPoints, + ProjectCollection = projectCollection, + ProjectInstanceFactoryFunc = projectInstanceFactory, + DegreeOfParallelism = degreeOfParallelism + }, + cancellationToken) + { + } + + /// + /// Constructs a graph starting from the given graph options. + /// + /// A containing the entry projects, project collection, and other details about the graph. + /// The to observe. + /// If the evaluation of any project in the graph fails. + /// If the evaluation is successful but the project graph contains a circular dependency. + public ProjectGraph( + ProjectGraphOptions options, + CancellationToken cancellationToken = default) { - ErrorUtilities.VerifyThrowArgumentNull(projectCollection); + ErrorUtilities.VerifyThrowArgumentNull(options.ProjectCollection); var measurementInfo = BeginMeasurement(); + ProjectInstanceFactoryFunc projectInstanceFactory = options.ProjectInstanceFactoryFunc; + if (projectInstanceFactory is null) { _evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared); @@ -432,11 +455,12 @@ public ProjectGraph( } var graphBuilder = new GraphBuilder( - entryPoints, - projectCollection, + options.EntryPoints, + options.ProjectCollection, projectInstanceFactory, ProjectInterpretation.Instance, - degreeOfParallelism, + options.DegreeOfParallelism, + options.Mode, cancellationToken); graphBuilder.BuildGraph(); @@ -456,7 +480,7 @@ public ProjectGraph( if (MSBuildEventSource.Log.IsEnabled()) { - etwArgs = string.Join(";", entryPoints.Select( + etwArgs = string.Join(";", options.EntryPoints.Select( e => { var globalPropertyString = e.GlobalProperties == null diff --git a/src/Build/Graph/ProjectGraphOptions.cs b/src/Build/Graph/ProjectGraphOptions.cs new file mode 100644 index 00000000000..61f29859520 --- /dev/null +++ b/src/Build/Graph/ProjectGraphOptions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Build.Evaluation; + +namespace Microsoft.Build.Graph +{ + /// + /// Represents the mode to use when constructing a . + /// + public enum ProjectGraphMode + { + /// + /// Loads only the projects needed for a build, as specified by the entry points. This is the default mode. + /// + Default = 0, + + /// + /// Loads a complete representation of the graph, even if they are not needed for the build. + /// + Full = 1, + } + + /// + /// Represents options to use when constructing a . + /// + public readonly struct ProjectGraphOptions() + { + /// + /// The degree of parallelism to use when constructing the graph. Defaults to the number of logical cores on the machine. + /// + public int DegreeOfParallelism { get; init; } = NativeMethodsShared.GetLogicalCoreCount(); + + /// + /// A list of objects representing the entry points to use when constructing the graph. + /// + public required IEnumerable EntryPoints { get; init; } + + /// + /// The to use when constructing the graph. Defaults to . + /// + public ProjectGraphMode Mode { get; init; } = ProjectGraphMode.Default; + + /// + /// The to load projects into when constructing the graph. Defaults to . + /// + public ProjectCollection ProjectCollection { get; init; } = ProjectCollection.GlobalProjectCollection; + + /// + /// An optional to use when evaluating individual projects in the graph. + /// + public ProjectGraph.ProjectInstanceFactoryFunc? ProjectInstanceFactoryFunc { get; init; } + } +} diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 12ae373485d..89acd824f35 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -74,7 +74,7 @@ public TargetSpecification(string target, bool skipIfNonexistent) public bool SkipIfNonexistent { get; } } - public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory) + public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory, ProjectGraphMode graphMode) { IEnumerable projectReferenceItems; IEnumerable globalPropertiesModifiers = null; @@ -198,6 +198,56 @@ public IEnumerable GetReferences(ProjectGraphNode projectGraphNod var referenceConfig = new ConfigurationMetadata(projectReferenceFullPath, referenceGlobalProperties); + // When in Full graph mode, enumerate all target frameworks regardless of SetTargetFramework + if (graphMode == ProjectGraphMode.Full && projectReferenceItem.HasMetadata(SetTargetFrameworkMetadataName)) + { + // Evaluate the referenced project with no global properties to discover all its target frameworks + var projectInstanceForDiscovery = projectInstanceFactory( + projectReferenceFullPath, + null, + projectCollection); + + string targetFrameworksValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFrameworks); + + // If the project has multiple target frameworks, yield the outer build only + // The normal ConstructInnerBuildReferences logic will discover and create all inner builds + if (!string.IsNullOrWhiteSpace(targetFrameworksValue)) + { + // Create base properties without SetTargetFramework constraint + // GetGlobalPropertiesForItem processes SetTargetFramework and adds TargetFramework to properties, + // so we need to create properties without that processing + var baseProperties = GetGlobalPropertiesForItem( + projectReferenceItem, + requesterInstance.GlobalPropertiesDictionary, + allowCollectionReuse: false, + // Use empty list to skip ProjectReferenceGlobalPropertiesModifier which processes SetTargetFramework + []); + + // Yield only the outer build (no TargetFramework override) + // The ConstructInnerBuildReferences logic will discover and create the inner builds + yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, baseProperties), projectReferenceItem); + + yield break; + } + else + { + string targetFrameworkValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFramework); + + // Single target framework project - just yield it as is without SetTargetFramework constraint + if (!string.IsNullOrWhiteSpace(targetFrameworkValue)) + { + var baseProperties = GetGlobalPropertiesForItem( + projectReferenceItem, + requesterInstance.GlobalPropertiesDictionary, + allowCollectionReuse: false, + // Use empty list to skip SetTargetFramework processing + []); + yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, baseProperties), projectReferenceItem); + yield break; + } + } + } + yield return new ReferenceInfo(referenceConfig, projectReferenceItem); static void SetProperty(PropertyDictionary properties, string propertyName, string propertyValue) diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 1162535689a..22876de62da 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -211,6 +211,7 @@ +