From 5d2270733cd3636d67a32421774791cfbc13c359 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Thu, 23 Jul 2026 10:52:10 +0200 Subject: [PATCH 1/6] Share SDK resolution during release property discovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet/ReleasePropertyProjectLocator.cs | 60 ++++++++++++++----- ...SolutionWithPublishReleaseOrPackRelease.cs | 21 +++++-- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 7c0aab32bdd4..d6cfc322f0bb 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,10 @@ 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. + EvaluationContext evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.SharedSDKCache); + // 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 +99,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 +110,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 +129,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject if (!string.IsNullOrEmpty(potentialSln)) { - return GetArbitraryProjectFromSolution(potentialSln, globalProps); + return GetArbitraryProjectFromSolution(potentialSln, globalProps, evaluationContext); } } } @@ -135,7 +138,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)) @@ -156,22 +159,50 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject List configuredProjects = []; HashSet configValues = []; object projectDataLock = new(); + var solutionProjects = sln.SolutionProjects.AsEnumerable().ToList(); 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); + } + + int firstAnalyzableProjectIndex = -1; + for (int index = 0; index < solutionProjects.Count; index++) + { + SolutionProjectModel project = solutionProjects[index]; +#pragma warning disable CS8604 // Possible null reference argument. + string projectFullPath = Path.GetFullPath(project.FilePath, Path.GetDirectoryName(slnFullPath)); +#pragma warning restore CS8604 // Possible null reference argument. + if (!IsUnanalyzableProjectInSolution(project, projectFullPath)) + { + // Seed the shared SDK cache in solution order before the remaining projects evaluate in parallel. + firstAnalyzableProjectIndex = index; + EvaluateProject(projectFullPath); + break; + } } - Parallel.ForEach(sln.SolutionProjects.AsEnumerable(), (project, state) => + Parallel.For(0, solutionProjects.Count, index => { + if (index == firstAnalyzableProjectIndex) + { + return; + } + + SolutionProjectModel project = solutionProjects[index]; #pragma warning disable CS8604 // Possible null reference argument. string projectFullPath = Path.GetFullPath(project.FilePath, Path.GetDirectoryName(slnFullPath)); #pragma warning restore CS8604 // Possible null reference argument. if (IsUnanalyzableProjectInSolution(project, projectFullPath)) return; - var projectData = TryGetProjectInstance(projectFullPath, globalProps); + EvaluateProject(projectFullPath); + }); + + void EvaluateProject(string projectFullPath) + { + var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext); if (projectData == null) { return; @@ -186,7 +217,7 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject configValues.Add(pReleasePropertyValue.ToLower()); } } - }); + } if (configuredProjects.Any() && configValues.Count > 1) { @@ -205,7 +236,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 +246,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 +269,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 +281,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/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs index f3ca0967bd1d..3c80de90c5c2 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs @@ -288,12 +288,25 @@ public void ItDoesNotErrorWithLegacyNet7ProjectAndNet6ProjectSolutionWithNoPubli } [TestMethod] - [DataRow(PublishRelease)] - [DataRow(PackRelease)] - public void It_fails_with_conflicting_PublishRelease_or_PackRelease_values_in_solution_file(string pReleaseVar) + [DataRow(PublishRelease, false)] + [DataRow(PublishRelease, true)] + [DataRow(PackRelease, false)] + [DataRow(PackRelease, true)] + public void It_fails_with_conflicting_PublishRelease_or_PackRelease_values_in_solution_file(string pReleaseVar, bool useSlnx) { var tfm = ToolsetInfo.CurrentTargetFramework; - var (testAsset, _) = Setup(new List { tfm }, new List { tfm }, pReleaseVar, "true", "false"); + var (testAsset, _) = Setup(new List { tfm }, new List { tfm }, pReleaseVar, "true", "false", identifier: $"{pReleaseVar}-{useSlnx}"); + + if (useSlnx) + { + string slnPath = Directory.GetFiles(testAsset.Path, "*.sln").Single(); + new DotnetCommand(Log) + .WithWorkingDirectory(testAsset.Path) + .Execute("sln", Path.GetFileName(slnPath), "migrate") + .Should() + .Pass(); + File.Delete(slnPath); + } var expectedError = string.Format(Strings.SolutionProjectConfigurationsConflict, pReleaseVar, ""); From 44e429c27971fcaa66dc2f412bc89312867a01a8 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Mon, 27 Jul 2026 10:34:22 +0200 Subject: [PATCH 2/6] Use shared evaluation context for release discovery Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet/ReleasePropertyProjectLocator.cs | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index d6cfc322f0bb..974b9d223d3a 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -64,7 +64,7 @@ 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. - EvaluationContext evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.SharedSDKCache); + EvaluationContext evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared); // Determine the project being acted upon ProjectInstance? project = GetTargetedProject(globalProperties, evaluationContext); @@ -159,7 +159,6 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject List configuredProjects = []; HashSet configValues = []; object projectDataLock = new(); - var solutionProjects = sln.SolutionProjects.AsEnumerable().ToList(); if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS), "true", StringComparison.OrdinalIgnoreCase)) { @@ -167,41 +166,14 @@ public readonly struct DependentCommandOptions(IEnumerable? slnOrProject return GetSingleProjectFromSolution(sln, slnFullPath, globalProps, evaluationContext); } - int firstAnalyzableProjectIndex = -1; - for (int index = 0; index < solutionProjects.Count; index++) + Parallel.ForEach(sln.SolutionProjects.AsEnumerable(), (project, state) => { - SolutionProjectModel project = solutionProjects[index]; -#pragma warning disable CS8604 // Possible null reference argument. - string projectFullPath = Path.GetFullPath(project.FilePath, Path.GetDirectoryName(slnFullPath)); -#pragma warning restore CS8604 // Possible null reference argument. - if (!IsUnanalyzableProjectInSolution(project, projectFullPath)) - { - // Seed the shared SDK cache in solution order before the remaining projects evaluate in parallel. - firstAnalyzableProjectIndex = index; - EvaluateProject(projectFullPath); - break; - } - } - - Parallel.For(0, solutionProjects.Count, index => - { - if (index == firstAnalyzableProjectIndex) - { - return; - } - - SolutionProjectModel project = solutionProjects[index]; #pragma warning disable CS8604 // Possible null reference argument. string projectFullPath = Path.GetFullPath(project.FilePath, Path.GetDirectoryName(slnFullPath)); #pragma warning restore CS8604 // Possible null reference argument. if (IsUnanalyzableProjectInSolution(project, projectFullPath)) return; - EvaluateProject(projectFullPath); - }); - - void EvaluateProject(string projectFullPath) - { var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext); if (projectData == null) { @@ -217,7 +189,7 @@ void EvaluateProject(string projectFullPath) configValues.Add(pReleasePropertyValue.ToLower()); } } - } + }); if (configuredProjects.Any() && configValues.Count > 1) { From ab473a5c50b9b780ebe7078fb3c8b155a745c468 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 28 Jul 2026 17:20:32 +0200 Subject: [PATCH 3/6] Test shared release discovery caches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReleasePropertyProjectLocatorTests.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs diff --git a/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs new file mode 100644 index 000000000000..cc8dd7b593a7 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs @@ -0,0 +1,157 @@ +// 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; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Pack.Tests; + +[TestClass] +[DoNotParallelize] +public class ReleasePropertyProjectLocatorTests : SdkTest +{ + private string? _disablePublishAndPackRelease; + private string? _lazyPublishAndPackReleaseForSolutions; + + [TestInitialize] + public void ClearReleaseDiscoveryEnvironmentVariables() + { + _disablePublishAndPackRelease = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE); + _lazyPublishAndPackReleaseForSolutions = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS); + + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, null); + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS, null); + } + + [TestCleanup] + public void RestoreReleaseDiscoveryEnvironmentVariables() + { + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, _disablePublishAndPackRelease); + Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS, _lazyPublishAndPackReleaseForSolutions); + } + + [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); + } + + [TestMethod] + public void SharedContextKeepsSameNamedImportedFilesSeparate() + { + var testDirectory = TestAssetsManager.CreateTestDirectory().Path; + string projectA = CreateProjectWithImport(testDirectory, "A", "Release.props"); + string projectB = CreateProjectWithImport(testDirectory, "B", "Release.props"); + + WritePackRelease(Path.Combine(testDirectory, "A", "Release.props"), value: true); + WritePackRelease(Path.Combine(testDirectory, "B", "Release.props"), value: false); + + string solutionPath = CreateSolution(testDirectory, projectA, projectB); + + var exception = Assert.ThrowsExactly(() => DiscoverPackProperties(solutionPath)); + + exception.Message.Should().Contain("NETSDK1197"); + } + + [TestMethod] + public void SharedContextKeepsMissingImportsProjectSpecific() + { + var testDirectory = TestAssetsManager.CreateTestDirectory().Path; + string projectA = CreateProjectWithConditionalImport(testDirectory, "A"); + string projectB = CreateProjectWithConditionalImport(testDirectory, "B"); + + WritePackRelease(Path.Combine(testDirectory, "A", "Release.props"), value: true); + + string solutionPath = CreateSolution(testDirectory, projectA, projectB); + + var exception = Assert.ThrowsExactly(() => DiscoverPackProperties(solutionPath)); + + exception.Message.Should().Contain("NETSDK1197"); + } + + private static IReadOnlyDictionary? DiscoverPackProperties(string projectOrSolutionPath) + { + var locator = new ReleasePropertyProjectLocator( + userSpecifiedExplicitMSBuildProperties: null, + propertyToCheck: "PackRelease", + commandOptions: new ReleasePropertyProjectLocator.DependentCommandOptions([projectOrSolutionPath])); + + return locator.GetCustomDefaultConfigurationValueIfSpecified(); + } + + private static string CreateProjectWithImport(string root, string projectName, string import) + { + string projectDirectory = Directory.CreateDirectory(Path.Combine(root, projectName)).FullName; + string projectPath = Path.Combine(projectDirectory, $"{projectName}.csproj"); + File.WriteAllText(projectPath, $""" + + + + """); + return projectPath; + } + + private static string CreateProjectWithConditionalImport(string root, string projectName) + { + string projectDirectory = Directory.CreateDirectory(Path.Combine(root, projectName)).FullName; + string projectPath = Path.Combine(projectDirectory, $"{projectName}.csproj"); + File.WriteAllText(projectPath, """ + + + + false + + + """); + return projectPath; + } + + private static string CreateSolution(string root, params string[] projectPaths) + { + string projects = string.Join( + Environment.NewLine, + projectPaths.Select(path => $" ")); + string solutionPath = Path.Combine(root, "Test.slnx"); + File.WriteAllText(solutionPath, $""" + + {projects} + + """); + return solutionPath; + } + + private static void WritePackRelease(string path, bool value) + { + File.WriteAllText(path, $""" + + + {value.ToString().ToLowerInvariant()} + + + """); + } +} From a631d251d11bdbcbeb4962241051c42229395979 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 29 Jul 2026 09:19:54 +0200 Subject: [PATCH 4/6] Remove unrelated slnx conflict coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...SolutionWithPublishReleaseOrPackRelease.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs index 3c80de90c5c2..f3ca0967bd1d 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToTestAMultitargetedSolutionWithPublishReleaseOrPackRelease.cs @@ -288,25 +288,12 @@ public void ItDoesNotErrorWithLegacyNet7ProjectAndNet6ProjectSolutionWithNoPubli } [TestMethod] - [DataRow(PublishRelease, false)] - [DataRow(PublishRelease, true)] - [DataRow(PackRelease, false)] - [DataRow(PackRelease, true)] - public void It_fails_with_conflicting_PublishRelease_or_PackRelease_values_in_solution_file(string pReleaseVar, bool useSlnx) + [DataRow(PublishRelease)] + [DataRow(PackRelease)] + public void It_fails_with_conflicting_PublishRelease_or_PackRelease_values_in_solution_file(string pReleaseVar) { var tfm = ToolsetInfo.CurrentTargetFramework; - var (testAsset, _) = Setup(new List { tfm }, new List { tfm }, pReleaseVar, "true", "false", identifier: $"{pReleaseVar}-{useSlnx}"); - - if (useSlnx) - { - string slnPath = Directory.GetFiles(testAsset.Path, "*.sln").Single(); - new DotnetCommand(Log) - .WithWorkingDirectory(testAsset.Path) - .Execute("sln", Path.GetFileName(slnPath), "migrate") - .Should() - .Pass(); - File.Delete(slnPath); - } + var (testAsset, _) = Setup(new List { tfm }, new List { tfm }, pReleaseVar, "true", "false"); var expectedError = string.Format(Strings.SolutionProjectConfigurationsConflict, pReleaseVar, ""); From 1cd41caa3e7e95a54b0a73133e952136a3b30c85 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 29 Jul 2026 09:36:46 +0200 Subject: [PATCH 5/6] Focus release discovery cache coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReleasePropertyProjectLocatorTests.cs | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs index cc8dd7b593a7..4a60ad7e43a1 100644 --- a/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs +++ b/test/dotnet.Tests/CommandTests/Pack/ReleasePropertyProjectLocatorTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli; -using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Pack.Tests; @@ -11,23 +10,19 @@ namespace Microsoft.DotNet.Pack.Tests; public class ReleasePropertyProjectLocatorTests : SdkTest { private string? _disablePublishAndPackRelease; - private string? _lazyPublishAndPackReleaseForSolutions; [TestInitialize] public void ClearReleaseDiscoveryEnvironmentVariables() { _disablePublishAndPackRelease = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE); - _lazyPublishAndPackReleaseForSolutions = Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS); Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, null); - Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS, null); } [TestCleanup] public void RestoreReleaseDiscoveryEnvironmentVariables() { Environment.SetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE, _disablePublishAndPackRelease); - Environment.SetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS, _lazyPublishAndPackReleaseForSolutions); } [TestMethod] @@ -60,39 +55,6 @@ public void EvaluationContextIsNotRetainedBetweenDiscoveryCalls() Assert.IsEmpty(secondResult); } - [TestMethod] - public void SharedContextKeepsSameNamedImportedFilesSeparate() - { - var testDirectory = TestAssetsManager.CreateTestDirectory().Path; - string projectA = CreateProjectWithImport(testDirectory, "A", "Release.props"); - string projectB = CreateProjectWithImport(testDirectory, "B", "Release.props"); - - WritePackRelease(Path.Combine(testDirectory, "A", "Release.props"), value: true); - WritePackRelease(Path.Combine(testDirectory, "B", "Release.props"), value: false); - - string solutionPath = CreateSolution(testDirectory, projectA, projectB); - - var exception = Assert.ThrowsExactly(() => DiscoverPackProperties(solutionPath)); - - exception.Message.Should().Contain("NETSDK1197"); - } - - [TestMethod] - public void SharedContextKeepsMissingImportsProjectSpecific() - { - var testDirectory = TestAssetsManager.CreateTestDirectory().Path; - string projectA = CreateProjectWithConditionalImport(testDirectory, "A"); - string projectB = CreateProjectWithConditionalImport(testDirectory, "B"); - - WritePackRelease(Path.Combine(testDirectory, "A", "Release.props"), value: true); - - string solutionPath = CreateSolution(testDirectory, projectA, projectB); - - var exception = Assert.ThrowsExactly(() => DiscoverPackProperties(solutionPath)); - - exception.Message.Should().Contain("NETSDK1197"); - } - private static IReadOnlyDictionary? DiscoverPackProperties(string projectOrSolutionPath) { var locator = new ReleasePropertyProjectLocator( @@ -103,47 +65,6 @@ public void SharedContextKeepsMissingImportsProjectSpecific() return locator.GetCustomDefaultConfigurationValueIfSpecified(); } - private static string CreateProjectWithImport(string root, string projectName, string import) - { - string projectDirectory = Directory.CreateDirectory(Path.Combine(root, projectName)).FullName; - string projectPath = Path.Combine(projectDirectory, $"{projectName}.csproj"); - File.WriteAllText(projectPath, $""" - - - - """); - return projectPath; - } - - private static string CreateProjectWithConditionalImport(string root, string projectName) - { - string projectDirectory = Directory.CreateDirectory(Path.Combine(root, projectName)).FullName; - string projectPath = Path.Combine(projectDirectory, $"{projectName}.csproj"); - File.WriteAllText(projectPath, """ - - - - false - - - """); - return projectPath; - } - - private static string CreateSolution(string root, params string[] projectPaths) - { - string projects = string.Join( - Environment.NewLine, - projectPaths.Select(path => $" ")); - string solutionPath = Path.Combine(root, "Test.slnx"); - File.WriteAllText(solutionPath, $""" - - {projects} - - """); - return solutionPath; - } - private static void WritePackRelease(string path, bool value) { File.WriteAllText(path, $""" From 286af78dc4e143358f7b3287cb6c1c803e84dbc8 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 29 Jul 2026 09:58:37 +0200 Subject: [PATCH 6/6] Add comment --- src/Cli/dotnet/ReleasePropertyProjectLocator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 974b9d223d3a..9ec839e6a066 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -64,6 +64,7 @@ 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