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

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Godot;
using GodotTools.BuildLogger;
using GodotTools.Internals;
using GodotTools.Utils;
using Directory = GodotTools.Utils.Directory;

namespace GodotTools.Build
{
    public static class BuildSystem
    {
        private static Process LaunchBuild(BuildInfo buildInfo, Action<string?>? stdOutHandler,
            Action<string?>? stdErrHandler)
        {
            string? dotnetPath = DotNetFinder.FindDotNetExe();

            if (dotnetPath == null)
                throw new FileNotFoundException("Cannot find the dotnet executable.");

            var editorSettings = EditorInterface.Singleton.GetEditorSettings();

            var startInfo = new ProcessStartInfo(dotnetPath);

            BuildArguments(buildInfo, startInfo.ArgumentList, editorSettings);

            string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString();
            stdOutHandler?.Invoke(launchMessage);
            if (Godot.OS.IsStdOutVerbose())
                Console.WriteLine(launchMessage);

            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.UseShellExecute = false;
            startInfo.CreateNoWindow = true;
            startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"]
                = ((string)editorSettings.GetSetting("interface/editor/editor_language")).Replace('_', '-');

            if (OperatingSystem.IsWindows())
            {
                startInfo.StandardOutputEncoding = Encoding.UTF8;
                startInfo.StandardErrorEncoding = Encoding.UTF8;
            }

            // Needed when running from Developer Command Prompt for VS
            RemovePlatformVariable(startInfo.EnvironmentVariables);

            var process = new Process { StartInfo = startInfo };

            if (stdOutHandler != null)
                process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data);
            if (stdErrHandler != null)
                process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data);

            process.Start();

            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            return process;
        }

        public static int Build(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
        {
            using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
            {
                process.WaitForExit();

                return process.ExitCode;
            }
        }

        public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string?>? stdOutHandler,
            Action<string?>? stdErrHandler)
        {
            using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
            {
                await process.WaitForExitAsync();

                return process.ExitCode;
            }
        }

        private static Process LaunchPublish(BuildInfo buildInfo, Action<string?>? stdOutHandler,
            Action<string?>? stdErrHandler)
        {
            string? dotnetPath = DotNetFinder.FindDotNetExe();

            if (dotnetPath == null)
                throw new FileNotFoundException("Cannot find the dotnet executable.");

            var editorSettings = EditorInterface.Singleton.GetEditorSettings();

            var startInfo = new ProcessStartInfo(dotnetPath);

            BuildPublishArguments(buildInfo, startInfo.ArgumentList, editorSettings);

            string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Running: ")).ToString();
            stdOutHandler?.Invoke(launchMessage);
            if (Godot.OS.IsStdOutVerbose())
                Console.WriteLine(launchMessage);

            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.UseShellExecute = false;
            startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"]
                = ((string)editorSettings.GetSetting("interface/editor/editor_language")).Replace('_', '-');

            if (OperatingSystem.IsWindows())
            {
                startInfo.StandardOutputEncoding = Encoding.UTF8;
                startInfo.StandardErrorEncoding = Encoding.UTF8;
            }

            // Needed when running from Developer Command Prompt for VS
            RemovePlatformVariable(startInfo.EnvironmentVariables);

            var process = new Process { StartInfo = startInfo };

            if (stdOutHandler != null)
                process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data);
            if (stdErrHandler != null)
                process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data);

            process.Start();

            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            return process;
        }

        public static int Publish(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
        {
            using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler))
            {
                process.WaitForExit();

                return process.ExitCode;
            }
        }

        private static void BuildArguments(BuildInfo buildInfo, Collection<string> arguments,
            EditorSettings editorSettings)
        {
            // `dotnet clean` / `dotnet build` commands
            arguments.Add(buildInfo.OnlyClean ? "clean" : "build");

            // C# Project
            arguments.Add(buildInfo.Project);

            // `dotnet clean` doesn't recognize these options
            if (!buildInfo.OnlyClean)
            {
                // Restore
                // `dotnet build` restores by default, unless requested not to
                if (!buildInfo.Restore)
                    arguments.Add("--no-restore");

                // Incremental or rebuild
                if (buildInfo.Rebuild)
                    arguments.Add("--no-incremental");
            }

            // Configuration
            arguments.Add("-c");
            arguments.Add(buildInfo.Configuration);

            // Verbosity
            AddVerbosityArguments(buildInfo, arguments, editorSettings);

            // Logger
            AddLoggerArgument(buildInfo, arguments);

            // Binary log
            AddBinaryLogArgument(buildInfo, arguments, editorSettings);

            // Custom properties
            foreach (var customProperty in buildInfo.CustomProperties)
            {
                arguments.Add("-p:" + (string)customProperty);
            }
        }

        private static void BuildPublishArguments(BuildInfo buildInfo, Collection<string> arguments,
            EditorSettings editorSettings)
        {
            arguments.Add("publish"); // `dotnet publish` command

            // C# Project
            arguments.Add(buildInfo.Project);

            // Restore
            // `dotnet publish` restores by default, unless requested not to
            if (!buildInfo.Restore)
                arguments.Add("--no-restore");

            // Incremental or rebuild
            // TODO: Not supported in `dotnet publish` (https://github.com/dotnet/sdk/issues/11099)
            // if (buildInfo.Rebuild)
            //     arguments.Add("--no-incremental");

            // Configuration
            arguments.Add("-c");
            arguments.Add(buildInfo.Configuration);

            // Runtime Identifier
            arguments.Add("-r");
            arguments.Add(buildInfo.RuntimeIdentifier!);

            // Self-published
            arguments.Add("--self-contained");
            arguments.Add("true");

            // Verbosity
            AddVerbosityArguments(buildInfo, arguments, editorSettings);

            // Logger
            AddLoggerArgument(buildInfo, arguments);

            // Binary log
            AddBinaryLogArgument(buildInfo, arguments, editorSettings);

            // Custom properties
            foreach (var customProperty in buildInfo.CustomProperties)
            {
                arguments.Add("-p:" + (string)customProperty);
            }

            // Publish output directory
            if (buildInfo.PublishOutputDir != null)
            {
                arguments.Add("-o");
                arguments.Add(buildInfo.PublishOutputDir);
            }
        }

        private static void AddVerbosityArguments(BuildInfo buildInfo, Collection<string> arguments,
            EditorSettings editorSettings)
        {
            var verbosityLevel =
                editorSettings.GetSetting(GodotSharpEditor.Settings.VerbosityLevel).As<VerbosityLevelId>();
            arguments.Add("-v");
            arguments.Add(verbosityLevel switch
            {
                VerbosityLevelId.Quiet => "quiet",
                VerbosityLevelId.Minimal => "minimal",
                VerbosityLevelId.Detailed => "detailed",
                VerbosityLevelId.Diagnostic => "diagnostic",
                _ => "normal",
            });

            if ((bool)editorSettings.GetSetting(GodotSharpEditor.Settings.NoConsoleLogging))
                arguments.Add("-noconlog");
        }

        private static void AddLoggerArgument(BuildInfo buildInfo, Collection<string> arguments)
        {
            string buildLoggerPath = Path.Combine(Internals.GodotSharpDirs.DataEditorToolsDir,
                "GodotTools.BuildLogger.dll");

            arguments.Add(
                $"-l:{typeof(GodotBuildLogger).FullName},{buildLoggerPath};{buildInfo.LogsDirPath}");
        }

        private static void AddBinaryLogArgument(BuildInfo buildInfo, Collection<string> arguments,
            EditorSettings editorSettings)
        {
            if (!(bool)editorSettings.GetSetting(GodotSharpEditor.Settings.CreateBinaryLog))
                return;

            arguments.Add($"-bl:{Path.Combine(buildInfo.LogsDirPath, "msbuild.binlog")}");
            arguments.Add("-ds:False"); // Honestly never understood why -bl also switches -ds on.
        }

        private static void RemovePlatformVariable(StringDictionary environmentVariables)
        {
            // EnvironmentVariables is case sensitive? Seriously?

            var platformEnvironmentVariables = new List<string>();

            foreach (string env in environmentVariables.Keys)
            {
                if (env.ToUpperInvariant() == "PLATFORM")
                    platformEnvironmentVariables.Add(env);
            }

            foreach (string env in platformEnvironmentVariables)
                environmentVariables.Remove(env);
        }

        private static Process DoGenerateXCFramework(List<string> outputPaths, string xcFrameworkPath,
            Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
        {
            if (Directory.Exists(xcFrameworkPath))
            {
                Directory.Delete(xcFrameworkPath, true);
            }

            var startInfo = new ProcessStartInfo("xcrun");

            BuildXCFrameworkArguments(outputPaths, xcFrameworkPath, startInfo.ArgumentList);

            string launchMessage = startInfo.GetCommandLineDisplay(new StringBuilder("Packaging: ")).ToString();
            stdOutHandler?.Invoke(launchMessage);
            if (Godot.OS.IsStdOutVerbose())
                Console.WriteLine(launchMessage);

            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            startInfo.UseShellExecute = false;

            if (OperatingSystem.IsWindows())
            {
                startInfo.StandardOutputEncoding = Encoding.UTF8;
                startInfo.StandardErrorEncoding = Encoding.UTF8;
            }

            // Needed when running from Developer Command Prompt for VS.
            RemovePlatformVariable(startInfo.EnvironmentVariables);

            var process = new Process { StartInfo = startInfo };

            if (stdOutHandler != null)
                process.OutputDataReceived += (_, e) => stdOutHandler.Invoke(e.Data);
            if (stdErrHandler != null)
                process.ErrorDataReceived += (_, e) => stdErrHandler.Invoke(e.Data);

            process.Start();

            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            return process;
        }

        public static int GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
        {
            using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler))
            {
                process.WaitForExit();

                return process.ExitCode;
            }
        }

        private static void BuildXCFrameworkArguments(List<string> outputPaths,
            string xcFrameworkPath, Collection<string> arguments)
        {
            var baseDylib = $"{GodotSharpDirs.ProjectAssemblyName}.dylib";
            var baseSym = $"{GodotSharpDirs.ProjectAssemblyName}.framework.dSYM";

            arguments.Add("xcodebuild");
            arguments.Add("-create-xcframework");

            foreach (var outputPath in outputPaths)
            {
                arguments.Add("-library");
                arguments.Add(Path.Combine(outputPath, baseDylib));
                arguments.Add("-debug-symbols");
                arguments.Add(Path.Combine(outputPath, baseSym));
            }

            arguments.Add("-output");
            arguments.Add(xcFrameworkPath);
        }
    }
}