diff --git a/Directory.Packages.props b/Directory.Packages.props index 104dedfd9eaf..07229dedec94 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ + diff --git a/NuGet.config b/NuGet.config index 09870da28882..b7be4e0c5b7f 100644 --- a/NuGet.config +++ b/NuGet.config @@ -32,6 +32,7 @@ + diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs index d5dd7f30c73a..a8bcca667ff9 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs @@ -86,12 +86,12 @@ private static IEnumerable EscapeArgArrayForCmd(IEnumerable argu return escapedArgs; } - public static string EscapeSingleArg(string arg) + public static string EscapeSingleArg(string arg, Func? additionalShouldSurroundWithQuotes = null) { var sb = new StringBuilder(); var length = arg.Length; - var needsQuotes = length == 0 || ShouldSurroundWithQuotes(arg); + var needsQuotes = length == 0 || ShouldSurroundWithQuotes(arg) || additionalShouldSurroundWithQuotes?.Invoke(arg) == true; var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg); if (needsQuotes) sb.Append("\""); diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs index 8d61c427aace..60f6aaf151f5 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs @@ -5,18 +5,32 @@ namespace Microsoft.DotNet.Cli.Utils; -public class Product +public static class Product { public static string LongName => LocalizableStrings.DotNetSdkInfo; - public static readonly string Version = GetProductVersion(); + public static readonly string Version; + public static readonly string TargetFrameworkVersion; - private static string GetProductVersion() + static Product() { DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; - return versionFile.BuildNumber ?? + Version = versionFile.BuildNumber ?? System.Diagnostics.FileVersionInfo.GetVersionInfo( typeof(Product).GetTypeInfo().Assembly.Location) .ProductVersion ?? string.Empty; + + int firstDotIndex = Version.IndexOf('.'); + if (firstDotIndex >= 0) + { + int secondDotIndex = Version.IndexOf('.', firstDotIndex + 1); + TargetFrameworkVersion = secondDotIndex >= 0 + ? Version.Substring(0, secondDotIndex) + : Version; + } + else + { + TargetFrameworkVersion = string.Empty; + } } } diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 75c76f009030..4bed58e12ecd 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1539,9 +1539,13 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man {0} is an option name like '--no-build'. - Warning: Binary log option was specified but build will be skipped because output is up to date, specify '--no-cache' to force build. + Warning: Binary log option was specified but build will be skipped because output is up to date. Specify '--no-cache' to force build. {Locked="--no-cache"} + + Warning: Binary log option was specified but MSBuild will be skipped because running just csc is enough. Specify '--no-cache' to force full build. + {Locked="--no-cache"}{Locked="MSBuild"}{Locked="csc"} + Publisher for the .NET Platform diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs new file mode 100644 index 000000000000..51cc8774cceb --- /dev/null +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.Generated.cs @@ -0,0 +1,236 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Cli.Commands.Run; + +// Generated by test `RunFileTests.CscArguments`. +partial class CSharpCompilerCommand +{ + private IEnumerable GetCscArguments( + string fileNameWithoutExtension, + string objDir, + string binDir) + { + return + [ + "/unsafe-", + "/checked-", + "/nowarn:1701,1702,IL2121,1701,1702", + "/fullpaths", + "/nostdlib+", + "/errorreport:prompt", + "/warn:10", + "/define:TRACE;DEBUG;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER", + "/highentropyva+", + "/nullable:enable", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/Microsoft.CSharp.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/Microsoft.VisualBasic.Core.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/Microsoft.VisualBasic.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/Microsoft.Win32.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/Microsoft.Win32.Registry.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/mscorlib.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/netstandard.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.AppContext.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Buffers.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Collections.Concurrent.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Collections.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Collections.Immutable.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Collections.NonGeneric.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Collections.Specialized.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.Annotations.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.DataAnnotations.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.EventBasedAsync.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ComponentModel.TypeConverter.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Configuration.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Console.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Core.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Data.Common.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Data.DataSetExtensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Data.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.Contracts.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.Debug.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.DiagnosticSource.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.FileVersionInfo.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.Process.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.StackTrace.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.TextWriterTraceListener.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.Tools.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.TraceSource.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Diagnostics.Tracing.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Drawing.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Drawing.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Dynamic.Runtime.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Formats.Asn1.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Formats.Tar.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Globalization.Calendars.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Globalization.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Globalization.Extensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Compression.Brotli.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Compression.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Compression.FileSystem.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Compression.ZipFile.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.FileSystem.AccessControl.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.FileSystem.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.FileSystem.DriveInfo.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.FileSystem.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.FileSystem.Watcher.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.IsolatedStorage.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.MemoryMappedFiles.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Pipelines.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Pipes.AccessControl.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.Pipes.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.IO.UnmanagedMemoryStream.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Linq.AsyncEnumerable.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Linq.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Linq.Expressions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Linq.Parallel.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Linq.Queryable.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Memory.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Http.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Http.Json.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.HttpListener.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Mail.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.NameResolution.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.NetworkInformation.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Ping.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Quic.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Requests.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Security.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.ServerSentEvents.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.ServicePoint.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.Sockets.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.WebClient.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.WebHeaderCollection.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.WebProxy.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.WebSockets.Client.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Net.WebSockets.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Numerics.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Numerics.Vectors.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ObjectModel.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.DispatchProxy.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Emit.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Emit.ILGeneration.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Emit.Lightweight.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Extensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Metadata.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Reflection.TypeExtensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Resources.Reader.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Resources.ResourceManager.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Resources.Writer.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.CompilerServices.Unsafe.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.CompilerServices.VisualC.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Extensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Handles.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.InteropServices.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.InteropServices.JavaScript.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.InteropServices.RuntimeInformation.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Intrinsics.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Loader.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Numerics.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Serialization.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Serialization.Formatters.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Serialization.Json.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Serialization.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Runtime.Serialization.Xml.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.AccessControl.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Claims.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.Algorithms.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.Cng.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.Csp.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.Encoding.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.OpenSsl.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.Primitives.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Cryptography.X509Certificates.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Principal.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.Principal.Windows.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Security.SecureString.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ServiceModel.Web.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ServiceProcess.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.Encoding.CodePages.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.Encoding.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.Encoding.Extensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.Encodings.Web.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.Json.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Text.RegularExpressions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.AccessControl.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Channels.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Overlapped.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Tasks.Dataflow.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Tasks.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Tasks.Extensions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Tasks.Parallel.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Thread.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.ThreadPool.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Threading.Timer.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Transactions.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Transactions.Local.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.ValueTuple.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Web.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Web.HttpUtility.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Windows.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.Linq.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.ReaderWriter.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.Serialization.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.XDocument.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.XmlDocument.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.XmlSerializer.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.XPath.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/System.Xml.XPath.XDocument.dll", + $"/reference:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/ref/net10.0/WindowsBase.dll", + "/debug+", + "/debug:portable", + "/filealign:512", + "/optimize-", + $"/out:{binDir}/{fileNameWithoutExtension}.dll", + "/target:exe", + "/warnaserror-", + "/utf8output", + "/deterministic+", + "/langversion:13.0", + "/features:FileBasedProgram", + $"/analyzerconfig:{SdkPath}/Sdks/Microsoft.NET.Sdk/codestyle/cs/build/config/analysislevelstyle_default.globalconfig", + $"/analyzerconfig:{objDir}/{fileNameWithoutExtension}.GeneratedMSBuildEditorConfig.editorconfig", + $"/analyzerconfig:{SdkPath}/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig", + $"/analyzer:{SdkPath}/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll", + $"/analyzer:{SdkPath}/Sdks/Microsoft.NET.Sdk/targets/../analyzers/Microsoft.CodeAnalysis.NetAnalyzers.dll", + $"/analyzer:{NuGetCachePath}/microsoft.net.illink.tasks/{RuntimeVersion}/analyzers/dotnet/cs/ILLink.CodeFixProvider.dll", + $"/analyzer:{NuGetCachePath}/microsoft.net.illink.tasks/{RuntimeVersion}/analyzers/dotnet/cs/ILLink.RoslynAnalyzer.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll", + $"/analyzer:{DotNetRootPath}/packs/Microsoft.NETCore.App.Ref/{RuntimeVersion}/analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll", + $"{EntryPointFileFullPath}", + $"{objDir}/{fileNameWithoutExtension}.GlobalUsings.g.cs", + $"{objDir}/.NETCoreApp,Version=v10.0.AssemblyAttributes.cs", + $"{objDir}/{fileNameWithoutExtension}.AssemblyInfo.cs", + "/warnaserror+:NU1605,SYSLIB0011", + ]; + } + + /// + /// Files that come from referenced NuGet packages (e.g., analyzers for NativeAOT) need to be checked specially (if they don't exist, MSBuild needs to run). + /// + public static IEnumerable GetPathsOfCscInputsFromNuGetCache() + { + return + [ + $"{NuGetCachePath}/microsoft.net.illink.tasks/{RuntimeVersion}/analyzers/dotnet/cs/ILLink.CodeFixProvider.dll", + $"{NuGetCachePath}/microsoft.net.illink.tasks/{RuntimeVersion}/analyzers/dotnet/cs/ILLink.RoslynAnalyzer.dll", + ]; + } +} diff --git a/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs new file mode 100644 index 000000000000..c07736b79d79 --- /dev/null +++ b/src/Cli/dotnet/Commands/Run/CSharpCompilerCommand.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text.Json; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CommandLine; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.NET.HostModel.AppHost; +using NuGet.Configuration; + +namespace Microsoft.DotNet.Cli.Commands.Run; + +/// +/// Used to invoke C# compiler in some optimized paths of dotnet run file.cs. +/// +internal sealed partial class CSharpCompilerCommand +{ + private static readonly SearchValues s_additionalShouldSurroundWithQuotes = SearchValues.Create('=', ','); + + /// + /// Options which denote paths and which might appear in the simple app compilation that we optimize for. + /// + private static readonly ImmutableArray s_pathOptions = + [ + "reference:", + "analyzer:", + "additionalfile:", + "analyzerconfig:", + "embed:", + "resource:", + "linkresource:", + "ruleset:", + "keyfile:", + "link:", + ]; + + private static string SdkPath => field ??= PathUtility.EnsureNoTrailingDirectorySeparator(AppContext.BaseDirectory); + private static string DotNetRootPath => field ??= Path.GetDirectoryName(Path.GetDirectoryName(SdkPath)!)!; + private static string ClientDirectory => field ??= Path.Combine(SdkPath, "Roslyn", "bincore"); + private static string NuGetCachePath => field ??= SettingsUtility.GetGlobalPackagesFolder(Settings.LoadDefaultSettings(null)); + internal static string RuntimeVersion => field ??= RuntimeInformation.FrameworkDescription.Split(' ').Last(); + private static string TargetFrameworkVersion => Product.TargetFrameworkVersion; + + public required string EntryPointFileFullPath { get; init; } + public required string ArtifactsPath { get; init; } + public required bool CanReuseAuxiliaryFiles { get; init; } + + /// + /// Whether the returned error code should not cause the build to fail but instead fallback to full MSBuild. + /// + public int Execute(out bool fallbackToNormalBuild) + { + // Write .rsp file and other intermediate build outputs. + PrepareAuxiliaryFiles(out string rspPath); + + // Create a request for the compiler server + // (this is much faster than starting a csc.dll process, especially on Windows). + var buildRequest = BuildServerConnection.CreateBuildRequest( + requestId: EntryPointFileFullPath, + language: RequestLanguage.CSharpCompile, + arguments: ["/noconfig", "/nologo", $"@{EscapeSingleArg(rspPath)}"], + workingDirectory: Environment.CurrentDirectory, + tempDirectory: Path.GetTempPath(), + keepAlive: null, + libDirectory: null, + compilerHash: GetCompilerCommitHash()); + + // Get pipe name. + var pipeName = BuildServerConnection.GetPipeName(clientDirectory: ClientDirectory); + + // Create logger. + var logger = new CompilerServerLogger( + identifier: $"dotnet run file {Environment.ProcessId}", + loggingFilePath: null); + + // Send the request. + var responseTask = BuildServerConnection.RunServerBuildRequestAsync( + buildRequest, + pipeName: pipeName, + clientDirectory: ClientDirectory, + logger, + cancellationToken: default); + + // Process the response. + return ProcessBuildResponse(responseTask.Result, out fallbackToNormalBuild); + + static string GetCompilerCommitHash() + { + return typeof(CSharpCompilation).Assembly.GetCustomAttributesData() + .FirstOrDefault(attr => attr.AttributeType.FullName == "Microsoft.CodeAnalysis.CommitHashAttribute")? + .ConstructorArguments + .FirstOrDefault() + .Value as string + ?? throw new InvalidOperationException("Could not find compiler commit hash in the assembly attributes."); + } + + static int ProcessBuildResponse(BuildResponse response, out bool fallbackToNormalBuild) + { + switch (response) + { + case CompletedBuildResponse completed: + Reporter.Verbose.WriteLine("Compiler server processed compilation."); + Reporter.Output.Write(completed.Output); + fallbackToNormalBuild = false; + return completed.ReturnCode; + + case IncorrectHashBuildResponse: + Reporter.Error.WriteLine("Error: Compiler server reports a different hash version than the SDK.".Red()); + fallbackToNormalBuild = false; + return 1; + + case null: + Reporter.Output.WriteLine("Warning: Could not launch the compiler server.".Yellow()); + fallbackToNormalBuild = true; + return 1; + + default: + Reporter.Error.WriteLine($"Warning: Compiler server returned unexpected response: {response.GetType().Name}".Yellow()); + fallbackToNormalBuild = true; + return 1; + } + } + } + + private void PrepareAuxiliaryFiles(out string rspPath) + { + Reporter.Verbose.WriteLine(CanReuseAuxiliaryFiles + ? "CSC auxiliary files can be reused." + : "CSC auxiliary files can NOT be reused."); + + string fileDirectory = Path.GetDirectoryName(EntryPointFileFullPath) ?? string.Empty; + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(EntryPointFileFullPath); + + // Note that Release builds won't go through this optimized code path because `-c Release` translates to global property `Configuration=Release` + // and customizing global properties triggers a full MSBuild run. + string objDir = Path.Join(ArtifactsPath, "obj", "debug"); + Directory.CreateDirectory(objDir); + string binDir = Path.Join(ArtifactsPath, "bin", "debug"); + Directory.CreateDirectory(binDir); + + string assemblyAttributes = Path.Join(objDir, $".NETCoreApp,Version=v{TargetFrameworkVersion}.AssemblyAttributes.cs"); + if (ShouldEmit(assemblyAttributes)) + { + File.WriteAllText(assemblyAttributes, /* lang=C#-test */ $""" + // + using System; + using System.Reflection; + [assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v{TargetFrameworkVersion}", FrameworkDisplayName = ".NET {TargetFrameworkVersion}")] + + """); + } + + string globalUsings = Path.Join(objDir, $"{fileNameWithoutExtension}.GlobalUsings.g.cs"); + if (ShouldEmit(globalUsings)) + { + File.WriteAllText(globalUsings, /* lang=C#-test */ """ + // + global using System; + global using System.Collections.Generic; + global using System.IO; + global using System.Linq; + global using System.Net.Http; + global using System.Threading; + global using System.Threading.Tasks; + + """); + } + + string assemblyInfo = Path.Join(objDir, $"{fileNameWithoutExtension}.AssemblyInfo.cs"); + if (ShouldEmit(assemblyInfo)) + { + File.WriteAllText(assemblyInfo, /* lang=C#-test */ $""" + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + using System; + using System.Reflection; + + [assembly: System.Reflection.AssemblyCompanyAttribute("{fileNameWithoutExtension}")] + [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] + [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] + [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] + [assembly: System.Reflection.AssemblyProductAttribute("{fileNameWithoutExtension}")] + [assembly: System.Reflection.AssemblyTitleAttribute("{fileNameWithoutExtension}")] + [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + + // Generated by the MSBuild WriteCodeFragment class. + + + """); + } + + string editorconfig = Path.Join(objDir, $"{fileNameWithoutExtension}.GeneratedMSBuildEditorConfig.editorconfig"); + if (ShouldEmit(editorconfig)) + { + File.WriteAllText(editorconfig, $""" + is_global = true + build_property.EnableAotAnalyzer = true + build_property.EnableSingleFileAnalyzer = true + build_property.EnableTrimAnalyzer = true + build_property.IncludeAllContentForSelfExtract = + build_property.TargetFramework = net{TargetFrameworkVersion} + build_property.TargetFrameworkIdentifier = .NETCoreApp + build_property.TargetFrameworkVersion = v{TargetFrameworkVersion} + build_property.TargetPlatformMinVersion = + build_property.UsingMicrosoftNETSdkWeb = + build_property.ProjectTypeGuids = + build_property.InvariantGlobalization = + build_property.PlatformNeutralAssembly = + build_property.EnforceExtendedAnalyzerRules = + build_property._SupportedPlatformList = Linux,macOS,Windows + build_property.RootNamespace = {fileNameWithoutExtension} + build_property.ProjectDir = {fileDirectory}{Path.DirectorySeparatorChar} + build_property.EnableComHosting = + build_property.EnableGeneratedComInterfaceComImportInterop = false + build_property.EffectiveAnalysisLevelStyle = {TargetFrameworkVersion} + build_property.EnableCodeStyleSeverity = + + """); + } + + var apphostTarget = Path.Join(binDir, $"{fileNameWithoutExtension}{FileNameSuffixes.CurrentPlatform.Exe}"); + if (ShouldEmit(apphostTarget)) + { + var rid = RuntimeInformation.RuntimeIdentifier; + var apphostSource = Path.Join(SdkPath, "..", "..", "packs", $"Microsoft.NETCore.App.Host.{rid}", RuntimeVersion, "runtimes", rid, "native", $"apphost{FileNameSuffixes.CurrentPlatform.Exe}"); + HostWriter.CreateAppHost( + appHostSourceFilePath: apphostSource, + appHostDestinationFilePath: apphostTarget, + appBinaryFilePath: $"{fileNameWithoutExtension}.dll", + enableMacOSCodeSign: OperatingSystem.IsMacOS()); + } + + var runtimeConfig = Path.Join(binDir, $"{fileNameWithoutExtension}{FileNameSuffixes.RuntimeConfigJson}"); + if (ShouldEmit(runtimeConfig)) + { + File.WriteAllText(runtimeConfig, $$""" + { + "runtimeOptions": { + "tfm": "net{{TargetFrameworkVersion}}", + "framework": { + "name": "Microsoft.NETCore.App", + "version": {{JsonSerializer.Serialize(RuntimeVersion)}} + }, + "configProperties": { + "EntryPointFilePath": {{JsonSerializer.Serialize(EntryPointFileFullPath)}}, + "EntryPointFileDirectoryPath": {{JsonSerializer.Serialize(fileDirectory)}}, + "Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability": true, + "System.ComponentModel.DefaultValueAttribute.IsSupported": false, + "System.ComponentModel.Design.IDesignerHost.IsSupported": false, + "System.ComponentModel.TypeConverter.EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization": false, + "System.ComponentModel.TypeDescriptor.IsComObjectDescriptorSupported": false, + "System.Data.DataSet.XmlSerializationIsSupported": false, + "System.Diagnostics.Tracing.EventSource.IsSupported": false, + "System.Linq.Enumerable.IsSizeOptimized": true, + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Resources.ResourceManager.AllowCustomResourceTypes": false, + "System.Resources.UseSystemResourceKeys": false, + "System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported": false, + "System.Runtime.InteropServices.BuiltInComInterop.IsSupported": false, + "System.Runtime.InteropServices.EnableConsumingManagedCodeFromNativeHosting": false, + "System.Runtime.InteropServices.EnableCppCLIHostActivation": false, + "System.Runtime.InteropServices.Marshalling.EnableGeneratedComInterfaceComImportInterop": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false, + "System.StartupHookProvider.IsSupported": false, + "System.Text.Encoding.EnableUnsafeUTF7Encoding": false, + "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault": false, + "System.Threading.Thread.EnableAutoreleasePool": false, + "System.Linq.Expressions.CanEmitObjectArrayDelegate": false + } + } + } + """); + } + + rspPath = Path.Join(ArtifactsPath, "csc.rsp"); + if (ShouldEmit(rspPath)) + { + IEnumerable args = GetCscArguments( + fileNameWithoutExtension: fileNameWithoutExtension, + objDir: objDir, + binDir: binDir); + + File.WriteAllLines(rspPath, args.Select(EscapeSingleArg)); + } + + bool ShouldEmit(string file) + { + if (!CanReuseAuxiliaryFiles) + { + return true; + } + + if (!File.Exists(file)) + { + Reporter.Verbose.WriteLine($"Generating CSC auxiliary file because it does not exist: {file}"); + return true; + } + + return false; + } + } + + private static string EscapeSingleArg(string arg) + { + if (IsPathOption(arg, out var colonIndex)) + { + return arg[..(colonIndex + 1)] + EscapeCore(arg[(colonIndex + 1)..]); + } + + return EscapeCore(arg); + + static string EscapeCore(string arg) + { + return ArgumentEscaper.EscapeSingleArg(arg, additionalShouldSurroundWithQuotes: static (string arg) => + { + return arg.ContainsAny(s_additionalShouldSurroundWithQuotes); + }); + } + } + + public static bool IsPathOption(string arg, out int colonIndex) + { + if (!arg.StartsWith('/')) + { + colonIndex = -1; + return false; + } + + var span = arg.AsSpan(start: 1); + foreach (var optionName in s_pathOptions) + { + Debug.Assert(!optionName.StartsWith('/') && optionName.EndsWith(':')); + + if (span.StartsWith(optionName, StringComparison.OrdinalIgnoreCase)) + { + colonIndex = optionName.Length; + return true; + } + } + + colonIndex = -1; + return false; + } +} diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index efdf946c313e..73ca0f93f8ff 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -275,8 +275,8 @@ private void EnsureProjectIsBuilt(out Func? if (EntryPointFileFullPath is not null) { var command = CreateVirtualCommand(); - projectFactory = command.CreateProjectInstance; buildResult = command.Execute(); + projectFactory = command.LastBuildLevel is BuildLevel.Csc ? null : command.CreateProjectInstance; } else { @@ -340,6 +340,14 @@ internal static VerbosityOptions GetDefaultVerbosity(bool interactive) internal ICommand GetTargetCommand(Func? projectFactory) { + if (projectFactory is null && ProjectFileFullPath is null) + { + // If we are running a file-based app and projectFactory is null, it means csc was used instead of full msbuild. + // So we can skip project evaluation to continue the optimized path. + Debug.Assert(EntryPointFileFullPath is not null); + return CreateCommandForCscBuiltProgram(EntryPointFileFullPath); + } + FacadeLogger? logger = LoggerUtility.DetermineBinlogger([..MSBuildArgs.OtherMSBuildArgs], "dotnet-run"); var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger); ValidatePreconditions(project); @@ -394,15 +402,41 @@ static ICommand CreateCommandFromRunProperties(ProjectInstance project, RunPrope var command = CommandFactoryUsingResolver.Create(commandSpec) .WorkingDirectory(runProperties.RunWorkingDirectory); - var rootVariableName = EnvironmentVariableNames.TryGetDotNetRootVariableName( + SetRootVariableName( + command, project.GetPropertyValue("RuntimeIdentifier"), project.GetPropertyValue("DefaultAppHostRuntimeIdentifier"), project.GetPropertyValue("TargetFrameworkVersion")); + return command; + } + + static void SetRootVariableName(ICommand command, string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, string targetFrameworkVersion) + { + var rootVariableName = EnvironmentVariableNames.TryGetDotNetRootVariableName( + runtimeIdentifier, + defaultAppHostRuntimeIdentifier, + targetFrameworkVersion); if (rootVariableName != null && Environment.GetEnvironmentVariable(rootVariableName) == null) { command.EnvironmentVariable(rootVariableName, Path.GetDirectoryName(new Muxer().MuxerPath)); } + } + + static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath) + { + var artifactsPath = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointFileFullPath); + var exePath = Path.Join(artifactsPath, "bin", "debug", Path.GetFileNameWithoutExtension(entryPointFileFullPath) + FileNameSuffixes.CurrentPlatform.Exe); + var commandSpec = new CommandSpec(path: exePath, args: null); + var command = CommandFactoryUsingResolver.Create(commandSpec) + .WorkingDirectory(Path.GetDirectoryName(entryPointFileFullPath)); + + SetRootVariableName( + command, + runtimeIdentifier: RuntimeInformation.RuntimeIdentifier, + defaultAppHostRuntimeIdentifier: RuntimeInformation.RuntimeIdentifier, + targetFrameworkVersion: $"v{VirtualProjectBuildingCommand.TargetFrameworkVersion}"); + return command; } diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index d042e5f638b6..838b590b2ab6 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; -using System.Collections.Frozen; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics; @@ -44,23 +42,46 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase /// private const string BuildSuccessCacheFileName = "build-success.cache"; - private static readonly ImmutableArray s_implicitBuildFileNames = + /// + /// IsMSBuildFile is if the presence of the implicit build file (even if there are no s) + /// implies that CSC is not enough and MSBuild is needed to build the project, i.e., the file alone can affect MSBuild props or targets. + /// + /// + /// For example, the simple programs our CSC optimized path handles do not need NuGet restore, hence we can ignore NuGet config files. + /// + private static readonly ImmutableArray<(string Name, bool IsMSBuildFile)> s_implicitBuildFiles = [ - "global.json", + ("global.json", false), // All these casings are recognized on case-sensitive platforms: // https://github.com/NuGet/NuGet.Client/blob/ab6b96fd9ba07ed3bf629ee389799ca4fb9a20fb/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L32-L37 - "nuget.config", - "NuGet.config", - "NuGet.Config", - - "Directory.Build.props", - "Directory.Build.targets", - "Directory.Packages.props", - "Directory.Build.rsp", - "MSBuild.rsp", + ("nuget.config", false), + ("NuGet.config", false), + ("NuGet.Config", false), + + ("Directory.Build.props", true), + ("Directory.Build.targets", true), + ("Directory.Packages.props", true), + ("Directory.Build.rsp", true), + ("MSBuild.rsp", true), + ]; + + /// + /// For purposes of determining whether CSC is enough to build as opposed to full MSBuild, + /// we can ignore properties that do not affect the build on their own. + /// See also the IsMSBuildFile flag in . + /// + /// + /// This is an rather than to avoid boxing at the use site. + /// + private static readonly IEnumerable s_ignorableProperties = + [ + // This is set by default by `dotnet run`, so it must be ignored otherwise the CSC optimization would not kick in by default. + "NuGetInteractive", ]; + public static string TargetFrameworkVersion => Product.TargetFrameworkVersion; + internal static readonly string TargetOverrides = """ + diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs index edb3e4fe661e..fd980814d9de 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs @@ -1174,17 +1174,7 @@ public static void Main() var result = runCommand.Execute(); if (expectedRoot != null) { - // SDK tests use /tmp for test assets. On macOS, it is a symlink - the app will print the resolved path - if (OperatingSystem.IsMacOS()) - { - string tmpPath = "/tmp/"; - DirectoryInfo tmp = new DirectoryInfo(tmpPath[..^1]); // No trailing slash in order to properly check the link target - if (tmp.LinkTarget != null && expectedRoot.StartsWith(tmpPath)) - { - expectedRoot = Path.Combine(tmp.ResolveLinkTarget(true).FullName, expectedRoot[tmpPath.Length..]); - } - } - + expectedRoot = TestPathUtility.ResolveTempPrefixLink(expectedRoot); result.Should().Pass() .And.HaveStdOutContaining($"Runtime directory: {expectedRoot}"); } diff --git a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs index 52809ebe67ca..e05bdbe22572 100644 --- a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs +++ b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs @@ -151,9 +151,9 @@ private TestAsset CreateTestProjectsInDirectory( return testAsset; } - public TestDirectory CreateTestDirectory([CallerMemberName] string? testName = null, string? identifier = null) + public TestDirectory CreateTestDirectory([CallerMemberName] string? testName = null, string? identifier = null, string? baseDirectory = null) { - string dir = GetTestDestinationDirectoryPath(testName, testName, identifier ?? string.Empty); + string dir = GetTestDestinationDirectoryPath(testName, testName, identifier ?? string.Empty, baseDirectory: baseDirectory); return new TestDirectory(dir, TestContext.Current.SdkVersion); } @@ -177,9 +177,10 @@ public static string GetTestDestinationDirectoryPath( string? testProjectName, string? callingMethodAndFileName, string? identifier, - bool allowCopyIfPresent = false) + bool allowCopyIfPresent = false, + string? baseDirectory = null) { - string? baseDirectory = TestContext.Current.TestExecutionDirectory; + baseDirectory ??= TestContext.Current.TestExecutionDirectory; var directoryName = new StringBuilder(callingMethodAndFileName).Append(identifier); if (testProjectName != callingMethodAndFileName) diff --git a/test/Microsoft.NET.TestFramework/ToolsetInfo.cs b/test/Microsoft.NET.TestFramework/ToolsetInfo.cs index 6290312c17d1..d063e6b30177 100644 --- a/test/Microsoft.NET.TestFramework/ToolsetInfo.cs +++ b/test/Microsoft.NET.TestFramework/ToolsetInfo.cs @@ -68,6 +68,8 @@ public string? MSBuildVersion public string? SdkResolverPath { get; set; } + public string? RepoRoot { get; set; } + public ToolsetInfo(string dotNetRoot) { DotNetRoot = dotNetRoot; @@ -291,7 +293,10 @@ public static ToolsetInfo Create(string? repoRoot, string? repoArtifactsDir, str throw new FileNotFoundException($"Host '{dotnetHost}' not found. {hostNotFoundReason}"); } - var ret = new ToolsetInfo(dotnetRoot); + var ret = new ToolsetInfo(dotnetRoot) + { + RepoRoot = repoRoot, + }; if (!string.IsNullOrEmpty(commandLine.FullFrameworkMSBuildPath)) { diff --git a/test/Microsoft.NET.TestFramework/Utilities/TestPathUtility.cs b/test/Microsoft.NET.TestFramework/Utilities/TestPathUtility.cs new file mode 100644 index 000000000000..46971a48fd35 --- /dev/null +++ b/test/Microsoft.NET.TestFramework/Utilities/TestPathUtility.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.TestFramework.Utilities; + +public static class TestPathUtility +{ +#if NET + /// + /// For path like /tmp/something, returns /private/tmp/something on macOS. + /// + public static string ResolveTempPrefixLink(string path) + { + // SDK tests use /tmp for test assets. On macOS, it is a symlink - the app will print the resolved path + if (OperatingSystem.IsMacOS()) + { + string tmpPath = "/tmp/"; + var tmp = new DirectoryInfo(tmpPath[..^1]); // No trailing slash in order to properly check the link target + if (tmp.LinkTarget != null && path.StartsWith(tmpPath) && tmp.ResolveLinkTarget(true) is { } linkTarget) + { + return Path.Combine(linkTarget.FullName, path[tmpPath.Length..]); + } + } + + return path; + } +#endif +} diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index a7887db1c0fb..d793ec056950 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -3,11 +3,13 @@ using System.Runtime.Versioning; using System.Text.Json; +using Basic.CompilerLog.Util; using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; using Microsoft.CodeAnalysis; using Microsoft.DotNet.Cli.Commands; using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.Cli.Run.Tests; @@ -74,6 +76,12 @@ public static string GetMessage() } """; + /// + /// Used when we need an out-of-tree base test directory to avoid having implicit build files + /// like Directory.Build.props in scope and negating the optimizations we want to test. + /// + private static string OutOfTreeBaseDirectory => field ??= PrepareOutOfTreeBaseDirectory(); + private static bool HasCaseInsensitiveFileSystem { get @@ -83,6 +91,25 @@ private static bool HasCaseInsensitiveFileSystem } } + /// + private static string PrepareOutOfTreeBaseDirectory() + { + string outOfTreeBaseDirectory = TestPathUtility.ResolveTempPrefixLink(Path.Join(Path.GetTempPath(), "dotnetSdkTests")); + Directory.CreateDirectory(outOfTreeBaseDirectory); + + // Create NuGet.config in our out-of-tree base directory. + var sourceNuGetConfig = Path.Join(TestContext.Current.TestExecutionDirectory, "NuGet.config"); + var targetNuGetConfig = Path.Join(outOfTreeBaseDirectory, "NuGet.config"); + File.Copy(sourceNuGetConfig, targetNuGetConfig, overwrite: true); + + // Check there are no implicit build files that would prevent testing optimizations. + VirtualProjectBuildingCommand.CollectImplicitBuildFiles(new DirectoryInfo(outOfTreeBaseDirectory), [], out var exampleMSBuildFile); + exampleMSBuildFile.Should().BeNull(because: "there should not be any implicit build files in the temp directory or its parents " + + "so we can test optimizations that would be disabled with implicit build files present"); + + return outOfTreeBaseDirectory; + } + /// /// dotnet run file.cs succeeds without a project file. /// @@ -1406,10 +1433,10 @@ public void ArtifactsDirectory_Permissions() .Should().Be(actualMode, artifactsDir); } - [Fact] - public void LaunchProfile() + [Theory, CombinatorialData] + public void LaunchProfile(bool cscOnly) { - var testInstance = _testAssetsManager.CreateTestDirectory(); + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null); File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program + """ Console.WriteLine($"Message: '{Environment.GetEnvironmentVariable("Message")}'"); @@ -1417,29 +1444,35 @@ public void LaunchProfile() Directory.CreateDirectory(Path.Join(testInstance.Path, "Properties")); File.WriteAllText(Path.Join(testInstance.Path, "Properties", "launchSettings.json"), s_launchSettings); - new DotnetCommand(Log, "run", "--no-launch-profile", "Program.cs") + var prefix = cscOnly + ? CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine + : string.Empty; + + new DotnetCommand(Log, "run", "-bl", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOut(""" + .And.HaveStdOutContaining(prefix + """ Hello from Program - Message: '' + Message: 'TestProfileMessage1' """); - new DotnetCommand(Log, "run", "Program.cs") + prefix = CliCommandStrings.NoBinaryLogBecauseUpToDate + Environment.NewLine; + + new DotnetCommand(Log, "run", "-bl", "--no-launch-profile", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOutContaining(""" + .And.HaveStdOut(prefix + """ Hello from Program - Message: 'TestProfileMessage1' + Message: '' """); - new DotnetCommand(Log, "run", "-lp", "TestProfile2", "Program.cs") + new DotnetCommand(Log, "run", "-bl", "-lp", "TestProfile2", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOutContaining(""" + .And.HaveStdOutContaining(prefix + """ Hello from Program Message: 'TestProfileMessage2' """); @@ -1649,29 +1682,397 @@ public void ProjectReference_Errors() string.Format(CliStrings.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, "dir/")))); } + /// + /// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs. + /// Can regenerate CSC arguments template in . + /// + [Fact] + public void CscArguments() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + const string programName = "TestProgram"; + const string fileName = $"{programName}.cs"; + string entryPointPath = Path.Join(testInstance.Path, fileName); + File.WriteAllText(entryPointPath, s_program); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + // Build using MSBuild. + new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($"Hello from {programName}"); + + // Find the csc args used by the build. + var msbuildCall = FindCompilerCall(Path.Join(testInstance.Path, "msbuild.binlog")); + var msbuildCallArgs = msbuildCall.GetArguments(); + var msbuildCallArgsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(msbuildCallArgs); + + // Generate argument template code. + string sdkPath = NormalizePath(TestContext.Current.ToolsetUnderTest.SdkFolderUnderTest); + string dotNetRootPath = NormalizePath(TestContext.Current.ToolsetUnderTest.DotNetRoot); + string nuGetCachePath = NormalizePath(TestContext.Current.NuGetCachePath!); + string artifactsDirNormalized = NormalizePath(artifactsDir); + string objPath = $"{artifactsDirNormalized}/obj/debug"; + string entryPointPathNormalized = NormalizePath(entryPointPath); + var msbuildArgsToVerify = new List(); + var nuGetPackageFilePaths = new List(); + var code = new StringBuilder(); + code.AppendLine($$""" + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + + namespace Microsoft.DotNet.Cli.Commands.Run; + + // Generated by test `{{nameof(RunFileTests)}}.{{nameof(CscArguments)}}`. + partial class CSharpCompilerCommand + { + private IEnumerable GetCscArguments( + string fileNameWithoutExtension, + string objDir, + string binDir) + { + return + [ + """); + foreach (var arg in msbuildCallArgs) + { + // This option needs to be passed on the command line, not in an RSP file. + if (arg is "/noconfig") + { + continue; + } + + // We don't need to generate a ref assembly. + if (arg.StartsWith("/refout:", StringComparison.Ordinal)) + { + continue; + } + + // There should be no source link arguments. + if (arg.StartsWith("/sourcelink:", StringComparison.Ordinal)) + { + Assert.Fail($"Unexpected source link argument: {arg}"); + } + + // PreferredUILang is normally not set by default but can be in builds, so ignore it. + if (arg.StartsWith("/preferreduilang:", StringComparison.Ordinal)) + { + continue; + } + + bool needsInterpolation = false; + bool fromNuGetPackage = false; + + // Normalize slashes in paths. + string rewritten = NormalizePathArg(arg); + + // Remove quotes. + rewritten = RemoveQuotes(rewritten); + + string msbuildArgToVerify = rewritten; + + // Use variable SDK path. + if (rewritten.Contains(sdkPath, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(sdkPath, "{SdkPath}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable .NET root path. + if (rewritten.Contains(dotNetRootPath, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(dotNetRootPath, "{DotNetRootPath}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable NuGet cache path. + if (rewritten.Contains(nuGetCachePath, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(nuGetCachePath, "{NuGetCachePath}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + fromNuGetPackage = true; + } + + // Use variable intermediate dir path. + if (rewritten.Contains(objPath, StringComparison.OrdinalIgnoreCase)) + { + // We want to emit the resulting DLL directly into the bin folder. + bool isOut = arg.StartsWith("/out", StringComparison.Ordinal); + string replacement = isOut ? "{binDir}" : "{objDir}"; + + if (isOut) + { + msbuildArgToVerify = msbuildArgToVerify.Replace("/obj/", "/bin/", StringComparison.OrdinalIgnoreCase); + } + + rewritten = rewritten.Replace(objPath, replacement, StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable file name. + if (rewritten.Contains(entryPointPathNormalized, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(entryPointPathNormalized, "{" + nameof(CSharpCompilerCommand.EntryPointFileFullPath) + "}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable program name. + if (rewritten.Contains(programName, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(programName, "{fileNameWithoutExtension}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Use variable runtime version. + if (rewritten.Contains(CSharpCompilerCommand.RuntimeVersion, StringComparison.OrdinalIgnoreCase)) + { + rewritten = rewritten.Replace(CSharpCompilerCommand.RuntimeVersion, "{" + nameof(CSharpCompilerCommand.RuntimeVersion) + "}", StringComparison.OrdinalIgnoreCase); + needsInterpolation = true; + } + + // Ignore `/analyzerconfig` which is not variable (so it comes from the machine or sdk repo). + if (!needsInterpolation && arg.StartsWith("/analyzerconfig", StringComparison.Ordinal)) + { + continue; + } + + string prefix = needsInterpolation ? "$" : string.Empty; + + code.AppendLine($""" + {prefix}"{rewritten}", + """); + + msbuildArgsToVerify.Add(msbuildArgToVerify); + + if (fromNuGetPackage) + { + nuGetPackageFilePaths.Add(CSharpCompilerCommand.IsPathOption(rewritten, out int colonIndex) + ? rewritten.Substring(colonIndex + 1) + : rewritten); + } + } + code.AppendLine(""" + ]; + } + + /// + /// Files that come from referenced NuGet packages (e.g., analyzers for NativeAOT) need to be checked specially (if they don't exist, MSBuild needs to run). + /// + public static IEnumerable GetPathsOfCscInputsFromNuGetCache() + { + return + [ + """); + foreach (var nuGetPackageFilePath in nuGetPackageFilePaths) + { + code.AppendLine($""" + $"{nuGetPackageFilePath}", + """); + } + code.AppendLine(""" + ]; + } + } + """); + + // Save the code. + var codeFolder = new DirectoryInfo(Path.Join( + TestContext.Current.ToolsetUnderTest.RepoRoot, + "src", "Cli", "dotnet", "Commands", "Run")); + var nonGeneratedFile = codeFolder.File("CSharpCompilerCommand.cs"); + if (!nonGeneratedFile.Exists) + { + Log.WriteLine($"Skipping code generation because file does not exist: {nonGeneratedFile.FullName}"); + } + else + { + var codeFilePath = codeFolder.File("CSharpCompilerCommand.Generated.cs"); + var existingText = codeFilePath.Exists ? File.ReadAllText(codeFilePath.FullName) : string.Empty; + var newText = code.ToString(); + if (existingText != newText) + { + Log.WriteLine($"{codeFilePath.FullName} needs to be updated:"); + Log.WriteLine(newText); + if (Environment.GetEnvironmentVariable("CI") == "true") + { + throw new InvalidOperationException($"Not updating file in CI: {codeFilePath.FullName}"); + } + else + { + File.WriteAllText(codeFilePath.FullName, newText); + throw new InvalidOperationException($"File outdated, commit the changes: {codeFilePath.FullName}"); + } + } + } + + // Build using CSC. + Directory.Delete(artifactsDir, recursive: true); + new DotnetCommand(Log, "run", fileName, "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc} + Hello from {programName} + """); + + // Read args from csc.rsp file. + var rspFilePath = Path.Join(artifactsDir, "csc.rsp"); + var cscOnlyCallArgs = File.ReadAllLines(rspFilePath); + var cscOnlyCallArgsString = string.Join(' ', cscOnlyCallArgs); + + // Check that csc args between MSBuild run and CSC-only run are equivalent. + var normalizedCscOnlyArgs = cscOnlyCallArgs + .Select(static a => NormalizePathArg(RemoveQuotes(a))); + Log.WriteLine("CSC-only args:"); + Log.WriteLine(string.Join(Environment.NewLine, normalizedCscOnlyArgs)); + Log.WriteLine("MSBuild args:"); + Log.WriteLine(string.Join(Environment.NewLine, msbuildArgsToVerify)); + normalizedCscOnlyArgs.Should().Equal(msbuildArgsToVerify, + "the generated file might be outdated, run this test locally to regenerate it"); + + static CompilerCall FindCompilerCall(string binaryLogPath) + { + using var reader = BinaryLogReader.Create(binaryLogPath); + return reader.ReadAllCompilerCalls().Should().ContainSingle().Subject; + } + + static string NormalizePathArg(string arg) + { + return CSharpCompilerCommand.IsPathOption(arg, out int colonIndex) + ? string.Concat(arg.AsSpan(0, colonIndex + 1), NormalizePath(arg.Substring(colonIndex + 1))) + : NormalizePath(arg); + } + + static string NormalizePath(string path) + { + return PathUtility.GetPathWithForwardSlashes(TestPathUtility.ResolveTempPrefixLink(path)); + } + + static string RemoveQuotes(string arg) + { + return arg.Replace("\"", string.Empty); + } + } + + /// + /// Verifies that csc-only runs emit auxiliary files equivalent to msbuild-based runs. + /// + [Theory] + [InlineData("Program.cs")] + [InlineData("test.cs")] + [InlineData("noext")] + public void CscVsMSBuild(string fileName) + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + string entryPointPath = Path.Join(testInstance.Path, fileName); + File.WriteAllText(entryPointPath, $""" + #!/test + {s_program} + """); + + string programName = Path.GetFileNameWithoutExtension(fileName); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointPath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + var artifactsBackupDir = Path.ChangeExtension(artifactsDir, ".bak"); + if (Directory.Exists(artifactsBackupDir)) Directory.Delete(artifactsBackupDir, recursive: true); + + // Build using CSC. + new DotnetCommand(Log, "run", fileName, "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc} + Hello from {programName} + """); + + // Backup the artifacts directory. + Directory.Move(artifactsDir, artifactsBackupDir); + + // Build using MSBuild. + new DotnetCommand(Log, "run", fileName, "-bl", "--no-cache") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($"Hello from {programName}"); + + // Check that files generated by MSBuild and CSC-only runs are equivalent. + var cscOnlyFiles = Directory.EnumerateFiles(artifactsBackupDir, "*", SearchOption.AllDirectories) + .Where(f => + Path.GetDirectoryName(f) != artifactsBackupDir && // exclude top-level marker files + Path.GetFileName(f) != programName && // binary on unix + Path.GetExtension(f) is not (".dll" or ".exe" or ".pdb")); // other binaries + bool hasErrors = false; + foreach (var cscOnlyFile in cscOnlyFiles) + { + var relativePath = Path.GetRelativePath(relativeTo: artifactsBackupDir, path: cscOnlyFile); + var msbuildFile = Path.Join(artifactsDir, relativePath); + + if (!File.Exists(msbuildFile)) + { + throw new InvalidOperationException($"File exists in CSC-only run but not in MSBuild run: {cscOnlyFile}"); + } + + var cscOnlyFileText = File.ReadAllText(cscOnlyFile); + var msbuildFileText = File.ReadAllText(msbuildFile); + if (cscOnlyFileText.ReplaceLineEndings() != msbuildFileText.ReplaceLineEndings()) + { + Log.WriteLine($"File differs between MSBuild and CSC-only runs: {cscOnlyFile}"); + const int limit = 3_000; + if (cscOnlyFileText.Length < limit && msbuildFileText.Length < limit) + { + Log.WriteLine("MSBuild file content:"); + Log.WriteLine(msbuildFileText); + Log.WriteLine("CSC-only file content:"); + Log.WriteLine(cscOnlyFileText); + } + else + { + Log.WriteLine($"MSBuild file size: {msbuildFileText.Length} chars"); + Log.WriteLine($"CSC-only file size: {cscOnlyFileText.Length} chars"); + } + hasErrors = true; + } + } + hasErrors.Should().BeFalse(); + } + [Fact] public void UpToDate() { - var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hello v1"); + """); - Build(expectedUpToDate: false); + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs")); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "Hello v1"); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1"); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None, expectedOutput: "Hello v1"); // Change the source file (a rebuild is necessary). - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program + " "); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); - Build(expectedUpToDate: false); + Build(testInstance, BuildLevel.Csc); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None); // Change an unrelated source file (no rebuild necessary). File.WriteAllText(Path.Join(testInstance.Path, "Program2.cs"), "test"); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None); // Add an implicit build file (a rebuild is necessary). string buildPropsFile = Path.Join(testInstance.Path, "Directory.Build.props"); @@ -1683,12 +2084,12 @@ public void UpToDate() """); - Build(expectedUpToDate: false, expectedOutput: """ + Build(testInstance, BuildLevel.All, expectedOutput: """ Hello from Program Custom define """); - Build(expectedUpToDate: true, expectedOutput: """ + Build(testInstance, BuildLevel.None, expectedOutput: """ Hello from Program Custom define """); @@ -1705,7 +2106,7 @@ Custom define """); - Build(expectedUpToDate: false); + Build(testInstance, BuildLevel.All); // Change the imported build file (this is not recognized). File.WriteAllText(importedFile, """ @@ -1716,43 +2117,43 @@ Custom define """); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None); // Force rebuild. - Build(expectedUpToDate: false, args: ["--no-cache"], expectedOutput: """ + Build(testInstance, BuildLevel.All, args: ["--no-cache"], expectedOutput: """ Hello from Program Custom define """); // Remove an implicit build file (a rebuild is necessary). File.Delete(buildPropsFile); - Build(expectedUpToDate: false); + Build(testInstance, BuildLevel.Csc); // Force rebuild. - Build(expectedUpToDate: false, args: ["--no-cache"]); + Build(testInstance, BuildLevel.All, args: ["--no-cache"]); - Build(expectedUpToDate: true); + Build(testInstance, BuildLevel.None); // Pass argument (no rebuild necessary). - Build(expectedUpToDate: true, args: ["--", "test-arg"], expectedOutput: """ + Build(testInstance, BuildLevel.None, args: ["--", "test-arg"], expectedOutput: """ echo args:test-arg Hello from Program """); // Change config (a rebuild is necessary). - Build(expectedUpToDate: false, args: ["-c", "Release"], expectedOutput: """ + Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """ Hello from Program Release config """); // Keep changed config (no rebuild necessary). - Build(expectedUpToDate: true, args: ["-c", "Release"], expectedOutput: """ + Build(testInstance, BuildLevel.None, args: ["-c", "Release"], expectedOutput: """ Hello from Program Release config """); // Change config back (a rebuild is necessary). - Build(expectedUpToDate: false); + Build(testInstance, BuildLevel.Csc); // Build with a failure. new DotnetCommand(Log, ["run", "Program.cs", "-p:LangVersion=Invalid"]) @@ -1762,34 +2163,41 @@ Release config .And.HaveStdOutContaining("error CS1617"); // Invalid option 'Invalid' for /langversion. // A rebuild is necessary since the last build failed. - Build(expectedUpToDate: false); + Build(testInstance, BuildLevel.Csc); + } - void Build(bool expectedUpToDate, ReadOnlySpan args = default, string expectedOutput = "Hello from Program") + private void Build(TestDirectory testInstance, BuildLevel level, ReadOnlySpan args = default, string expectedOutput = "Hello from Program") + { + string prefix = level switch { - new DotnetCommand(Log, ["run", "Program.cs", "-bl", .. args]) - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOut(expectedUpToDate - ? $""" - {CliCommandStrings.NoBinaryLogBecauseUpToDate} - {expectedOutput} - """ - : expectedOutput); - - var binlogs = new DirectoryInfo(testInstance.Path) - .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly); - - binlogs.Select(f => f.Name) - .Should().BeEquivalentTo( - expectedUpToDate - ? ["msbuild-dotnet-run.binlog"] - : ["msbuild.binlog", "msbuild-dotnet-run.binlog"]); - - foreach (var binlog in binlogs) - { - binlog.Delete(); - } + BuildLevel.None => CliCommandStrings.NoBinaryLogBecauseUpToDate + Environment.NewLine, + BuildLevel.Csc => CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine, + BuildLevel.All => string.Empty, + _ => throw new ArgumentOutOfRangeException(paramName: nameof(level)), + }; + + new DotnetCommand(Log, ["run", "Program.cs", "-bl", .. args]) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut(prefix + expectedOutput); + + var binlogs = new DirectoryInfo(testInstance.Path) + .EnumerateFiles("*.binlog", SearchOption.TopDirectoryOnly); + + binlogs.Select(f => f.Name) + .Should().BeEquivalentTo( + level switch + { + BuildLevel.Csc => [], + BuildLevel.None => ["msbuild-dotnet-run.binlog"], + BuildLevel.All => ["msbuild.binlog", "msbuild-dotnet-run.binlog"], + _ => throw new ArgumentOutOfRangeException(paramName: nameof(level), message: level.ToString()), + }); + + foreach (var binlog in binlogs) + { + binlog.Delete(); } } @@ -1806,6 +2214,210 @@ public void UpToDate_InvalidOptions() .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name)); } + [Fact] + public void CscOnly() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("v1"); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs")); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v1"); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("v2"); + #if !DEBUG + Console.WriteLine("Release config"); + #endif + """); + + Build(testInstance, BuildLevel.Csc, expectedOutput: "v2"); + + // Customizing a property forces MSBuild to be used. + Build(testInstance, BuildLevel.All, args: ["-c", "Release"], expectedOutput: """ + v2 + Release config + """); + } + + [Fact] + public void CscOnly_CompilationDiagnostics() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + string x = null; + Console.WriteLine("ran" + x); + """); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + // warning CS8600: Converting null literal or possible null value to non-nullable type. + .And.HaveStdOutContaining("warning CS8600") + .And.HaveStdOutContaining("ran"); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.Write + """); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + // error CS1002: ; expected + .And.HaveStdOutContaining("error CS1002") + .And.HaveStdErrContaining(CliCommandStrings.RunCommandException); + } + + /// + /// Checks that the DOTNET_ROOT env var is set the same in csc mode as in msbuild mode. + /// + [Fact] + public void CscOnly_DotNetRoot() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + foreach (var entry in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process) + .Cast() + .Where(e => ((string)e.Key).StartsWith("DOTNET_ROOT"))) + { + Console.WriteLine($"{entry.Key}={entry.Value}"); + } + """); + + var expectedDotNetRoot = TestContext.Current.ToolsetUnderTest.DotNetRoot; + + var cscResult = new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute(); + + cscResult.Should().Pass() + .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + .And.HaveStdOutContaining("DOTNET_ROOT") + .And.HaveStdOutContaining($"={expectedDotNetRoot}"); + + // Add an implicit build file to force use of msbuild instead of csc. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), ""); + + var msbuildResult = new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute(); + + msbuildResult.Should().Pass() + .And.NotHaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + .And.HaveStdOutContaining("DOTNET_ROOT") + .And.HaveStdOutContaining($"={expectedDotNetRoot}"); + + // The set of DOTNET_ROOT env vars should be the same in both cases. + var cscVars = cscResult.StdOut! + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Where(line => line.StartsWith("DOTNET_ROOT")); + var msbuildVars = msbuildResult.StdOut! + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Where(line => line.StartsWith("DOTNET_ROOT")); + cscVars.Should().BeEquivalentTo(msbuildVars); + } + + /// + /// In CSC-only mode, the SDK needs to manually create intermediate files + /// like GlobalUsings.g.cs which are normally generated by MSBuild targets. + /// This tests the SDK recreates the files when they are outdated. + /// + [Fact] + public void CscOnly_IntermediateFiles() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Expression> e = () => 1 + 1; + Console.WriteLine(e); + """); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs")); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), ""); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0246: The type or namespace name 'Expression<>' could not be found + .And.HaveStdOutContaining("error CS0246"); + + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + + + + """); + + Build(testInstance, BuildLevel.All, expectedOutput: "() => 2"); + + File.Delete(Path.Join(testInstance.Path, "Directory.Build.props")); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc) + // error CS0246: The type or namespace name 'Expression<>' could not be found + .And.HaveStdOutContaining("error CS0246"); + } + + /// + /// If a file from a NuGet package (which would be used by CSC-only build) does not exist, full MSBuild should be used instead. + /// + [Fact] + public void CscOnly_NotRestored() + { + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: OutOfTreeBaseDirectory); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), s_program); + + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(Path.Join(testInstance.Path, "Program.cs")); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + new DotnetCommand(Log, "run", "Program.cs", "-bl", "--no-restore") + .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages")) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error NETSDK1004: Assets file '...\obj\project.assets.json' not found. Run a NuGet package restore to generate this file. + .And.HaveStdOutContaining("NETSDK1004"); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages")) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from Program"); + + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("v2"); + """); + + new DotnetCommand(Log, "run", "Program.cs", "-bl") + .WithEnvironmentVariable("NUGET_PACKAGES", Path.Join(testInstance.Path, "packages")) + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut($""" + {CliCommandStrings.NoBinaryLogBecauseRunningJustCsc} + v2 + """); + } + private static string ToJson(string s) => JsonSerializer.Serialize(s); /// @@ -2082,21 +2694,29 @@ public void Api_RunCommand() """); } - [Fact] - public void EntryPointFilePath() + [Theory, CombinatorialData] + public void EntryPointFilePath(bool cscOnly) { - var testInstance = _testAssetsManager.CreateTestDirectory(); + var testInstance = _testAssetsManager.CreateTestDirectory(baseDirectory: cscOnly ? OutOfTreeBaseDirectory : null); var filePath = Path.Join(testInstance.Path, "Program.cs"); File.WriteAllText(filePath, """" var entryPointFilePath = AppContext.GetData("EntryPointFilePath") as string; Console.WriteLine($"""EntryPointFilePath: {entryPointFilePath}"""); """"); - new DotnetCommand(Log, "run", "Program.cs") + // Remove artifacts from possible previous runs of this test. + var artifactsDir = VirtualProjectBuildingCommand.GetArtifactsPath(filePath); + if (Directory.Exists(artifactsDir)) Directory.Delete(artifactsDir, recursive: true); + + var prefix = cscOnly + ? CliCommandStrings.NoBinaryLogBecauseRunningJustCsc + Environment.NewLine + : string.Empty; + + new DotnetCommand(Log, "run", "-bl", "Program.cs") .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Pass() - .And.HaveStdOut($"EntryPointFilePath: {filePath}"); + .And.HaveStdOut(prefix + $"EntryPointFilePath: {filePath}"); } [Fact]