Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions src/Cli/dotnet/ReleasePropertyProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,8 +64,11 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject
if (commandOptions.ConfigurationOption != null || globalProperties is not null && globalProperties.ContainsKey(MSBuildPropertyNames.CONFIGURATION))
return new Dictionary<string, string>(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);
Comment thread
OvesN marked this conversation as resolved.

// Determine the project being acted upon
ProjectInstance? project = GetTargetedProject(globalProperties);
ProjectInstance? project = GetTargetedProject(globalProperties, evaluationContext);

// Determine the correct value to return
if (project != null)
Expand Down Expand Up @@ -96,7 +100,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject
/// <returns>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.</returns>
[RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")]
public ProjectInstance? GetTargetedProject(ReadOnlyDictionary<string, string>? globalProps)
public ProjectInstance? GetTargetedProject(ReadOnlyDictionary<string, string>? globalProps, EvaluationContext evaluationContext)
{
foreach (string arg in commandOptions.SlnOrProjectArgs.Append(Directory.GetCurrentDirectory()))
{
Expand All @@ -107,26 +111,26 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? 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.
string? potentialSln = SlnFileFactory.ListSolutionFilesInDirectory(arg, false).FirstOrDefault();

if (!string.IsNullOrEmpty(potentialSln))
{
return GetArbitraryProjectFromSolution(potentialSln, globalProps);
return GetArbitraryProjectFromSolution(potentialSln, globalProps, evaluationContext);
}
}
}
Expand All @@ -135,7 +139,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject

/// <returns>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.</returns>
public ProjectInstance? GetArbitraryProjectFromSolution(string slnPath, ReadOnlyDictionary<string, string>? globalProps)
public ProjectInstance? GetArbitraryProjectFromSolution(string slnPath, ReadOnlyDictionary<string, string>? globalProps, EvaluationContext evaluationContext)
{
string slnFullPath = Path.GetFullPath(slnPath);
if (!Path.Exists(slnFullPath))
Expand All @@ -160,7 +164,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? 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) =>
Expand All @@ -171,7 +175,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject
if (IsUnanalyzableProjectInSolution(project, projectFullPath))
return;

var projectData = TryGetProjectInstance(projectFullPath, globalProps);
var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext);
if (projectData == null)
{
return;
Expand Down Expand Up @@ -205,7 +209,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject
/// <param name="solution">The solution to get an arbitrary project from.</param>
/// <param name="globalProps">The global properties to load into the project.</param>
/// <returns>null if no project exists in the solution that can be evaluated properly. Else, the first project in the solution that can be.</returns>
private ProjectInstance? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, ReadOnlyDictionary<string, string>? globalProps)
private ProjectInstance? GetSingleProjectFromSolution(SolutionModel sln, string slnPath, ReadOnlyDictionary<string, string>? globalProps, EvaluationContext evaluationContext)
{
foreach (var project in sln.SolutionProjects.AsEnumerable())
{
Expand All @@ -215,7 +219,7 @@ public readonly struct DependentCommandOptions(IEnumerable<string>? slnOrProject
if (IsUnanalyzableProjectInSolution(project, projectFullPath))
continue;

var projectData = TryGetProjectInstance(projectFullPath, globalProps);
var projectData = TryGetProjectInstance(projectFullPath, globalProps, evaluationContext);
if (projectData != null)
{
return projectData;
Expand All @@ -238,7 +242,7 @@ private bool IsUnanalyzableProjectInSolution(SolutionProjectModel project, strin
}

/// <returns>Creates a ProjectInstance if the project is valid, elsewise, fails.</returns>
private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary<string, string>? globalProperties)
private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary<string, string>? globalProperties, EvaluationContext evaluationContext)
{
try
{
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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, """
<Project>
<Import Project="Directory.Build.props" Condition="Exists('Directory.Build.props')" />
<PropertyGroup>
<PackRelease Condition="'$(PackRelease)' == ''">false</PackRelease>
</PropertyGroup>
</Project>
""");
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<string, string>? 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, $"""
<Project>
<PropertyGroup>
<PackRelease>{value.ToString().ToLowerInvariant()}</PackRelease>
</PropertyGroup>
</Project>
""");
}
}
Loading