diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 52aac61ab583..2ef5a232da3f 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -23,7 +23,7 @@ class ReleasePropertyProjectLocator private bool checkSolutions; /// - /// 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. /// The boolean property to check the project for. Ex: PublishRelease /// The arguments or solution passed to a dotnet invocation. @@ -67,7 +67,7 @@ public ReleasePropertyProjectLocator(bool shouldCheckSolutionsForProjects) /// A property to enforce if we are looking into SLN files. If projects disagree on the property, throws exception. /// A project instance that will be targeted to publish/pack, etc. null if one does not exist. - public ProjectInstance GetTargetedProject(IEnumerable slnOrProjectArgs, Dictionary globalProps, string slnProjectPropertytoCheck = "") + public ProjectInstance GetTargetedProject(IEnumerable slnOrProjectArgs, Dictionary globalProps, string slnProjectPropertytoCheck = "", bool includeSolutions = true) { string potentialProject = ""; @@ -85,6 +85,11 @@ public ProjectInstance GetTargetedProject(IEnumerable 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(); @@ -92,7 +97,7 @@ public ProjectInstance GetTargetedProject(IEnumerable slnOrProjectArgs, { 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. } } @@ -185,7 +190,7 @@ private bool IsValidProjectFilePath(string path) } /// A case-insensitive dictionary of any properties passed from the user and their values. - private Dictionary GetGlobalPropertiesFromUserArgs(ParseResult parseResult) + public Dictionary GetGlobalPropertiesFromUserArgs(ParseResult parseResult) { Dictionary globalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Cli/dotnet/commands/dotnet-publish/Program.cs b/src/Cli/dotnet/commands/dotnet-publish/Program.cs index 4ddfabd66e24..9cf137368dfe 100644 --- a/src/Cli/dotnet/commands/dotnet-publish/Program.cs +++ b/src/Cli/dotnet/commands/dotnet-publish/Program.cs @@ -23,6 +23,27 @@ namespace Microsoft.DotNet.Tools.Publish { public class PublishCommand : RestoringCommand { + + /// + /// The list of properties that should be forwarded from the publish profile to the publish invocation. + /// + /// + /// 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. + /// + 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 msbuildArgs, bool noRestore, @@ -43,30 +64,73 @@ public static PublishCommand FromParseResult(ParseResult parseResult, string msb parseResult.HandleDebugSwitch(); parseResult.ShowHelpOrErrorIfAppropriate(); - var msbuildArgs = new List() - { - "-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 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()); - msbuildArgs.AddRange(slnOrProjectArgs ?? Array.Empty()); - 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 CreatePropertyListForPublishInvocation(ParseResult parseResult) { + var msbuildArgs = new List() + { + "-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 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()); + msbuildArgs.AddRange(slnOrProjectArgs ?? Array.Empty()); + + return msbuildArgs; + } + + /// + /// 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. + /// + List 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(); + } + 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; + var propertiesToForward = properties.Where(p => PropertiesToForwardFromProfile.Contains(p.Name)); + return propertiesToForward.Select(p => $"--property:{p.Name}=\"{p.Value}\"").ToList(); + } + } catch (IOException) { + return new List(); + } + } + return new List(); + }; } public static int Run(ParseResult parseResult) diff --git a/src/Common/MSBuildPropertyNames.cs b/src/Common/MSBuildPropertyNames.cs index ab9080c914cb..89163445f348 100644 --- a/src/Common/MSBuildPropertyNames.cs +++ b/src/Common/MSBuildPropertyNames.cs @@ -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"; } } diff --git a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs index 978dc4677f8a..6ab1d96d33d3 100644 --- a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs +++ b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs @@ -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] @@ -794,16 +794,8 @@ public void PublishRelease_interacts_similarly_with_PublishProfile_Configuration .Execute("/p:PublishProfile=test"); 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] @@ -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]