diff --git a/documentation/general/SelfContainedBreakingChangeNotification.md b/documentation/general/SelfContainedBreakingChangeNotification.md new file mode 100644 index 000000000000..66b41ff99f57 --- /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 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` +- 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 `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 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 fe94fca6be87..9e0b461f27c3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -52,6 +52,38 @@ protected override void ExecuteCore() bool shouldBeValidatedAsExecutableReference = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["ShouldBeValidatedAsExecutableReference"], true); bool referencedProjectIsExecutable = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["_IsExecutable"]); bool referencedProjectIsSelfContained = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["SelfContained"]); + bool referencedProjectHadSelfContainedSpecified = MSBuildUtilities.ConvertStringToBool(projectAdditionalProperties["_SelfContainedWasSpecified"]); + + var globalProperties = BuildEngine6.GetGlobalProperties(); + + bool selfContainedIsGlobalProperty = globalProperties.ContainsKey("SelfContained"); + bool runtimeIdentifierIsGlobalProperty = globalProperties.ContainsKey("RuntimeIdentifier"); + + bool projectIsRidAgnostic = true; + if (projectAdditionalProperties.TryGetValue("IsRidAgnostic", out string isRidAgnostic) && + bool.TryParse(isRidAgnostic, out bool isRidAgnosticParseResult)) + { + projectIsRidAgnostic = isRidAgnosticParseResult; + } + + if (!projectIsRidAgnostic) + { + // If the project is NOT RID agnostic, and SelfContained was set as a global property, + // then SelfContained will flow across the project reference when we go to build it, + // despite the fact that we ignored it when doing the GetTargetFrameworks negotiation + if (selfContainedIsGlobalProperty && SelfContained) + { + referencedProjectIsSelfContained = true; + } + + // If the project is NOT RID agnostic, then a global RuntimeIdentifier will flow to it. + // If the project didn't explicitly specify a value for SelfContained, then this will + // set SelfContained to true + if (runtimeIdentifierIsGlobalProperty && !referencedProjectHadSelfContainedSpecified) + { + referencedProjectIsSelfContained = true; + } + } if (referencedProjectIsExecutable && shouldBeValidatedAsExecutableReference) { 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..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 @@ -22,8 +22,9 @@ Copyright (c) .NET Foundation. All rights reserved. <_IsExecutable Condition="'$(OutputType)' == 'Exe' or '$(OutputType)'=='WinExe'">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 e207114a17af..b7ebf6dd1c37 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,15 @@ Copyright (c) .NET Foundation. All rights reserved. true + + + false + true + + @@ -1069,9 +1078,11 @@ Copyright (c) .NET Foundation. All rights reserved. + + - + false 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/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs b/src/Tests/Microsoft.NET.Build.Tests/GlobalPropertyFlowTests.cs new file mode 100644 index 000000000000..774907d7511b --- /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; + } + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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); + } + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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); + } + + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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); + } + } + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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); + } + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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"); + } + + [RequiresMSBuildVersionTheory("17.4.0.41702")] + [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 85188252743e..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 @@ -327,11 +334,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 +348,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 { @@ -390,6 +404,41 @@ public class {this.Name}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) @@ -402,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); diff --git a/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs b/src/Tests/dotnet-build.Tests/GivenDotnetBuildBuildsCsproj.cs index 36150c4b10f0..e1e34c69ff5d 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"); } + [RequiresMSBuildVersionFact("17.4.0.41702")] + 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")]