diff --git a/Directory.Packages.props b/Directory.Packages.props index 91b6fe5ce9bb..8b7dd4136423 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -77,6 +78,7 @@ + diff --git a/documentation/general/decouple-vs-and-net-sdk.md b/documentation/general/decouple-vs-and-net-sdk.md index 84d6e739c9dd..d2a201cd391e 100644 --- a/documentation/general/decouple-vs-and-net-sdk.md +++ b/documentation/general/decouple-vs-and-net-sdk.md @@ -117,13 +117,19 @@ There is nothing preventing NuGet based analyzers from following the same model Solutions that mix .NET SDK and Visual Studio projects will end up with multiple compiler servers running. This is a result of the .NET SDK projects using the compiler from the .NET SDK and non-SDK projects using the compiler from Visual Studio. There is nothing functionally wrong with this but it's possible customers will notice this and ask questions about it. -The compiler will offer a property that allows SDK projects to use the MSBuild version of the compiler when being built with `msbuild`: `true`. This can be added to a `Directory.Build.props` file to impact the entire solution. This is not expected to be a common scenario but is available for customers who need it. This property will be ignored when using `dotnet build` as there is no way to fall back to the Visual Studio compiler in that scenario. +The compiler will offer a property that allows SDK projects to use the MSBuild version of the compiler when being built with `msbuild`: `Framework`. This can be added to a `Directory.Build.props` file to impact the entire solution. This is not expected to be a common scenario but is available for customers who need it. This property will be ignored when using `dotnet build` as there is no way to fall back to the Visual Studio compiler in that scenario. + +> [!NOTE] +> These values are recognized for property `RoslynCompilerType`: +> - `Core`: use the compiler that comes with the .NET SDK +> - `Framework`: use the compiler that comes with .NET Framework MSBuild +> - `FrameworkPackage`: download package with .NET Framework compiler corresponding to the .NET SDK version ### .NET Framework based analyzers There are a few analyzers which are built against .NET Framework TFMs. That means when loaded in a .NET Core compiler it could lead to compatibility issues. This is not expected to be a significant issue as our processes have been pushing customers to target `netstandard` in analyzers for 5+ years. However it is possible that some customers will run into issues here. -For those customers we will recommend that they set `true` in their build to ensure a .NET Framework based compiler is used. However, it is not our intent to support loading .NET Framework based analyzers in perpetuity. Starting in .NET 12 the compiler will begin issueing warnings is this setup when it detects framework analyzers, and this will become an error in .NET 13. Non-SDK projects will support loading framework based analyzers for the foreseeable future. +For those customers we will recommend that they set `Framework` in their build to ensure a .NET Framework based compiler is used. However, it is not our intent to support loading .NET Framework based analyzers in perpetuity. Starting in .NET 12 the compiler will begin issueing warnings is this setup when it detects framework analyzers, and this will become an error in .NET 13. Non-SDK projects will support loading framework based analyzers for the foreseeable future. ### Build server shutdown diff --git a/documentation/general/torn-sdk.md b/documentation/general/torn-sdk.md index e8de8b9f945d..53f83199ef0e 100644 --- a/documentation/general/torn-sdk.md +++ b/documentation/general/torn-sdk.md @@ -1,5 +1,8 @@ # Torn .NET SDK +> [!CAUTION] +> This document has been superseded by [Decoupling the .NET SDK and Visual Studio](decouple-vs-and-net-sdk.md). + ## Terminology - **msbuild**: refers the .NET Framework based msbuild included with Visual Studio. diff --git a/sdk.sln b/sdk.sln index 30558ecf99fa..153855a4c2f3 100644 --- a/sdk.sln +++ b/sdk.sln @@ -19,7 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build.sh = build.sh Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props LICENSE.TXT = LICENSE.TXT + NuGet.config = NuGet.config README.md = README.md restore.cmd = restore.cmd restore.sh = restore.sh diff --git a/src/Layout/redist/targets/Crossgen.targets b/src/Layout/redist/targets/Crossgen.targets index 44803ba946af..3a94406077cf 100644 --- a/src/Layout/redist/targets/Crossgen.targets +++ b/src/Layout/redist/targets/Crossgen.targets @@ -57,7 +57,7 @@ - + diff --git a/src/Layout/redist/targets/GenerateLayout.targets b/src/Layout/redist/targets/GenerateLayout.targets index 4e85b6f2fd60..7938cfcd2869 100644 --- a/src/Layout/redist/targets/GenerateLayout.targets +++ b/src/Layout/redist/targets/GenerateLayout.targets @@ -50,6 +50,8 @@ + + diff --git a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets index 3bd2e53fe1eb..0b008ea415bd 100644 --- a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets +++ b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Compilation.targets @@ -129,6 +129,7 @@ Copyright (c) .NET Foundation. All rights reserved. ChecksumAlgorithm="$(ChecksumAlgorithm)" CodeAnalysisRuleSet="$(ResolvedCodeAnalysisRuleSet)" CodePage="$(CodePage)" + CompilerType="$(RoslynCompilerType)" DebugType="$(DebugType)" DefineConstants="$(DefineConstants)" DelaySign="$(DelaySign)" diff --git a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Component.targets b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Component.targets index f7d59437130e..e5fc1a9a2ce3 100644 --- a/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Component.targets +++ b/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.Component.targets @@ -195,6 +195,7 @@ Copyright (c) .NET Foundation. All rights reserved. ChecksumAlgorithm="$(ChecksumAlgorithm)" CodeAnalysisRuleSet="$(ResolvedCodeAnalysisRuleSet)" CodePage="$(CodePage)" + CompilerType="$(RoslynCompilerType)" DebugType="$(DebugType)" DefineConstants="$(DefineConstants)" DelaySign="$(DelaySign)" 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 51c3f9b12aec..6f90f3514f0b 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 @@ -299,4 +299,45 @@ Copyright (c) .NET Foundation. All rights reserved. true + + + + + + + + FrameworkPackage + + + + + Core + + + + + FrameworkPackage + + + + + Framework + + + + + $(MSBuildThisFileDirectory)..\..\..\Roslyn + $(MSBuildThisFileDirectory)..\..\..\Roslyn\binfx + $(MSBuildThisFileDirectory)..\..\..\Roslyn\Microsoft.CSharp.Core.targets + $(MSBuildThisFileDirectory)..\..\..\Roslyn\Microsoft.VisualBasic.Core.targets + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets index 56a460df4ff0..6420fa1f7377 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets @@ -75,17 +75,7 @@ Copyright (c) .NET Foundation. All rights reserved. - - - true - - - + $(NuGetPackageRoot)\microsoft.net.sdk.compilers.toolset\$(NETCoreSdkVersion) <_NeedToDownloadMicrosoftNetSdkCompilersToolsetPackage>true <_MicrosoftNetSdkCompilersToolsetPackageRootEmpty Condition="'$(NuGetPackageRoot)' == ''">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 8688eee3481f..464dd8659a7c 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 @@ -1235,6 +1235,7 @@ Copyright (c) .NET Foundation. All rights reserved. PublicSign="$(PublicSign)" PathMap="$(PathMap)" Features="$(Features)" + CompilerType="$(RoslynCompilerType)" DelaySign="$(DelaySign)" Deterministic="$(Deterministic)" DisabledWarnings="$(DisabledWarnings)" diff --git a/test/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj b/test/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj index 7d6c4864d635..cda2e95e19e4 100644 --- a/test/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj +++ b/test/Microsoft.NET.Build.Tests/Microsoft.NET.Build.Tests.csproj @@ -21,13 +21,16 @@ + + + diff --git a/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs new file mode 100644 index 000000000000..5cb8703ef2df --- /dev/null +++ b/test/Microsoft.NET.Build.Tests/RoslynBuildTaskTests.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Basic.CompilerLog.Util; +using Microsoft.Build.Logging.StructuredLogger; + +namespace Microsoft.NET.Build.Tests; + +public sealed class RoslynBuildTaskTests(ITestOutputHelper log) : SdkTest(log) +{ + private const string CoreCompilerFileName = "csc.dll"; + private const string FxCompilerFileName = "csc.exe"; + + [FullMSBuildOnlyTheory, CombinatorialData] + public void FullMSBuild_SdkStyle(bool useSharedCompilation) + { + var testAsset = CreateProject(useSharedCompilation); + var buildCommand = BuildAndRunUsingMSBuild(testAsset); + VerifyCompiler(buildCommand, CoreCompilerFileName, useSharedCompilation); + } + + [FullMSBuildOnlyTheory, CombinatorialData] + public void FullMSBuild_SdkStyle_OptOut(bool useSharedCompilation) + { + var testAsset = CreateProject(useSharedCompilation).WithProjectChanges(static doc => + { + doc.Root!.Element("PropertyGroup")!.Add(new XElement("RoslynCompilerType", "Framework")); + }); + var buildCommand = BuildAndRunUsingMSBuild(testAsset); + VerifyCompiler(buildCommand, FxCompilerFileName, useSharedCompilation); + } + + [FullMSBuildOnlyTheory, CombinatorialData] + public void FullMSBuild_NonSdkStyle(bool useSharedCompilation) + { + var testAsset = CreateProject(useSharedCompilation, static project => + { + project.IsSdkProject = false; + project.TargetFrameworkVersion = "v4.7.2"; + }); + var buildCommand = BuildAndRunUsingMSBuild(testAsset); + VerifyCompiler(buildCommand, FxCompilerFileName, useSharedCompilation); + } + + [Theory, CombinatorialData] + public void DotNet(bool useSharedCompilation) + { + var testAsset = CreateProject(useSharedCompilation); + var buildCommand = BuildAndRunUsingDotNet(testAsset); + VerifyCompiler(buildCommand, CoreCompilerFileName, useSharedCompilation); + } + + private TestAsset CreateProject(bool useSharedCompilation, Action? configure = null, [CallerMemberName] string callingMethod = "") + { + var project = new TestProject + { + Name = "App1", + IsExe = true, + SourceFiles = + { + ["Program.cs"] = """ + class Program + { + static void Main() + { + System.Console.WriteLine(40 + 2); + } + } + """, + }, + }; + + // UseSharedCompilation should be the default, so set it only if false. + if (!useSharedCompilation) + { + project.AdditionalProperties["UseSharedCompilation"] = "false"; + } + + configure?.Invoke(project); + return _testAssetsManager.CreateTestProject(project, callingMethod: callingMethod); + } + + private TestCommand BuildAndRunUsingMSBuild(TestAsset testAsset) + { + var buildCommand = new MSBuildCommand(testAsset, "Build"); + buildCommand.WithWorkingDirectory(testAsset.Path) + .Execute("-bl").Should().Pass(); + + Run(buildCommand.GetOutputDirectory().File(testAsset.TestProject!.GetOutputFileName())); + + return buildCommand; + } + + private TestCommand BuildAndRunUsingDotNet(TestAsset testAsset) + { + var buildCommand = new DotnetBuildCommand(testAsset); + buildCommand.Execute("-bl").Should().Pass(); + + Run(buildCommand.GetOutputDirectory().File(testAsset.TestProject!.GetOutputFileName())); + + return buildCommand; + } + + private void Run(FileInfo outputFile) + { + var runCommand = new RunExeCommand(Log, outputFile.FullName); + runCommand.Execute().Should().Pass() + .And.HaveStdOut("42"); + } + + private static void VerifyCompiler(TestCommand buildCommand, string compilerFileName, bool usedCompilerServer) + { + var binaryLogPath = Path.Join(buildCommand.WorkingDirectory, "msbuild.binlog"); + using (var reader = BinaryLogReader.Create(binaryLogPath)) + { + var call = reader.ReadAllCompilerCalls().Should().ContainSingle().Subject; + Path.GetFileName(call.CompilerFilePath).Should().Be(compilerFileName); + } + + // Verify compiler server message. + var compilerServerMesssages = BinaryLog.ReadBuild(binaryLogPath).FindChildrenRecursive( + static message => message.Text.StartsWith("CompilerServer:", StringComparison.Ordinal)); + compilerServerMesssages.Should().ContainSingle().Which.Text.Should().StartWith(usedCompilerServer + ? "CompilerServer: server - server processed compilation - " + : "CompilerServer: tool - using command line tool by design"); + } +} diff --git a/test/Microsoft.NET.Restore.Tests/GivenThatWeWantToUseFrameworkRoslyn.cs b/test/Microsoft.NET.Restore.Tests/GivenThatWeWantToUseFrameworkRoslyn.cs index 0aaf506b7854..e5bd7e1fec70 100644 --- a/test/Microsoft.NET.Restore.Tests/GivenThatWeWantToUseFrameworkRoslyn.cs +++ b/test/Microsoft.NET.Restore.Tests/GivenThatWeWantToUseFrameworkRoslyn.cs @@ -59,6 +59,9 @@ public void It_downloads_Microsoft_Net_Compilers_Toolset_Framework_when_MSBuild_ // simulate mismatched MSBuild versions project.AdditionalProperties.Add("_IsDisjointMSBuildVersion", "true"); + // avoid opt in to RoslynCompilerType=Core + string[] args = ["-p:DOTNET_HOST_PATH=", "-p:DOTNET_EXPERIMENTAL_HOST_PATH="]; + var testAsset = _testAssetsManager .CreateTestProject(project); @@ -68,7 +71,7 @@ public void It_downloads_Microsoft_Net_Compilers_Toolset_Framework_when_MSBuild_ testAsset.GetRestoreCommand(Log, relativePath: testProjectName) .WithEnvironmentVariable("NUGET_PACKAGES", customPackagesDir) - .Execute().Should().Pass(); + .Execute(args).Should().Pass(); var toolsetPackageDir = Path.Combine(customPackagesDir, "microsoft.net.sdk.compilers.toolset"); @@ -78,7 +81,7 @@ public void It_downloads_Microsoft_Net_Compilers_Toolset_Framework_when_MSBuild_ new BuildCommand(testAsset) .WithEnvironmentVariable("NUGET_PACKAGES", customPackagesDir) - .Execute().Should().Pass().And + .Execute(args).Should().Pass().And .HaveStdOutContaining(Path.Combine(toolsetPackageDir, toolsetPackageVersion, "csc.exe") + " /noconfig"); } @@ -144,6 +147,9 @@ public void It_throws_a_warning_when_NuGetPackageRoot_is_empty() // simulate mismatched MSBuild versions project.AdditionalProperties.Add("_IsDisjointMSBuildVersion", "true"); + // avoid opt in to RoslynCompilerType=Core + string[] args = ["-p:DOTNET_HOST_PATH=", "-p:DOTNET_EXPERIMENTAL_HOST_PATH="]; + var testAsset = _testAssetsManager .CreateTestProject(project); @@ -153,7 +159,7 @@ public void It_throws_a_warning_when_NuGetPackageRoot_is_empty() var command = (MSBuildCommand)new MSBuildCommand(testAsset, "Restore;Build") .WithEnvironmentVariable("NUGET_PACKAGES", customPackagesDir); - command.ExecuteWithoutRestore() + command.ExecuteWithoutRestore(args) .Should().Pass().And.HaveStdOutContaining("NETSDK1221"); // The package is downloaded, but the targets cannot find the path to it @@ -178,7 +184,8 @@ public void It_does_not_throw_a_warning_when_NuGetPackageRoot_is_empty_in_wpftmp }; // simulate mismatched MSBuild versions via _IsDisjointMSBuildVersion - buildCommand.Execute("-p:_IsDisjointMSBuildVersion=true") + // avoid opt in to RoslynCompilerType=Core by unsetting DOTNET_HOST_PATH and DOTNET_EXPERIMENTAL_HOST_PATH + buildCommand.Execute("-p:_IsDisjointMSBuildVersion=true", "-p:DOTNET_HOST_PATH=", "-p:DOTNET_EXPERIMENTAL_HOST_PATH=") .Should().Pass().And.NotHaveStdOutContaining("NETSDK1221"); Assert.True(File.Exists(Path.Combine(testAsset.Path, "obj", "net472", "MainWindow.g.cs"))); diff --git a/test/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs b/test/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs index 8c5cce77583d..ad01fa1aed01 100644 --- a/test/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs +++ b/test/Microsoft.NET.TestFramework/Commands/DotnetBuildCommand.cs @@ -1,10 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; + namespace Microsoft.NET.TestFramework.Commands { public class DotnetBuildCommand : DotnetCommand { + public TestAsset? TestAsset { get; } + public DotnetBuildCommand(ITestOutputHelper log, params string[] args) : base(log) { Arguments.Add("build"); @@ -13,6 +17,8 @@ public DotnetBuildCommand(ITestOutputHelper log, params string[] args) : base(lo public DotnetBuildCommand(TestAsset testAsset, params string[] args) : this(testAsset.Log, args) { + TestAsset = testAsset; + if (testAsset.TestProject != null && testAsset.TestProject.Name is not null) { WorkingDirectory = Path.Combine(testAsset.TestRoot, testAsset.TestProject.Name); @@ -22,5 +28,13 @@ public DotnetBuildCommand(TestAsset testAsset, params string[] args) : this(test WorkingDirectory = testAsset.TestRoot; } } + + public DirectoryInfo GetOutputDirectory(string? targetFramework = null, string configuration = "Debug", string? runtimeIdentifier = null, string? platform = null) + { + Debug.Assert(TestAsset?.TestProject?.Name != null); + var projectPath = Path.Combine(TestAsset.Path, TestAsset.TestProject.Name); + return new DirectoryInfo(OutputPathCalculator.FromProject(projectPath, TestAsset) + .GetOutputDirectory(targetFramework, configuration, runtimeIdentifier, platform)); + } } } diff --git a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs index 0c329ae25b30..8e8939ee5afb 100644 --- a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs +++ b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.Build.Utilities; using NuGet.Frameworks; @@ -533,5 +534,14 @@ public string GetOutputDirectory(string testRoot, string? targetFramework = null return GetOutputPathCalculator(testRoot) .GetOutputDirectory(targetFramework, configuration, runtimeIdentifier); } + + public string GetOutputFileName() + { + Debug.Assert(Name != null); + var extension = IsExe + ? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty) + : ".dll"; + return Name + extension; + } } }