Skip to content
Closed
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
13 changes: 9 additions & 4 deletions src/Cli/dotnet/ReleasePropertyProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ReleasePropertyProjectLocator
private bool checkSolutions;

/// <summary>
/// Returns dotnet CLI command-line parameters (or an empty list) to change configuration based on
/// Returns dotnet CLI command-line parameters (or an empty list) to change configuration based on
/// a boolean that may or may not exist in the targeted project.
/// <param name="defaultedConfigurationProperty">The boolean property to check the project for. Ex: PublishRelease</param>
/// <param name="slnOrProjectArgs">The arguments or solution passed to a dotnet invocation.</param>
Expand Down Expand Up @@ -67,7 +67,7 @@ public ReleasePropertyProjectLocator(bool shouldCheckSolutionsForProjects)

/// <param name="slnProjectPropertytoCheck">A property to enforce if we are looking into SLN files. If projects disagree on the property, throws exception.</param>
/// <returns>A project instance that will be targeted to publish/pack, etc. null if one does not exist.</returns>
public ProjectInstance GetTargetedProject(IEnumerable<string> slnOrProjectArgs, Dictionary<string, string> globalProps, string slnProjectPropertytoCheck = "")
public ProjectInstance GetTargetedProject(IEnumerable<string> slnOrProjectArgs, Dictionary<string, string> globalProps, string slnProjectPropertytoCheck = "", bool includeSolutions = true)
{
string potentialProject = "";

Expand All @@ -85,14 +85,19 @@ public ProjectInstance GetTargetedProject(IEnumerable<string> slnOrProjectArgs,
}
catch (GracefulException)
{
if (!includeSolutions)
{
continue;
}

// Fall back to looking for a solution if multiple project files are found.
string potentialSln = Directory.GetFiles(arg, "*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault();

if (!string.IsNullOrEmpty(potentialSln))
{
return GetSlnProject(potentialSln, globalProps, slnProjectPropertytoCheck);
}
} // If nothing can be found: that's caught by MSBuild XMake::ProcessProjectSwitch -- don't change the behavior by failing here.
} // If nothing can be found: that's caught by MSBuild XMake::ProcessProjectSwitch -- don't change the behavior by failing here.
}
}

Expand Down Expand Up @@ -185,7 +190,7 @@ private bool IsValidProjectFilePath(string path)
}

/// <returns>A case-insensitive dictionary of any properties passed from the user and their values.</returns>
private Dictionary<string, string> GetGlobalPropertiesFromUserArgs(ParseResult parseResult)
public Dictionary<string, string> GetGlobalPropertiesFromUserArgs(ParseResult parseResult)
{
Dictionary<string, string> globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

Expand Down
102 changes: 83 additions & 19 deletions src/Cli/dotnet/commands/dotnet-publish/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ namespace Microsoft.DotNet.Tools.Publish
{
public class PublishCommand : RestoringCommand
{

/// <summary>
/// The list of properties that should be forwarded from the publish profile to the publish invocation.
/// </summary>
/// <remarks>
/// While we could forward along every property, the intent of this array is to mimic the behavior of VS,
/// whose Publish operation only forwards along a few properties. Of particular interest are properties that
/// are set very early on in the build (RID, Configuration, etc). The remainder will be imported during the
/// build via the Microsoft.Net.Sdk.Publish props and targets.
/// </remarks>
private static string[] PropertiesToForwardFromProfile = new [] {
MSBuildPropertyNames.CONFIGURATION,
MSBuildPropertyNames.LAST_USED_BUILD_CONFIGURATION,
MSBuildPropertyNames.LAST_USED_PLATFORM,
MSBuildPropertyNames.PLATFORM,
MSBuildPropertyNames.RUNTIME_IDENTIFIER,
MSBuildPropertyNames.RUNTIME_IDENTIFIERS,
MSBuildPropertyNames.TARGET_FRAMEWORK,
MSBuildPropertyNames.TARGET_FRAMEWORKS,
};

private PublishCommand(
IEnumerable<string> msbuildArgs,
bool noRestore,
Expand All @@ -43,30 +64,73 @@ public static PublishCommand FromParseResult(ParseResult parseResult, string msb
parseResult.HandleDebugSwitch();
parseResult.ShowHelpOrErrorIfAppropriate();

var msbuildArgs = new List<string>()
{
"-target:Publish",
"--property:_IsPublishing=true" // This property will not hold true for MSBuild /t:Publish. VS should also inject this property when publishing in the future.
};

IEnumerable<string> slnOrProjectArgs = parseResult.GetValueForArgument(PublishCommandParser.SlnOrProjectArgument);

CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption),
parseResult.HasOption(PublishCommandParser.NoSelfContainedOption));

msbuildArgs.AddRange(parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand()));
ReleasePropertyProjectLocator projectLocator = new ReleasePropertyProjectLocator(Environment.GetEnvironmentVariable(EnvironmentVariableNames.ENABLE_PUBLISH_RELEASE_FOR_SOLUTIONS) != null);
msbuildArgs.AddRange(projectLocator.GetCustomDefaultConfigurationValueIfSpecified(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE,
slnOrProjectArgs, PublishCommandParser.ConfigurationOption) ?? Array.Empty<string>());
msbuildArgs.AddRange(slnOrProjectArgs ?? Array.Empty<string>());

bool noRestore = parseResult.HasOption(PublishCommandParser.NoRestoreOption)
|| parseResult.HasOption(PublishCommandParser.NoBuildOption);


var publishProfileProperties = DiscoverPropertiesFromPublishProfile(parseResult);
var standardMSbuildProperties = CreatePropertyListForPublishInvocation(parseResult);
return new PublishCommand(
msbuildArgs,
// properties defined by the selected publish profile should override any other properties,
// so they should be added after the other properties.
standardMSbuildProperties.Concat(publishProfileProperties),
noRestore,
msbuildPath);
msbuildPath
);

List<string> CreatePropertyListForPublishInvocation(ParseResult parseResult) {
var msbuildArgs = new List<string>()
{
"-target:Publish",
"--property:_IsPublishing=true" // This property will not hold true for MSBuild /t:Publish. VS should also inject this property when publishing in the future.
};

IEnumerable<string> slnOrProjectArgs = parseResult.GetValueForArgument(PublishCommandParser.SlnOrProjectArgument);

CommonOptions.ValidateSelfContainedOptions(parseResult.HasOption(PublishCommandParser.SelfContainedOption),
parseResult.HasOption(PublishCommandParser.NoSelfContainedOption));

msbuildArgs.AddRange(parseResult.OptionValuesToBeForwarded(PublishCommandParser.GetCommand()));
ReleasePropertyProjectLocator projectLocator = new ReleasePropertyProjectLocator(Environment.GetEnvironmentVariable(EnvironmentVariableNames.ENABLE_PUBLISH_RELEASE_FOR_SOLUTIONS) != null);
msbuildArgs.AddRange(projectLocator.GetCustomDefaultConfigurationValueIfSpecified(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, slnOrProjectArgs, PublishCommandParser.ConfigurationOption) ?? Array.Empty<string>());
msbuildArgs.AddRange(slnOrProjectArgs ?? Array.Empty<string>());

return msbuildArgs;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is a breaking change because you could have a publish profile in your directory that currently isn't being used by the .NET SDK, and so the Config, Rid, etc, could suddenly change with this change. IMO it would be better to take in .NET 8, but maybe this qualifies for a 300 band break. Either way, we would need a breaking change doc.

}

/// <summary>
/// Evaulates the project specified by the user and returns the list of properties that should be forwarded
/// to the actual call to the Publish MSBuild target. These properties are derived by the publish profile (if any)
/// specified by the user on the command line. If no publish profile is specified, this method returns an empty list.
/// If a publish profile is specified, it is loaded as a standalone file and specific properties are pulled out of it if they exist.
/// If it is specified but does not exist, we do not error because the current behavior of the build is to silently ignore
/// missing profiles.
/// </summary>
List<string> DiscoverPropertiesFromPublishProfile(ParseResult parseResult)
{
ReleasePropertyProjectLocator projectLocator = new ReleasePropertyProjectLocator(Environment.GetEnvironmentVariable(EnvironmentVariableNames.ENABLE_PUBLISH_RELEASE_FOR_SOLUTIONS) != null);
var cliProps = projectLocator.GetGlobalPropertiesFromUserArgs(parseResult);
var solutionOrProjectToPublish = parseResult.GetValueForArgument(PublishCommandParser.SlnOrProjectArgument);
var projectInstance = projectLocator.GetTargetedProject(solutionOrProjectToPublish, cliProps, includeSolutions: false);
// this can happen if the project wasn't loadable
if (projectInstance == null)
{
return new List<string>();
}
var importedPropValue = projectInstance.GetPropertyValue(MSBuildPropertyNames.PUBLISH_PROFILE_IMPORTED);
if (!String.IsNullOrEmpty(importedPropValue) && bool.TryParse(importedPropValue, out var wasImported) && wasImported) {
try {
if (projectInstance.GetPropertyValue(MSBuildPropertyNames.PUBLISH_PROFILE_FULL_PATH) is {} fullPathPropValue) {
var properties = new ProjectInstance(fullPathPropValue).ToProjectRootElement().PropertyGroups.First().Properties;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I haven't done the side-by-side comparison, I imagine that it would be faster to loop through the properties in the propertiesToForwardFromProfile, instead of loading in every property on the project into memory

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use TryGetProjectInstance as race conditions could occur and cause failure if the project file is changed on disk in between this and the above lines of code

var propertiesToForward = properties.Where(p => PropertiesToForwardFromProfile.Contains(p.Name));
return propertiesToForward.Select(p => $"--property:{p.Name}=\"{p.Value}\"").ToList();
}
} catch (IOException) {
return new List<string>();
}
}
return new List<string>();
};
}

public static int Run(ParseResult parseResult)
Expand Down
15 changes: 12 additions & 3 deletions src/Common/MSBuildPropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@ namespace Microsoft.DotNet.Cli
{
static class MSBuildPropertyNames
{
public static readonly string PUBLISH_RELEASE = "PublishRelease";
public static readonly string PACK_RELEASE = "PackRelease";
public static readonly string CONFIGURATION = "Configuration";
public static readonly string CONFIGURATION_RELEASE_VALUE = "Release";
public static readonly string CONFIGURATION_DEBUG_VALUE = "Debug";
public static readonly string CONFIGURATION_RELEASE_VALUE = "Release";
public static readonly string LAST_USED_BUILD_CONFIGURATION = "LastUsedBuildConfiguration";
public static readonly string LAST_USED_PLATFORM = "LastUsedPlatform";
public static readonly string OUTPUT_TYPE = "OutputType";
public static readonly string OUTPUT_TYPE_EXECUTABLE = "Exe";
public static readonly string PACK_RELEASE = "PackRelease";
public static readonly string PLATFORM = "Platform";
public static readonly string PUBLISH_PROFILE_FULL_PATH = "PublishProfileFullPath";
public static readonly string PUBLISH_PROFILE_IMPORTED = "PublishProfileImported";
public static readonly string PUBLISH_RELEASE = "PublishRelease";
public static readonly string RUNTIME_IDENTIFIER = "RuntimeIdentifier";
public static readonly string RUNTIME_IDENTIFIERS = "RuntimeIdentifiers";
public static readonly string TARGET_FRAMEWORK = "TargetFramework";
public static readonly string TARGET_FRAMEWORKS = "TargetFrameworks";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ public void PublishRelease_does_not_override_custom_Configuration_on_proj_and_lo
.HaveStdOutContaining("PublishRelease");

var releaseAssetPath = System.IO.Path.Combine(helloWorldAsset.Path, "bin", "Release", ToolsetInfo.CurrentTargetFramework, "HelloWorld.dll");
Assert.False(File.Exists(releaseAssetPath)); // build will produce a debug asset, need to make sure this doesn't exist either.
Assert.False(File.Exists(releaseAssetPath)); // build will produce a debug asset, need to make sure this doesn't exist either.
}

[Theory]
Expand Down Expand Up @@ -794,16 +794,8 @@ public void PublishRelease_interacts_similarly_with_PublishProfile_Configuration
.Execute("/p:PublishProfile=test");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferable to add a test showing that publish profile proeprties are imported correctly, and for properties like configuration, they actually are changed (so DebugSymbols and Optimize also change, not just configuration)

publishOutput.Should().Pass();
var releaseAssetPath = System.IO.Path.Combine(helloWorldAsset.Path, "bin", "Release", ToolsetInfo.CurrentTargetFramework, rid, "HelloWorld.dll");
if (config == "Debug")
{
Assert.True(File.Exists(releaseAssetPath)); // We ignore Debug configuration and override it, IF its custom though, we dont use publishrelease.
}
else
{
Assert.False(File.Exists(releaseAssetPath)); // build will produce a debug asset, need to make sure this doesn't exist either.
publishOutput.Should().HaveStdOutContaining("PublishRelease");
}
var configAssetPath = System.IO.Path.Combine(helloWorldAsset.Path, "bin", config, ToolsetInfo.CurrentTargetFramework, rid, "HelloWorld.dll");
Assert.True(File.Exists(configAssetPath)); // We cannot ignore Debug configuration and override it, because it's set as a globalproperty during publish.
}

[Fact]
Expand Down Expand Up @@ -1098,7 +1090,7 @@ public void It_publishes_with_implicit_rid_with_rid_specific_properties(string e
.Should()
.Pass()
.And
.NotHaveStdErrContaining("NETSDK1191"); // Publish Properties Requiring RID Checks
.NotHaveStdErrContaining("NETSDK1191"); // Publish Properties Requiring RID Checks
}

[Fact]
Expand Down