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_MonoWindowsBinDir(); [MethodImpl(MethodImplOptions.InternalCall)] private extern static bool godot_icall_BuildInstance_get_UsingMonoMSBuildOnWindows(); [MethodImpl(MethodImplOptions.InternalCall)] private extern static bool godot_icall_BuildInstance_get_PrintBuildOutput(); 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 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 static bool PrintBuildOutput { get { return godot_icall_BuildInstance_get_PrintBuildOutput(); } } 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) { List customPropertiesList = new List(); if (customProperties != null) customPropertiesList.AddRange(customProperties); string compilerArgs = BuildArguments(loggerAssemblyPath, loggerOutputDir, customPropertiesList); ProcessStartInfo startInfo = new ProcessStartInfo(GetMSBuildPath(), compilerArgs); bool redirectOutput = !IsDebugMSBuildRequested() && !PrintBuildOutput; if (!redirectOutput) // TODO: or if stdout verbose Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"); 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) { if (process != null) throw new InvalidOperationException("Already in use"); List customPropertiesList = new List(); if (customProperties != null) customPropertiesList.AddRange(customProperties); string compilerArgs = BuildArguments(loggerAssemblyPath, loggerOutputDir, customPropertiesList); ProcessStartInfo startInfo = new ProcessStartInfo(GetMSBuildPath(), compilerArgs); bool redirectOutput = !IsDebugMSBuildRequested() && !PrintBuildOutput; if (!redirectOutput) // TODO: or if stdout verbose Console.WriteLine($"Running: \"{startInfo.FileName}\" {startInfo.Arguments}"); 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 customProperties) { string arguments = string.Format(@"""{0}"" /v:normal /t:Rebuild ""/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 platformEnvironmentVariables = new List(); 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); } /// /// Write a line to the log, adding the SenderName /// 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); } } /// /// Write a line to the log, adding the SenderName and Message /// (these parameters are on all MSBuild event argument objects) /// 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; } }