diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 7c0aab32bdd4..9ec839e6a066 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Definition; using Microsoft.Build.Evaluation; +using Microsoft.Build.Evaluation.Context; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; @@ -63,8 +64,11 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (commandOptions.ConfigurationOption != null || globalProperties is not null && globalProperties.ContainsKey(MSBuildPropertyNames.CONFIGURATION)) return new Dictionary(1, StringComparer.OrdinalIgnoreCase) { [EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE] = "true" }.AsReadOnly(); // Don't throw error if publish* conflicts but global config specified. + // The Shared policy is chosen because release-property discovery is short-lived and the context is discarded before the subsequent MSBuild invocation. + EvaluationContext evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared); + // Determine the project being acted upon - ProjectInstance? project = GetTargetedProject(globalProperties); + ProjectInstance? project = GetTargetedProject(globalProperties, evaluationContext); // Determine the correct value to return if (project != null) @@ -96,7 +100,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject /// A project instance that will be targeted to publish/pack, etc. null if one does not exist. /// Will return an arbitrary project in the solution if one exists in the solution and there's no project targeted. [RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")] - public ProjectInstance? GetTargetedProject(ReadOnlyDictionary? globalProps) + public ProjectInstance? GetTargetedProject(ReadOnlyDictionary? globalProps, EvaluationContext evaluationContext) { foreach (string arg in commandOptions.SlnOrProjectArgs.Append(Directory.GetCurrentDirectory())) { @@ -107,18 +111,18 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject } else if (IsValidProjectFilePath(arg)) { - return TryGetProjectInstance(arg, globalProps); + return TryGetProjectInstance(arg, globalProps, evaluationContext); } else if (IsValidSlnFilePath(arg)) { - return GetArbitraryProjectFromSolution(arg, globalProps); + return GetArbitraryProjectFromSolution(arg, globalProps, evaluationContext); } else if (Directory.Exists(arg)) // Get here if the user did not provide a .proj or a .sln. (See CWD appended to args above) { // First, look for a project in the directory. if (MsbuildProject.TryGetProjectFileFromDirectory(arg, out var projectFilePath)) { - return TryGetProjectInstance(projectFilePath, globalProps); + return TryGetProjectInstance(projectFilePath, globalProps, evaluationContext); } // Fall back to looking for a solution if multiple project files are found, or there's no project in the directory. @@ -126,7 +130,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (!string.IsNullOrEmpty(potentialSln)) { - return GetArbitraryProjectFromSolution(potentialSln, globalProps); + return GetArbitraryProjectFromSolution(potentialSln, globalProps, evaluationContext); } } } @@ -135,7 +139,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject /// An arbitrary existant project in a solution file. Returns null if no projects exist. /// Throws exception if two+ projects disagree in PublishRelease, PackRelease, or whatever _propertyToCheck is, and have it defined. - public ProjectInstance? GetArbitraryProjectFromSolution(string slnPath, ReadOnlyDictionary? globalProps) + public ProjectInstance? GetArbitraryProjectFromSolution(string slnPath, ReadOnlyDictionary? globalProps, EvaluationContext evaluationContext) { string slnFullPath = Path.GetFullPath(slnPath); if (!Path.Exists(slnFullPath)) @@ -160,7 +164,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS), "true", StringComparison.OrdinalIgnoreCase)) { // Evaluate only one project for speed if this environment variable is used. Will break more customers if enabled (adding 8.0 project to SLN with other project TFMs with no Publish or PackRelease.) - return GetSingleProjectFromSolution(sln, slnFullPath, globalProps); + return GetSingleProjectFromSolution(sln, slnFullPath, globalProps, evaluationContext); } Parallel.ForEach(sln.SolutionProjects.AsEnumerable(), (project, state) => @@ -171,7 +175,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (IsUnanalyzableProjectInSolution(project, projectFullPath)) return; - var projectData = TryGetProjectInstance(projectFullPath, globalProps); + var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext); if (projectData == null) { return; @@ -205,7 +209,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject /// The solution to get an arbitrary project from. /// The global properties to load into the project. /// null if no project exists in the solution that can be evaluated properly. Else, the first project in the solution that can be. - private ProjectInstance? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, ReadOnlyDictionary? globalProps) + private ProjectInstance? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, ReadOnlyDictionary? globalProps, EvaluationContext evaluationContext) { foreach (var project in sln.SolutionProjects.AsEnumerable()) { @@ -215,7 +219,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (IsUnanalyzableProjectInSolution(project, projectFullPath)) continue; - var projectData = TryGetProjectInstance(projectFullPath, globalProps); + var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext); if (projectData != null) { return projectData; @@ -238,7 +242,7 @@ private bool IsUnanalyzableProjectInSolution(SolutionProjectModel project, strin } /// Creates a ProjectInstance if the project is valid, elsewise, fails. - private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary? globalProperties) + private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary? globalProperties, EvaluationContext evaluationContext) { try { @@ -250,6 +254,7 @@ private bool IsUnanalyzableProjectInSolution(SolutionProjectModel project, strin GlobalProperties = globalProperties, ToolsVersion = "Current", EvaluationStage = ProjectEvaluationStage.Properties, + EvaluationContext = evaluationContext, }); } catch (Exception e) // Catch failed file access, or invalid project files that cause errors when read into memory, diff --git a/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs new file mode 100644 index 000000000000..4a60ad7e43a1 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Cli; + +namespace Microsoft.DotNet.Pack.Tests; + +[TestClass] +[DoNotParallelize] +public class ReleasePropertyProjectLocatorTests : SdkTest +{ + private string? _disablePublishAndPackRelease; + + [TestInitialize] + public void ClearReleaseDiscoveryEnvironmentVariables() + { + _disablePublishAndPackRelease = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE); + + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, null); + } + + [TestCleanup] + public void RestoreReleaseDiscoveryEnvironmentVariables() + { + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, _disablePublishAndPackRelease); + } + + [TestMethod] + public void EvaluationContextIsNotRetainedBetweenDiscoveryCalls() + { + var testDirectory = TestAssetsManager.CreateTestDirectory().Path; + string projectPath = Path.Combine(testDirectory, "Test.csproj"); + string propsPath = Path.Combine(testDirectory, "Directory.Build.props"); + + File.WriteAllText(projectPath, """ + + + + false + + + """); + WritePackRelease(propsPath, value: true); + + var firstResult = DiscoverPackProperties(projectPath); + + Assert.IsNotNull(firstResult); + Assert.AreEqual("Release", firstResult["Configuration"]); + + File.Delete(propsPath); + + var secondResult = DiscoverPackProperties(projectPath); + + Assert.IsNotNull(secondResult); + Assert.IsEmpty(secondResult); + } + + private static IReadOnlyDictionary? DiscoverPackProperties(string projectOrSolutionPath) + { + var locator = new ReleasePropertyProjectLocator( + userSpecifiedExplicitMSBuildProperties: null, + propertyToCheck: "PackRelease", + commandOptions: new ReleasePropertyProjectLocator.DependentCommandOptions([projectOrSolutionPath])); + + return locator.GetCustomDefaultConfigurationValueIfSpecified(); + } + + private static void WritePackRelease(string path, bool value) + { + File.WriteAllText(path, $""" + + + {value.ToString().ToLowerInvariant()} + + + """); + } +}