//********************************** 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;
}
/** @} */
}