virtualx-engine/modules/mono/editor/GodotSharpTools/Build/BuildSystem.cs
Ignacio Etcheverry ca8100f29f Workaround for bug with Mono's MSBuild and BaseIntermediateOutputPath
BaseIntermediateOutputPath seems to be empty by default. The workaround is to explicitly set it.

Also fixed passing char instead of char[] to String.Split. Why was this even working with Mono?
2019-01-21 00:38:28 +01:00

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(new[] { ';' });
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;
}
}