C#: Re-work solution build output panel

- Removed item list that displayed multiple build
  configurations launched. Now we only display
  the last build that was launched.
- Display build output next to the issues list.
  Its visibility can be toggled off/on.
  This build output is obtained from the MSBuild
  process rather than the MSBuild logger. As such
  it displays some MSBuild fatal errors that
  previously couldn't be displayed.
- Added a context menu to the issues list with
  the option to copy the issue text.
- Replaced the 'Build Project' button in the panel
  with a popup menu with the options:
  - Build Solution
  - Rebuild Solution
  - Clean Solution
- The bottom panel button was renamed from 'Mono'
  to 'MSBuild' and now display an error/warning icon
  if the last build had issues.
This commit is contained in:
Ignacio Etcheverry 2020-10-04 02:11:53 +02:00
parent 32be9299ba
commit f06f91281c
11 changed files with 512 additions and 539 deletions

View file

@ -15,14 +15,14 @@ namespace GodotTools.BuildLogger
public void Initialize(IEventSource eventSource) public void Initialize(IEventSource eventSource)
{ {
if (null == Parameters) if (null == Parameters)
throw new LoggerException("Log directory was not set."); throw new LoggerException("Log directory parameter not specified.");
var parameters = Parameters.Split(new[] { ';' }); var parameters = Parameters.Split(new[] { ';' });
string logDir = parameters[0]; string logDir = parameters[0];
if (string.IsNullOrEmpty(logDir)) if (string.IsNullOrEmpty(logDir))
throw new LoggerException("Log directory was not set."); throw new LoggerException("Log directory parameter is empty.");
if (parameters.Length > 1) if (parameters.Length > 1)
throw new LoggerException("Too many parameters passed."); throw new LoggerException("Too many parameters passed.");
@ -51,22 +51,31 @@ namespace GodotTools.BuildLogger
{ {
throw new LoggerException("Failed to create log file: " + ex.Message); throw new LoggerException("Failed to create log file: " + ex.Message);
} }
else
{
// Unexpected failure // Unexpected failure
throw; throw;
} }
}
eventSource.ProjectStarted += eventSource_ProjectStarted; eventSource.ProjectStarted += eventSource_ProjectStarted;
eventSource.TaskStarted += eventSource_TaskStarted; eventSource.ProjectFinished += eventSource_ProjectFinished;
eventSource.MessageRaised += eventSource_MessageRaised; eventSource.MessageRaised += eventSource_MessageRaised;
eventSource.WarningRaised += eventSource_WarningRaised; eventSource.WarningRaised += eventSource_WarningRaised;
eventSource.ErrorRaised += eventSource_ErrorRaised; eventSource.ErrorRaised += eventSource_ErrorRaised;
eventSource.ProjectFinished += eventSource_ProjectFinished;
} }
void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e) private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
indent++;
}
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
indent--;
WriteLine(e.Message);
}
private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
{ {
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}"; string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}";
@ -81,7 +90,7 @@ namespace GodotTools.BuildLogger
issuesStreamWriter.WriteLine(errorLine); issuesStreamWriter.WriteLine(errorLine);
} }
void eventSource_WarningRaised(object sender, BuildWarningEventArgs e) private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{ {
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}"; string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
@ -108,40 +117,6 @@ namespace GodotTools.BuildLogger
} }
} }
private void eventSource_TaskStarted(object sender, TaskStartedEventArgs e)
{
// TaskStartedEventArgs adds ProjectFile, TaskFile, TaskName
// To keep this log clean, this logger will ignore these events.
}
private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
indent++;
}
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
indent--;
WriteLine(e.Message);
}
/// <summary>
/// Write a line to the log, adding the SenderName
/// </summary>
private void WriteLineWithSender(string line, BuildEventArgs e)
{
if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
{
// Well, if the sender name is MSBuild, let's leave it out for prettiness
WriteLine(line);
}
else
{
WriteLine(e.SenderName + ": " + line);
}
}
/// <summary> /// <summary>
/// Write a line to the log, adding the SenderName and Message /// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects) /// (these parameters are on all MSBuild event argument objects)

View file

@ -1,339 +0,0 @@
using Godot;
using System;
using System.IO;
using Godot.Collections;
using GodotTools.Internals;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
using Path = System.IO.Path;
namespace GodotTools
{
public class BottomPanel : VBoxContainer
{
private EditorInterface editorInterface;
private TabContainer panelTabs;
private VBoxContainer panelBuildsTab;
private ItemList buildTabsList;
private TabContainer buildTabs;
private Button warningsBtn;
private Button errorsBtn;
private Button viewLogBtn;
private void _UpdateBuildTab(int index, int? currentTab)
{
var tab = (BuildTab)buildTabs.GetChild(index);
string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution);
itemName += " [" + tab.BuildInfo.Configuration + "]";
buildTabsList.AddItem(itemName, tab.IconTexture);
string itemTooltip = "Solution: " + tab.BuildInfo.Solution;
itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration;
itemTooltip += "\nStatus: ";
if (tab.BuildExited)
itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored";
else
itemTooltip += "Running";
if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error)
itemTooltip += $"\nErrors: {tab.ErrorCount}";
itemTooltip += $"\nWarnings: {tab.WarningCount}";
buildTabsList.SetItemTooltip(index, itemTooltip);
// If this tab was already selected before the changes or if no tab was selected
if (currentTab == null || currentTab == index)
{
buildTabsList.Select(index);
_BuildTabsItemSelected(index);
}
}
private void _UpdateBuildTabsList()
{
buildTabsList.Clear();
int? currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
currentTab = null;
for (int i = 0; i < buildTabs.GetChildCount(); i++)
_UpdateBuildTab(i, currentTab);
}
public BuildTab GetBuildTabFor(BuildInfo buildInfo)
{
foreach (var buildTab in new Array<BuildTab>(buildTabs.GetChildren()))
{
if (buildTab.BuildInfo.Equals(buildInfo))
return buildTab;
}
var newBuildTab = new BuildTab(buildInfo);
AddBuildTab(newBuildTab);
return newBuildTab;
}
private void _BuildTabsItemSelected(int idx)
{
if (idx < 0 || idx >= buildTabs.GetTabCount())
throw new IndexOutOfRangeException();
buildTabs.CurrentTab = idx;
if (!buildTabs.Visible)
buildTabs.Visible = true;
warningsBtn.Visible = true;
errorsBtn.Visible = true;
viewLogBtn.Visible = true;
}
private void _BuildTabsNothingSelected()
{
if (buildTabs.GetTabCount() != 0)
{
// just in case
buildTabs.Visible = false;
// This callback is called when clicking on the empty space of the list.
// ItemList won't deselect the items automatically, so we must do it ourselves.
buildTabsList.UnselectAll();
}
warningsBtn.Visible = false;
errorsBtn.Visible = false;
viewLogBtn.Visible = false;
}
private void _WarningsToggled(bool pressed)
{
int currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
throw new InvalidOperationException("No tab selected");
var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
buildTab.WarningsVisible = pressed;
buildTab.UpdateIssuesList();
}
private void _ErrorsToggled(bool pressed)
{
int currentTab = buildTabs.CurrentTab;
if (currentTab < 0 || currentTab >= buildTabs.GetTabCount())
throw new InvalidOperationException("No tab selected");
var buildTab = (BuildTab)buildTabs.GetChild(currentTab);
buildTab.ErrorsVisible = pressed;
buildTab.UpdateIssuesList();
}
public void BuildProjectPressed()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // 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))
{
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
GD.PushError($"Failed to copy scripts metadata file. Exception message: {e.Message}");
return;
}
}
bool buildSuccess = BuildManager.BuildProjectBlocking("Debug");
if (!buildSuccess)
return;
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
private void _ViewLogPressed()
{
if (!buildTabsList.IsAnythingSelected())
return;
var selectedItems = buildTabsList.GetSelectedItems();
if (selectedItems.Length != 1)
throw new InvalidOperationException($"Expected 1 selected item, got {selectedItems.Length}");
int selectedItem = selectedItems[0];
var buildTab = (BuildTab)buildTabs.GetTabControl(selectedItem);
OS.ShellOpen(Path.Combine(buildTab.BuildInfo.LogsDirPath, BuildManager.MsBuildLogFileName));
}
public override void _Notification(int what)
{
base._Notification(what);
if (what == EditorSettings.NotificationEditorSettingsChanged)
{
var editorBaseControl = editorInterface.GetBaseControl();
panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
}
}
public void AddBuildTab(BuildTab buildTab)
{
buildTabs.AddChild(buildTab);
RaiseBuildTab(buildTab);
}
public void RaiseBuildTab(BuildTab buildTab)
{
if (buildTab.GetParent() != buildTabs)
throw new InvalidOperationException("Build tab is not in the tabs list");
buildTabs.MoveChild(buildTab, 0);
_UpdateBuildTabsList();
}
public void ShowBuildTab()
{
for (int i = 0; i < panelTabs.GetTabCount(); i++)
{
if (panelTabs.GetTabControl(i) == panelBuildsTab)
{
panelTabs.CurrentTab = i;
GodotSharpEditor.Instance.MakeBottomPanelItemVisible(this);
return;
}
}
GD.PushError("Builds tab not found");
}
public override void _Ready()
{
base._Ready();
editorInterface = GodotSharpEditor.Instance.GetEditorInterface();
var editorBaseControl = editorInterface.GetBaseControl();
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
SetAnchorsAndMarginsPreset(LayoutPreset.Wide);
panelTabs = new TabContainer
{
TabAlign = TabContainer.TabAlignEnum.Left,
RectMinSize = new Vector2(0, 228) * EditorScale,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
panelTabs.AddThemeStyleboxOverride("panel", editorBaseControl.GetThemeStylebox("DebuggerPanel", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_fg", editorBaseControl.GetThemeStylebox("DebuggerTabFG", "EditorStyles"));
panelTabs.AddThemeStyleboxOverride("tab_bg", editorBaseControl.GetThemeStylebox("DebuggerTabBG", "EditorStyles"));
AddChild(panelTabs);
{
// Builds tab
panelBuildsTab = new VBoxContainer
{
Name = "Builds".TTR(),
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill
};
panelTabs.AddChild(panelBuildsTab);
var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
panelBuildsTab.AddChild(toolBarHBox);
var buildProjectBtn = new Button
{
Text = "Build Project".TTR(),
FocusMode = FocusModeEnum.None
};
buildProjectBtn.PressedSignal += BuildProjectPressed;
toolBarHBox.AddChild(buildProjectBtn);
toolBarHBox.AddSpacer(begin: false);
warningsBtn = new Button
{
Text = "Warnings".TTR(),
ToggleMode = true,
Pressed = true,
Visible = false,
FocusMode = FocusModeEnum.None
};
warningsBtn.Toggled += _WarningsToggled;
toolBarHBox.AddChild(warningsBtn);
errorsBtn = new Button
{
Text = "Errors".TTR(),
ToggleMode = true,
Pressed = true,
Visible = false,
FocusMode = FocusModeEnum.None
};
errorsBtn.Toggled += _ErrorsToggled;
toolBarHBox.AddChild(errorsBtn);
toolBarHBox.AddSpacer(begin: false);
viewLogBtn = new Button
{
Text = "View log".TTR(),
FocusMode = FocusModeEnum.None,
Visible = false
};
viewLogBtn.PressedSignal += _ViewLogPressed;
toolBarHBox.AddChild(viewLogBtn);
var hsc = new HSplitContainer
{
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
panelBuildsTab.AddChild(hsc);
buildTabsList = new ItemList {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
buildTabsList.ItemSelected += _BuildTabsItemSelected;
buildTabsList.NothingSelected += _BuildTabsNothingSelected;
hsc.AddChild(buildTabsList);
buildTabs = new TabContainer
{
TabAlign = TabContainer.TabAlignEnum.Left,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
TabsVisible = false
};
hsc.AddChild(buildTabs);
}
}
}
}

View file

@ -4,7 +4,7 @@ using Godot.Collections;
using GodotTools.Internals; using GodotTools.Internals;
using Path = System.IO.Path; using Path = System.IO.Path;
namespace GodotTools namespace GodotTools.Build
{ {
[Serializable] [Serializable]
public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
@ -20,7 +20,9 @@ namespace GodotTools
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (obj is BuildInfo other) if (obj is BuildInfo other)
return other.Solution == Solution && other.Configuration == Configuration; return other.Solution == Solution && other.Targets == Targets &&
other.Configuration == Configuration && other.Restore == Restore &&
other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath;
return false; return false;
} }
@ -31,7 +33,11 @@ namespace GodotTools
{ {
int hash = 17; int hash = 17;
hash = hash * 29 + Solution.GetHashCode(); hash = hash * 29 + Solution.GetHashCode();
hash = hash * 29 + Targets.GetHashCode();
hash = hash * 29 + Configuration.GetHashCode(); hash = hash * 29 + Configuration.GetHashCode();
hash = hash * 29 + Restore.GetHashCode();
hash = hash * 29 + CustomProperties.GetHashCode();
hash = hash * 29 + LogsDirPath.GetHashCode();
return hash; return hash;
} }
} }

View file

@ -1,20 +1,19 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using GodotTools.Build;
using GodotTools.Ides.Rider; using GodotTools.Ides.Rider;
using GodotTools.Internals; using GodotTools.Internals;
using GodotTools.Utils;
using JetBrains.Annotations; using JetBrains.Annotations;
using static GodotTools.Internals.Globals; using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File; using File = GodotTools.Utils.File;
using OS = GodotTools.Utils.OS;
using Path = System.IO.Path;
namespace GodotTools namespace GodotTools.Build
{ {
public static class BuildManager public static class BuildManager
{ {
private static readonly List<BuildInfo> BuildsInProgress = new List<BuildInfo>(); private static BuildInfo _buildInProgress;
public const string PropNameMSBuildMono = "MSBuild (Mono)"; public const string PropNameMSBuildMono = "MSBuild (Mono)";
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
@ -24,6 +23,14 @@ namespace GodotTools
public const string MsBuildIssuesFileName = "msbuild_issues.csv"; public const string MsBuildIssuesFileName = "msbuild_issues.csv";
public const string MsBuildLogFileName = "msbuild_log.txt"; public 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;
private static void RemoveOldIssuesFile(BuildInfo buildInfo) private static void RemoveOldIssuesFile(BuildInfo buildInfo)
{ {
var issuesFile = GetIssuesFilePath(buildInfo); var issuesFile = GetIssuesFilePath(buildInfo);
@ -36,12 +43,13 @@ namespace GodotTools
private static void ShowBuildErrorDialog(string message) private static void ShowBuildErrorDialog(string message)
{ {
GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error"); var plugin = GodotSharpEditor.Instance;
GodotSharpEditor.Instance.BottomPanel.ShowBuildTab(); plugin.ShowErrorDialog(message, "Build error");
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
} }
public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException(); public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException(); public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
private static string GetLogFilePath(BuildInfo buildInfo) private static string GetLogFilePath(BuildInfo buildInfo)
{ {
@ -61,15 +69,14 @@ namespace GodotTools
public static bool Build(BuildInfo buildInfo) public static bool Build(BuildInfo buildInfo)
{ {
if (BuildsInProgress.Contains(buildInfo)) if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress"); throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo); _buildInProgress = buildInfo;
try try
{ {
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); BuildStarted?.Invoke(buildInfo);
buildTab.OnBuildStart();
// Required in order to update the build tasks list // Required in order to update the build tasks list
Internal.GodotMainIteration(); Internal.GodotMainIteration();
@ -80,44 +87,44 @@ namespace GodotTools
} }
catch (IOException e) catch (IOException e)
{ {
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
} }
try try
{ {
int exitCode = BuildSystem.Build(buildInfo); int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0) if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0; return exitCode == 0;
} }
catch (Exception e) catch (Exception e)
{ {
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
return false; return false;
} }
} }
finally finally
{ {
BuildsInProgress.Remove(buildInfo); _buildInProgress = null;
} }
} }
public static async Task<bool> BuildAsync(BuildInfo buildInfo) public static async Task<bool> BuildAsync(BuildInfo buildInfo)
{ {
if (BuildsInProgress.Contains(buildInfo)) if (_buildInProgress != null)
throw new InvalidOperationException("A build is already in progress"); throw new InvalidOperationException("A build is already in progress");
BuildsInProgress.Add(buildInfo); _buildInProgress = buildInfo;
try try
{ {
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo); BuildStarted?.Invoke(buildInfo);
try try
{ {
@ -125,43 +132,57 @@ namespace GodotTools
} }
catch (IOException e) catch (IOException e)
{ {
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}"); BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
} }
try try
{ {
int exitCode = await BuildSystem.BuildAsync(buildInfo); int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
if (exitCode != 0) if (exitCode != 0)
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}"); PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
buildTab.OnBuildExit(exitCode == 0 ? BuildTab.BuildResults.Success : BuildTab.BuildResults.Error); BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
return exitCode == 0; return exitCode == 0;
} }
catch (Exception e) catch (Exception e)
{ {
buildTab.OnBuildExecFailed($"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}"); BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
return false; return false;
} }
} }
finally finally
{ {
BuildsInProgress.Remove(buildInfo); _buildInProgress = null;
} }
} }
public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null) public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null)
{ {
if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? 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");
return BuildProjectBlocking(buildInfo);
}
private static bool BuildProjectBlocking(BuildInfo buildInfo)
{
if (!File.Exists(buildInfo.Solution))
return true; // No solution to build return true; // No solution to build
// Make sure the API assemblies are up to date before building the project. // 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 // 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. // 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"); string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
if (!string.IsNullOrEmpty(apiAssembliesUpdateError)) if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
{ {
@ -173,15 +194,6 @@ namespace GodotTools
{ {
pr.Step("Building project solution", 0); pr.Step("Building project solution", 0);
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: 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)) if (!Build(buildInfo))
{ {
ShowBuildErrorDialog("Failed to build project solution"); ShowBuildErrorDialog("Failed to build project solution");
@ -197,13 +209,7 @@ namespace GodotTools
if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return true; // No solution to build return true; // No solution to build
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor"); GenerateEditorScriptMetadata();
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (File.Exists(editorScriptsMetadataPath))
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
return true; // Requested play from an external editor/IDE which already built the project return true; // Requested play from an external editor/IDE which already built the project
@ -211,6 +217,35 @@ namespace GodotTools
return BuildProjectBlocking("Debug"); return BuildProjectBlocking("Debug");
} }
// NOTE: This will be replaced with C# source generators in 4.0
public static void GenerateEditorScriptMetadata()
{
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))
return;
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
throw new IOException("Failed to copy scripts metadata file.", innerException: e);
}
}
// NOTE: This will be replaced with C# source generators in 4.0
public static string GenerateExportedGameScriptMetadata(bool isDebug)
{
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
return scriptsMetadataPath;
}
public static void Initialize() public static void Initialize()
{ {
// Build tool settings // Build tool settings
@ -254,8 +289,6 @@ namespace GodotTools
["hint"] = Godot.PropertyHint.Enum, ["hint"] = Godot.PropertyHint.Enum,
["hint_string"] = hintString ["hint_string"] = hintString
}); });
EditorDef("mono/builds/print_build_output", false);
} }
} }
} }

View file

@ -5,16 +5,10 @@ using GodotTools.Internals;
using File = GodotTools.Utils.File; using File = GodotTools.Utils.File;
using Path = System.IO.Path; using Path = System.IO.Path;
namespace GodotTools namespace GodotTools.Build
{ {
public class BuildTab : VBoxContainer public class BuildOutputView : VBoxContainer, ISerializationListener
{ {
public enum BuildResults
{
Error,
Success
}
[Serializable] [Serializable]
private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization
{ {
@ -29,10 +23,14 @@ namespace GodotTools
private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization private readonly Array<BuildIssue> issues = new Array<BuildIssue>(); // TODO Use List once we have proper serialization
private ItemList issuesList; private ItemList issuesList;
private TextEdit buildLog;
private PopupMenu issuesListContextMenu;
public bool BuildExited { get; private set; } = false; [Signal] public event Action BuildStateChanged;
public BuildResults? BuildResult { get; private set; } = null; public bool HasBuildExited { get; private set; } = false;
public BuildResult? BuildResult { get; private set; } = null;
public int ErrorCount { get; private set; } = 0; public int ErrorCount { get; private set; } = 0;
@ -41,23 +39,31 @@ namespace GodotTools
public bool ErrorsVisible { get; set; } = true; public bool ErrorsVisible { get; set; } = true;
public bool WarningsVisible { get; set; } = true; public bool WarningsVisible { get; set; } = true;
public Texture2D IconTexture public Texture2D BuildStateIcon
{ {
get get
{ {
if (!BuildExited) if (!HasBuildExited)
return GetThemeIcon("Stop", "EditorIcons"); return GetThemeIcon("Stop", "EditorIcons");
if (BuildResult == BuildResults.Error) if (BuildResult == Build.BuildResult.Error)
return GetThemeIcon("StatusError", "EditorIcons"); return GetThemeIcon("Error", "EditorIcons");
return GetThemeIcon("StatusSuccess", "EditorIcons"); if (WarningCount > 1)
return GetThemeIcon("Warning", "EditorIcons");
return null;
} }
} }
public BuildInfo BuildInfo { get; private set; } private BuildInfo BuildInfo { get; set; }
private void _LoadIssuesFromFile(string csvFile) public bool LogVisible
{
set => buildLog.Visible = value;
}
private void LoadIssuesFromFile(string csvFile)
{ {
using (var file = new Godot.File()) using (var file = new Godot.File())
{ {
@ -107,7 +113,7 @@ namespace GodotTools
} }
} }
private void _IssueActivated(int idx) private void IssueActivated(int idx)
{ {
if (idx < 0 || idx >= issuesList.GetItemCount()) if (idx < 0 || idx >= issuesList.GetItemCount())
throw new IndexOutOfRangeException("Item list index out of range"); throw new IndexOutOfRangeException("Item list index out of range");
@ -190,49 +196,79 @@ namespace GodotTools
} }
} }
public void OnBuildStart() private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
{ {
BuildExited = false; HasBuildExited = true;
BuildResult = Build.BuildResult.Error;
issues.Clear();
WarningCount = 0;
ErrorCount = 0;
UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
}
public void OnBuildExit(BuildResults result)
{
BuildExited = true;
BuildResult = result;
_LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this);
}
public void OnBuildExecFailed(string cause)
{
BuildExited = true;
BuildResult = BuildResults.Error;
issuesList.Clear(); issuesList.Clear();
var issue = new BuildIssue { Message = cause, Warning = false }; var issue = new BuildIssue {Message = cause, Warning = false};
ErrorCount += 1; ErrorCount += 1;
issues.Add(issue); issues.Add(issue);
UpdateIssuesList(); UpdateIssuesList();
GodotSharpEditor.Instance.BottomPanel.RaiseBuildTab(this); EmitSignal(nameof(BuildStateChanged));
}
private void BuildStarted(BuildInfo buildInfo)
{
BuildInfo = buildInfo;
HasBuildExited = false;
issues.Clear();
WarningCount = 0;
ErrorCount = 0;
buildLog.Text = string.Empty;
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void BuildFinished(BuildResult result)
{
HasBuildExited = true;
BuildResult = result;
LoadIssuesFromFile(Path.Combine(BuildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
UpdateIssuesList();
EmitSignal(nameof(BuildStateChanged));
}
private void StdOutputReceived(string text)
{
buildLog.Text += text + "\n";
ScrollToLastNonEmptyLogLine();
}
private void StdErrorReceived(string text)
{
buildLog.Text += text + "\n";
ScrollToLastNonEmptyLogLine();
}
private void ScrollToLastNonEmptyLogLine()
{
int line;
for (line = buildLog.GetLineCount(); line > 0; line--)
{
string lineText = buildLog.GetLine(line);
if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim()))
break;
}
buildLog.CursorSetLine(line);
} }
public void RestartBuild() public void RestartBuild()
{ {
if (!BuildExited) if (!HasBuildExited)
throw new InvalidOperationException("Build already started"); throw new InvalidOperationException("Build already started");
BuildManager.RestartBuild(this); BuildManager.RestartBuild(this);
@ -240,28 +276,118 @@ namespace GodotTools
public void StopBuild() public void StopBuild()
{ {
if (!BuildExited) if (!HasBuildExited)
throw new InvalidOperationException("Build is not in progress"); throw new InvalidOperationException("Build is not in progress");
BuildManager.StopBuild(this); BuildManager.StopBuild(this);
} }
private enum IssuesContextMenuOption
{
Copy
}
private void IssuesListContextOptionPressed(int id)
{
switch ((IssuesContextMenuOption)id)
{
case IssuesContextMenuOption.Copy:
{
// We don't allow multi-selection but just in case that changes later...
string text = null;
foreach (int issueIndex in issuesList.GetSelectedItems())
{
if (text != null)
text += "\n";
text += issuesList.GetItemText(issueIndex);
}
if (text != null)
DisplayServer.ClipboardSet(text);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option");
}
}
private void IssuesListRmbSelected(int index, Vector2 atPosition)
{
_ = index; // Unused
issuesListContextMenu.Clear();
issuesListContextMenu.Size = new Vector2i(1, 1);
if (issuesList.IsAnythingSelected())
{
// Add menu entries for the selected item
issuesListContextMenu.AddIconItem(GetThemeIcon("ActionCopy", "EditorIcons"),
label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy);
}
if (issuesListContextMenu.GetItemCount() > 0)
{
issuesListContextMenu.Position = (Vector2i)(issuesList.RectGlobalPosition + atPosition);
issuesListContextMenu.Popup();
}
}
public override void _Ready() public override void _Ready()
{ {
base._Ready(); base._Ready();
issuesList = new ItemList { SizeFlagsVertical = (int)SizeFlags.ExpandFill }; SizeFlagsVertical = (int)SizeFlags.ExpandFill;
issuesList.ItemActivated += _IssueActivated;
AddChild(issuesList); var hsc = new HSplitContainer
{
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
SizeFlagsVertical = (int)SizeFlags.ExpandFill
};
AddChild(hsc);
issuesList = new ItemList
{
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log
};
issuesList.ItemActivated += IssueActivated;
issuesList.AllowRmbSelect = true;
issuesList.ItemRmbSelected += IssuesListRmbSelected;
hsc.AddChild(issuesList);
issuesListContextMenu = new PopupMenu();
issuesListContextMenu.IdPressed += IssuesListContextOptionPressed;
issuesList.AddChild(issuesListContextMenu);
buildLog = new TextEdit
{
Readonly = true,
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list
};
hsc.AddChild(buildLog);
AddBuildEventListeners();
} }
private BuildTab() private void AddBuildEventListeners()
{
BuildManager.BuildLaunchFailed += BuildLaunchFailed;
BuildManager.BuildStarted += BuildStarted;
BuildManager.BuildFinished += BuildFinished;
// StdOutput/Error can be received from different threads, so we need to use CallDeferred
BuildManager.StdOutputReceived += line => CallDeferred(nameof(StdOutputReceived), line);
BuildManager.StdErrorReceived += line => CallDeferred(nameof(StdErrorReceived), line);
}
public void OnBeforeSerialize()
{ {
} }
public BuildTab(BuildInfo buildInfo) public void OnAfterDeserialize()
{ {
BuildInfo = buildInfo; AddBuildEventListeners(); // Re-add them
} }
} }
} }

View file

@ -0,0 +1,8 @@
namespace GodotTools.Build
{
public enum BuildResult
{
Error,
Success
}
}

View file

@ -44,10 +44,7 @@ namespace GodotTools.Build
} }
} }
private static bool PrintBuildOutput => private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
(bool)EditorSettings.GetSetting("mono/builds/print_build_output");
private static Process LaunchBuild(BuildInfo buildInfo)
{ {
(string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
@ -58,13 +55,13 @@ namespace GodotTools.Build
var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput; string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}";
stdOutHandler?.Invoke(launchMessage);
if (Godot.OS.IsStdoutVerbose())
Console.WriteLine(launchMessage);
if (!redirectOutput || Godot.OS.IsStdoutVerbose()) startInfo.RedirectStandardOutput = true;
Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"); startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = redirectOutput;
startInfo.RedirectStandardError = redirectOutput;
startInfo.UseShellExecute = false; startInfo.UseShellExecute = false;
if (UsingMonoMsBuildOnWindows) if (UsingMonoMsBuildOnWindows)
@ -82,20 +79,22 @@ namespace GodotTools.Build
var process = new Process {StartInfo = startInfo}; var process = new Process {StartInfo = startInfo};
if (stdOutHandler != null)
process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data);
if (stdErrHandler != null)
process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data);
process.Start(); process.Start();
if (redirectOutput)
{
process.BeginOutputReadLine(); process.BeginOutputReadLine();
process.BeginErrorReadLine(); process.BeginErrorReadLine();
}
return process; return process;
} }
public static int Build(BuildInfo buildInfo) public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{ {
using (var process = LaunchBuild(buildInfo)) using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{ {
process.WaitForExit(); process.WaitForExit();
@ -103,9 +102,9 @@ namespace GodotTools.Build
} }
} }
public static async Task<int> BuildAsync(BuildInfo buildInfo) public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
{ {
using (var process = LaunchBuild(buildInfo)) using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{ {
await process.WaitForExitAsync(); await process.WaitForExitAsync();
@ -152,10 +151,5 @@ namespace GodotTools.Build
foreach (string env in platformEnvironmentVariables) foreach (string env in platformEnvironmentVariables)
environmentVariables.Remove(env); environmentVariables.Remove(env);
} }
private static bool IsDebugMsBuildRequested()
{
return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1";
}
} }
} }

View file

@ -0,0 +1,165 @@
using System;
using Godot;
using GodotTools.Internals;
using JetBrains.Annotations;
using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File;
namespace GodotTools.Build
{
public class MSBuildPanel : VBoxContainer
{
public BuildOutputView BuildOutputView { get; private set; }
private Button errorsBtn;
private Button warningsBtn;
private Button viewLogBtn;
private void WarningsToggled(bool pressed)
{
BuildOutputView.WarningsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
private void ErrorsToggled(bool pressed)
{
BuildOutputView.ErrorsVisible = pressed;
BuildOutputView.UpdateIssuesList();
}
[UsedImplicitly]
public void BuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug"))
return; // Build failed
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void RebuildSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Rebuild"}))
return; // Build failed
// Notify running game for hot-reload
Internal.EditorDebuggerNodeReloadScripts();
// Hot-reload in the editor
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
if (Internal.IsAssembliesReloadingNeeded())
Internal.ReloadAssemblies(softReload: false);
}
[UsedImplicitly]
private void CleanSolution()
{
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; // No solution to build
BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Clean"});
}
private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
private void BuildMenuOptionPressed(int id)
{
switch ((BuildMenuOptions)id)
{
case BuildMenuOptions.BuildSolution:
BuildSolution();
break;
case BuildMenuOptions.RebuildSolution:
RebuildSolution();
break;
case BuildMenuOptions.CleanSolution:
CleanSolution();
break;
default:
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
}
}
private enum BuildMenuOptions
{
BuildSolution,
RebuildSolution,
CleanSolution
}
public override void _Ready()
{
base._Ready();
RectMinSize = new Vector2(0, 228) * EditorScale;
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill};
AddChild(toolBarHBox);
var buildMenuBtn = new MenuButton {Text = "Build", Icon = GetThemeIcon("Play", "EditorIcons")};
toolBarHBox.AddChild(buildMenuBtn);
var buildMenu = buildMenuBtn.GetPopup();
buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution);
buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution);
buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution);
buildMenu.IdPressed += BuildMenuOptionPressed;
errorsBtn = new Button
{
HintTooltip = "Show Errors".TTR(),
Icon = GetThemeIcon("StatusError", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
errorsBtn.Toggled += ErrorsToggled;
toolBarHBox.AddChild(errorsBtn);
warningsBtn = new Button
{
HintTooltip = "Show Warnings".TTR(),
Icon = GetThemeIcon("NodeWarning", "EditorIcons"),
ExpandIcon = false,
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
warningsBtn.Toggled += WarningsToggled;
toolBarHBox.AddChild(warningsBtn);
viewLogBtn = new Button
{
Text = "Show Output".TTR(),
ToggleMode = true,
Pressed = true,
FocusMode = FocusModeEnum.None
};
viewLogBtn.Toggled += ViewLogToggled;
toolBarHBox.AddChild(viewLogBtn);
BuildOutputView = new BuildOutputView();
AddChild(BuildOutputView);
}
}
}

View file

@ -31,7 +31,7 @@ namespace GodotTools.Build
string dotnetCliPath = OS.PathWhich("dotnet"); string dotnetCliPath = OS.PathWhich("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath)) if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli); return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio."); GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio.");
goto case BuildTool.MsBuildVs; goto case BuildTool.MsBuildVs;
} }
case BuildTool.MsBuildVs: case BuildTool.MsBuildVs:
@ -89,7 +89,7 @@ namespace GodotTools.Build
string dotnetCliPath = OS.PathWhich("dotnet"); string dotnetCliPath = OS.PathWhich("dotnet");
if (!string.IsNullOrEmpty(dotnetCliPath)) if (!string.IsNullOrEmpty(dotnetCliPath))
return (dotnetCliPath, BuildTool.DotnetCli); return (dotnetCliPath, BuildTool.DotnetCli);
GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono."); GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono.");
goto case BuildTool.MsBuildMono; goto case BuildTool.MsBuildMono;
} }
case BuildTool.MsBuildMono: case BuildTool.MsBuildMono:
@ -161,7 +161,7 @@ namespace GodotTools.Build
// Try to find 15.0 with vswhere // Try to find 15.0 with vswhere
var envNames = Internal.GodotIs32Bits() ? new[] { "ProgramFiles", "ProgramW6432" } : new[] { "ProgramFiles(x86)", "ProgramFiles" }; var envNames = Internal.GodotIs32Bits() ? new[] {"ProgramFiles", "ProgramW6432"} : new[] {"ProgramFiles(x86)", "ProgramFiles"};
string vsWherePath = null; string vsWherePath = null;
foreach (var envName in envNames) foreach (var envName in envNames)

View file

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using GodotTools.Build;
using GodotTools.Core; using GodotTools.Core;
using GodotTools.Internals; using GodotTools.Internals;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -143,6 +144,8 @@ namespace GodotTools.Export
private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags) private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
{ {
_ = flags; // Unused
if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
return; return;
@ -154,12 +157,10 @@ namespace GodotTools.Export
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease"; string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}"); string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
AddFile(scriptsMetadataPath, scriptsMetadataPath); AddFile(scriptsMetadataPath, scriptsMetadataPath);
if (!BuildManager.BuildProjectBlocking(buildConfig, platform)) if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
throw new Exception("Failed to build project"); throw new Exception("Failed to build project");
// Add dependency assemblies // Add dependency assemblies

View file

@ -4,9 +4,9 @@ using GodotTools.Export;
using GodotTools.Utils; using GodotTools.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using GodotTools.Build;
using GodotTools.Ides; using GodotTools.Ides;
using GodotTools.Ides.Rider; using GodotTools.Ides.Rider;
using GodotTools.Internals; using GodotTools.Internals;
@ -19,7 +19,6 @@ using Path = System.IO.Path;
namespace GodotTools namespace GodotTools
{ {
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
public class GodotSharpEditor : EditorPlugin, ISerializationListener public class GodotSharpEditor : EditorPlugin, ISerializationListener
{ {
private EditorSettings editorSettings; private EditorSettings editorSettings;
@ -37,7 +36,7 @@ namespace GodotTools
private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization private WeakRef exportPluginWeak; // TODO Use WeakReference once we have proper serialization
public BottomPanel BottomPanel { get; private set; } public MSBuildPanel MSBuildPanel { get; private set; }
public bool SkipBuildBeforePlaying { get; set; } = false; public bool SkipBuildBeforePlaying { get; set; } = false;
@ -153,7 +152,7 @@ namespace GodotTools
} }
} }
private void _BuildSolutionPressed() private void BuildSolutionPressed()
{ {
if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
{ {
@ -161,15 +160,15 @@ namespace GodotTools
return; // Failed to create solution return; // Failed to create solution
} }
Instance.BottomPanel.BuildProjectPressed(); Instance.MSBuildPanel.BuildSolution();
} }
public override void _Notification(int what) public override void _Ready()
{ {
base._Notification(what); base._Ready();
MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged;
if (what == NotificationReady)
{
bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start"); bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
if (showInfoDialog) if (showInfoDialog)
{ {
@ -179,7 +178,6 @@ namespace GodotTools
aboutDialog.Exclusive = false; aboutDialog.Exclusive = false;
} }
} }
}
private enum MenuOptions private enum MenuOptions
{ {
@ -393,6 +391,12 @@ namespace GodotTools
} }
} }
private void BuildStateChanged()
{
if (bottomPanelBtn != null)
bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
}
public override void EnablePlugin() public override void EnablePlugin()
{ {
base.EnablePlugin(); base.EnablePlugin();
@ -409,16 +413,15 @@ namespace GodotTools
errorDialog = new AcceptDialog(); errorDialog = new AcceptDialog();
editorBaseControl.AddChild(errorDialog); editorBaseControl.AddChild(errorDialog);
BottomPanel = new BottomPanel(); MSBuildPanel = new MSBuildPanel();
bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR());
AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"}); AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
menuPopup = new PopupMenu(); menuPopup = new PopupMenu();
menuPopup.Hide(); menuPopup.Hide();
AddToolSubmenuItem("Mono", menuPopup); AddToolSubmenuItem("C#", menuPopup);
// TODO: Remove or edit this info dialog once Mono support is no longer in alpha // TODO: Remove or edit this info dialog once Mono support is no longer in alpha
{ {
@ -476,7 +479,7 @@ namespace GodotTools
HintTooltip = "Build solution", HintTooltip = "Build solution",
FocusMode = Control.FocusModeEnum.None FocusMode = Control.FocusModeEnum.None
}; };
toolBarBuildButton.PressedSignal += _BuildSolutionPressed; toolBarBuildButton.PressedSignal += BuildSolutionPressed;
AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton); AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton);
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
@ -570,6 +573,7 @@ namespace GodotTools
public static GodotSharpEditor Instance { get; private set; } public static GodotSharpEditor Instance { get; private set; }
[UsedImplicitly]
private GodotSharpEditor() private GodotSharpEditor()
{ {
} }