diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs index 841a48e4e64..d07f8cc81fc 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 4e3337867ab..65ab4706791 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; @@ -416,6 +417,7 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( /// /// /// + /// The list of targets to build per node. /// The isolation mode under which to run. /// internal static Dictionary BuildUsingCaches( @@ -427,6 +429,7 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( bool assertBuildResults = true, // (current node, expected output dictionary) -> actual expected output for current node Func expectedOutputProducer = null, + IReadOnlyDictionary> targetListsPerNode = null, ProjectIsolationMode projectIsolationMode = ProjectIsolationMode.False) { expectedOutputProducer ??= ((node, expectedOutputs) => expectedOutputs[node]); @@ -460,7 +463,7 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( buildParameters.OutputResultsCacheFile = outputCaches[node]; } - var logger = new MockLogger(); + var logger = new MockLogger(env.Output); buildParameters.Loggers = new[] { logger }; @@ -468,7 +471,7 @@ public void MissingResultFromCacheShouldErrorDueToIsolatedBuildCacheEnforcement( node.ProjectInstance.FullPath, null, buildParameters, - node.ProjectInstance.DefaultTargets); + targetListsPerNode?[node] != null ? targetListsPerNode?[node] : node.ProjectInstance.DefaultTargets); results[ProjectNumber(node)] = (result, logger); diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 73ce1de0e23..0c828f621ff 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1397,7 +1397,7 @@ private void LoadSolutionIntoConfiguration(BuildRequestConfiguration config, Bui ((IBuildComponentHost)this).LoggingService, request.BuildEventContext, false /* loaded by solution parser*/, - config.TargetNames, + config.RequestedTargets, SdkResolverService, request.SubmissionId); @@ -2336,23 +2336,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/BuildManager/CacheSerialization.cs b/src/Build/BackEnd/BuildManager/CacheSerialization.cs index fa61c34aa64..3fa59bfe28b 100644 --- a/src/Build/BackEnd/BuildManager/CacheSerialization.cs +++ b/src/Build/BackEnd/BuildManager/CacheSerialization.cs @@ -97,7 +97,7 @@ public static string SerializeCaches( // due to its dependency on a cached target whose side effects would // not be taken into account. (E.g., the definition of a property.) resultsCacheToSerialize.GetResultsForConfiguration(smallestConfigId) - .KeepSpecificTargetResults(configCacheToSerialize[smallestConfigId].TargetNames); + .KeepSpecificTargetResults(configCacheToSerialize[smallestConfigId].RequestedTargets); } translator.Translate(ref configCacheToSerialize); diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 99fff072659..250d4b4c8bf 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 41a142a83fd..0c11f731cb5 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -1963,7 +1963,8 @@ private bool CheckIfCacheMissOnReferencedProjectIsAllowedAndErrorIfNot(int nodeF var configCache = (IConfigCache)_componentHost.GetComponent(BuildComponentType.ConfigCache); // do not check root requests as nothing depends on them - if (isolateProjects == ProjectIsolationMode.False || request.IsRootRequest || request.SkipStaticGraphIsolationConstraints) + if (isolateProjects == ProjectIsolationMode.False || request.IsRootRequest || request.SkipStaticGraphIsolationConstraints + || SkipNonexistentTargetsIfExistentTargetsHaveResults(request)) { bool logComment = ((isolateProjects == ProjectIsolationMode.True || isolateProjects == ProjectIsolationMode.MessageUponIsolationViolation) && request.SkipStaticGraphIsolationConstraints); if (logComment) @@ -1975,14 +1976,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)) @@ -2021,20 +2022,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); } @@ -2042,6 +2043,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 b1616f43858..b720431fc5e 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 = GetProjectTargets(data.ProjectInstance.Targets); 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 = GetProjectTargets(instance.Targets); 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 = GetProjectTargets(_project.Targets); if (IsCached) { @@ -548,6 +557,23 @@ public List ProjectDefaultTargets } } + /// + /// Gets or sets the targets defined for the project. + /// + internal HashSet ProjectTargets + { + [DebuggerStepThrough] + get => _projectTargets; + [DebuggerStepThrough] + set + { + ErrorUtilities.VerifyThrow( + _projectTargets == null, + "Targets cannot be reset once set."); + _projectTargets = value; + } + } + /// /// Returns the node packet type /// @@ -879,6 +905,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); } @@ -961,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/BackEnd/Shared/BuildResult.cs b/src/Build/BackEnd/Shared/BuildResult.cs index 7d4e073cb97..ca443451880 100644 --- a/src/Build/BackEnd/Shared/BuildResult.cs +++ b/src/Build/BackEnd/Shared/BuildResult.cs @@ -118,6 +118,8 @@ public class BuildResult : INodePacket, IBuildResults private string _schedulerInducedError; + private HashSet _projectTargets; + /// /// Constructor for serialization. /// @@ -229,6 +231,7 @@ internal BuildResult(BuildResult result, int nodeRequestId) _circularDependency = result._circularDependency; _initialTargets = result._initialTargets; _defaultTargets = result._defaultTargets; + _projectTargets = result._projectTargets; _baseOverallResult = result.OverallResult == BuildResultCode.Success; } @@ -245,6 +248,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; } @@ -434,6 +438,17 @@ internal List DefaultTargets { _defaultTargets = value; } } + /// + /// The defined targets for the project associated with this build result. + /// + internal 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) @@ -557,6 +572,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 35b57e90b01..fa1642fb36b 100644 --- a/src/Build/Graph/ProjectInterpretation.cs +++ b/src/Build/Graph/ProjectInterpretation.cs @@ -63,6 +63,25 @@ public ReferenceInfo(ConfigurationMetadata referenceConfiguration, ProjectItemIn } } + 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)), + 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; @@ -391,10 +410,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; @@ -416,23 +435,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); @@ -450,11 +468,20 @@ public static TargetsToPropagate FromProjectAndEntryTargets(ProjectInstance proj public ImmutableList GetApplicableTargetsForReference(ProjectInstance reference) { - return (GetProjectType(reference)) switch + 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 2a3a575218c..68d24c68b48 100644 --- a/src/Shared/UnitTests/ObjectModelHelpers.cs +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -1364,7 +1364,7 @@ public static BuildResult BuildProjectFileUsingBuildManager( string projectFile, MockLogger logger = null, BuildParameters parameters = null, - List defaultTargets = null) + IList targetsToBuild = null) { using (var buildManager = new BuildManager()) { @@ -1381,7 +1381,7 @@ public static BuildResult BuildProjectFileUsingBuildManager( projectFile, new Dictionary(), MSBuildConstants.CurrentToolsVersion, - defaultTargets?.ToArray() ?? Array.Empty(), + targetsToBuild?.ToArray() ?? Array.Empty(), null); var result = buildManager.Build( diff --git a/src/Tasks/Microsoft.Managed.After.targets b/src/Tasks/Microsoft.Managed.After.targets index 48264892122..fc33d41e1c4 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 - GetTargetFrameworks;$(ProjectReferenceTargetsForBuildInOuterBuild) - $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForBuild) + $(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild) - GetTargetFrameworks;$(ProjectReferenceTargetsForCleanInOuterBuild) - Clean;GetTargetFrameworksWithPlatformForSingleTargetFramework;$(ProjectReferenceTargetsForClean) + Clean;$(ProjectReferenceTargetsForClean) $(ProjectReferenceTargetsForClean);$(ProjectReferenceTargetsForBuild);$(ProjectReferenceTargetsForRebuild) @@ -59,11 +57,21 @@ Copyright (C) Microsoft Corporation. All rights reserved. + - + + + + + + +