From 65ef8ae48cca49d50fad82a40de0b0a1621b0b04 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Fri, 20 Jan 2023 11:47:26 -0500 Subject: [PATCH 1/9] Support `SkipNonexistentTargets` in project reference target protocol --- .../Graph/ProjectGraph_Tests.cs | 108 ++++++++++++++++++ .../Graph/ResultCacheBasedBuilds_Tests.cs | 25 ++-- .../BackEnd/BuildManager/BuildManager.cs | 20 ++-- .../BuildRequestEngine/BuildRequestEngine.cs | 1 + .../BackEnd/Components/Scheduler/Scheduler.cs | 55 +++++++-- .../Shared/BuildRequestConfiguration.cs | 35 +++++- src/Build/BackEnd/Shared/BuildResult.cs | 16 +++ src/Build/Graph/ProjectInterpretation.cs | 59 +++++++--- src/Shared/UnitTests/ObjectModelHelpers.cs | 4 +- 9 files changed, 268 insertions(+), 55 deletions(-) diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index 273d6d4a7eb..bafc8369c4c 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -1277,6 +1277,114 @@ public void GetTargetsListProjectReferenceTargetsOrDefaultComplexPropagation() } } + [Fact] + public void GetTargetsListSupportsTargetsMarkedSkipNonexistentTargets() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentPerProjectNumber: new Dictionary + { + { + 1, + @" + + + + + + + + + + " + }, + { + 2, + @" + + + + + " + }, + }); + IReadOnlyDictionary> targetLists = graph.GetTargetLists(entryProjectTargets: new[] { "Build" }); + targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 1)].ShouldBe(expected: new[] { "Build" }); + targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 2)].ShouldBe(expected: new[] { "NonskippableTarget1", "NonskippableTarget2", "SkippableExistingTarget" }); + Dictionary results = ResultCacheBasedBuilds_Tests.BuildUsingCaches( + _env, + topoSortedNodes: graph.ProjectNodesTopologicallySorted, + generateCacheFiles: true, + expectedNodeBuildOutput: new Dictionary(), + outputCaches: new Dictionary(), + assertBuildResults: false, + targetListsPerNode: targetLists); + foreach (KeyValuePair result in results) + { + result.Value.Result.OverallResult.ShouldBe(BuildResultCode.Success); + } + } + + [Fact] + public void SkipNonexistentTargetsDoesNotHideMissedTargetResults() + { + ProjectGraph graph = Helpers.CreateProjectGraph( + env: _env, + dependencyEdges: new Dictionary + { + { 1, new[] { 2 } }, + }, + extraContentPerProjectNumber: new Dictionary + { + { + 1, + @" + + + + + + + + " + }, + { + 2, + @" + + + + + " + }, + }); + IReadOnlyDictionary> targetLists = graph.GetTargetLists(entryProjectTargets: new[] { "Build" }); + targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 1)].ShouldBe(expected: new[] { "Build" }); + targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 2)].ShouldBe(expected: new[] { "NonskippableTarget1", "SkippableExistingTarget" }); + Dictionary results = ResultCacheBasedBuilds_Tests.BuildUsingCaches( + env: _env, + topoSortedNodes: graph.ProjectNodesTopologicallySorted, + generateCacheFiles: true, + expectedNodeBuildOutput: new Dictionary(), + outputCaches: new Dictionary(), + assertBuildResults: false, + targetListsPerNode: targetLists); + results["2"].Result.OverallResult.ShouldBe(BuildResultCode.Success); + BuildResult project1BuildResult = results["1"].Result; + project1BuildResult.OverallResult.ShouldBe(BuildResultCode.Failure); + MockLogger project1MockLogger = results["1"].Logger; + project1MockLogger.ErrorCount.ShouldBe(1); + string project1ErrorMessage = project1MockLogger.Errors.First().Message; + project1ErrorMessage.ShouldContain("MSB4252"); + project1ErrorMessage.ShouldContain("1.proj"); + project1ErrorMessage.ShouldContain("2.proj"); + project1ErrorMessage.ShouldContain(" with the (NonskippableTarget1;NonskippableTarget2;SkippableExistingTarget;SkippableNonexistentTarget) target(s) but the build result for the built project is not in the engine cache"); + } + [Fact] public void ReferencedMultitargetingEntryPointNodeTargetListContainsDefaultTarget() { diff --git a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs index cb40be2fbad..bc6cb37b753 100644 --- a/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs +++ b/src/Build.UnitTests/Graph/ResultCacheBasedBuilds_Tests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text; @@ -298,7 +299,7 @@ public void BuildProjectGraphUsingCaches(Dictionary edges) var outputCaches = new OutputCacheDictionary(); // Build unchanged project files using caches. - BuildUsingCaches(topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); + BuildUsingCaches(_env, topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); // Change the project files to remove all items. var collection = _env.CreateProjectCollection().Collection; @@ -318,6 +319,7 @@ public void BuildProjectGraphUsingCaches(Dictionary edges) // Build again using the first caches. Project file changes from references should not be visible. BuildUsingCaches( + _env, topoSortedNodes, expectedOutput, outputCaches, @@ -343,7 +345,7 @@ public void OutputCacheShouldNotContainInformationFromInputCaches() var outputCaches = new OutputCacheDictionary(); - BuildUsingCaches(topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); + BuildUsingCaches(_env, topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); var rootNode = topoSortedNodes.First(n => Path.GetFileNameWithoutExtension(n.ProjectInstance.FullPath) == "1"); var outputCache = outputCaches[rootNode]; @@ -381,12 +383,12 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( var outputCaches = new OutputCacheDictionary(); - BuildUsingCaches(topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); + BuildUsingCaches(_env, topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: true); // remove cache for project 3 to cause a cache miss outputCaches.Remove(expectedOutput.Keys.First(n => ProjectNumber(n) == "3")); - var results = BuildUsingCaches(topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: false, assertBuildResults: false); + var results = BuildUsingCaches(_env, topoSortedNodes, expectedOutput, outputCaches, generateCacheFiles: false, assertBuildResults: false); results["3"].Result.OverallResult.ShouldBe(BuildResultCode.Success); results["2"].Result.OverallResult.ShouldBe(BuildResultCode.Success); @@ -408,21 +410,25 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( /// When it is false, it uses the filled in and to simulate a fully cached build. /// /// + /// The test environment. /// /// /// /// /// /// + /// The list of targets to build per node. /// - private Dictionary BuildUsingCaches( + internal static Dictionary BuildUsingCaches( + TestEnvironment env, IReadOnlyCollection topoSortedNodes, ExpectedNodeBuildOutput expectedNodeBuildOutput, OutputCacheDictionary outputCaches, bool generateCacheFiles, bool assertBuildResults = true, // (current node, expected output dictionary) -> actual expected output for current node - Func expectedOutputProducer = null) + Func expectedOutputProducer = null, + IReadOnlyDictionary> targetListsPerNode = null) { expectedOutputProducer ??= ((node, expectedOutputs) => expectedOutputs[node]); @@ -450,18 +456,19 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( if (generateCacheFiles) { - outputCaches[node] = _env.DefaultTestDirectory.CreateFile($"OutputCache-{ProjectNumber(node)}").Path; + outputCaches[node] = env.DefaultTestDirectory.CreateFile($"OutputCache-{ProjectNumber(node)}").Path; buildParameters.OutputResultsCacheFile = outputCaches[node]; } - var logger = new MockLogger(); + var logger = new MockLogger(env.Output); buildParameters.Loggers = new[] {logger}; var result = BuildProjectFileUsingBuildManager( node.ProjectInstance.FullPath, null, - buildParameters); + buildParameters, + targetListsPerNode?[node].ToArray()); results[ProjectNumber(node)] = (result, logger); diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 12a3d83376e..383c5f4529b 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1389,7 +1389,7 @@ private void LoadSolutionIntoConfiguration(BuildRequestConfiguration config, Bui ((IBuildComponentHost) this).LoggingService, request.BuildEventContext, false /* loaded by solution parser*/, - config.TargetNames, + config.RequestedTargets, SdkResolverService, request.SubmissionId); @@ -2328,23 +2328,17 @@ private void HandleConfigurationRequest(int node, BuildRequestConfiguration unre /// private void HandleResult(int node, BuildResult result) { - // Update cache with the default and initial targets, as needed. + // Update cache with the default, initial, and project targets, as needed. BuildRequestConfiguration configuration = _configCache[result.ConfigurationId]; if (result.DefaultTargets != null) { - // If the result has Default and Initial targets, we populate the configuration cache with them if it + // If the result has Default, Initial, and project targets, we populate the configuration cache with them if it // doesn't already have entries. This can happen if we created a configuration based on a request from // an external node, but hadn't yet received a result since we may not have loaded the Project locally - // and thus wouldn't know what the default and initial targets were. - if (configuration.ProjectDefaultTargets == null) - { - configuration.ProjectDefaultTargets = result.DefaultTargets; - } - - if (configuration.ProjectInitialTargets == null) - { - configuration.ProjectInitialTargets = result.InitialTargets; - } + // and thus wouldn't know what the default, initial, and project targets were. + configuration.ProjectDefaultTargets ??= result.DefaultTargets; + configuration.ProjectInitialTargets ??= result.InitialTargets; + configuration.ProjectTargets ??= result.ProjectTargets; } IEnumerable response = _scheduler.ReportResult(node, result); diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 6618dc945c8..17bbe97ee28 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -809,6 +809,7 @@ private void EvaluateRequestStates() // own cache. completedEntry.Result.DefaultTargets = configuration.ProjectDefaultTargets; completedEntry.Result.InitialTargets = configuration.ProjectInitialTargets; + completedEntry.Result.ProjectTargets = configuration.ProjectTargets; } TraceEngine("ERS: Request is now {0}({1}) (nr {2}) has had its builder cleaned up.", completedEntry.Request.GlobalRequestId, completedEntry.Request.ConfigurationId, completedEntry.Request.NodeRequestId); diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index de7bd580e4a..256e10b716f 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -1964,10 +1964,11 @@ private bool CheckIfCacheMissOnReferencedProjectIsAllowedAndErrorIfNot(int nodeF emitNonErrorLogs = _ => { }; var isIsolatedBuild = _componentHost.BuildParameters.IsolateProjects; - var configCache = (IConfigCache) _componentHost.GetComponent(BuildComponentType.ConfigCache); + var configCache = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); // do not check root requests as nothing depends on them - if (!isIsolatedBuild || request.IsRootRequest || request.SkipStaticGraphIsolationConstraints) + if (!isIsolatedBuild || request.IsRootRequest || request.SkipStaticGraphIsolationConstraints + || SkipNonexistentTargetsIfExistentTargetsHaveResults(request)) { if (isIsolatedBuild && request.SkipStaticGraphIsolationConstraints) { @@ -1978,14 +1979,14 @@ private bool CheckIfCacheMissOnReferencedProjectIsAllowedAndErrorIfNot(int nodeF NewBuildEventContext(), MessageImportance.Normal, "SkippedConstraintsOnRequest", - configs.parentConfig.ProjectFullPath, - configs.requestConfig.ProjectFullPath); + configs.ParentConfig.ProjectFullPath, + configs.RequestConfig.ProjectFullPath); } return true; } - var (requestConfig, parentConfig) = GetConfigurations(); + (BuildRequestConfiguration requestConfig, BuildRequestConfiguration parentConfig) = GetConfigurations(); // allow self references (project calling the msbuild task on itself, potentially with different global properties) if (parentConfig.ProjectFullPath.Equals(requestConfig.ProjectFullPath, StringComparison.OrdinalIgnoreCase)) @@ -2024,20 +2025,20 @@ BuildEventContext NewBuildEventContext() BuildEventContext.InvalidTaskId); } - (BuildRequestConfiguration requestConfig, BuildRequestConfiguration parentConfig) GetConfigurations() + (BuildRequestConfiguration RequestConfig, BuildRequestConfiguration ParentConfig) GetConfigurations() { - var buildRequestConfiguration = configCache[request.ConfigurationId]; + BuildRequestConfiguration buildRequestConfiguration = configCache[request.ConfigurationId]; // Need the parent request. It might be blocked or executing; check both. - var parentRequest = _schedulingData.BlockedRequests.FirstOrDefault(r => r.BuildRequest.GlobalRequestId == request.ParentGlobalRequestId) - ?? _schedulingData.ExecutingRequests.FirstOrDefault(r => r.BuildRequest.GlobalRequestId == request.ParentGlobalRequestId); + SchedulableRequest parentRequest = _schedulingData.BlockedRequests.FirstOrDefault(r => r.BuildRequest.GlobalRequestId == request.ParentGlobalRequestId) + ?? _schedulingData.ExecutingRequests.FirstOrDefault(r => r.BuildRequest.GlobalRequestId == request.ParentGlobalRequestId); ErrorUtilities.VerifyThrowInternalNull(parentRequest, nameof(parentRequest)); ErrorUtilities.VerifyThrow( configCache.HasConfiguration(parentRequest.BuildRequest.ConfigurationId), "All non root requests should have a parent with a loaded configuration"); - var parentConfiguration = configCache[parentRequest.BuildRequest.ConfigurationId]; + BuildRequestConfiguration parentConfiguration = configCache[parentRequest.BuildRequest.ConfigurationId]; return (buildRequestConfiguration, parentConfiguration); } @@ -2045,6 +2046,40 @@ string ConcatenateGlobalProperties(BuildRequestConfiguration configuration) { return string.Join("; ", configuration.GlobalProperties.Select(p => $"{p.Name}={p.EvaluatedValue}")); } + + bool SkipNonexistentTargetsIfExistentTargetsHaveResults(BuildRequest buildRequest) + { + // Return early if the top-level target(s) of this build request weren't requested to be skipped if nonexistent. + if ((buildRequest.BuildRequestDataFlags & BuildRequestDataFlags.SkipNonexistentTargets) != BuildRequestDataFlags.SkipNonexistentTargets) + { + return false; + } + + BuildResult requestResults = _resultsCache.GetResultsForConfiguration(buildRequest.ConfigurationId); + + // On a self-referenced build, cache misses are allowed. + if (requestResults == null) + { + return false; + } + + // A cache miss on at least one existing target without results is disallowed, + // as it violates isolation constraints. + foreach (string target in request.Targets) + { + if (_configCache[buildRequest.ConfigurationId] + .ProjectTargets + .Contains(target) && + !requestResults.HasResultsForTarget(target)) + { + return false; + } + } + + // A cache miss on nonexistent targets on the reference is allowed, given the request + // to skip nonexistent targets. + return true; + } } /// diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index b0b4f1204e3..0f47f09b8a7 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -107,6 +107,11 @@ internal class BuildRequestConfiguration : IEquatable /// private List _projectDefaultTargets; + /// + /// The defined targets for the project. + /// + private HashSet _projectTargets; + /// /// This is the lookup representing the current project items and properties 'state'. /// @@ -138,7 +143,7 @@ internal class BuildRequestConfiguration : IEquatable /// /// The target names that were requested to execute. /// - internal IReadOnlyCollection TargetNames { get; } + internal IReadOnlyCollection RequestedTargets { get; } /// /// Initializes a configuration from a BuildRequestData structure. Used by the BuildManager. @@ -170,7 +175,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d _explicitToolsVersionSpecified = data.ExplicitToolsVersionSpecified; _toolsVersion = ResolveToolsVersion(data, defaultToolsVersion); _globalProperties = data.GlobalPropertiesDictionary; - TargetNames = new List(data.TargetNames); + RequestedTargets = new List(data.TargetNames); // The following information only exists when the request is populated with an existing project. if (data.ProjectInstance != null) @@ -178,7 +183,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d _project = data.ProjectInstance; _projectInitialTargets = data.ProjectInstance.InitialTargets; _projectDefaultTargets = data.ProjectInstance.DefaultTargets; - + _projectTargets = data.ProjectInstance.Targets.Keys.ToHashSet(); if (data.PropertiesToTransfer != null) { _transferredProperties = new List(); @@ -215,6 +220,7 @@ internal BuildRequestConfiguration(int configId, ProjectInstance instance) _project = instance; _projectInitialTargets = instance.InitialTargets; _projectDefaultTargets = instance.DefaultTargets; + _projectTargets = instance.Targets.Keys.ToHashSet(); IsCacheable = false; } @@ -231,13 +237,14 @@ private BuildRequestConfiguration(int configId, BuildRequestConfiguration other) _transferredProperties = other._transferredProperties; _projectDefaultTargets = other._projectDefaultTargets; _projectInitialTargets = other._projectInitialTargets; + _projectTargets = other._projectTargets; _projectFullPath = other._projectFullPath; _toolsVersion = other._toolsVersion; _explicitToolsVersionSpecified = other._explicitToolsVersionSpecified; _globalProperties = other._globalProperties; IsCacheable = other.IsCacheable; _configId = configId; - TargetNames = other.TargetNames; + RequestedTargets = other.RequestedTargets; } /// @@ -404,9 +411,11 @@ private void SetProjectBasedState(ProjectInstance project) // Clear these out so the other accessors don't complain. We don't want to generally enable resetting these fields. _projectDefaultTargets = null; _projectInitialTargets = null; + _projectTargets = null; ProjectDefaultTargets = _project.DefaultTargets; ProjectInitialTargets = _project.InitialTargets; + ProjectTargets = _project.Targets.Keys.ToHashSet(); if (IsCached) { @@ -549,6 +558,23 @@ public List ProjectDefaultTargets } } + /// + /// Gets or sets the targets defined for the project. + /// + public HashSet ProjectTargets + { + [DebuggerStepThrough] + get => _projectTargets; + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow( + _projectTargets == null, + "Targets cannot be reset once set."); + _projectTargets = value; + } + } + /// /// Returns the node packet type /// @@ -880,6 +906,7 @@ internal void TranslateForFutureUse(ITranslator translator) translator.Translate(ref _explicitToolsVersionSpecified); translator.Translate(ref _projectDefaultTargets); translator.Translate(ref _projectInitialTargets); + translator.Translate(ref _projectTargets); translator.TranslateDictionary(ref _globalProperties, ProjectPropertyInstance.FactoryForDeserialization); } diff --git a/src/Build/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs index 97b64d603a3..92d1a544055 100644 --- a/src/Build/BackEnd/Shared/BuildResult.cs +++ b/src/Build/BackEnd/Shared/BuildResult.cs @@ -117,6 +117,8 @@ public class BuildResult : INodePacket, IBuildResults private string _schedulerInducedError; + private HashSet _projectTargets; + /// /// Constructor for serialization. /// @@ -228,6 +230,7 @@ internal BuildResult(BuildResult result, int nodeRequestId) _circularDependency = result._circularDependency; _initialTargets = result._initialTargets; _defaultTargets = result._defaultTargets; + _projectTargets = result._projectTargets; _baseOverallResult = result.OverallResult == BuildResultCode.Success; } @@ -244,6 +247,7 @@ internal BuildResult(BuildResult result, int submissionId, int configurationId, _circularDependency = result._circularDependency; _initialTargets = result._initialTargets; _defaultTargets = result._defaultTargets; + _projectTargets = result._projectTargets; _baseOverallResult = result.OverallResult == BuildResultCode.Success; } @@ -433,6 +437,17 @@ internal List DefaultTargets { _defaultTargets = value; } } + /// + /// The defined targets for the project associated with this build result. + /// + public HashSet ProjectTargets + { + [DebuggerStepThrough] + get => _projectTargets; + [DebuggerStepThrough] + set => _projectTargets = value; + } + /// /// Container used to transport errors from the scheduler (issued while computing a build result) /// to the TaskHost that has the proper logging context (project id, target id, task id, file location) @@ -537,6 +552,7 @@ void ITranslatable.Translate(ITranslator translator) translator.Translate(ref _nodeRequestId); translator.Translate(ref _initialTargets); translator.Translate(ref _defaultTargets); + translator.Translate(ref _projectTargets); translator.Translate(ref _circularDependency); translator.TranslateException(ref _requestException); translator.TranslateDictionary(ref _resultsByTarget, TargetResult.FactoryForDeserialization, CreateTargetResultDictionary); diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index a293483551b..08dcafe615a 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -63,6 +63,23 @@ public ReferenceInfo(ConfigurationMetadata referenceConfiguration, ProjectItemIn } } + private readonly struct TargetSpecification + { + public TargetSpecification(string target, bool skipIfNonexistent) + { + ErrorUtilities.VerifyThrow( + !skipIfNonexistent || target.Equals(MSBuildConstants.DefaultTargetsMarker) + || target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker), + target + " cannot be marked as SkipNonexistentTargets"); + Target = target; + SkipIfNonexistent = skipIfNonexistent; + } + + public string Target { get; } + + public bool SkipIfNonexistent { get; } + } + public IEnumerable GetReferences(ProjectInstance requesterInstance, ProjectCollection _projectCollection, ProjectGraph.ProjectInstanceFactoryFunc _projectInstanceFactory) { IEnumerable projectReferenceItems; @@ -392,10 +409,10 @@ private static void RemoveFromPropertyDictionary( public readonly struct TargetsToPropagate { - private readonly ImmutableList _outerBuildTargets; - private readonly ImmutableList _allTargets; + private readonly ImmutableList _outerBuildTargets; + private readonly ImmutableList _allTargets; - private TargetsToPropagate(ImmutableList outerBuildTargets, ImmutableList nonOuterBuildTargets) + private TargetsToPropagate(ImmutableList outerBuildTargets, ImmutableList nonOuterBuildTargets) { _outerBuildTargets = outerBuildTargets; @@ -417,23 +434,22 @@ private TargetsToPropagate(ImmutableList outerBuildTargets, ImmutableLis /// public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance project, ImmutableList entryTargets) { - var targetsForOuterBuild = ImmutableList.CreateBuilder(); - var targetsForInnerBuild = ImmutableList.CreateBuilder(); + ImmutableList.Builder targetsForOuterBuild = ImmutableList.CreateBuilder(); + ImmutableList.Builder targetsForInnerBuild = ImmutableList.CreateBuilder(); - var projectReferenceTargets = project.GetItems(ItemTypeNames.ProjectReferenceTargets); + ICollection projectReferenceTargets = project.GetItems(ItemTypeNames.ProjectReferenceTargets); - foreach (var entryTarget in entryTargets) + foreach (string entryTarget in entryTargets) { - foreach (var projectReferenceTarget in projectReferenceTargets) + foreach (ProjectItemInstance projectReferenceTarget in projectReferenceTargets) { if (projectReferenceTarget.EvaluatedInclude.Equals(entryTarget, StringComparison.OrdinalIgnoreCase)) { - var targetsMetadataValue = projectReferenceTarget.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName); - - var targetsAreForOuterBuild = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue(ProjectReferenceTargetIsOuterBuildMetadataName)); - - var targets = ExpressionShredder.SplitSemiColonSeparatedList(targetsMetadataValue).ToArray(); - + string targetsMetadataValue = projectReferenceTarget.GetMetadataValue(ItemMetadataNames.ProjectReferenceTargetsMetadataName); + bool skipNonexistentTargets = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue("SkipNonexistentTargets")); + bool targetsAreForOuterBuild = MSBuildStringIsTrue(projectReferenceTarget.GetMetadataValue(ProjectReferenceTargetIsOuterBuildMetadataName)); + TargetSpecification[] targets = ExpressionShredder.SplitSemiColonSeparatedList(targetsMetadataValue) + .Select(t => new TargetSpecification(t, skipNonexistentTargets)).ToArray(); if (targetsAreForOuterBuild) { targetsForOuterBuild.AddRange(targets); @@ -451,11 +467,20 @@ public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance proj public ImmutableList GetApplicableTargetsForReference(ProjectInstance reference) { + ImmutableList RemoveNonexistentTargetsIfSkippable(ImmutableList targets) + { + // Keep targets that are non-skippable or that exist but are skippable. + return targets + .Where(t => !t.SkipIfNonexistent || reference.Targets.ContainsKey(t.Target)) + .Select(t => t.Target) + .ToImmutableList(); + } + return (GetProjectType(reference)) switch { - ProjectType.InnerBuild => _allTargets, - ProjectType.OuterBuild => _outerBuildTargets, - ProjectType.NonMultitargeting => _allTargets, + ProjectType.InnerBuild => RemoveNonexistentTargetsIfSkippable(_allTargets), + ProjectType.OuterBuild => RemoveNonexistentTargetsIfSkippable(_outerBuildTargets), + ProjectType.NonMultitargeting => RemoveNonexistentTargetsIfSkippable(_allTargets), _ => throw new ArgumentOutOfRangeException(), }; } diff --git a/src/Shared/UnitTests/ObjectModelHelpers.cs b/src/Shared/UnitTests/ObjectModelHelpers.cs index a8f02e7d2cc..7b92c6a2a83 100644 --- a/src/Shared/UnitTests/ObjectModelHelpers.cs +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -1377,7 +1377,7 @@ public static BuildResult BuildProjectContentUsingBuildManager(string content, M } } - public static BuildResult BuildProjectFileUsingBuildManager(string projectFile, MockLogger logger = null, BuildParameters parameters = null) + public static BuildResult BuildProjectFileUsingBuildManager(string projectFile, MockLogger logger = null, BuildParameters parameters = null, string[] targetsToBuild = null) { using (var buildManager = new BuildManager()) { @@ -1394,7 +1394,7 @@ public static BuildResult BuildProjectFileUsingBuildManager(string projectFile, projectFile, new Dictionary(), MSBuildConstants.CurrentToolsVersion, - Array.Empty(), + targetsToBuild ?? Array.Empty(), null); var result = buildManager.Build( From 773f369184dadcadf2202c1e63e40aca4fd72c9a Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Mon, 23 Jan 2023 10:31:53 -0500 Subject: [PATCH 2/9] Fix error-checking --- src/Build/Graph/ProjectInterpretation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 08dcafe615a..ce60703d15e 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -68,8 +68,8 @@ private readonly struct TargetSpecification public TargetSpecification(string target, bool skipIfNonexistent) { ErrorUtilities.VerifyThrow( - !skipIfNonexistent || target.Equals(MSBuildConstants.DefaultTargetsMarker) - || target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker), + !skipIfNonexistent || (!target.Equals(MSBuildConstants.DefaultTargetsMarker) + && !target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker)), target + " cannot be marked as SkipNonexistentTargets"); Target = target; SkipIfNonexistent = skipIfNonexistent; From ac665852852cbc680e2e868f379e05d101b0dab2 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Wed, 25 Jan 2023 10:13:03 -0500 Subject: [PATCH 3/9] Add `SkipNonExistentTargets` to `GetTargetFrameworks` in project reference protocol --- .../Graph/ProjectGraph_Tests.cs | 33 +++++++++++++++++++ src/Build/Graph/ProjectInterpretation.cs | 30 +++++++++++++---- src/Tasks/Microsoft.Managed.After.targets | 7 ++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index bafc8369c4c..813cea3dcf1 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -1040,6 +1040,39 @@ public void GetTargetListsDoesNotUseTargetsMetadataOnInnerBuildsFromRootOuterBui } } + [Fact] + public void GetTargetFrameworksWithPlatformForSingleTargetFrameworkRemovedIfGetTargetFrameworksRemoved() + { + string entryProject = CreateProjectFile( + _env, + 1, + new int[] { 2 }, + extraContent: @" + + + + + ").Path; + CreateProjectFile( + _env, + 2, + extraContent: MultitargetingSpecificationPropertyGroup); + var graph = new ProjectGraph(entryProject); + graph.ToDot(); + ProjectGraphNode rootOuterBuild = GetFirstNodeWithProjectNumber(graph, 1); + IReadOnlyDictionary> targetLists = graph.GetTargetLists(new[] { "A" }); + targetLists[rootOuterBuild].ShouldBe(new[] { "A" }); + ProjectGraphNode referencedNode = GetOuterBuild(graph, 2); + targetLists[referencedNode].ShouldBe(new[] { "B" }); + + // None of the inner builds should have GetTargetFrameworksWithPlatformForSingleTargetFramework + // in their target lists. + foreach (ProjectGraphNode projectGraphNode in GetInnerBuilds(graph, 2)) + { + targetLists[projectGraphNode].ShouldBe(new[] { "B", "C" }); + } + } + [Fact] public void GetTargetListsForComplexMultitargetingGraph() { diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index ce60703d15e..56ca3f53f1a 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -469,14 +469,32 @@ public ImmutableList GetApplicableTargetsForReference(ProjectInstance re { ImmutableList RemoveNonexistentTargetsIfSkippable(ImmutableList targets) { - // Keep targets that are non-skippable or that exist but are skippable. - return targets - .Where(t => !t.SkipIfNonexistent || reference.Targets.ContainsKey(t.Target)) - .Select(t => t.Target) - .ToImmutableList(); + var targetsToKeep = new List(); + bool getTargetFrameworksRemoved = false; + foreach (TargetSpecification target in targets) + { + // Keep targets that are non-skippable or that exist but are skippable. + if (!target.SkipIfNonexistent || reference.Targets.ContainsKey(target.Target)) + { + targetsToKeep.Add(target.Target); + } + else if (target.Target.Equals("GetTargetFrameworks")) + { + getTargetFrameworksRemoved = true; + } + } + + // If GetTargetFrameworks is removed, also remove GetTargetFrameworksWithPlatformForSingleTargetFramework + // since in the non-graph case it is only called when GetTargetFrameworks is called. + if (getTargetFrameworksRemoved && targetsToKeep.Contains("GetTargetFrameworksWithPlatformForSingleTargetFramework")) + { + targetsToKeep.Remove("GetTargetFrameworksWithPlatformForSingleTargetFramework"); + } + + return targetsToKeep.ToImmutableList(); } - return (GetProjectType(reference)) switch + return GetProjectType(reference) switch { ProjectType.InnerBuild => RemoveNonexistentTargetsIfSkippable(_allTargets), ProjectType.OuterBuild => RemoveNonexistentTargetsIfSkippable(_outerBuildTargets), diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index 48264892122..7cfc296e530 100644 --- a/src/Tasks/Microsoft.Managed.After.targets +++ b/src/Tasks/Microsoft.Managed.After.targets @@ -41,10 +41,10 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath - GetTargetFrameworks;$(ProjectReferenceTargetsForBuildInOuterBuild) + $(ProjectReferenceTargetsForBuildInOuterBuild) $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForBuild) - GetTargetFrameworks;$(ProjectReferenceTargetsForCleanInOuterBuild) + $(ProjectReferenceTargetsForCleanInOuterBuild) Clean;GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForClean) $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) @@ -59,9 +59,10 @@ Copyright (C) Microsoft Corporation. All rights reserved. + - + From 7c53b6949d535f068fe7ebd0580480a0344e52c2 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Wed, 25 Jan 2023 17:04:26 -0500 Subject: [PATCH 4/9] Make `BuildResult.ProjectTargets` `internal` --- src/Build/BackEnd/Shared/BuildResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs index 92d1a544055..34b19abe946 100644 --- a/src/Build/BackEnd/Shared/BuildResult.cs +++ b/src/Build/BackEnd/Shared/BuildResult.cs @@ -440,7 +440,7 @@ internal List DefaultTargets /// /// The defined targets for the project associated with this build result. /// - public HashSet ProjectTargets + internal HashSet ProjectTargets { [DebuggerStepThrough] get => _projectTargets; From 11cb7102fb90a28a5a70bb054e1e436ff50fe774 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Wed, 25 Jan 2023 18:48:12 -0500 Subject: [PATCH 5/9] Make `BuildRequestConfiguration.ProjectTargets` `internal` & add `SkipNonexistentTargets='true'` to `GetTargetFrameworksWithPlatformForSingleTargetFramework` --- .../Graph/ProjectGraph_Tests.cs | 33 ------------------- .../Shared/BuildRequestConfiguration.cs | 2 +- src/Build/Graph/ProjectInterpretation.cs | 28 +++------------- src/Tasks/Microsoft.Managed.After.targets | 22 +++++++++---- 4 files changed, 21 insertions(+), 64 deletions(-) diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index 813cea3dcf1..bafc8369c4c 100644 --- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs +++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs @@ -1040,39 +1040,6 @@ public void GetTargetListsDoesNotUseTargetsMetadataOnInnerBuildsFromRootOuterBui } } - [Fact] - public void GetTargetFrameworksWithPlatformForSingleTargetFrameworkRemovedIfGetTargetFrameworksRemoved() - { - string entryProject = CreateProjectFile( - _env, - 1, - new int[] { 2 }, - extraContent: @" - - - - - ").Path; - CreateProjectFile( - _env, - 2, - extraContent: MultitargetingSpecificationPropertyGroup); - var graph = new ProjectGraph(entryProject); - graph.ToDot(); - ProjectGraphNode rootOuterBuild = GetFirstNodeWithProjectNumber(graph, 1); - IReadOnlyDictionary> targetLists = graph.GetTargetLists(new[] { "A" }); - targetLists[rootOuterBuild].ShouldBe(new[] { "A" }); - ProjectGraphNode referencedNode = GetOuterBuild(graph, 2); - targetLists[referencedNode].ShouldBe(new[] { "B" }); - - // None of the inner builds should have GetTargetFrameworksWithPlatformForSingleTargetFramework - // in their target lists. - foreach (ProjectGraphNode projectGraphNode in GetInnerBuilds(graph, 2)) - { - targetLists[projectGraphNode].ShouldBe(new[] { "B", "C" }); - } - } - [Fact] public void GetTargetListsForComplexMultitargetingGraph() { diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index 0f47f09b8a7..91754ec5bae 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -561,7 +561,7 @@ public List ProjectDefaultTargets /// /// Gets or sets the targets defined for the project. /// - public HashSet ProjectTargets + internal HashSet ProjectTargets { [DebuggerStepThrough] get => _projectTargets; diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index 56ca3f53f1a..305f7810471 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -469,29 +469,11 @@ public ImmutableList GetApplicableTargetsForReference(ProjectInstance re { ImmutableList RemoveNonexistentTargetsIfSkippable(ImmutableList targets) { - var targetsToKeep = new List(); - bool getTargetFrameworksRemoved = false; - foreach (TargetSpecification target in targets) - { - // Keep targets that are non-skippable or that exist but are skippable. - if (!target.SkipIfNonexistent || reference.Targets.ContainsKey(target.Target)) - { - targetsToKeep.Add(target.Target); - } - else if (target.Target.Equals("GetTargetFrameworks")) - { - getTargetFrameworksRemoved = true; - } - } - - // If GetTargetFrameworks is removed, also remove GetTargetFrameworksWithPlatformForSingleTargetFramework - // since in the non-graph case it is only called when GetTargetFrameworks is called. - if (getTargetFrameworksRemoved && targetsToKeep.Contains("GetTargetFrameworksWithPlatformForSingleTargetFramework")) - { - targetsToKeep.Remove("GetTargetFrameworksWithPlatformForSingleTargetFramework"); - } - - return targetsToKeep.ToImmutableList(); + // Keep targets that are non-skippable or that exist but are skippable. + return targets + .Where(t => !t.SkipIfNonexistent || reference.Targets.ContainsKey(t.Target)) + .Select(t => t.Target) + .ToImmutableList(); } return GetProjectType(reference) switch diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index 7cfc296e530..281d5692973 100644 --- a/src/Tasks/Microsoft.Managed.After.targets +++ b/src/Tasks/Microsoft.Managed.After.targets @@ -41,11 +41,9 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath - $(ProjectReferenceTargetsForBuildInOuterBuild) - $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForBuild) + $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild) - $(ProjectReferenceTargetsForCleanInOuterBuild) - Clean;GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForClean) + Clean;$(ProjectReferenceTargetsForClean) $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) @@ -58,13 +56,23 @@ Copyright (C) Microsoft Corporation. All rights reserved. - - + + + + + + + @@ -72,4 +80,4 @@ Copyright (C) Microsoft Corporation. All rights reserved. - \ No newline at end of file + From 417874e5dcfe618c586c72259ca27d343fc5c67b Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Thu, 26 Jan 2023 09:57:01 -0500 Subject: [PATCH 6/9] Fix indentation --- src/Tasks/Microsoft.Managed.After.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index 281d5692973..ec37b79cdf5 100644 --- a/src/Tasks/Microsoft.Managed.After.targets +++ b/src/Tasks/Microsoft.Managed.After.targets @@ -56,8 +56,8 @@ Copyright (C) Microsoft Corporation. All rights reserved. - - + + From bc8f8ae6502f3d64e0ec3e56455ff3fa27187204 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Thu, 26 Jan 2023 10:15:49 -0500 Subject: [PATCH 7/9] Remove ending newline --- src/Tasks/Microsoft.Managed.After.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index ec37b79cdf5..311c99eea92 100644 --- a/src/Tasks/Microsoft.Managed.After.targets +++ b/src/Tasks/Microsoft.Managed.After.targets @@ -80,4 +80,4 @@ Copyright (C) Microsoft Corporation. All rights reserved. - + \ No newline at end of file From ff426c8f3d8e9c0419b101290c49b02d3fda6b17 Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Thu, 26 Jan 2023 11:57:49 -0500 Subject: [PATCH 8/9] Add `GetTargetFrameworksWithPlatformForSingleTargetFramework` to `Rebuild` and address documentation --- src/Tasks/Microsoft.Managed.After.targets | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index 311c99eea92..fc33d41e1c4 100644 --- a/src/Tasks/Microsoft.Managed.After.targets +++ b/src/Tasks/Microsoft.Managed.After.targets @@ -65,13 +65,12 @@ Copyright (C) Microsoft Corporation. All rights reserved. + From e34ff600d1dcab543e22e7ee54f10b1d43b0951e Mon Sep 17 00:00:00 2001 From: Dmitriy Shepelev Date: Mon, 30 Jan 2023 10:00:51 -0500 Subject: [PATCH 9/9] Fix nits --- .../BackEnd/Shared/BuildRequestConfiguration.cs | 13 ++++++++++--- src/Build/Graph/ProjectInterpretation.cs | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index 8b6e4ecb984..b720431fc5e 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -183,7 +183,7 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d _project = data.ProjectInstance; _projectInitialTargets = data.ProjectInstance.InitialTargets; _projectDefaultTargets = data.ProjectInstance.DefaultTargets; - _projectTargets = data.ProjectInstance.Targets.Keys.ToHashSet(); + _projectTargets = GetProjectTargets(data.ProjectInstance.Targets); if (data.PropertiesToTransfer != null) { _transferredProperties = new List(); @@ -220,7 +220,7 @@ internal BuildRequestConfiguration(int configId, ProjectInstance instance) _project = instance; _projectInitialTargets = instance.InitialTargets; _projectDefaultTargets = instance.DefaultTargets; - _projectTargets = instance.Targets.Keys.ToHashSet(); + _projectTargets = GetProjectTargets(instance.Targets); IsCacheable = false; } @@ -415,7 +415,7 @@ private void SetProjectBasedState(ProjectInstance project) ProjectDefaultTargets = _project.DefaultTargets; ProjectInitialTargets = _project.InitialTargets; - ProjectTargets = _project.Targets.Keys.ToHashSet(); + ProjectTargets = GetProjectTargets(_project.Targets); if (IsCached) { @@ -988,6 +988,13 @@ private bool InternalEquals(BuildRequestConfiguration other) } } + /// + /// Gets the set of project targets for this . + /// + /// The project targets to transform into a set. + /// The set of project targets for this . + private HashSet GetProjectTargets(IDictionary projectTargets) => projectTargets.Keys.ToHashSet(); + /// /// Determines what the real tools version is. /// diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs index b3ce7cdc5e7..fa1642fb36b 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -67,6 +67,8 @@ private readonly struct TargetSpecification { public TargetSpecification(string target, bool skipIfNonexistent) { + // Verify that if this target is skippable then it equals neither + // ".default" nor ".projectReferenceTargetsOrDefaultTargets". ErrorUtilities.VerifyThrow( !skipIfNonexistent || (!target.Equals(MSBuildConstants.DefaultTargetsMarker) && !target.Equals(MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker)),