430 lines
16 KiB
C#
430 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
using Microsoft.Build.Framework;
|
|
|
|
namespace GodotSharpTools.Build
|
|
{
|
|
public class BuildInstance : IDisposable
|
|
{
|
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
|
private extern static void godot_icall_BuildInstance_ExitCallback(string solution, string config, int exitCode);
|
|
|
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
|
private extern static string godot_icall_BuildInstance_get_MSBuildPath();
|
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
|
private extern static string godot_icall_BuildInstance_get_FrameworkPath();
|
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
|
private extern static string godot_icall_BuildInstance_get_MonoWindowsBinDir();
|
|
[MethodImpl(MethodImplOptions.InternalCall)]
|
|
private extern static bool godot_icall_BuildInstance_get_UsingMonoMSBuildOnWindows();
|
|
|
|
private static string GetMSBuildPath()
|
|
{
|
|
string msbuildPath = godot_icall_BuildInstance_get_MSBuildPath();
|
|
|
|
if (msbuildPath == null)
|
|
throw new FileNotFoundException("Cannot find the MSBuild executable.");
|
|
|
|
return msbuildPath;
|
|
}
|
|
|
|
private static string GetFrameworkPath()
|
|
{
|
|
return godot_icall_BuildInstance_get_FrameworkPath();
|
|
}
|
|
|
|
private static string MonoWindowsBinDir
|
|
{
|
|
get
|
|
{
|
|
string monoWinBinDir = godot_icall_BuildInstance_get_MonoWindowsBinDir();
|
|
|
|
if (monoWinBinDir == null)
|
|
throw new FileNotFoundException("Cannot find the Windows Mono binaries directory.");
|
|
|
|
return monoWinBinDir;
|
|
}
|
|
}
|
|
|
|
private static bool UsingMonoMSBuildOnWindows
|
|
{
|
|
get
|
|
{
|
|
return godot_icall_BuildInstance_get_UsingMonoMSBuildOnWindows();
|
|
}
|
|
}
|
|
|
|
private string solution;
|
|
private string config;
|
|
|
|
private Process process;
|
|
|
|
private int exitCode;
|
|
public int ExitCode { get { return exitCode; } }
|
|
|
|
public bool IsRunning { get { return process != null && !process.HasExited; } }
|
|
|
|
public BuildInstance(string solution, string config)
|
|
{
|
|
this.solution = solution;
|
|
this.config = config;
|
|
}
|
|
|
|
public bool Build(string loggerAssemblyPath, string loggerOutputDir, string[] customProperties = null)
|
|
{
|
|
bool debugMSBuild = IsDebugMSBuildRequested();
|
|
|
|
List<string> customPropertiesList = new List<string>();
|
|
|
|
if (customProperties != null)
|
|
customPropertiesList.AddRange(customProperties);
|
|
|
|
string frameworkPath = GetFrameworkPath();
|
|
|
|
if (!string.IsNullOrEmpty(frameworkPath))
|
|
customPropertiesList.Add("FrameworkPathOverride=" + frameworkPath);
|
|
|
|
string compilerArgs = BuildArguments(loggerAssemblyPath, loggerOutputDir, customPropertiesList);
|
|
|
|
ProcessStartInfo startInfo = new ProcessStartInfo(GetMSBuildPath(), compilerArgs);
|
|
|
|
bool redirectOutput = !debugMSBuild;
|
|
|
|
startInfo.RedirectStandardOutput = redirectOutput;
|
|
startInfo.RedirectStandardError = redirectOutput;
|
|
startInfo.UseShellExecute = false;
|
|
|
|
if (UsingMonoMSBuildOnWindows)
|
|
{
|
|
// These environment variables are required for Mono's MSBuild to find the compilers.
|
|
// We use the batch files in Mono's bin directory to make sure the compilers are executed with mono.
|
|
string monoWinBinDir = MonoWindowsBinDir;
|
|
startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat"));
|
|
startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat"));
|
|
startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat"));
|
|
}
|
|
|
|
// Needed when running from Developer Command Prompt for VS
|
|
RemovePlatformVariable(startInfo.EnvironmentVariables);
|
|
|
|
using (Process process = new Process())
|
|
{
|
|
process.StartInfo = startInfo;
|
|
|
|
process.Start();
|
|
|
|
if (redirectOutput)
|
|
{
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
}
|
|
|
|
process.WaitForExit();
|
|
|
|
exitCode = process.ExitCode;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool BuildAsync(string loggerAssemblyPath, string loggerOutputDir, string[] customProperties = null)
|
|
{
|
|
bool debugMSBuild = IsDebugMSBuildRequested();
|
|
|
|
if (process != null)
|
|
throw new InvalidOperationException("Already in use");
|
|
|
|
List<string> customPropertiesList = new List<string>();
|
|
|
|
if (customProperties != null)
|
|
customPropertiesList.AddRange(customProperties);
|
|
|
|
string frameworkPath = GetFrameworkPath();
|
|
|
|
if (!string.IsNullOrEmpty(frameworkPath))
|
|
customPropertiesList.Add("FrameworkPathOverride=" + frameworkPath);
|
|
|
|
string compilerArgs = BuildArguments(loggerAssemblyPath, loggerOutputDir, customPropertiesList);
|
|
|
|
ProcessStartInfo startInfo = new ProcessStartInfo(GetMSBuildPath(), compilerArgs);
|
|
|
|
bool redirectOutput = !debugMSBuild;
|
|
|
|
startInfo.RedirectStandardOutput = redirectOutput;
|
|
startInfo.RedirectStandardError = redirectOutput;
|
|
startInfo.UseShellExecute = false;
|
|
|
|
if (UsingMonoMSBuildOnWindows)
|
|
{
|
|
// These environment variables are required for Mono's MSBuild to find the compilers.
|
|
// We use the batch files in Mono's bin directory to make sure the compilers are executed with mono.
|
|
string monoWinBinDir = MonoWindowsBinDir;
|
|
startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat"));
|
|
startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat"));
|
|
startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat"));
|
|
}
|
|
|
|
// Needed when running from Developer Command Prompt for VS
|
|
RemovePlatformVariable(startInfo.EnvironmentVariables);
|
|
|
|
process = new Process();
|
|
process.StartInfo = startInfo;
|
|
process.EnableRaisingEvents = true;
|
|
process.Exited += new EventHandler(BuildProcess_Exited);
|
|
|
|
process.Start();
|
|
|
|
if (redirectOutput)
|
|
{
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private string BuildArguments(string loggerAssemblyPath, string loggerOutputDir, List<string> customProperties)
|
|
{
|
|
string arguments = string.Format(@"""{0}"" /v:normal /t:Build ""/p:{1}"" ""/l:{2},{3};{4}""",
|
|
solution,
|
|
"Configuration=" + config,
|
|
typeof(GodotBuildLogger).FullName,
|
|
loggerAssemblyPath,
|
|
loggerOutputDir
|
|
);
|
|
|
|
foreach (string customProperty in customProperties)
|
|
{
|
|
arguments += " \"/p:" + customProperty + "\"";
|
|
}
|
|
|
|
return arguments;
|
|
}
|
|
|
|
private void RemovePlatformVariable(StringDictionary environmentVariables)
|
|
{
|
|
// EnvironmentVariables is case sensitive? Seriously?
|
|
|
|
List<string> platformEnvironmentVariables = new List<string>();
|
|
|
|
foreach (string env in environmentVariables.Keys)
|
|
{
|
|
if (env.ToUpper() == "PLATFORM")
|
|
platformEnvironmentVariables.Add(env);
|
|
}
|
|
|
|
foreach (string env in platformEnvironmentVariables)
|
|
environmentVariables.Remove(env);
|
|
}
|
|
|
|
private void BuildProcess_Exited(object sender, System.EventArgs e)
|
|
{
|
|
exitCode = process.ExitCode;
|
|
|
|
godot_icall_BuildInstance_ExitCallback(solution, config, exitCode);
|
|
|
|
Dispose();
|
|
}
|
|
|
|
private static bool IsDebugMSBuildRequested()
|
|
{
|
|
return Environment.GetEnvironmentVariable("GODOT_DEBUG_MSBUILD")?.Trim() == "1";
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (process != null)
|
|
{
|
|
process.Dispose();
|
|
process = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class GodotBuildLogger : ILogger
|
|
{
|
|
public string Parameters { get; set; }
|
|
public LoggerVerbosity Verbosity { get; set; }
|
|
|
|
public void Initialize(IEventSource eventSource)
|
|
{
|
|
if (null == Parameters)
|
|
throw new LoggerException("Log directory was not set.");
|
|
|
|
string[] parameters = Parameters.Split(';');
|
|
|
|
string logDir = parameters[0];
|
|
|
|
if (String.IsNullOrEmpty(logDir))
|
|
throw new LoggerException("Log directory was not set.");
|
|
|
|
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);
|
|
|
|
this.logStreamWriter = new StreamWriter(logFile);
|
|
this.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);
|
|
}
|
|
else
|
|
{
|
|
// Unexpected failure
|
|
throw;
|
|
}
|
|
}
|
|
|
|
eventSource.ProjectStarted += new ProjectStartedEventHandler(eventSource_ProjectStarted);
|
|
eventSource.TaskStarted += new TaskStartedEventHandler(eventSource_TaskStarted);
|
|
eventSource.MessageRaised += new BuildMessageEventHandler(eventSource_MessageRaised);
|
|
eventSource.WarningRaised += new BuildWarningEventHandler(eventSource_WarningRaised);
|
|
eventSource.ErrorRaised += new BuildErrorEventHandler(eventSource_ErrorRaised);
|
|
eventSource.ProjectFinished += new ProjectFinishedEventHandler(eventSource_ProjectFinished);
|
|
}
|
|
|
|
void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
|
|
{
|
|
string line = String.Format("{0}({1},{2}): error {3}: {4}", e.File, e.LineNumber, e.ColumnNumber, e.Code, e.Message);
|
|
|
|
if (e.ProjectFile.Length > 0)
|
|
line += string.Format(" [{0}]", e.ProjectFile);
|
|
|
|
WriteLine(line);
|
|
|
|
string errorLine = String.Format(@"error,{0},{1},{2},{3},{4},{5}",
|
|
e.File.CsvEscape(), e.LineNumber, e.ColumnNumber,
|
|
e.Code.CsvEscape(), e.Message.CsvEscape(), e.ProjectFile.CsvEscape());
|
|
issuesStreamWriter.WriteLine(errorLine);
|
|
}
|
|
|
|
void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
|
|
{
|
|
string line = String.Format("{0}({1},{2}): warning {3}: {4}", e.File, e.LineNumber, e.ColumnNumber, e.Code, e.Message, e.ProjectFile);
|
|
|
|
if (e.ProjectFile != null && e.ProjectFile.Length > 0)
|
|
line += string.Format(" [{0}]", e.ProjectFile);
|
|
|
|
WriteLine(line);
|
|
|
|
string warningLine = String.Format(@"warning,{0},{1},{2},{3},{4},{5}",
|
|
e.File.CsvEscape(), e.LineNumber, e.ColumnNumber,
|
|
e.Code.CsvEscape(), e.Message.CsvEscape(), e.ProjectFile != null ? e.ProjectFile.CsvEscape() : string.Empty);
|
|
issuesStreamWriter.WriteLine(warningLine);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void eventSource_TaskStarted(object sender, TaskStartedEventArgs e)
|
|
{
|
|
// TaskStartedEventArgs adds ProjectFile, TaskFile, TaskName
|
|
// To keep this log clean, this logger will ignore these events.
|
|
}
|
|
|
|
void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
|
|
{
|
|
WriteLine(e.Message);
|
|
indent++;
|
|
}
|
|
|
|
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", true /*ignore case*/))
|
|
{
|
|
// 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)
|
|
/// </summary>
|
|
private void WriteLineWithSenderAndMessage(string line, BuildEventArgs e)
|
|
{
|
|
if (0 == String.Compare(e.SenderName, "MSBuild", true /*ignore case*/))
|
|
{
|
|
// 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();
|
|
}
|
|
|
|
public bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity)
|
|
{
|
|
return this.Verbosity >= checkVerbosity;
|
|
}
|
|
|
|
private StreamWriter logStreamWriter;
|
|
private StreamWriter issuesStreamWriter;
|
|
private int indent;
|
|
}
|
|
}
|