File: Commands\Run\CSharpCompilerCommand.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// 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;
 
/// <summary>
/// Used to invoke C# compiler in some optimized paths of <c>dotnet run file.cs</c>.
/// </summary>
internal sealed partial class CSharpCompilerCommand
{
    private static readonly SearchValues<char> s_additionalShouldSurroundWithQuotes = SearchValues.Create('=', ',');
 
    /// <summary>
    /// Options which denote paths and which might appear in the simple app compilation that we optimize for.
    /// </summary>
    private static readonly ImmutableArray<string> 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; }
 
    public string BaseDirectory => field ??= Path.GetDirectoryName(EntryPointFileFullPath)!;
 
    /// <summary>
    /// Compiler command line arguments to use. If empty, default arguments are used.
    /// These should be already properly escaped.
    /// </summary>
    public required ImmutableArray<string> CscArguments { get; init; }
 
    /// <summary>
    /// Path to the <c>bin/Program.dll</c> file. If specified,
    /// the compiled output (<c>obj/Program.dll</c>) will be copied to this location.
    /// </summary>
    public required string? BuildResultFile { get; init; }
 
    /// <param name="fallbackToNormalBuild">
    /// Whether the returned error code should not cause the build to fail but instead fallback to full MSBuild.
    /// </param>
    public int Execute(out bool fallbackToNormalBuild)
    {
        // Write .rsp file and other intermediate build outputs.
        PrepareAuxiliaryFiles(out string rspPath);
 
        // Ensure the compiler is launched with the correct dotnet.
        Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", new Muxer().MuxerPath);
 
        // 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: BaseDirectory,
            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.
        var exitCode = ProcessBuildResponse(responseTask.Result, out fallbackToNormalBuild);
 
        // Copy from obj to bin.
        if (BuildResultFile != null &&
            CSharpCommandLineParser.Default.Parse(CscArguments, BaseDirectory, sdkDirectory: null) is { OutputFileName: { } outputFileName } parsedArgs)
        {
            var objFile = parsedArgs.GetOutputFilePath(outputFileName);
            Reporter.Verbose.WriteLine($"Copying '{objFile}' to '{BuildResultFile}'.");
            File.Copy(objFile, BuildResultFile, overwrite: true);
        }
 
        return exitCode;
 
        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)
    {
        rspPath = Path.Join(ArtifactsPath, "csc.rsp");
 
        if (!CscArguments.IsDefaultOrEmpty)
        {
            File.WriteAllLines(rspPath, CscArguments);
            return;
        }
 
        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 */ $"""
                // <autogenerated />
                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 */ """
                // <auto-generated/>
                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 */ $"""
                //------------------------------------------------------------------------------
                // <auto-generated>
                //     This code was generated by a tool.
                //
                //     Changes to this file may cause incorrect behavior and will be lost if
                //     the code is regenerated.
                // </auto-generated>
                //------------------------------------------------------------------------------
 
                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.VerifyReferenceTrimCompatibility = 
                build_property.VerifyReferenceAotCompatibility = 
                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.Net.SocketsHttpHandler.Http3Support": false,
                      "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
                    }
                  }
                }
                """);
        }
 
        if (ShouldEmit(rspPath))
        {
            IEnumerable<string> 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)] + EscapePathArgument(arg[(colonIndex + 1)..]);
        }
 
        return EscapePathArgument(arg);
    }
 
    internal static string EscapePathArgument(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;
    }
}