mirror of
https://github.com/godotengine/godot.git
synced 2024-11-23 04:33:29 +00:00
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:
parent
32be9299ba
commit
f06f91281c
@ -15,14 +15,14 @@ namespace GodotTools.BuildLogger
|
||||
public void Initialize(IEventSource eventSource)
|
||||
{
|
||||
if (null == Parameters)
|
||||
throw new LoggerException("Log directory was not set.");
|
||||
throw new LoggerException("Log directory parameter not specified.");
|
||||
|
||||
var parameters = Parameters.Split(new[] { ';' });
|
||||
|
||||
string logDir = parameters[0];
|
||||
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
throw new LoggerException("Log directory was not set.");
|
||||
throw new LoggerException("Log directory parameter is empty.");
|
||||
|
||||
if (parameters.Length > 1)
|
||||
throw new LoggerException("Too many parameters passed.");
|
||||
@ -51,22 +51,31 @@ namespace GodotTools.BuildLogger
|
||||
{
|
||||
throw new LoggerException("Failed to create log file: " + ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unexpected failure
|
||||
throw;
|
||||
}
|
||||
|
||||
// Unexpected failure
|
||||
throw;
|
||||
}
|
||||
|
||||
eventSource.ProjectStarted += eventSource_ProjectStarted;
|
||||
eventSource.TaskStarted += eventSource_TaskStarted;
|
||||
eventSource.ProjectFinished += eventSource_ProjectFinished;
|
||||
eventSource.MessageRaised += eventSource_MessageRaised;
|
||||
eventSource.WarningRaised += eventSource_WarningRaised;
|
||||
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}";
|
||||
|
||||
@ -81,7 +90,7 @@ namespace GodotTools.BuildLogger
|
||||
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}";
|
||||
|
||||
@ -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>
|
||||
/// Write a line to the log, adding the SenderName and Message
|
||||
/// (these parameters are on all MSBuild event argument objects)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using Godot.Collections;
|
||||
using GodotTools.Internals;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
[Serializable]
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@ -31,7 +33,11 @@ namespace GodotTools
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 29 + Solution.GetHashCode();
|
||||
hash = hash * 29 + Targets.GetHashCode();
|
||||
hash = hash * 29 + Configuration.GetHashCode();
|
||||
hash = hash * 29 + Restore.GetHashCode();
|
||||
hash = hash * 29 + CustomProperties.GetHashCode();
|
||||
hash = hash * 29 + LogsDirPath.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
@ -1,20 +1,19 @@
|
||||
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;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
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 PropNameMSBuildVs = "MSBuild (VS Build Tools)";
|
||||
@ -24,6 +23,14 @@ namespace GodotTools
|
||||
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
|
||||
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)
|
||||
{
|
||||
var issuesFile = GetIssuesFilePath(buildInfo);
|
||||
@ -36,12 +43,13 @@ namespace GodotTools
|
||||
|
||||
private static void ShowBuildErrorDialog(string message)
|
||||
{
|
||||
GodotSharpEditor.Instance.ShowErrorDialog(message, "Build error");
|
||||
GodotSharpEditor.Instance.BottomPanel.ShowBuildTab();
|
||||
var plugin = GodotSharpEditor.Instance;
|
||||
plugin.ShowErrorDialog(message, "Build error");
|
||||
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
|
||||
}
|
||||
|
||||
public static void RestartBuild(BuildTab buildTab) => throw new NotImplementedException();
|
||||
public static void StopBuild(BuildTab buildTab) => throw new NotImplementedException();
|
||||
public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
|
||||
public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
|
||||
|
||||
private static string GetLogFilePath(BuildInfo buildInfo)
|
||||
{
|
||||
@ -61,15 +69,14 @@ namespace GodotTools
|
||||
|
||||
public static bool Build(BuildInfo buildInfo)
|
||||
{
|
||||
if (BuildsInProgress.Contains(buildInfo))
|
||||
if (_buildInProgress != null)
|
||||
throw new InvalidOperationException("A build is already in progress");
|
||||
|
||||
BuildsInProgress.Add(buildInfo);
|
||||
_buildInProgress = buildInfo;
|
||||
|
||||
try
|
||||
{
|
||||
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
|
||||
buildTab.OnBuildStart();
|
||||
BuildStarted?.Invoke(buildInfo);
|
||||
|
||||
// Required in order to update the build tasks list
|
||||
Internal.GodotMainIteration();
|
||||
@ -80,44 +87,44 @@ namespace GodotTools
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int exitCode = BuildSystem.Build(buildInfo);
|
||||
int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
|
||||
|
||||
if (exitCode != 0)
|
||||
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;
|
||||
}
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BuildsInProgress.Remove(buildInfo);
|
||||
_buildInProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> BuildAsync(BuildInfo buildInfo)
|
||||
{
|
||||
if (BuildsInProgress.Contains(buildInfo))
|
||||
if (_buildInProgress != null)
|
||||
throw new InvalidOperationException("A build is already in progress");
|
||||
|
||||
BuildsInProgress.Add(buildInfo);
|
||||
_buildInProgress = buildInfo;
|
||||
|
||||
try
|
||||
{
|
||||
BuildTab buildTab = GodotSharpEditor.Instance.BottomPanel.GetBuildTabFor(buildInfo);
|
||||
BuildStarted?.Invoke(buildInfo);
|
||||
|
||||
try
|
||||
{
|
||||
@ -125,43 +132,57 @@ namespace GodotTools
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
buildTab.OnBuildExecFailed($"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int exitCode = await BuildSystem.BuildAsync(buildInfo);
|
||||
int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
|
||||
|
||||
if (exitCode != 0)
|
||||
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;
|
||||
}
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
// 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");
|
||||
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
|
||||
|
||||
if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
|
||||
{
|
||||
@ -173,15 +194,6 @@ namespace GodotTools
|
||||
{
|
||||
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))
|
||||
{
|
||||
ShowBuildErrorDialog("Failed to build project solution");
|
||||
@ -197,13 +209,7 @@ namespace GodotTools
|
||||
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);
|
||||
GenerateEditorScriptMetadata();
|
||||
|
||||
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
|
||||
return true; // Requested play from an external editor/IDE which already built the project
|
||||
@ -211,6 +217,35 @@ namespace GodotTools
|
||||
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()
|
||||
{
|
||||
// Build tool settings
|
||||
@ -254,8 +289,6 @@ namespace GodotTools
|
||||
["hint"] = Godot.PropertyHint.Enum,
|
||||
["hint_string"] = hintString
|
||||
});
|
||||
|
||||
EditorDef("mono/builds/print_build_output", false);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,16 +5,10 @@ using GodotTools.Internals;
|
||||
using File = GodotTools.Utils.File;
|
||||
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]
|
||||
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 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;
|
||||
|
||||
@ -41,23 +39,31 @@ namespace GodotTools
|
||||
public bool ErrorsVisible { get; set; } = true;
|
||||
public bool WarningsVisible { get; set; } = true;
|
||||
|
||||
public Texture2D IconTexture
|
||||
public Texture2D BuildStateIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!BuildExited)
|
||||
if (!HasBuildExited)
|
||||
return GetThemeIcon("Stop", "EditorIcons");
|
||||
|
||||
if (BuildResult == BuildResults.Error)
|
||||
return GetThemeIcon("StatusError", "EditorIcons");
|
||||
if (BuildResult == Build.BuildResult.Error)
|
||||
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())
|
||||
{
|
||||
@ -107,7 +113,7 @@ namespace GodotTools
|
||||
}
|
||||
}
|
||||
|
||||
private void _IssueActivated(int idx)
|
||||
private void IssueActivated(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= issuesList.GetItemCount())
|
||||
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;
|
||||
|
||||
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;
|
||||
HasBuildExited = true;
|
||||
BuildResult = Build.BuildResult.Error;
|
||||
|
||||
issuesList.Clear();
|
||||
|
||||
var issue = new BuildIssue { Message = cause, Warning = false };
|
||||
var issue = new BuildIssue {Message = cause, Warning = false};
|
||||
|
||||
ErrorCount += 1;
|
||||
issues.Add(issue);
|
||||
|
||||
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()
|
||||
{
|
||||
if (!BuildExited)
|
||||
if (!HasBuildExited)
|
||||
throw new InvalidOperationException("Build already started");
|
||||
|
||||
BuildManager.RestartBuild(this);
|
||||
@ -240,28 +276,118 @@ namespace GodotTools
|
||||
|
||||
public void StopBuild()
|
||||
{
|
||||
if (!BuildExited)
|
||||
if (!HasBuildExited)
|
||||
throw new InvalidOperationException("Build is not in progress");
|
||||
|
||||
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()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
issuesList = new ItemList { SizeFlagsVertical = (int)SizeFlags.ExpandFill };
|
||||
issuesList.ItemActivated += _IssueActivated;
|
||||
AddChild(issuesList);
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public enum BuildResult
|
||||
{
|
||||
Error,
|
||||
Success
|
||||
}
|
||||
}
|
@ -44,10 +44,7 @@ namespace GodotTools.Build
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PrintBuildOutput =>
|
||||
(bool)EditorSettings.GetSetting("mono/builds/print_build_output");
|
||||
|
||||
private static Process LaunchBuild(BuildInfo buildInfo)
|
||||
private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
|
||||
{
|
||||
(string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
|
||||
|
||||
@ -58,13 +55,13 @@ namespace GodotTools.Build
|
||||
|
||||
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())
|
||||
Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}");
|
||||
|
||||
startInfo.RedirectStandardOutput = redirectOutput;
|
||||
startInfo.RedirectStandardError = redirectOutput;
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
startInfo.UseShellExecute = false;
|
||||
|
||||
if (UsingMonoMsBuildOnWindows)
|
||||
@ -82,20 +79,22 @@ namespace GodotTools.Build
|
||||
|
||||
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();
|
||||
|
||||
if (redirectOutput)
|
||||
{
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
}
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -152,10 +151,5 @@ namespace GodotTools.Build
|
||||
foreach (string env in platformEnvironmentVariables)
|
||||
environmentVariables.Remove(env);
|
||||
}
|
||||
|
||||
private static bool IsDebugMsBuildRequested()
|
||||
{
|
||||
return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
165
modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
Normal file
165
modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ namespace GodotTools.Build
|
||||
string dotnetCliPath = OS.PathWhich("dotnet");
|
||||
if (!string.IsNullOrEmpty(dotnetCliPath))
|
||||
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;
|
||||
}
|
||||
case BuildTool.MsBuildVs:
|
||||
@ -89,7 +89,7 @@ namespace GodotTools.Build
|
||||
string dotnetCliPath = OS.PathWhich("dotnet");
|
||||
if (!string.IsNullOrEmpty(dotnetCliPath))
|
||||
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;
|
||||
}
|
||||
case BuildTool.MsBuildMono:
|
||||
@ -161,7 +161,7 @@ namespace GodotTools.Build
|
||||
|
||||
// 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;
|
||||
foreach (var envName in envNames)
|
||||
|
@ -5,6 +5,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GodotTools.Build;
|
||||
using GodotTools.Core;
|
||||
using GodotTools.Internals;
|
||||
using JetBrains.Annotations;
|
||||
@ -143,6 +144,8 @@ namespace GodotTools.Export
|
||||
|
||||
private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
|
||||
{
|
||||
_ = flags; // Unused
|
||||
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return;
|
||||
|
||||
@ -154,12 +157,10 @@ namespace GodotTools.Export
|
||||
|
||||
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
|
||||
|
||||
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
|
||||
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
|
||||
|
||||
string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
|
||||
AddFile(scriptsMetadataPath, scriptsMetadataPath);
|
||||
|
||||
if (!BuildManager.BuildProjectBlocking(buildConfig, platform))
|
||||
if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
|
||||
throw new Exception("Failed to build project");
|
||||
|
||||
// Add dependency assemblies
|
||||
|
@ -4,9 +4,9 @@ using GodotTools.Export;
|
||||
using GodotTools.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GodotTools.Build;
|
||||
using GodotTools.Ides;
|
||||
using GodotTools.Ides.Rider;
|
||||
using GodotTools.Internals;
|
||||
@ -19,7 +19,6 @@ using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools
|
||||
{
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||
public class GodotSharpEditor : EditorPlugin, ISerializationListener
|
||||
{
|
||||
private EditorSettings editorSettings;
|
||||
@ -37,7 +36,7 @@ namespace GodotTools
|
||||
|
||||
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;
|
||||
|
||||
@ -153,7 +152,7 @@ namespace GodotTools
|
||||
}
|
||||
}
|
||||
|
||||
private void _BuildSolutionPressed()
|
||||
private void BuildSolutionPressed()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
{
|
||||
@ -161,23 +160,22 @@ namespace GodotTools
|
||||
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();
|
||||
|
||||
if (what == NotificationReady)
|
||||
MSBuildPanel.BuildOutputView.BuildStateChanged += BuildStateChanged;
|
||||
|
||||
bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
|
||||
if (showInfoDialog)
|
||||
{
|
||||
bool showInfoDialog = (bool)editorSettings.GetSetting("mono/editor/show_info_on_start");
|
||||
if (showInfoDialog)
|
||||
{
|
||||
aboutDialog.Exclusive = true;
|
||||
_ShowAboutDialog();
|
||||
// Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
|
||||
aboutDialog.Exclusive = false;
|
||||
}
|
||||
aboutDialog.Exclusive = true;
|
||||
_ShowAboutDialog();
|
||||
// Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on.
|
||||
aboutDialog.Exclusive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,6 +391,12 @@ namespace GodotTools
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildStateChanged()
|
||||
{
|
||||
if (bottomPanelBtn != null)
|
||||
bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
|
||||
}
|
||||
|
||||
public override void EnablePlugin()
|
||||
{
|
||||
base.EnablePlugin();
|
||||
@ -409,16 +413,15 @@ namespace GodotTools
|
||||
errorDialog = new AcceptDialog();
|
||||
editorBaseControl.AddChild(errorDialog);
|
||||
|
||||
BottomPanel = new BottomPanel();
|
||||
|
||||
bottomPanelBtn = AddControlToBottomPanel(BottomPanel, "Mono".TTR());
|
||||
MSBuildPanel = new MSBuildPanel();
|
||||
bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
|
||||
|
||||
AddChild(new HotReloadAssemblyWatcher {Name = "HotReloadAssemblyWatcher"});
|
||||
|
||||
menuPopup = new PopupMenu();
|
||||
menuPopup.Hide();
|
||||
|
||||
AddToolSubmenuItem("Mono", menuPopup);
|
||||
AddToolSubmenuItem("C#", menuPopup);
|
||||
|
||||
// TODO: Remove or edit this info dialog once Mono support is no longer in alpha
|
||||
{
|
||||
@ -476,7 +479,7 @@ namespace GodotTools
|
||||
HintTooltip = "Build solution",
|
||||
FocusMode = Control.FocusModeEnum.None
|
||||
};
|
||||
toolBarBuildButton.PressedSignal += _BuildSolutionPressed;
|
||||
toolBarBuildButton.PressedSignal += BuildSolutionPressed;
|
||||
AddControlToContainer(CustomControlContainer.Toolbar, toolBarBuildButton);
|
||||
|
||||
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
|
||||
@ -570,6 +573,7 @@ namespace GodotTools
|
||||
|
||||
public static GodotSharpEditor Instance { get; private set; }
|
||||
|
||||
[UsedImplicitly]
|
||||
private GodotSharpEditor()
|
||||
{
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user