From 5c8503a7f84ac12f4390a8318812c683ca474c14 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Mon, 11 Oct 2021 17:26:11 -0700 Subject: [PATCH 01/12] Improve handling of RuntimeIdentifier and SelfContained across project references --- .../ValidateExecutableReferences.cs | 10 ++++++++++ .../targets/Microsoft.NET.Sdk.BeforeCommon.targets | 9 +++++++++ .../targets/Microsoft.NET.Sdk.targets | 13 +++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs index fe94fca6be87..61ccf4f2293f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -14,6 +14,8 @@ public class ValidateExecutableReferences : TaskBase { public bool SelfContained { get; set; } + public bool SelfContainedIsGlobalProperty { get; set; } + public bool IsExecutable { get; set; } public ITaskItem[] ReferencedProjects { get; set; } = Array.Empty(); @@ -53,6 +55,14 @@ protected override void ExecuteCore() bool referencedProjectIsExecutable = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["_IsExecutable"]); bool referencedProjectIsSelfContained = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["SelfContained"]); + if (SelfContainedIsGlobalProperty && project.GetBooleanMetadata("AcceptsRuntimeIdentifier") == true) + { + // If AcceptsRuntimeIdentifier is true for the project, and SelfContained was set as a global property, + // then the SelfContained value will flow across the project reference when we go to build it, despite the + // fact that we ignored it when doing the GetTargetFrameworks negotiation. + referencedProjectIsSelfContained = SelfContained; + } + if (referencedProjectIsExecutable && shouldBeValidatedAsExecutableReference) { if (SelfContained && !referencedProjectIsSelfContained) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets index 6bb5e58f3aff..c0dd1a09997e 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets @@ -22,6 +22,15 @@ Copyright (c) .NET Foundation. All rights reserved. <_IsExecutable Condition="'$(OutputType)' == 'Exe' or '$(OutputType)'=='WinExe'">true + + + + + + true diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index e207114a17af..41c3d43aaef0 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -1096,12 +1096,25 @@ Copyright (c) .NET Foundation. All rights reserved. <_UseAttributeForTargetFrameworkInfoPropertyNames Condition="$([MSBuild]::VersionGreaterThanOrEquals($(MSBuildVersion), '17.0'))">true + + + <_SelfContainedBackupValue>$(SelfContained) + NotAValue + <_SelfContainedIsGlobalProperty Condition="'$(SelfContained)' == '$(_SelfContainedBackupValue)'">true + $(_SelfContainedBackupValue) + <_SelfContainedBackupValue> + + Date: Mon, 11 Oct 2021 17:26:36 -0700 Subject: [PATCH 02/12] Add test for SelfContained flowing across project reference --- .../ProjectConstruction/TestProject.cs | 33 +++++++++++-------- .../GivenDotnetBuildBuildsCsproj.cs | 27 +++++++++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs index 85188252743e..411c8f96139b 100644 --- a/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs +++ b/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs @@ -327,11 +327,9 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder, if (SourceFiles.Count == 0) { - string source; - if (this.IsExe || this.IsWinExe) { - source = + string source = @"using System; class Program @@ -343,40 +341,49 @@ static void Main(string[] args) foreach (var dependency in this.ReferencedProjects) { - source += $" Console.WriteLine({dependency.Name}.{dependency.Name}Class.Name);" + Environment.NewLine; - source += $" Console.WriteLine({dependency.Name}.{dependency.Name}Class.List);" + Environment.NewLine; + string safeDependencyName = dependency.Name.Replace('.', '_'); + + source += $" Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.Name);" + Environment.NewLine; + source += $" Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.List);" + Environment.NewLine; } source += @" } }"; + string sourcePath = Path.Combine(targetFolder, this.Name + "Program.cs"); + + File.WriteAllText(sourcePath, source); } - else + { - source = + string safeThisName = this.Name.Replace('.', '_'); + string source = $@"using System; using System.Collections.Generic; -namespace {this.Name} +namespace {safeThisName} {{ - public class {this.Name}Class + public class {safeThisName}Class {{ public static string Name {{ get {{ return ""{this.Name}""; }} }} public static List List {{ get {{ return null; }} }} "; foreach (var dependency in this.ReferencedProjects) { - source += $" public string {dependency.Name}Name {{ get {{ return {dependency.Name}.{dependency.Name}Class.Name; }} }}" + Environment.NewLine; - source += $" public List {dependency.Name}List {{ get {{ return {dependency.Name}.{dependency.Name}Class.List; }} }}" + Environment.NewLine; + string safeDependencyName = dependency.Name.Replace('.', '_'); + + source += $" public string {safeDependencyName}Name {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.Name; }} }}" + Environment.NewLine; + source += $" public List {safeDependencyName}List {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.List; }} }}" + Environment.NewLine; } source += @" } }"; + string sourcePath = Path.Combine(targetFolder, this.Name + ".cs"); + + File.WriteAllText(sourcePath, source); } - string sourcePath = Path.Combine(targetFolder, this.Name + ".cs"); - File.WriteAllText(sourcePath, source); } else { diff --git a/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs b/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs index 36150c4b10f0..10981b5b5e5a 100644 --- a/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs +++ b/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs @@ -269,6 +269,33 @@ public void It_builds_with_implicit_rid_with_self_contained_option() .NotHaveStdOutContaining("NETSDK1031"); } + [Fact] + public void It_builds_referenced_exe_with_self_contained_specified_via_command_line_argument() + { + var referencedProject = new TestProject("ReferencedProject") + { + TargetFrameworks = ToolsetInfo.CurrentTargetFramework, + IsExe = true + }; + + var testProject = new TestProject("TestProject") + { + TargetFrameworks = ToolsetInfo.CurrentTargetFramework, + IsExe = true + }; + testProject.ReferencedProjects.Add(referencedProject); + + var testAsset = _testAssetsManager.CreateTestProject(testProject); + + new DotnetCommand(Log) + .WithWorkingDirectory(Path.Combine(testAsset.Path, testProject.Name)) + .Execute("build", "-r", EnvironmentInfo.GetCompatibleRid(), "--self-contained") + .Should() + .Pass() + .And + .NotHaveStdOutContaining("NETSDK1179"); + } + [Theory] [InlineData("roslyn3.9")] [InlineData("roslyn4.0")] From 63ae00b1af54cc74a8d1dd7b426e2b783e0d6911 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Mon, 11 Oct 2021 19:06:17 -0700 Subject: [PATCH 03/12] Move AcceptsRuntimeIdentifier logic and exclude test projects --- .../targets/Microsoft.NET.Sdk.BeforeCommon.targets | 8 -------- .../targets/Microsoft.NET.Sdk.targets | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets index c0dd1a09997e..2ff2c1de9993 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.BeforeCommon.targets @@ -25,14 +25,6 @@ Copyright (c) .NET Foundation. All rights reserved. - - - true - - $(_IsExecutable) <_UsingDefaultForHasRuntimeOutput>true diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index 41c3d43aaef0..42cd29e1c4bb 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -79,6 +79,14 @@ Copyright (c) .NET Foundation. All rights reserved. true + + + true + + From bf9b7a3cafc1891db2c14ea890cea6e286660162 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Tue, 12 Oct 2021 12:38:05 -0700 Subject: [PATCH 04/12] Add breaking change information --- ...SelfContainedBreakingChangeNotification.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 documentation/general/SelfContainedBreakingChangeNotification.md diff --git a/documentation/general/SelfContainedBreakingChangeNotification.md b/documentation/general/SelfContainedBreakingChangeNotification.md new file mode 100644 index 000000000000..cc7554e75ab1 --- /dev/null +++ b/documentation/general/SelfContainedBreakingChangeNotification.md @@ -0,0 +1,53 @@ +# [Breaking change]: Handling of command-line RuntimeIdentifier and SelfContained properties across project references + +## Description + +The `RuntimeIdentifier` and `SelfContained` properties can be specified on the command line to commands such as `dotnet build` and `dotnet publish`. +They can be specified either via parameters such as `-r` or `--self-contained`, or via the generic `-p:Key=Value` parameter, such as `-p:SelfContained=true`. + +If these properties are specified on the command line, we've updated how they are applied (or not applied) to projects referenced by the initial project that is being built. + +## Version + +??? + +## Previous behavior + +If `SelfContained` was specified on the command line, it would always flow to referenced projects. + +`RuntimeIdentifier` would flow to referenced projects where either the `RuntimeIdentifier` or `RuntimeIdentifiers` properties were non-empty. + +## New Behavior + +Both `SelfContained` and `RuntimeIdentifier` will flow a referenced project if any of the following are true for the referenced project: + +- The `AcceptsRuntimeIdentifier` property is set to `true` +- The `OutputType` is `Exe` or `WinExe` +- Either the `RuntimeIdentifer` or `RuntimeIdentifiers` property is non-empty + +## Type of breaking change + +Source incompatible + +## Reason for change + +As of .NET SDK 6.0.100, we recommend specifying the value for self-contained on the command line if you specify the RuntimeIdentifier. +(This is because in the future we are considering [changing the logic](https://github.com/dotnet/designs/blob/main/accepted/2021/architecture-targeting.md) +so that specifying the RuntimeIdentifier on the command line doesn't automatically set the app to self-contained.) We also added a warning message +to guide you to do so. + +However, if you followed the warning and switched to a command specifying both the RuntimeIdentifier and the value for self-contained (for example +`dotnet build -r win-x64 --self-contained`), the command could fail if you referenced an Exe project, because the `RuntimeIdentifier` you specified +would not apply to the referenced project, but the `SelfContained` value would, and it's an error for an Exe project to have `SelfContained` set to +true without having a `RuntimeIdentifier` set. + +## Recommended action + +If you were relying on the `SelfContained` property to apply to all projects when it was specified on the command line, then you can get similar behavior +by setting `AcceptsRuntimeIdentifier` to true (either in a file [such as Directory.Build.props](https://docs.microsoft.com/visualstudio/msbuild/customize-your-build#directorybuildprops-and-directorybuildtargets)), +or as a command-line parameter such as `-p:AcceptsRuntimeIdentifier=true`. + +## Open Questions + +TODO: How does this apply to solutions? Could a solution build set AcceptsRuntimeIdentifier for all projects, and would that fix other issues we have when specifying the RuntimeIdentifier for a solution build? +TODO: What happens if there's an Exe1 -> Library -> Exe2 reference, especially if there's also a direct reference from Exe1 -> Exe2 From 45f8979059e8f787d48f8b1b01e81a7952d31992 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Mon, 18 Oct 2021 15:16:39 -0700 Subject: [PATCH 05/12] Detect whether SelfContained is a global property inside task --- .../ValidateExecutableReferences.cs | 6 +++--- .../targets/Microsoft.NET.Sdk.targets | 13 ------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs index 61ccf4f2293f..901afe6edb9f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -14,8 +14,6 @@ public class ValidateExecutableReferences : TaskBase { public bool SelfContained { get; set; } - public bool SelfContainedIsGlobalProperty { get; set; } - public bool IsExecutable { get; set; } public ITaskItem[] ReferencedProjects { get; set; } = Array.Empty(); @@ -55,7 +53,9 @@ protected override void ExecuteCore() bool referencedProjectIsExecutable = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["_IsExecutable"]); bool referencedProjectIsSelfContained = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["SelfContained"]); - if (SelfContainedIsGlobalProperty && project.GetBooleanMetadata("AcceptsRuntimeIdentifier") == true) + bool selfContainedIsGlobalProperty = BuildEngine6.GetGlobalProperties().ContainsKey("SelfContained"); + + if (selfContainedIsGlobalProperty && project.GetBooleanMetadata("AcceptsRuntimeIdentifier") == true) { // If AcceptsRuntimeIdentifier is true for the project, and SelfContained was set as a global property, // then the SelfContained value will flow across the project reference when we go to build it, despite the diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index 42cd29e1c4bb..af69938c62d4 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -1104,25 +1104,12 @@ Copyright (c) .NET Foundation. All rights reserved. <_UseAttributeForTargetFrameworkInfoPropertyNames Condition="$([MSBuild]::VersionGreaterThanOrEquals($(MSBuildVersion), '17.0'))">true - - - <_SelfContainedBackupValue>$(SelfContained) - NotAValue - <_SelfContainedIsGlobalProperty Condition="'$(SelfContained)' == '$(_SelfContainedBackupValue)'">true - $(_SelfContainedBackupValue) - <_SelfContainedBackupValue> - - Date: Fri, 3 Dec 2021 15:43:43 -0800 Subject: [PATCH 06/12] Handle consuming AcceptsRuntimeIdentifier metadata from multi-targeted projects --- .../ValidateExecutableReferences.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs index 901afe6edb9f..686b0591f320 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -55,7 +55,14 @@ protected override void ExecuteCore() bool selfContainedIsGlobalProperty = BuildEngine6.GetGlobalProperties().ContainsKey("SelfContained"); - if (selfContainedIsGlobalProperty && project.GetBooleanMetadata("AcceptsRuntimeIdentifier") == true) + bool projectAcceptsRuntimeIdentifier = false; + if (projectAdditionalProperties.TryGetValue("AcceptsRuntimeIdentifier", out string acceptsRID) && + bool.TryParse(acceptsRID, out bool acceptsRIDParseResult)) + { + projectAcceptsRuntimeIdentifier = acceptsRIDParseResult; + } + + if (selfContainedIsGlobalProperty && projectAcceptsRuntimeIdentifier) { // If AcceptsRuntimeIdentifier is true for the project, and SelfContained was set as a global property, // then the SelfContained value will flow across the project reference when we go to build it, despite the From 26df179427e874b66290b31cedb305262853e013 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Thu, 11 Aug 2022 20:48:22 -0400 Subject: [PATCH 07/12] Use IsRidAgnostic instead of AcceptsRuntimeIdentifier --- .../SelfContainedBreakingChangeNotification.md | 8 ++++---- .../ValidateExecutableReferences.cs | 12 ++++++------ .../targets/Microsoft.NET.Sdk.targets | 12 +++++++----- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/documentation/general/SelfContainedBreakingChangeNotification.md b/documentation/general/SelfContainedBreakingChangeNotification.md index cc7554e75ab1..bbaeceb3fd30 100644 --- a/documentation/general/SelfContainedBreakingChangeNotification.md +++ b/documentation/general/SelfContainedBreakingChangeNotification.md @@ -21,7 +21,7 @@ If `SelfContained` was specified on the command line, it would always flow to re Both `SelfContained` and `RuntimeIdentifier` will flow a referenced project if any of the following are true for the referenced project: -- The `AcceptsRuntimeIdentifier` property is set to `true` +- The `IsRidAgnostic` property is set to `false` - The `OutputType` is `Exe` or `WinExe` - Either the `RuntimeIdentifer` or `RuntimeIdentifiers` property is non-empty @@ -44,10 +44,10 @@ true without having a `RuntimeIdentifier` set. ## Recommended action If you were relying on the `SelfContained` property to apply to all projects when it was specified on the command line, then you can get similar behavior -by setting `AcceptsRuntimeIdentifier` to true (either in a file [such as Directory.Build.props](https://docs.microsoft.com/visualstudio/msbuild/customize-your-build#directorybuildprops-and-directorybuildtargets)), -or as a command-line parameter such as `-p:AcceptsRuntimeIdentifier=true`. +by setting `IsRidAgnostic` to false either in a file ([such as Directory.Build.props](https://docs.microsoft.com/visualstudio/msbuild/customize-your-build#directorybuildprops-and-directorybuildtargets)), +or as a command-line parameter such as `-p:IsRidAgnostic=false`. ## Open Questions -TODO: How does this apply to solutions? Could a solution build set AcceptsRuntimeIdentifier for all projects, and would that fix other issues we have when specifying the RuntimeIdentifier for a solution build? +TODO: How does this apply to solutions? Could a solution build set IsRidAgnostic to false for all projects, and would that fix other issues we have when specifying the RuntimeIdentifier for a solution build? TODO: What happens if there's an Exe1 -> Library -> Exe2 reference, especially if there's also a direct reference from Exe1 -> Exe2 diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs index 686b0591f320..6bf05749558b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -55,16 +55,16 @@ protected override void ExecuteCore() bool selfContainedIsGlobalProperty = BuildEngine6.GetGlobalProperties().ContainsKey("SelfContained"); - bool projectAcceptsRuntimeIdentifier = false; - if (projectAdditionalProperties.TryGetValue("AcceptsRuntimeIdentifier", out string acceptsRID) && - bool.TryParse(acceptsRID, out bool acceptsRIDParseResult)) + bool projectIsRidAgnostic = true; + if (projectAdditionalProperties.TryGetValue("IsRidAgnostic", out string isRidAgnostic) && + bool.TryParse(isRidAgnostic, out bool isRidAgnosticParseResult)) { - projectAcceptsRuntimeIdentifier = acceptsRIDParseResult; + projectIsRidAgnostic = isRidAgnosticParseResult; } - if (selfContainedIsGlobalProperty && projectAcceptsRuntimeIdentifier) + if (selfContainedIsGlobalProperty && !projectIsRidAgnostic) { - // If AcceptsRuntimeIdentifier is true for the project, and SelfContained was set as a global property, + // If a project is NOT RID agnostic, and SelfContained was set as a global property, // then the SelfContained value will flow across the project reference when we go to build it, despite the // fact that we ignored it when doing the GetTargetFrameworks negotiation. referencedProjectIsSelfContained = SelfContained; diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index af69938c62d4..b09eebd44d49 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -79,12 +79,13 @@ Copyright (c) .NET Foundation. All rights reserved. true - - - true + + false + true false From 4468b0bdc925a964563db14541ab408d0fe524ee Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Thu, 18 Aug 2022 15:21:36 -0400 Subject: [PATCH 09/12] Add tests for global property flow across project references --- .../GlobalPropertyFlowTests.cs | 276 ++++++++++++++++++ .../Commands/DotnetBuildCommand.cs | 6 + .../ProjectConstruction/TestProject.cs | 71 +++++ 3 files changed, 353 insertions(+) create mode 100644 src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs diff --git a/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs b/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs new file mode 100644 index 000000000000..9d1faa3cfb64 --- /dev/null +++ b/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs @@ -0,0 +1,276 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Xml.Linq; +using FluentAssertions; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.ProjectConstruction; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.NET.Build.Tests +{ + public class GlobalPropertyFlowTests : SdkTest + { + TestProject _testProject; + TestProject _referencedProject; + + public GlobalPropertyFlowTests(ITestOutputHelper log) : base(log) + { + _referencedProject = new TestProject("ReferencedProject") + { + TargetFrameworks = ToolsetInfo.CurrentTargetFramework, + IsExe = false + }; + + _testProject = new TestProject("TestProject") + { + TargetFrameworks = ToolsetInfo.CurrentTargetFramework, + IsExe = true + }; + _testProject.ReferencedProjects.Add(_referencedProject); + + _testProject.RecordProperties("RuntimeIdentifier", "SelfContained"); + _referencedProject.RecordProperties("RuntimeIdentifier", "SelfContained"); + } + + TestAsset Build(bool passSelfContained, bool passRuntimeIdentifier, [CallerMemberName] string callingMethod = "", string identifier = "") + { + var testAsset = _testAssetsManager.CreateTestProject(_testProject, identifier:identifier); + + var arguments = GetDotnetArguments(passSelfContained, passRuntimeIdentifier); + + new DotnetBuildCommand(testAsset, arguments.ToArray()) + .Execute() + .Should() + .Pass(); + + return testAsset; + } + + List GetDotnetArguments(bool passSelfContained, bool passRuntimeIdentifier) + { + var runtimeIdentifier = EnvironmentInfo.GetCompatibleRid(); + + List arguments = new List(); + if (passSelfContained) + { + arguments.Add("--self-contained"); + } + if (passRuntimeIdentifier) + { + arguments.Add("-r"); + arguments.Add(runtimeIdentifier); + } + + return arguments; + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowToLibrary(bool passSelfContained, bool passRuntimeIdentifier) + { + var testAsset = Build(passSelfContained, passRuntimeIdentifier, identifier: passSelfContained.ToString() + "_" + passRuntimeIdentifier); + + bool buildingSelfContained = passSelfContained || passRuntimeIdentifier; + + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained); + ValidateProperties(testAsset, _referencedProject, expectSelfContained: false, expectRuntimeIdentifier: false); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowToExe(bool passSelfContained, bool passRuntimeIdentifier) + { + _referencedProject.IsExe = true; + + var testAsset = Build(passSelfContained, passRuntimeIdentifier, identifier: passSelfContained.ToString() + "_" + passRuntimeIdentifier); + + bool buildingSelfContained = passSelfContained || passRuntimeIdentifier; + + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained); + ValidateProperties(testAsset, _referencedProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained); + } + + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowToExeWithSelfContainedFalse(bool passSelfContained, bool passRuntimeIdentifier) + { + _referencedProject.IsExe = true; + _referencedProject.AdditionalProperties["SelfContained"] = "false"; + + string identifier = passSelfContained.ToString() + "_" + passRuntimeIdentifier; + + if (!passSelfContained && passRuntimeIdentifier) + { + // This combination results in a build error because it ends up being a self-contained Exe referencing a framework dependent one + var testAsset = _testAssetsManager.CreateTestProject(_testProject, identifier: identifier); + + new DotnetBuildCommand(testAsset, "-r", EnvironmentInfo.GetCompatibleRid()) + .Execute() + .Should() + .Fail() + .And + .HaveStdOutContaining("NETSDK1150"); + } + else + { + + var testAsset = Build(passSelfContained, passRuntimeIdentifier, identifier: identifier); + + bool buildingSelfContained = passSelfContained || passRuntimeIdentifier; + + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained); + // SelfContained will only flow to referenced project if it's explicitly passed in this case + ValidateProperties(testAsset, _referencedProject, expectSelfContained: passSelfContained, expectRuntimeIdentifier: buildingSelfContained); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowToLibraryWithRuntimeIdentifier(bool passSelfContained, bool passRuntimeIdentifier) + { + // Set a RuntimeIdentifier in the referenced project that is different from what is passed in on the command line + _referencedProject.RuntimeIdentifier = "win7-x64"; + + var testAsset = Build(passSelfContained, passRuntimeIdentifier, identifier: passSelfContained.ToString() + "_" + passRuntimeIdentifier); + + bool buildingSelfContained = passSelfContained || passRuntimeIdentifier; + + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained); + ValidateProperties(testAsset, _referencedProject, expectSelfContained: passSelfContained, expectRuntimeIdentifier: buildingSelfContained, + // Right now passing "--self-contained" also causes the RuntimeIdentifier to be passed as a global property. + // That should change with https://github.com/dotnet/sdk/pull/26143, which will likely require updating this and other tests in this class + expectedRuntimeIdentifier: buildingSelfContained ? "" : _referencedProject.RuntimeIdentifier); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowToMultitargetedProject(bool passSelfContained, bool passRuntimeIdentifier) + { + _testProject.TargetFrameworks = "net6.0;net7.0"; + + _referencedProject.TargetFrameworks = "net6.0;net7.0"; + _referencedProject.IsExe = true; + _referencedProject.ProjectChanges.Add(project => + { + project.Root.Element("PropertyGroup").Add(XElement.Parse(@"Library")); + }); + + var testAsset = Build(passSelfContained, passRuntimeIdentifier, identifier: passSelfContained.ToString() + "_" + passRuntimeIdentifier); + + bool buildingSelfContained = passSelfContained || passRuntimeIdentifier; + + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained, + targetFramework: "net6.0"); + ValidateProperties(testAsset, _testProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained, + targetFramework: "net7.0"); + ValidateProperties(testAsset, _referencedProject, expectSelfContained: false, expectRuntimeIdentifier: false, + targetFramework: "net6.0"); + ValidateProperties(testAsset, _referencedProject, expectSelfContained: buildingSelfContained, expectRuntimeIdentifier: buildingSelfContained, + targetFramework: "net7.0"); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void TestGlobalPropertyFlowInSolution(bool passSelfContained, bool passRuntimeIdentifier) + { + var identifier = passSelfContained.ToString() + "_" + passRuntimeIdentifier; + + var testAsset = _testAssetsManager.CreateTestProject(_testProject, identifier: identifier); + + new DotnetCommand(Log, "new", "sln") + .WithWorkingDirectory(testAsset.TestRoot) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(Log, "sln", "add", _testProject.Name) + .WithWorkingDirectory(testAsset.TestRoot) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(Log, "sln", "add", _referencedProject.Name) + .WithWorkingDirectory(testAsset.TestRoot) + .Execute() + .Should() + .Pass(); + + var arguments = GetDotnetArguments(passSelfContained, passRuntimeIdentifier); + + if (passSelfContained || passRuntimeIdentifier) + { + new DotnetBuildCommand(Log, arguments.ToArray()) + .WithWorkingDirectory(testAsset.TestRoot) + .Execute() + .Should() + .Fail() + .And + .HaveStdOutContaining("NETSDK1134"); + } + else + { + new DotnetBuildCommand(Log, arguments.ToArray()) + .WithWorkingDirectory(testAsset.TestRoot) + .Execute() + .Should() + .Pass(); + } + } + + private static void ValidateProperties(TestAsset testAsset, TestProject testProject, bool expectSelfContained, bool expectRuntimeIdentifier, string targetFramework = null, string expectedRuntimeIdentifier = "") + { + targetFramework = targetFramework ?? testProject.TargetFrameworks; + + + if (string.IsNullOrEmpty(expectedRuntimeIdentifier) && (expectSelfContained || expectRuntimeIdentifier)) + { + // RuntimeIdentifier might be inferred, so look at the output path to figure out what the actual value used was + string dir = (Path.Combine(testAsset.TestRoot, testProject.Name, "bin", "Debug", targetFramework)); + expectedRuntimeIdentifier = Path.GetFileName(Directory.GetDirectories(dir).Single()); + } + + var properties = testProject.GetPropertyValues(testAsset.TestRoot, targetFramework: targetFramework, runtimeIdentifier: expectedRuntimeIdentifier); + if (expectSelfContained) + { + properties["SelfContained"].ToLowerInvariant().Should().Be("true"); + } + else + { + properties["SelfContained"].ToLowerInvariant().Should().BeOneOf("false", ""); + } + + properties["RuntimeIdentifier"].Should().Be(expectedRuntimeIdentifier); + } + + } +} diff --git a/src/Tests/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs b/src/Tests/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs index b8225e0a84b6..d34e6d5301d5 100644 --- a/src/Tests/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs +++ b/src/Tests/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using Xunit.Abstractions; @@ -12,5 +13,10 @@ public DotnetBuildCommand(ITestOutputHelper log, params string[] args) : base(lo Arguments.Add("build"); Arguments.AddRange(args); } + + public DotnetBuildCommand(TestAsset testAsset, params string[] args) : this(testAsset.Log, args) + { + WorkingDirectory = Path.Combine(testAsset.TestRoot, testAsset.TestProject.Name); + } } } diff --git a/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs index 411c8f96139b..9df9d44a4f25 100644 --- a/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs +++ b/src/Tests/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs @@ -6,6 +6,7 @@ using System.Xml.Linq; using Microsoft.Build.Utilities; using NuGet.Frameworks; +using Xunit.Sdk; namespace Microsoft.NET.TestFramework.ProjectConstruction { @@ -63,6 +64,12 @@ public TestProject([CallerMemberName] string name = null) public List> ProjectChanges { get; } = new List>(); + /// + /// A list of properties to record the values for when the project is built. + /// Values can be retrieved with + /// + public List PropertiesToRecord { get; } = new List(); + public IEnumerable TargetFrameworkIdentifiers { get @@ -397,6 +404,41 @@ public class {safeThisName}Class { File.WriteAllText(Path.Combine(targetFolder, kvp.Key), kvp.Value); } + + if (PropertiesToRecord.Any()) + { + string propertiesElements = ""; + foreach (var propertyName in PropertiesToRecord) + { + propertiesElements += $" " + Environment.NewLine; + } + + string injectTargetContents = + $@" + + +{propertiesElements} + + + +"; + + injectTargetContents = injectTargetContents.Replace('`', '"'); + + string targetPath = Path.Combine(targetFolder, "obj", Name + ".csproj.WriteValuesToFile.g.targets"); + + if (!Directory.Exists(Path.GetDirectoryName(targetPath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + } + + File.WriteAllText(targetPath, injectTargetContents); + } } public void AddItem(string itemName, string attributeName, string attributeValue) @@ -409,6 +451,35 @@ public void AddItem(string itemName, Dictionary attributes) AdditionalItems.Add(new(itemName, attributes)); } + public void RecordProperties(params string[] propertyNames) + { + PropertiesToRecord.AddRange(propertyNames); + } + + public Dictionary GetPropertyValues(string testRoot, string configuration = "Debug", string targetFramework = null, string runtimeIdentifier = null) + { + var propertyValues = new Dictionary(); + + string intermediateOutputPath = Path.Combine(testRoot, Name, "obj", configuration, targetFramework ?? TargetFrameworks); + if (!string.IsNullOrEmpty(runtimeIdentifier)) + { + intermediateOutputPath = Path.Combine(intermediateOutputPath, runtimeIdentifier); + } + + foreach (var line in File.ReadAllLines(Path.Combine(intermediateOutputPath, "PropertyValues.txt"))) + { + int colonIndex = line.IndexOf(':'); + if (colonIndex > 0) + { + string propertyName = line.Substring(0, colonIndex); + string propertyValue = line.Length == colonIndex + 1 ? String.Empty : line.Substring(colonIndex + 2); + propertyValues[propertyName] = propertyValue; + } + } + + return propertyValues; + } + public static bool ReferenceAssembliesAreInstalled(TargetDotNetFrameworkVersion targetFrameworkVersion) { var referenceAssemblies = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(targetFrameworkVersion); From 8a303dd7140d2350e0f0400080fefba6d590f290 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Fri, 19 Aug 2022 17:03:09 -0400 Subject: [PATCH 10/12] Code review feedback --- .../general/SelfContainedBreakingChangeNotification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/general/SelfContainedBreakingChangeNotification.md b/documentation/general/SelfContainedBreakingChangeNotification.md index bbaeceb3fd30..66b41ff99f57 100644 --- a/documentation/general/SelfContainedBreakingChangeNotification.md +++ b/documentation/general/SelfContainedBreakingChangeNotification.md @@ -19,7 +19,7 @@ If `SelfContained` was specified on the command line, it would always flow to re ## New Behavior -Both `SelfContained` and `RuntimeIdentifier` will flow a referenced project if any of the following are true for the referenced project: +Both `SelfContained` and `RuntimeIdentifier` will flow to a referenced project if any of the following are true for the referenced project: - The `IsRidAgnostic` property is set to `false` - The `OutputType` is `Exe` or `WinExe` From 27fe291a1f78b7c2a79ce63f0f3277e203d2bb4a Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Sun, 21 Aug 2022 22:07:44 -0400 Subject: [PATCH 11/12] Require updated MSBuild for global property flow tests --- .../GlobalPropertyFlowTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs b/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs index 9d1faa3cfb64..774907d7511b 100644 --- a/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs +++ b/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs @@ -75,7 +75,7 @@ List GetDotnetArguments(bool passSelfContained, bool passRuntimeIdentifi return arguments; } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] @@ -90,7 +90,7 @@ public void TestGlobalPropertyFlowToLibrary(bool passSelfContained, bool passRun ValidateProperties(testAsset, _referencedProject, expectSelfContained: false, expectRuntimeIdentifier: false); } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] @@ -108,7 +108,7 @@ public void TestGlobalPropertyFlowToExe(bool passSelfContained, bool passRuntime } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] @@ -145,7 +145,7 @@ public void TestGlobalPropertyFlowToExeWithSelfContainedFalse(bool passSelfConta } } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] @@ -166,7 +166,7 @@ public void TestGlobalPropertyFlowToLibraryWithRuntimeIdentifier(bool passSelfCo expectedRuntimeIdentifier: buildingSelfContained ? "" : _referencedProject.RuntimeIdentifier); } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] @@ -196,7 +196,7 @@ public void TestGlobalPropertyFlowToMultitargetedProject(bool passSelfContained, targetFramework: "net7.0"); } - [Theory] + [RequiresMSBuildVersionTheory("17.4.0.41702")] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] From 4d634e9f02ce7470455935b8a40ddce6a5c3e7ff Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Mon, 22 Aug 2022 00:17:26 -0400 Subject: [PATCH 12/12] Fix tests --- .../GivenThatWeWantToBuildALibrary.cs | 7 ++++--- .../Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs | 3 ++- .../dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Tests/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs b/src/Tests/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs index 1de9ed2085e1..a73c524413e4 100644 --- a/src/Tests/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs +++ b/src/Tests/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildALibrary.cs @@ -488,8 +488,8 @@ public void It_can_use_implicitly_defined_compilation_constants(string targetFra testProj.AdditionalProperties["TargetPlatformIdentifier"] = targetPlatformIdentifier; testProj.AdditionalProperties["TargetPlatformVersion"] = targetPlatformVersion; } - var testAsset = _testAssetsManager.CreateTestProject(testProj, targetFramework); - File.WriteAllText(Path.Combine(testAsset.Path, testProj.Name, $"{testProj.Name}.cs"), @" + + testProj.SourceFiles[$"{testProj.Name}.cs"] = @" using System; class Program { @@ -529,7 +529,8 @@ static void Main(string[] args) Console.WriteLine(""IOS""); #endif } -}"); +}"; + var testAsset = _testAssetsManager.CreateTestProject(testProj, targetFramework); var buildCommand = new BuildCommand(Log, Path.Combine(testAsset.Path, testProj.Name)); buildCommand diff --git a/src/Tests/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs b/src/Tests/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs index 49c606e125a0..cdc451ae48b5 100644 --- a/src/Tests/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs +++ b/src/Tests/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs @@ -392,6 +392,7 @@ public void It_does_not_include_items_in_any_group_if_group_specific_default_inc XElement itemGroup = new XElement(ns + "ItemGroup"); project.Root.Add(itemGroup); itemGroup.Add(new XElement(ns + "Compile", new XAttribute("Include", testProject.Name + ".cs"))); + itemGroup.Add(new XElement(ns + "Compile", new XAttribute("Include", testProject.Name + "Program.cs"))); }); var projectFolder = Path.Combine(testAsset.TestRoot, testProject.Name); @@ -409,7 +410,7 @@ public void It_does_not_include_items_in_any_group_if_group_specific_default_inc var compileItems = getCompileItemsCommand.GetValues(); RemoveGeneratedCompileItems(compileItems); - compileItems.ShouldBeEquivalentTo(new[] { testProject.Name + ".cs" }); + compileItems.ShouldBeEquivalentTo(new[] { testProject.Name + ".cs", testProject.Name + "Program.cs" }); // Validate None items. var getNoneItemsCommand = new GetValuesCommand(Log, projectFolder, testProject.TargetFrameworks, "None", GetValuesCommand.ValueType.Item); diff --git a/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs b/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs index 10981b5b5e5a..e1e34c69ff5d 100644 --- a/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs +++ b/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs @@ -269,7 +269,7 @@ public void It_builds_with_implicit_rid_with_self_contained_option() .NotHaveStdOutContaining("NETSDK1031"); } - [Fact] + [RequiresMSBuildVersionFact("17.4.0.41702")] public void It_builds_referenced_exe_with_self_contained_specified_via_command_line_argument() { var referencedProject = new TestProject("ReferencedProject")