godot/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
Ignacio Etcheverry f06f91281c 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.
2020-10-23 07:49:39 +02:00

176 lines
6.3 KiB
C#

using System;
using System.IO;
using System.Security;
using Microsoft.Build.Framework;
namespace GodotTools.BuildLogger
{
public class GodotBuildLogger : ILogger
{
public static readonly string AssemblyPath = Path.GetFullPath(typeof(GodotBuildLogger).Assembly.Location);
public string Parameters { get; set; }
public LoggerVerbosity Verbosity { get; set; }
public void Initialize(IEventSource eventSource)
{
if (null == Parameters)
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 parameter is empty.");
if (parameters.Length > 1)
throw new LoggerException("Too many parameters passed.");
string logFile = Path.Combine(logDir, "msbuild_log.txt");
string issuesFile = Path.Combine(logDir, "msbuild_issues.csv");
try
{
if (!Directory.Exists(logDir))
Directory.CreateDirectory(logDir);
logStreamWriter = new StreamWriter(logFile);
issuesStreamWriter = new StreamWriter(issuesFile);
}
catch (Exception ex)
{
if (ex is UnauthorizedAccessException
|| ex is ArgumentNullException
|| ex is PathTooLongException
|| ex is DirectoryNotFoundException
|| ex is NotSupportedException
|| ex is ArgumentException
|| ex is SecurityException
|| ex is IOException)
{
throw new LoggerException("Failed to create log file: " + ex.Message);
}
// Unexpected failure
throw;
}
eventSource.ProjectStarted += eventSource_ProjectStarted;
eventSource.ProjectFinished += eventSource_ProjectFinished;
eventSource.MessageRaised += eventSource_MessageRaised;
eventSource.WarningRaised += eventSource_WarningRaised;
eventSource.ErrorRaised += eventSource_ErrorRaised;
}
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}";
if (!string.IsNullOrEmpty(e.ProjectFile))
line += $" [{e.ProjectFile}]";
WriteLine(line);
string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
issuesStreamWriter.WriteLine(errorLine);
}
private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
if (!string.IsNullOrEmpty(e.ProjectFile))
line += $" [{e.ProjectFile}]";
WriteLine(line);
string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
issuesStreamWriter.WriteLine(warningLine);
}
private void eventSource_MessageRaised(object sender, BuildMessageEventArgs e)
{
// BuildMessageEventArgs adds Importance to BuildEventArgs
// Let's take account of the verbosity setting we've been passed in deciding whether to log the message
if (e.Importance == MessageImportance.High && IsVerbosityAtLeast(LoggerVerbosity.Minimal)
|| e.Importance == MessageImportance.Normal && IsVerbosityAtLeast(LoggerVerbosity.Normal)
|| e.Importance == MessageImportance.Low && IsVerbosityAtLeast(LoggerVerbosity.Detailed))
{
WriteLineWithSenderAndMessage(string.Empty, e);
}
}
/// <summary>
/// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects)
/// </summary>
private void WriteLineWithSenderAndMessage(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 + e.Message);
}
else
{
WriteLine(e.SenderName + ": " + line + e.Message);
}
}
private void WriteLine(string line)
{
for (int i = indent; i > 0; i--)
{
logStreamWriter.Write("\t");
}
logStreamWriter.WriteLine(line);
}
public void Shutdown()
{
logStreamWriter.Close();
issuesStreamWriter.Close();
}
private bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity)
{
return Verbosity >= checkVerbosity;
}
private StreamWriter logStreamWriter;
private StreamWriter issuesStreamWriter;
private int indent;
}
internal static class StringExtensions
{
public static string CsvEscape(this string value, char delimiter = ',')
{
bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1;
if (hasSpecialChar)
return "\"" + value.Replace("\"", "\"\"") + "\"";
return value;
}
}
}