godot/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs
Ignacio Etcheverry c3954441f3 3.2 New csproj style with backport of Godot.NET.Sdk
This is a cherry-pick of
ced77b1e9b
with several 3.2 specific alterations.

There are a lot of build issues coming from
old style projects. At this point fixing every
single one of those would require adding patch
after patch to the project file, which is a
considerable amount work and makes the csproj
even more bloated than it already is.

As such I decided this effort would be better
spent back-porting the Sdk style support that's
already available in 4.0-dev to the 3.2 branch.

This will prevent many issues, but it will also
introduce other benefits, among them:

- While target framework stays as .NET Framework
  v4.7.2, it can be changed to .NET Standard 2.0
  or greater if desired.
- It makes it much easier to add future patches.
  They are added to Godot.NET.Sdk and the only
  change required in Godot code is to update the
  Sdk version to use.
- Default Godot define constants are also
  backported, which fixes IDE issues with the
  preprocessor.

There are a few differences in the changes
applied during patching of the csproj compared
to 4.0 with the purpose of preventing breaking
builds:

- 'TargetFramework' stays net472 both for new
  projects and when importing old ones. It can
  be manually changed to netstandard 2.0+ if
  desired though.

The following features are enabled by default for
new projects. Enabling them in imported projects
may result in errors that must be fixed manually:

- 'EnableDefaultCompileItems' is disabled as it
  can result in undesired C# source files being
  included. Existing include items are kept.
  As long as 'EnableDefaultCompileItems' remains
  disabled, Godot will continue taking care of
  adding and removing C# files to the csproj.
- 'GenerateAssemblyInfo' is disabled as it
  guarantees a build error because of conflicts
  between the existing 'AssemblyInfo.cs' and the
  auto-generated one.
- 'Deterministic' is disabled because it doesn't
  like wildcards in the assembly version (1.0.*)
  that was in the old 'AssemblyInfo.cs'.

Of importance:

This is a breaking change. A great effort was
put in avoiding build errors after upgrading a
project, but there may still be exceptions.

This also breaks forward compatibility. Projects
opened with Godot 3.2.3 won't work out of the box
with older Godot versions. This was already the
case with changes introduced in 3.2.2.

Albeit C# support in 3.2.x was still labeled as
alpha, we've been trying to treat it as stable
for some time. Still the amount of problems this
change solves justifies it, but no more changes
that break project compatibility are to be
introduced from now on (at least for 3.x).
2020-08-20 21:48:59 +02:00

277 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GodotTools.Build;
using GodotTools.Ides.Rider;
using GodotTools.Internals;
using GodotTools.Utils;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
namespace GodotTools
{
public static class BuildManager
{
private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>();
public const string PropNameMSBuildMono = "MSBuild (Mono)";
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)";
public const string PropNameDotnetCli = "dotnet CLI";
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt";
private static void RemoveOldIssuesFile(BuildInfo buildInfo)
{
var issuesFile = GetIssuesFilePath(buildInfo);
if (!File.Exists(issuesFile))
return;
File.Delete(issuesFile);
}
private static void ShowBuildErrorDialog(string message)
{
GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error");
GodotSharpEditor.Instance.BottomPanel.ShowBuildTab();
}
public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException();
public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException();
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 (Godot.OS.IsStdoutVerbose())
Godot.GD.Print(text);
}
public static bool Build(BuildInfo buildInfo)
{
if (BuildsInProgress.Contains(buildInfo))
throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo);
try
{
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
buildTab.OnBuildStart();
// Required in order to update the build tasks list
Internal.GodotMainIteration();
try
{
RemoveOldIssuesFile(buildInfo);
}
catch (IOException e)
{
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = BuildSystem.Build(buildInfo);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
return exitCode == 0;
}
catch (Exception e)
{
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
BuildsInProgress.Remove(buildInfo);
}
}
public static async Task<bool> BuildAsync(BuildInfo buildInfo)
{
if (BuildsInProgress.Contains(buildInfo))
throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo);
try
{
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
try
{
RemoveOldIssuesFile(buildInfo);
}
catch (IOException e)
{
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e);
}
try
{
int exitCode = await BuildSystem.BuildAsync(buildInfo);
if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error);
return exitCode == 0;
}
catch (Exception e)
{
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e);
return false;
}
}
finally
{
BuildsInProgress.Remove(buildInfo);
}
}
public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null)
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return true; // No solution to build
// Make sure the API assemblies are up to date before building the project.
// We may not have had the chance to update the release API assemblies, and the debug ones
// may have been deleted by the user at some point after they were loaded by the Godot editor.
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(config == "ExportRelease" ? "Release" : "Debug");
if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
{
ShowBuildErrorDialog("Failed to update the Godot API assemblies");
return false;
}
using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
{
pr.Step("Building project solution", 0);
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, new[] {"Build"}, config, restore: true);
// If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
if (Internal.GodotIsRealTDouble())
buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
if (!Build(buildInfo))
{
ShowBuildErrorDialog("Failed to build project solution");
return false;
}
}
return true;
}
public static bool EditorBuildCallback()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return true; // No solution to build
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (File.Exists(editorScriptsMetadataPath))
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings;
if (currentPlayRequest != null)
{
if (currentPlayRequest.Value.HasDebugger)
{
// Set the environment variable that will tell the player to connect to the IDE debugger
// TODO: We should probably add a better way to do this
Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT",
"--debugger-agent=transport=dt_socket" +
$",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" +
",server=n");
}
if (!currentPlayRequest.Value.BuildBeforePlaying)
return true; // Requested play from an external editor/IDE which already built the project
}
return BuildProjectBlocking("Debug");
}
public static void Initialize()
{
// Build tool settings
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
BuildTool msbuildDefault;
if (OS.IsWindows)
{
if (RiderPathManager.IsExternalEditorSetToRider(editorSettings))
msbuildDefault = BuildTool.JetBrainsMsBuild;
else
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs;
}
else
{
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono;
}
EditorDef("mono/builds/build_tool", msbuildDefault);
string hintString;
if (OS.IsWindows)
{
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
$"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," +
$"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," +
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
}
else
{
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
}
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
{
["type"] = Godot.Variant.Type.Int,
["name"] = "mono/builds/build_tool",
["hint"] = Godot.PropertyHint.Enum,
["hint_string"] = hintString
});
EditorDef("mono/builds/print_build_output", false);
}
}
}