godot/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Godot;
using GodotTools.Internals;
using File = GodotTools.Utils.File;

namespace GodotTools.Build
{
    public static class BuildManager
    {
        private static BuildInfo? _buildInProgress;

        public const string MsBuildIssuesFileName = "msbuild_issues.csv";
        private const string MsBuildLogFileName = "msbuild_log.txt";

        public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);

        public static event BuildLaunchFailedEventHandler? BuildLaunchFailed;
        public static event Action<BuildInfo>? BuildStarted;
        public static event Action<BuildResult>? BuildFinished;
        public static event Action<string?>? StdOutputReceived;
        public static event Action<string?>? StdErrorReceived;

        public static DateTime LastValidBuildDateTime { get; private set; }

        static BuildManager()
        {
            UpdateLastValidBuildDateTime();
        }

        public static void UpdateLastValidBuildDateTime()
        {
            var dllName = $"{GodotSharpDirs.ProjectAssemblyName}.dll";
            var path = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "Debug", dllName);
            LastValidBuildDateTime = File.GetLastWriteTime(path);
        }

        private static void RemoveOldIssuesFile(BuildInfo buildInfo)
        {
            string issuesFile = GetIssuesFilePath(buildInfo);

            if (!File.Exists(issuesFile))
                return;

            File.Delete(issuesFile);
        }

        private static void ShowBuildErrorDialog(string message)
        {
            var plugin = GodotSharpEditor.Instance;
            plugin.ShowErrorDialog(message, "Build error");
            plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
        }

        private static string GetLogFilePath(BuildInfo buildInfo)
        {
            return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName);
        }

        private static string GetIssuesFilePath(BuildInfo buildInfo)
        {
            return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName);
        }

        private static void PrintVerbose(string text)
        {
            if (OS.IsStdOutVerbose())
                GD.Print(text);
        }

        private static bool Build(BuildInfo buildInfo)
        {
            if (_buildInProgress != null)
                throw new InvalidOperationException("A build is already in progress.");

            _buildInProgress = buildInfo;

            try
            {
                BuildStarted?.Invoke(buildInfo);

                // Required in order to update the build tasks list.
                Internal.GodotMainIteration();

                try
                {
                    RemoveOldIssuesFile(buildInfo);
                }
                catch (IOException e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
                    Console.Error.WriteLine(e);
                }

                try
                {
                    int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);

                    if (exitCode != 0)
                        PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");

                    BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);

                    return exitCode == 0;
                }
                catch (Exception e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo,
                        $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
                    Console.Error.WriteLine(e);
                    return false;
                }
            }
            finally
            {
                _buildInProgress = null;
            }
        }

        public static async Task<bool> BuildAsync(BuildInfo buildInfo)
        {
            if (_buildInProgress != null)
                throw new InvalidOperationException("A build is already in progress.");

            _buildInProgress = buildInfo;

            try
            {
                BuildStarted?.Invoke(buildInfo);

                try
                {
                    RemoveOldIssuesFile(buildInfo);
                }
                catch (IOException e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
                    Console.Error.WriteLine(e);
                }

                try
                {
                    int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);

                    if (exitCode != 0)
                        PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");

                    BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);

                    return exitCode == 0;
                }
                catch (Exception e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo,
                        $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
                    Console.Error.WriteLine(e);
                    return false;
                }
            }
            finally
            {
                _buildInProgress = null;
            }
        }

        private static bool Publish(BuildInfo buildInfo)
        {
            if (_buildInProgress != null)
                throw new InvalidOperationException("A build is already in progress.");

            _buildInProgress = buildInfo;

            try
            {
                BuildStarted?.Invoke(buildInfo);

                // Required in order to update the build tasks list.
                Internal.GodotMainIteration();

                try
                {
                    RemoveOldIssuesFile(buildInfo);
                }
                catch (IOException e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
                    Console.Error.WriteLine(e);
                }

                try
                {
                    int exitCode = BuildSystem.Publish(buildInfo, StdOutputReceived, StdErrorReceived);

                    if (exitCode != 0)
                        PrintVerbose(
                            $"dotnet publish exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");

                    BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);

                    return exitCode == 0;
                }
                catch (Exception e)
                {
                    BuildLaunchFailed?.Invoke(buildInfo,
                        $"The publish method threw an exception.\n{e.GetType().FullName}: {e.Message}");
                    Console.Error.WriteLine(e);
                    return false;
                }
            }
            finally
            {
                _buildInProgress = null;
            }
        }

        private static bool BuildProjectBlocking(BuildInfo buildInfo)
        {
            if (!File.Exists(buildInfo.Project))
                return true; // No project to build.

            bool success;
            using (var pr = new EditorProgress("dotnet_build_project", "Building .NET project...", 1))
            {
                pr.Step("Building project", 0);
                success = Build(buildInfo);
            }

            if (!success)
            {
                ShowBuildErrorDialog("Failed to build project");
            }

            return success;
        }

        private static bool CleanProjectBlocking(BuildInfo buildInfo)
        {
            if (!File.Exists(buildInfo.Project))
                return true; // No project to clean.

            bool success;
            using (var pr = new EditorProgress("dotnet_clean_project", "Cleaning .NET project...", 1))
            {
                pr.Step("Cleaning project", 0);
                success = Build(buildInfo);
            }

            if (!success)
            {
                ShowBuildErrorDialog("Failed to clean project");
            }

            return success;
        }

        private static bool PublishProjectBlocking(BuildInfo buildInfo)
        {
            bool success;
            using (var pr = new EditorProgress("dotnet_publish_project", "Publishing .NET project...", 1))
            {
                pr.Step("Running dotnet publish", 0);
                success = Publish(buildInfo);
            }

            return success;
        }

        private static BuildInfo CreateBuildInfo(
            string configuration,
            string? platform = null,
            bool rebuild = false,
            bool onlyClean = false
        )
        {
            var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, GodotSharpDirs.ProjectCsProjPath, configuration,
                restore: true, rebuild, onlyClean);

            // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
            if (platform != null || Utils.OS.PlatformNameMap.TryGetValue(OS.GetName(), out platform))
                buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");

            if (Internal.GodotIsRealTDouble())
                buildInfo.CustomProperties.Add("GodotFloat64=true");

            return buildInfo;
        }

        private static BuildInfo CreatePublishBuildInfo(
            string configuration,
            string platform,
            string runtimeIdentifier,
            string publishOutputDir,
            bool includeDebugSymbols = true
        )
        {
            var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, GodotSharpDirs.ProjectCsProjPath, configuration,
                runtimeIdentifier, publishOutputDir, restore: true, rebuild: false, onlyClean: false);

            if (!includeDebugSymbols)
            {
                buildInfo.CustomProperties.Add("DebugType=None");
                buildInfo.CustomProperties.Add("DebugSymbols=false");
            }

            buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");

            if (Internal.GodotIsRealTDouble())
                buildInfo.CustomProperties.Add("GodotFloat64=true");

            return buildInfo;
        }

        public static bool BuildProjectBlocking(
            string configuration,
            string? platform = null,
            bool rebuild = false
        ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild));

        public static bool CleanProjectBlocking(
            string configuration,
            string? platform = null
        ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false, onlyClean: true));

        public static bool PublishProjectBlocking(
            string configuration,
            string platform,
            string runtimeIdentifier,
            string publishOutputDir,
            bool includeDebugSymbols = true
        ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration,
            platform, runtimeIdentifier, publishOutputDir, includeDebugSymbols));

        public static bool GenerateXCFrameworkBlocking(
            List<string> outputPaths,
            string xcFrameworkPath)
        {
            using var pr = new EditorProgress("generate_xcframework", "Generating XCFramework...", 1);

            pr.Step("Running xcodebuild -create-xcframework", 0);

            if (!GenerateXCFramework(outputPaths, xcFrameworkPath))
            {
                ShowBuildErrorDialog("Failed to generate XCFramework");
                return false;
            }

            return true;
        }

        private static bool GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath)
        {
            // Required in order to update the build tasks list.
            Internal.GodotMainIteration();

            try
            {
                int exitCode = BuildSystem.GenerateXCFramework(outputPaths, xcFrameworkPath, StdOutputReceived, StdErrorReceived);

                if (exitCode != 0)
                    PrintVerbose(
                        $"xcodebuild create-xcframework exited with code: {exitCode}.");

                return exitCode == 0;
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e);
                return false;
            }
        }

        public static bool EditorBuildCallback()
        {
            if (!File.Exists(GodotSharpDirs.ProjectCsProjPath))
                return true; // No project to build.

            if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
                return true; // Requested play from an external editor/IDE which already built the project.

            return BuildProjectBlocking("Debug");
        }

        public static void Initialize()
        {
        }
    }
}