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")]