391 lines
13 KiB
C#
391 lines
13 KiB
C#
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.MSBuildPanel.MakeVisible();
|
|
}
|
|
|
|
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. Check MSBuild panel for details.");
|
|
}
|
|
|
|
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()
|
|
{
|
|
}
|
|
}
|
|
}
|