File: Commands\Run\VirtualProjectBuildingCommand.cs
Web Access
Project: src\sdk\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.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.Build.Logging.SimpleErrorLogger;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.FileBasedPrograms;

namespace Microsoft.DotNet.Cli.Commands.Run;

/// <summary>
/// Used to build a virtual project file in memory to support <c>dotnet run file.cs</c>.
/// </summary>
internal sealed class VirtualProjectBuildingCommand : CommandBase
{
    internal const string FileBasedProgramCanSkipMSBuild = nameof(FileBasedProgramCanSkipMSBuild);

    public static string TargetFrameworkVersion => Product.TargetFrameworkVersion;
    public static string TargetFramework => $"net{Product.TargetFrameworkVersion}";

    public bool NoRestore { get; init; }

    /// <summary>
    /// If <see langword="true"/>, build markers are not checked and hence MSBuild is always run.
    /// This property does not control whether the build markers are written, use <see cref="NoWriteBuildMarkers"/> for that.
    /// </summary>
    public bool NoCache { get; init; }

    public bool NoBuild { get; init; }

    /// <summary>
    /// Filled during <see cref="Execute"/>.
    /// </summary>
    public (BuildLevel Level, FileBasedAppCacheInfo? Cache) LastBuild { get; private set; }

    /// <summary>
    /// Filled during <see cref="Execute"/>.
    /// </summary>
    public RunProperties? LastRunProperties { get; private set; }

    /// <summary>
    /// If <see langword="true"/>, no build markers are written
    /// (like <see cref="FileBasedAppRunPlan.BuildStartCacheFileName"/> and <see cref="FileBasedAppRunPlan.BuildSuccessCacheFileName"/>).
    /// Also skips automatic cleanup.
    /// This property does not control whether the markers are checked, use <see cref="NoCache"/> for that.
    /// </summary>
    public bool NoWriteBuildMarkers { get; init; }

    public bool NoConsoleLogger { get; init; }

    public VirtualProjectBuilder Builder { get; }
    public MSBuildArgs MSBuildArgs { get; }

    public ImmutableArray<CSharpDirective> Directives
    {
        get
        {
            if (field.IsDefault)
            {
                field = FileLevelDirectiveHelpers.FindDirectives(Builder.EntryPointSourceFile, reportAllErrors: false, ThrowingReporter);
                Debug.Assert(!field.IsDefault);
            }

            return field;
        }

        set
        {
            field = value;
            EvaluatedDirectives = default;
        }
    }

    public ImmutableArray<CSharpDirective> EvaluatedDirectives { get; private set; }

    public VirtualProjectBuildingCommand(
        string entryPointFileFullPath,
        MSBuildArgs msbuildArgs,
        string? artifactsPath = null)
    {
        MSBuildArgs = msbuildArgs.CloneWithAdditionalProperties(
            CommonRunHelpers.CreateFileBasedRunGlobalProperties().AsReadOnly());

        NoConsoleLogger = LoggerUtility.HasNoConsoleLoggerArgument(MSBuildArgs.OtherMSBuildArgs);

        Builder = new VirtualProjectBuilder(BuildService.Instance, entryPointFileFullPath, TargetFramework, MSBuildArgs.GetResolvedTargets(), artifactsPath);
    }

#if !CLI_AOT
    [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Temporary unblock for dotnet/msbuild#14064 (MSBuild build APIs are now [RequiresUnreferencedCode]). dotnet CLI runs MSBuild in-proc (not trimmed). Remove when dotnet/sdk#55225 is fixed.")]
    [UnconditionalSuppressMessage("AOT", "IL3050", Justification ="In non-AOT mode we have MSBuild available, so using types from it is safe.")]
    public override int Execute()
    {
        bool msbuildGet = MSBuildArgs.GetProperty is [_, ..] || MSBuildArgs.GetItem is [_, ..] || MSBuildArgs.GetTargetResult is [_, ..];
        bool evalOnly = msbuildGet && Builder.RequestedTargets is null or [];
        bool minimizeStdOut = msbuildGet && MSBuildArgs.GetResultOutputFile is null or [];

        var verbosity = MSBuildArgs.Verbosity ?? MSBuildForwardingAppWithoutLogging.DefaultVerbosity;
        var consoleLogger = NoConsoleLogger
            ? null
            : minimizeStdOut
            ? new SimpleErrorLogger()
            : CommonRunHelpers.GetConsoleLogger(MSBuildArgs.CloneWithExplicitArgs([$"--verbosity:{verbosity}", .. MSBuildArgs.OtherMSBuildArgs]));
        var binaryLogger = GetBinaryLogger(MSBuildArgs.OtherMSBuildArgs);

        FileBasedAppCacheInfo? cache = null;

        if (msbuildGet)
        {
            LastBuild = (BuildLevel.None, Cache: null);
        }
        else if (NoBuild)
        {
            // This is reached only during `restore`, not `run --no-build`
            // (in the latter case, this virtual building command is not executed at all).
            Debug.Assert(!NoRestore);

            LastBuild = (BuildLevel.None, Cache: null);

            if (!NoWriteBuildMarkers)
            {
                CreateTempSubdirectory(Builder.ArtifactsPath);
                MarkArtifactsFolderUsed();
            }
        }
        else
        {
            var buildLevel = GetBuildLevel(out cache);
            cache?.CurrentEntry.BuildLevel = buildLevel;
            LastBuild = (buildLevel, cache);

            if (buildLevel is BuildLevel.None)
            {
                if (binaryLogger is not null)
                {
                    Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseUpToDate.Yellow());
                }

                // No rebuild, can reuse run properties.
                cache?.CurrentEntry.Run = cache.PreviousEntry?.Run;

                MarkArtifactsFolderUsed();
                return 0;
            }

            if (buildLevel is BuildLevel.Csc)
            {
                Debug.Assert(cache is not null);

                MarkBuildStart();

                // Execute CSC.
                int result = new CSharpCompilerCommand
                {
                    EntryPointFileFullPath = Builder.EntryPointFileFullPath,
                    ArtifactsPath = Builder.ArtifactsPath,
                    CanReuseAuxiliaryFiles = cache.DetermineFinalCanReuseAuxiliaryFiles(),
                    CscArguments = cache.PreviousEntry?.CscArguments ?? [],
                    BuildResultFile = cache.PreviousEntry?.BuildResultFile,
                }
                .Execute(out bool fallbackToNormalBuild);

                if (!fallbackToNormalBuild)
                {
                    if (result == 0)
                    {
                        ReuseInfoFromPreviousCacheEntry(cache);
                        MarkBuildSuccess(cache);
                    }

                    if (binaryLogger is not null)
                    {
                        Reporter.Output.WriteLine(CliCommandStrings.NoBinaryLogBecauseRunningJustCsc.Yellow());
                    }

                    return result;
                }

                Debug.Assert(result != 0);

                buildLevel = BuildLevel.All;
                cache.CurrentEntry.BuildLevel = buildLevel;
                LastBuild = (buildLevel, cache);
            }

            Debug.Assert(buildLevel is BuildLevel.All or BuildLevel.Csc);

            MarkBuildStart();
        }

        if (!NoWriteBuildMarkers && !msbuildGet)
        {
            CleanFileBasedAppArtifactsCommand.StartAutomaticCleanupIfNeeded();
        }

        IDisposable? environmentScope = null;
        try
        {
            environmentScope = MSBuildForwardingAppWithoutLogging.SetMSBuildRequiredEnvironmentVariables();

            // Set up MSBuild.
            ReadOnlySpan<ILogger> binaryLoggers = binaryLogger is null ? [] : [binaryLogger.Value];
            ReadOnlySpan<ILogger> consoleLoggers = consoleLogger is null ? [] : [consoleLogger];
            IEnumerable<ILogger> loggers = [.. binaryLoggers, .. consoleLoggers];
            var projectCollection = new ProjectCollection(
                MSBuildArgs.GlobalProperties,
                loggers,
                ToolsetDefinitionLocations.Default);
            var parameters = new BuildParameters(projectCollection)
            {
                Loggers = loggers,
                LogTaskInputs = binaryLoggers.Length != 0,
            };

            BuildManager.DefaultBuildManager.BeginBuild(parameters);

            int exitCode = 0;
            ProjectInstance? projectInstance = null;
            BuildResult? buildOrRestoreResult = null;

            // Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`).
            // See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838
            // and https://github.com/dotnet/msbuild/issues/11519.
            if (!NoRestore && !evalOnly)
            {
                var restoreRequest = new BuildRequestData(
                    CreateProjectInstance(projectCollection, additionalGlobalProperties: GetAdditionalRestoreGlobalProperties(MSBuildArgs.RestoreGlobalProperties)),
                    targetsToBuild: ["Restore"],
                    hostServices: null,
                    // We don't include ClearCachesAfterBuild flag unlike MSBuild's implicit restore
                    // to avoid evicting the virtual project asynchronously (https://github.com/dotnet/msbuild/issues/14148).
                    // It shouldn't make a difference for us because the restore has distinct global properties, so all projects will be re-evaluated anyway.
                    BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports | BuildRequestDataFlags.FailOnUnresolvedSdk);

                var restoreResult = BuildManager.DefaultBuildManager.BuildRequest(restoreRequest);
                if (restoreResult.OverallResult != BuildResultCode.Success)
                {
                    exitCode = 1;
                }

                projectInstance = restoreRequest.ProjectInstance;
                buildOrRestoreResult = restoreResult;
            }

            // Then do a build.
            if (exitCode == 0 && !NoBuild && !evalOnly)
            {
                var effectiveTargets = Builder.RequestedTargets is { Length: > 0 } requestedTargets
                    ? requestedTargets
                    : [Constants.Build, Constants.CoreCompile];
                var buildRequest = new BuildRequestData(
                    CreateProjectInstance(projectCollection),
                    targetsToBuild: effectiveTargets,
                    hostServices: null,
                    // SkipNonexistentTargets: CoreCompile doesn't exist in the outer build of multi-target projects.
                    BuildRequestDataFlags.SkipNonexistentTargets);

                var buildResult = BuildManager.DefaultBuildManager.BuildRequest(buildRequest);
                if (buildResult.OverallResult != BuildResultCode.Success)
                {
                    exitCode = 1;
                }

                if (exitCode == 0 && !msbuildGet)
                {
                    Debug.Assert(buildRequest.ProjectInstance != null);

                    // Cache run info (to avoid re-evaluating the project instance).
                    LastRunProperties = RunProperties.TryFromProject(buildRequest.ProjectInstance, out var runProperties)
                        ? runProperties
                        : null;

                    if (cache is not null && CanSaveCache(buildRequest.ProjectInstance))
                    {
                        cache.CurrentEntry.Run = LastRunProperties;

                        CacheCscArguments(cache, buildResult);
                        WriteCscRsp(cache);
                        CollectAdditionalSources(cache, buildRequest.ProjectInstance);

                        MarkBuildSuccess(cache);
                    }
                }

                projectInstance = buildRequest.ProjectInstance;
                buildOrRestoreResult = buildResult;
            }

            // Print build information.
            if (msbuildGet)
            {
                projectInstance ??= CreateProjectInstance(projectCollection);
                PrintBuildInformation(projectCollection, projectInstance, buildOrRestoreResult);
            }

            BuildManager.DefaultBuildManager.EndBuild();
            consoleLogger = null; // avoid double disposal which would throw

            return exitCode;
        }
        catch (Exception e)
        {
            Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose ?
                e.ToString().Red().Bold() :
                e.Message.Red().Bold());
            return 1;
        }
        finally
        {
            environmentScope?.Dispose();
            if (binaryLogger?.IsValueCreated == true) binaryLogger.Value.ReallyShutdown();
            consoleLogger?.Shutdown();
        }

        static IDictionary<string, string> GetAdditionalRestoreGlobalProperties(ReadOnlyDictionary<string, string>? restoreProperties)
        {
            // Compute the session ID outside the lambda to ensure it's the same for all project instances
            // (since there can be multiple project instances created while evaluating file-level directives).
            var sessionId = Guid.NewGuid().ToString("D");

            var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["MSBuildRestoreSessionId"] = sessionId,
                ["MSBuildIsRestoring"] = bool.TrueString,
            };

            foreach (var (key, value) in RestoringCommand.RestoreOptimizationProperties)
            {
                globalProperties[key] = value;
            }

            if (restoreProperties != null)
            {
                foreach (var (key, value) in restoreProperties)
                {
                    if (value is not null)
                    {
                        globalProperties[key] = value;
                    }
                }
            }

            return globalProperties;
        }

        static Lazy<FacadeLogger>? GetBinaryLogger(IReadOnlyList<string>? args)
        {
            if (args is null) return null;
            // Like in MSBuild, only the last binary logger is used.
            for (int i = args.Count - 1; i >= 0; i--)
            {
                var arg = args[i];
                if (LoggerUtility.IsBinLogArgument(arg))
                {
                    // We don't want to create the binlog file until actually needed, hence we wrap this in a Lazy.
                    return new(() =>
                    {
                        var logger = new BinaryLogger
                        {
                            Parameters = arg.IndexOf(':') is >= 0 and var index
                                ? arg[(index + 1)..]
                                : "msbuild.binlog",
                        };
                        return LoggerUtility.CreateFacadeLogger([logger]);
                    });
                }
            }

            return null;
        }

        void CacheCscArguments(FileBasedAppCacheInfo cache, BuildResult result)
        {
            if (result.TryGetResultsForTarget(Constants.CoreCompile, out var coreCompileResult) &&
                coreCompileResult.ResultCode == TargetResultCode.Success &&
                result.TryGetResultsForTarget(Constants.Build, out var buildResult) &&
                buildResult.ResultCode == TargetResultCode.Success &&
                buildResult.Items is [{ } buildResultItem])
            {
                if (coreCompileResult.Items.Length == 0)
                {
                    EnsurePreviousCacheEntry(cache);
                    cache.CurrentEntry.CscArguments = cache.PreviousEntry?.CscArguments ?? [];
                    cache.CurrentEntry.BuildResultFile = cache.PreviousEntry?.BuildResultFile;
                    Reporter.Verbose.WriteLine($"Reusing previous CSC arguments ({cache.CurrentEntry.CscArguments.Length}) because none were found in the {Constants.CoreCompile} target.");
                }
                else
                {
                    cache.CurrentEntry.CscArguments = coreCompileResult.Items
                        .Select(static i => i.GetMetadata(Constants.Identity))
                        .Where(static a => a != "/noconfig") // this option cannot be in the rsp file
                        .Select(Escape)
                        .ToImmutableArray();
                    cache.CurrentEntry.BuildResultFile = buildResultItem.GetMetadata(Constants.FullPath);
                    Reporter.Verbose.WriteLine($"Found CSC arguments ({cache.CurrentEntry.CscArguments.Length}) and build result path: {cache.CurrentEntry.BuildResultFile}");
                }
            }
            else
            {
                Reporter.Verbose.WriteLine($"No CSC arguments found in targets: {string.Join(", ", result.ResultsByTarget.Keys)}");
            }

            // Arguments coming from CoreCompile are escaped if they are in the form of `/option:"some path"`
            // but not if they are standalone paths - we need to escape the latter kind ourselves.
            static string Escape(string arg)
            {
                if (!Patterns.EscapedCompilerOption.IsMatch(arg))
                {
                    return CSharpCompilerCommand.EscapePathArgument(arg);
                }

                return arg;
            }
        }

        void ReuseInfoFromPreviousCacheEntry(FileBasedAppCacheInfo cache)
        {
            Debug.Assert(cache.CurrentEntry.AdditionalSources.Count == 0);

            if (cache.PreviousEntry != null)
            {
                foreach (var file in cache.PreviousEntry.AdditionalSources)
                {
                    cache.CurrentEntry.AdditionalSources.Add(file);
                }
            }
        }

        void WriteCscRsp(FileBasedAppCacheInfo cache)
        {
            if (cache.CurrentEntry.CscArguments.IsDefaultOrEmpty)
            {
                return;
            }

            string rspPath = CSharpCompilerCommand.WriteCscRspFile(Builder.ArtifactsPath, cache.CurrentEntry.CscArguments);
            Reporter.Verbose.WriteLine($"Wrote '{rspPath}'.");
        }

        bool CanSaveCache(ProjectInstance projectInstance)
        {
            if (!MSBuildUtilities.ConvertStringToBool(projectInstance.GetPropertyValue(FileBasedProgramCanSkipMSBuild), defaultValue: true))
            {
                Reporter.Verbose.WriteLine($"Not saving cache because there is an opt-out via MSBuild property {FileBasedProgramCanSkipMSBuild}.");
                return false;
            }

            if (EvaluatedDirectives.Any(static d => d is CSharpDirective.Project))
            {
                Reporter.Verbose.WriteLine("Not saving cache because there is a project directive.");
                return false;
            }

            if (EvaluatedDirectives.Any(static d => d is CSharpDirective.Ref))
            {
                Reporter.Verbose.WriteLine("Not saving cache because there is a ref directive.");
                return false;
            }

            if (EvaluatedDirectives.Any(static d =>
                    d is CSharpDirective.IncludeOrExclude { Kind: CSharpDirective.IncludeOrExcludeKind.Include } includeDirective &&
                    includeDirective.Name.AsSpan().ContainsAny('*', '?')))
            {
                Reporter.Verbose.WriteLine("Not saving cache because there is a glob include directive.");
                return false;
            }

            return true;
        }

        [RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")]
        void CollectAdditionalSources(FileBasedAppCacheInfo cache, ProjectInstance projectInstance)
        {
            Debug.Assert(cache.CurrentEntry.AdditionalSources.Count == 0);

            var entryPointFileDirectory = Path.GetDirectoryName(Builder.EntryPointFileFullPath);
            Debug.Assert(entryPointFileDirectory != null);

            var mapping = Builder.GetItemMappingAsync(projectInstance.Wrap(), ErrorReporters.IgnoringReporter).AsTask().GetAwaiter().GetResult();
            foreach (var entry in mapping)
            {
                if (string.Equals(entry.ItemType, "None", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                foreach (var item in projectInstance.GetItems(entry.ItemType))
                {
                    var fullPath = Path.GetFullPath(
                        path: item.GetMetadataValue("FullPath"),
                        basePath: entryPointFileDirectory);

                    cache.CurrentEntry.AdditionalSources.Add(fullPath);
                }
            }

            cache.CurrentEntry.AdditionalSources.Remove(Builder.EntryPointFileFullPath);
        }

        void PrintBuildInformation(ProjectCollection projectCollection, ProjectInstance projectInstance, BuildResult? buildOrRestoreResult)
        {
            var resultOutputFile = MSBuildArgs.GetResultOutputFile is [{ } file, ..] ? file : null;

            // If a single property is requested, don't print as JSON.
            if (MSBuildArgs is { GetProperty: [{ } singlePropertyName], GetItem: null or [], GetTargetResult: null or [] })
            {
                var result = projectInstance.GetPropertyValue(singlePropertyName);
                if (resultOutputFile == null)
                {
                    Console.WriteLine(result);
                }
                else
                {
                    File.WriteAllText(path: resultOutputFile, contents: result + Environment.NewLine);
                }
            }
            else
            {
                using var stream = resultOutputFile == null
                   ? Console.OpenStandardOutput()
                   : new FileStream(resultOutputFile, FileMode.Create, FileAccess.Write, FileShare.Read);
                using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
                writer.WriteStartObject();

                if (MSBuildArgs.GetProperty is [_, ..])
                {
                    writer.WritePropertyName("Properties");
                    writer.WriteStartObject();

                    foreach (var propertyName in MSBuildArgs.GetProperty)
                    {
                        writer.WriteString(propertyName, projectInstance.GetPropertyValue(propertyName));
                    }

                    writer.WriteEndObject();
                }

                if (MSBuildArgs.GetItem is [_, ..])
                {
                    writer.WritePropertyName("Items");
                    writer.WriteStartObject();

                    foreach (var itemName in MSBuildArgs.GetItem)
                    {
                        writer.WritePropertyName(itemName);
                        writer.WriteStartArray();

                        foreach (var item in projectInstance.GetItems(itemName))
                        {
                            writer.WriteStartObject();
                            writer.WriteString("Identity", item.GetMetadataValue("Identity"));

                            foreach (var metadatumName in item.MetadataNames)
                            {
                                if (metadatumName.Equals("Identity", StringComparison.OrdinalIgnoreCase))
                                {
                                    continue;
                                }

                                writer.WriteString(metadatumName, item.GetMetadataValue(metadatumName));
                            }

                            writer.WriteEndObject();
                        }

                        writer.WriteEndArray();
                    }

                    writer.WriteEndObject();
                }

                if (MSBuildArgs.GetTargetResult is [_, ..])
                {
                    Debug.Assert(buildOrRestoreResult != null);

                    writer.WritePropertyName("TargetResults");
                    writer.WriteStartObject();

                    foreach (var targetName in MSBuildArgs.GetTargetResult)
                    {
                        var targetResult = buildOrRestoreResult.ResultsByTarget[targetName];

                        writer.WritePropertyName(targetName);
                        writer.WriteStartObject();
                        writer.WriteString("Result", targetResult.TargetResultCodeToString());
                        writer.WritePropertyName("Items");
                        writer.WriteStartArray();

                        foreach (var item in targetResult.Items)
                        {
                            writer.WriteStartObject();
                            writer.WriteString("Identity", item.GetMetadata("Identity"));

                            foreach (string metadatumName in item.MetadataNames)
                            {
                                if (metadatumName.Equals("Identity", StringComparison.OrdinalIgnoreCase))
                                {
                                    continue;
                                }

                                writer.WriteString(metadatumName, item.GetMetadata(metadatumName));
                            }

                            writer.WriteEndObject();
                        }

                        writer.WriteEndArray();
                        writer.WriteEndObject();
                    }

                    writer.WriteEndObject();
                }

                writer.WriteEndObject();
                writer.Flush();
                stream.Write(Encoding.UTF8.GetBytes(Environment.NewLine));
            }
        }
    }

#endif

    private RunFileBuildCacheEntry? GetPreviousCacheEntry()
    {
        return FileBasedAppRunPlan.ReadCacheEntry(
            Path.Join(Builder.ArtifactsPath, FileBasedAppRunPlan.BuildSuccessCacheFileName));
    }

    private void EnsurePreviousCacheEntry(FileBasedAppCacheInfo cache)
    {
        if (cache.PreviousEntry is null && !cache.TriedDeserializingPreviousEntry)
        {
            cache.PreviousEntry = GetPreviousCacheEntry();
            cache.TriedDeserializingPreviousEntry = true;
        }
    }

    /// <summary>
    /// Determines the work required to make the virtual project outputs current.
    /// </summary>
    /// <param name="cache">Receives the computed cache state.</param>
    /// <returns>The required build level.</returns>
    public BuildLevel GetBuildLevel(out FileBasedAppCacheInfo? cache)
    {
        ImmutableArray<CSharpDirective> directives = Directives;
        bool canCache = !directives.Any(static directive => directive is CSharpDirective.Project or CSharpDirective.Ref);
        ImmutableArray<string> cacheDirectives = canCache
            ? directives
                .Where(static directive => directive is not CSharpDirective.Shebang)
                .Select(static directive => directive.ToString())
                .ToImmutableArray()
            : [];
        var inputs = new FileBasedAppRunPlanInputs(
            EntryPointFileFullPath: Builder.EntryPointFileFullPath,
            ArtifactsPath: Builder.ArtifactsPath,
            GlobalProperties: canCache
                ? MSBuildArgs.GlobalProperties?.ToDictionary(StringComparer.OrdinalIgnoreCase)
                    ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                : new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
            CanCache: canCache,
            Directives: cacheDirectives,
            SdkVersion: canCache ? Product.Version : string.Empty,
            RuntimeVersion: canCache ? CSharpCompilerCommand.RuntimeVersion : string.Empty,
            NoCache,
            GetCscInputPaths: CSharpCompilerCommand.GetPathsOfCscInputsFromNuGetCache);
        RunPlan plan = FileBasedAppRunPlan.Analyze(inputs);
        cache = plan.Cache;
        return plan.ToBuildLevel();
    }

    public void MarkArtifactsFolderUsed()
    {
        if (NoWriteBuildMarkers)
        {
            return;
        }

        FileBasedAppRunPlan.MarkArtifactsPathUsed(Builder.ArtifactsPath);
    }

    private void MarkBuildStart()
    {
        if (NoWriteBuildMarkers)
        {
            return;
        }

        string directory = Builder.ArtifactsPath;

        CreateTempSubdirectory(directory);

        MarkArtifactsFolderUsed();

        File.WriteAllText(Path.Join(directory, FileBasedAppRunPlan.BuildStartCacheFileName), Builder.EntryPointFileFullPath);
    }

    private void MarkBuildSuccess(FileBasedAppCacheInfo cache)
    {
        if (NoWriteBuildMarkers)
        {
            return;
        }

        string successCacheFile = Path.Join(Builder.ArtifactsPath, FileBasedAppRunPlan.BuildSuccessCacheFileName);
        using var stream = File.Open(successCacheFile, FileMode.Create, FileAccess.Write, FileShare.None);
        JsonSerializer.Serialize(stream, cache.CurrentEntry, RunFileBuildCacheJsonSerializerContext.Default.RunFileBuildCacheEntry);
    }

    [RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")]
    public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection)
    {
        return CreateProjectInstance(projectCollection, additionalGlobalProperties: null);
    }

    [RequiresDynamicCode("Uses MSBuild Object Model types, which are not AOT-safe")]
    public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection, IDictionary<string, string>? additionalGlobalProperties = null)
    {
        var projectCollectionWrapped = projectCollection.Wrap();

        var result = Builder.CreateProjectInstanceAsync(
            projectCollectionWrapped,
            ThrowingReporter,
            Directives,
            additionalGlobalProperties).AsTask().GetAwaiter().GetResult();

        EvaluatedDirectives = result.EvaluatedDirectives;

        return result.Project.Unwrap();
    }

    /// <summary>
    /// Creates a temporary subdirectory for file-based apps.
    /// Use <see cref="GetTempSubpath"/> to obtain the path.
    /// </summary>
    public static void CreateTempSubdirectory(string path)
    {
        if (OperatingSystem.IsWindows())
        {
            Directory.CreateDirectory(path);
        }
        else
        {
            // Ensure only the current user has access to the directory to avoid leaking the program to other users.
            // We don't mind that permissions might be different if the directory already exists,
            // since it's under user's local directory and its path should be unique.
            Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        }
    }

    public static readonly ErrorReporter ThrowingReporter =
        static (text, path, textSpan, message, innerException) =>
            throw new GracefulException(
            $"{new SourceFile(path, text).GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}",
            innerException);

    public static SourceFile RemoveDirectivesFromFile(SourceFile sourceFile)
    {
        var editor = FileBasedAppSourceEditor.Load(sourceFile);

        while (editor.Directives is [{ } directive, ..])
        {
            editor.Remove(directive);
        }

        return editor.SourceFile;
    }

    public static void RemoveDirectivesFromFile(SourceFile sourceFile, string targetFilePath)
    {
        var modifiedFile = RemoveDirectivesFromFile(sourceFile);
        (modifiedFile with { Path = targetFilePath }).Save();
    }
}