//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Threading; using bs; namespace bs.Editor { /** @addtogroup Script * @{ */ /// /// Type of assemblies that may be generated by the script compiler. /// public enum ScriptAssemblyType { Game, Editor } /// /// Data required for compiling a single assembly. /// public struct CompileData { public string[] files; public string defines; } /// /// Compiles script files in the project into script assemblies. /// public static class ScriptCompiler { /// /// Starts compilation of the script files in the project for the specified assembly for the specified platform. /// /// Type of the assembly to compile. This determines which script files are used as input. /// Platform to compile the assemblies for. /// Determines should the assemblies contain debug information. /// Absolute path to the directory where to output the assemblies. /// Compiler instance that contains the compiler process. Caller must ensure to properly dispose /// of this object when done. public static CompilerInstance CompileAsync(ScriptAssemblyType type, PlatformType platform, bool debug, string outputDir) { LibraryEntry[] scriptEntries = ProjectLibrary.Search("*", new ResourceType[] { ResourceType.ScriptCode }); List scriptFiles = new List(); for (int i = 0; i < scriptEntries.Length; i++) { if(scriptEntries[i].Type != LibraryEntryType.File) continue; FileEntry fileEntry = (FileEntry)scriptEntries[i]; ScriptCodeImportOptions io = (ScriptCodeImportOptions) fileEntry.Options; if (io.EditorScript && type == ScriptAssemblyType.Editor || !io.EditorScript && type == ScriptAssemblyType.Game) { scriptFiles.Add(Path.Combine(ProjectLibrary.ResourceFolder, scriptEntries[i].Path)); } } string[] assemblyFolders; string[] assemblies; string outputFile; string builtinAssemblyPath = debug ? EditorApplication.BuiltinDebugAssemblyPath : EditorApplication.BuiltinReleaseAssemblyPath; string[] frameworkAssemblies = BuildManager.GetFrameworkAssemblies(platform); if (type == ScriptAssemblyType.Game) { assemblyFolders = new string[] { builtinAssemblyPath, EditorApplication.FrameworkAssemblyPath }; assemblies = new string[frameworkAssemblies.Length + 1]; assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName; outputFile = Path.Combine(outputDir, EditorApplication.ScriptGameAssemblyName); } else { assemblyFolders = new string[] { builtinAssemblyPath, EditorApplication.FrameworkAssemblyPath, EditorApplication.ScriptAssemblyPath }; assemblies = new string[frameworkAssemblies.Length + 3]; assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName; assemblies[assemblies.Length - 2] = EditorApplication.EditorAssemblyName; assemblies[assemblies.Length - 3] = EditorApplication.ScriptGameAssemblyName; outputFile = Path.Combine(outputDir, EditorApplication.ScriptEditorAssemblyName); } Array.Copy(frameworkAssemblies, assemblies, frameworkAssemblies.Length); string defines = BuildManager.GetDefines(platform); return new CompilerInstance(scriptFiles.ToArray(), defines, assemblyFolders, assemblies, debug, outputFile); } } /// /// Represents a started compiler process used for compiling a set of script files into an assembly. /// public class CompilerInstance { private Process process; private Thread readErrorsThread; private List errors = new List(); private List warnings = new List(); private Regex compileErrorRegex = new Regex(@"\s*(?.*)\(\s*(?\d+)\s*,\s*(?\d+)\s*\)\s*:\s*(?warning|error)\s+(.*):\s*(?.*)"); private Regex compilerErrorRegex = new Regex(@"\s*error[^:]*:\s*(?.*)"); /// /// Creates a new compiler process and starts compilation of the provided files. /// /// Absolute paths to all the C# script files to compile. /// A set of semi-colon separated defines to provide to the compiler. /// A set of folders containing the assemblies referenced by the script files. /// Names of the assemblies containing code referenced by the script files. /// Determines should the assembly be compiled with additional debug information. /// Absolute path to the assembly file to generate. internal CompilerInstance(string[] files, string defines, string[] assemblyFolders, string[] assemblies, bool debugBuild, string outputFile) { ProcessStartInfo procStartInfo = new ProcessStartInfo(); StringBuilder argumentsBuilder = new StringBuilder(); argumentsBuilder.Append("\"" + EditorApplication.CompilerPath + "\""); string monoDir = Path.GetDirectoryName(EditorApplication.CompilerPath); monoDir = Path.Combine(monoDir, "../"); argumentsBuilder.Append(" \"" + monoDir + "\""); argumentsBuilder.Append(" -noconfig"); if (!string.IsNullOrEmpty(defines)) argumentsBuilder.Append(" -d:" + defines); if (assemblyFolders != null && assemblyFolders.Length > 0) { argumentsBuilder.Append(" -lib:\""); for (int i = 0; i < assemblyFolders.Length - 1; i++) argumentsBuilder.Append(assemblyFolders[i] + ","); argumentsBuilder.Append(assemblyFolders[assemblyFolders.Length - 1] + "\""); } if (assemblies != null && assemblies.Length > 0) { argumentsBuilder.Append(" -r:"); for (int i = 0; i < assemblies.Length - 1; i++) argumentsBuilder.Append(assemblies[i] + ","); argumentsBuilder.Append(assemblies[assemblies.Length - 1]); } if (debugBuild) argumentsBuilder.Append(" -debug+ -o-"); else argumentsBuilder.Append(" -debug- -o+"); argumentsBuilder.Append(" -target:library -out:" + "\"" + outputFile + "\""); for (int i = 0; i < files.Length; i++) argumentsBuilder.Append(" \"" + files[i] + "\""); if (File.Exists(outputFile)) File.Delete(outputFile); string outputDir = Path.GetDirectoryName(outputFile); if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir); procStartInfo.Arguments = argumentsBuilder.ToString(); procStartInfo.CreateNoWindow = true; procStartInfo.FileName = EditorApplication.MonoExecPath; procStartInfo.RedirectStandardError = true; procStartInfo.RedirectStandardOutput = false; procStartInfo.UseShellExecute = false; procStartInfo.WorkingDirectory = EditorApplication.ProjectPath; process = new Process(); process.StartInfo = procStartInfo; process.Start(); readErrorsThread = new Thread(ReadErrorStream); readErrorsThread.Start(); } /// /// Worker thread method that continually checks for compiler error messages and warnings. /// private void ReadErrorStream() { while (true) { if (process == null || process.HasExited) return; string line = process.StandardError.ReadLine(); if (string.IsNullOrEmpty(line)) continue; CompilerMessage message; if (TryParseCompilerMessage(line, out message)) { if (message.type == CompilerMessageType.Warning) { lock (warnings) warnings.Add(message); } else if (message.type == CompilerMessageType.Error) { lock (errors) errors.Add(message); } } } } /// /// Parses a compiler error or warning message into a more structured format. /// /// Text of the error or warning message. /// Parsed structured version of the message. /// True if the parsing was completed successfully, false otherwise. private bool TryParseCompilerMessage(string messageText, out CompilerMessage message) { message = new CompilerMessage(); Match matchCompile = compileErrorRegex.Match(messageText); if (matchCompile.Success) { message.file = matchCompile.Groups["file"].Value; message.line = Int32.Parse(matchCompile.Groups["line"].Value); message.column = Int32.Parse(matchCompile.Groups["column"].Value); message.type = matchCompile.Groups["type"].Value == "error" ? CompilerMessageType.Error : CompilerMessageType.Warning; message.message = matchCompile.Groups["message"].Value; return true; } Match matchCompiler = compilerErrorRegex.Match(messageText); if (matchCompiler.Success) { message.file = ""; message.line = 0; message.column = 0; message.type = CompilerMessageType.Error; message.message = matchCompiler.Groups["message"].Value; return true; } return false; } /// /// Checks is the compilation process done. /// public bool IsDone { get { return process.HasExited && readErrorsThread.ThreadState == System.Threading.ThreadState.Stopped; } } /// /// Checks has the compilAtion had any errors. Only valid after returns true. /// public bool HasErrors { get { return IsDone && process.ExitCode != 0; } } /// /// Returns all warning messages generated by the compiler. /// public CompilerMessage[] WarningMessages { get { lock (warnings) { return warnings.ToArray(); } } } /// /// Returns all error messages generated by the compiler. /// public CompilerMessage[] ErrorMessages { get { lock (errors) { return errors.ToArray(); } } } /// /// Disposes of the compiler process. Should be called when done when this object instance. /// public void Dispose() { if (process == null) return; if (!process.HasExited) { process.Kill(); process.WaitForExit(); } process.Dispose(); } } /// /// Type of messages reported by the script compiler. /// public enum CompilerMessageType { Warning, Error } /// /// Data about a message reported by the compiler. /// public struct CompilerMessage { /// Type of the message. public CompilerMessageType type; /// Body of the message. public string message; /// Path ot the file the message is referencing. public string file; /// Line the message is referencing. public int line; /// Column the message is referencing. public int column; } /** @} */ }