ScriptCompiler.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Text;
  9. using System.Text.RegularExpressions;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using BansheeEngine;
  13. namespace BansheeEditor
  14. {
  15. /// <summary>
  16. /// Type of assemblies that may be generated by the script compiler.
  17. /// </summary>
  18. public enum ScriptAssemblyType
  19. {
  20. Game, Editor
  21. }
  22. /// <summary>
  23. /// Data required for compiling a single assembly.
  24. /// </summary>
  25. public struct CompileData
  26. {
  27. public string[] files;
  28. public string defines;
  29. }
  30. /// <summary>
  31. /// Compiles script files in the project into script assemblies.
  32. /// </summary>
  33. public static class ScriptCompiler
  34. {
  35. /// <summary>
  36. /// Starts compilation of the script files in the project for the specified assembly for the specified platform.
  37. /// </summary>
  38. /// <param name="type">Type of the assembly to compile. This determines which script files are used as input.</param>
  39. /// <param name="platform">Platform to compile the assemblies for.</param>
  40. /// <param name="debug">Determines should the assemblies contain debug information.</param>
  41. /// <param name="outputDir">Absolute path to the directory where to output the assemblies.</param>
  42. /// <returns>Compiler instance that contains the compiler process. Caller must ensure to properly dispose
  43. /// of this object when done.</returns>
  44. public static CompilerInstance CompileAsync(ScriptAssemblyType type, PlatformType platform, bool debug, string outputDir)
  45. {
  46. LibraryEntry[] scriptEntries = ProjectLibrary.Search("*", new ResourceType[] { ResourceType.ScriptCode });
  47. List<string> scriptFiles = new List<string>();
  48. for (int i = 0; i < scriptEntries.Length; i++)
  49. {
  50. if(scriptEntries[i].Type != LibraryEntryType.File)
  51. continue;
  52. FileEntry fileEntry = (FileEntry)scriptEntries[i];
  53. ScriptCodeImportOptions io = (ScriptCodeImportOptions) fileEntry.Options;
  54. if (io.EditorScript && type == ScriptAssemblyType.Editor ||
  55. !io.EditorScript && type == ScriptAssemblyType.Game)
  56. {
  57. scriptFiles.Add(Path.Combine(ProjectLibrary.ResourceFolder, scriptEntries[i].Path));
  58. }
  59. }
  60. string[] assemblyFolders;
  61. string[] assemblies;
  62. string outputFile;
  63. string builtinAssemblyPath = debug
  64. ? EditorApplication.BuiltinDebugAssemblyPath
  65. : EditorApplication.BuiltinReleaseAssemblyPath;
  66. string[] frameworkAssemblies = BuildManager.GetFrameworkAssemblies(platform);
  67. if (type == ScriptAssemblyType.Game)
  68. {
  69. assemblyFolders = new string[]
  70. {
  71. builtinAssemblyPath,
  72. EditorApplication.FrameworkAssemblyPath
  73. };
  74. assemblies = new string[frameworkAssemblies.Length + 1];
  75. assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName;
  76. outputFile = Path.Combine(outputDir, EditorApplication.ScriptGameAssemblyName);
  77. }
  78. else
  79. {
  80. assemblyFolders = new string[]
  81. {
  82. builtinAssemblyPath,
  83. EditorApplication.FrameworkAssemblyPath,
  84. EditorApplication.ScriptAssemblyPath
  85. };
  86. assemblies = new string[frameworkAssemblies.Length + 3];
  87. assemblies[assemblies.Length - 1] = EditorApplication.EngineAssemblyName;
  88. assemblies[assemblies.Length - 2] = EditorApplication.EditorAssemblyName;
  89. assemblies[assemblies.Length - 3] = EditorApplication.ScriptGameAssemblyName;
  90. outputFile = Path.Combine(outputDir, EditorApplication.ScriptEditorAssemblyName);
  91. }
  92. Array.Copy(frameworkAssemblies, assemblies, frameworkAssemblies.Length);
  93. string defines = BuildManager.GetDefines(platform);
  94. return new CompilerInstance(scriptFiles.ToArray(), defines, assemblyFolders, assemblies, debug, outputFile);
  95. }
  96. }
  97. /// <summary>
  98. /// Represents a started compiler process used for compiling a set of script files into an assembly.
  99. /// </summary>
  100. public class CompilerInstance
  101. {
  102. private Process process;
  103. private Thread readErrorsThread;
  104. private List<CompilerMessage> errors = new List<CompilerMessage>();
  105. private List<CompilerMessage> warnings = new List<CompilerMessage>();
  106. private Regex compileErrorRegex = new Regex(@"\s*(?<file>.*)\(\s*(?<line>\d+)\s*,\s*(?<column>\d+)\s*\)\s*:\s*(?<type>warning|error)\s+(.*):\s*(?<message>.*)");
  107. private Regex compilerErrorRegex = new Regex(@"\s*error[^:]*:\s*(?<message>.*)");
  108. /// <summary>
  109. /// Creates a new compiler process and starts compilation of the provided files.
  110. /// </summary>
  111. /// <param name="files">Absolute paths to all the C# script files to compile.</param>
  112. /// <param name="defines">A set of semi-colon separated defines to provide to the compiler.</param>
  113. /// <param name="assemblyFolders">A set of folders containing the assemblies referenced by the script files.</param>
  114. /// <param name="assemblies">Names of the assemblies containing code referenced by the script files.</param>
  115. /// <param name="debugBuild">Determines should the assembly be compiled with additional debug information.</param>
  116. /// <param name="outputFile">Absolute path to the assembly file to generate.</param>
  117. internal CompilerInstance(string[] files, string defines, string[] assemblyFolders, string[] assemblies,
  118. bool debugBuild, string outputFile)
  119. {
  120. ProcessStartInfo procStartInfo = new ProcessStartInfo();
  121. StringBuilder argumentsBuilder = new StringBuilder();
  122. if (!string.IsNullOrEmpty(defines))
  123. argumentsBuilder.Append(" -d:" + defines);
  124. if (assemblyFolders != null && assemblyFolders.Length > 0)
  125. {
  126. argumentsBuilder.Append(" -lib:\"");
  127. for (int i = 0; i < assemblyFolders.Length - 1; i++)
  128. argumentsBuilder.Append(assemblyFolders[i] + ",");
  129. argumentsBuilder.Append(assemblyFolders[assemblyFolders.Length - 1] + "\"");
  130. }
  131. if (assemblies != null && assemblies.Length > 0)
  132. {
  133. argumentsBuilder.Append(" -r:");
  134. for (int i = 0; i < assemblies.Length - 1; i++)
  135. argumentsBuilder.Append(assemblies[i] + ",");
  136. argumentsBuilder.Append(assemblies[assemblies.Length - 1]);
  137. }
  138. if (debugBuild)
  139. argumentsBuilder.Append(" -debug+ -o-");
  140. else
  141. argumentsBuilder.Append(" -debug- -o+");
  142. argumentsBuilder.Append(" -target:library -out:" + "\"" + outputFile + "\"");
  143. for (int i = 0; i < files.Length; i++)
  144. argumentsBuilder.Append(" \"" + files[i] + "\"");
  145. if (File.Exists(outputFile))
  146. File.Delete(outputFile);
  147. string outputDir = Path.GetDirectoryName(outputFile);
  148. if (!Directory.Exists(outputDir))
  149. Directory.CreateDirectory(outputDir);
  150. procStartInfo.Arguments = argumentsBuilder.ToString();
  151. procStartInfo.CreateNoWindow = true;
  152. procStartInfo.FileName = EditorApplication.CompilerPath;
  153. procStartInfo.RedirectStandardError = true;
  154. procStartInfo.RedirectStandardOutput = false;
  155. procStartInfo.UseShellExecute = false;
  156. procStartInfo.WorkingDirectory = EditorApplication.ProjectPath;
  157. process = new Process();
  158. process.StartInfo = procStartInfo;
  159. process.Start();
  160. readErrorsThread = new Thread(ReadErrorStream);
  161. readErrorsThread.Start();
  162. }
  163. /// <summary>
  164. /// Worker thread method that continually checks for compiler error messages and warnings.
  165. /// </summary>
  166. private void ReadErrorStream()
  167. {
  168. while (true)
  169. {
  170. if (process == null || process.HasExited)
  171. return;
  172. string line = process.StandardError.ReadLine();
  173. if (string.IsNullOrEmpty(line))
  174. continue;
  175. CompilerMessage message;
  176. if (TryParseCompilerMessage(line, out message))
  177. {
  178. if (message.type == CompilerMessageType.Warning)
  179. {
  180. lock (warnings)
  181. warnings.Add(message);
  182. }
  183. else if (message.type == CompilerMessageType.Error)
  184. {
  185. lock (errors)
  186. errors.Add(message);
  187. }
  188. }
  189. }
  190. }
  191. /// <summary>
  192. /// Parses a compiler error or warning message into a more structured format.
  193. /// </summary>
  194. /// <param name="messageText">Text of the error or warning message.</param>
  195. /// <param name="message">Parsed structured version of the message.</param>
  196. /// <returns>True if the parsing was completed successfully, false otherwise.</returns>
  197. private bool TryParseCompilerMessage(string messageText, out CompilerMessage message)
  198. {
  199. message = new CompilerMessage();
  200. Match matchCompile = compileErrorRegex.Match(messageText);
  201. if (matchCompile.Success)
  202. {
  203. message.file = matchCompile.Groups["file"].Value;
  204. message.line = Int32.Parse(matchCompile.Groups["line"].Value);
  205. message.column = Int32.Parse(matchCompile.Groups["column"].Value);
  206. message.type = matchCompile.Groups["type"].Value == "error"
  207. ? CompilerMessageType.Error
  208. : CompilerMessageType.Warning;
  209. message.message = matchCompile.Groups["message"].Value;
  210. return true;
  211. }
  212. Match matchCompiler = compilerErrorRegex.Match(messageText);
  213. if (matchCompiler.Success)
  214. {
  215. message.file = "";
  216. message.line = 0;
  217. message.column = 0;
  218. message.type = CompilerMessageType.Error;
  219. message.message = matchCompiler.Groups["message"].Value;
  220. return true;
  221. }
  222. return false;
  223. }
  224. /// <summary>
  225. /// Checks is the compilation process done.
  226. /// </summary>
  227. public bool IsDone
  228. {
  229. get { return process.HasExited && readErrorsThread.ThreadState == System.Threading.ThreadState.Stopped; }
  230. }
  231. /// <summary>
  232. /// Checks has the compilAtion had any errors. Only valid after <see cref="IsDone"/> returns true.
  233. /// </summary>
  234. public bool HasErrors
  235. {
  236. get { return IsDone && process.ExitCode != 0; }
  237. }
  238. /// <summary>
  239. /// Returns all warning messages generated by the compiler.
  240. /// </summary>
  241. public CompilerMessage[] WarningMessages
  242. {
  243. get
  244. {
  245. lock (warnings)
  246. {
  247. return warnings.ToArray();
  248. }
  249. }
  250. }
  251. /// <summary>
  252. /// Returns all error messages generated by the compiler.
  253. /// </summary>
  254. public CompilerMessage[] ErrorMessages
  255. {
  256. get
  257. {
  258. lock (errors)
  259. {
  260. return errors.ToArray();
  261. }
  262. }
  263. }
  264. /// <summary>
  265. /// Disposes of the compiler process. Should be called when done when this object instance.
  266. /// </summary>
  267. public void Dispose()
  268. {
  269. if (process == null)
  270. return;
  271. if (!process.HasExited)
  272. {
  273. process.Kill();
  274. process.WaitForExit();
  275. }
  276. process.Dispose();
  277. }
  278. }
  279. /// <summary>
  280. /// Type of messages reported by the script compiler.
  281. /// </summary>
  282. public enum CompilerMessageType
  283. {
  284. Warning, Error
  285. }
  286. /// <summary>
  287. /// Data about a message reported by the compiler.
  288. /// </summary>
  289. public struct CompilerMessage
  290. {
  291. /// <summary>Type of the message.</summary>
  292. public CompilerMessageType type;
  293. /// <summary>Body of the message.</summary>
  294. public string message;
  295. /// <summary>Path ot the file the message is referencing.</summary>
  296. public string file;
  297. /// <summary>Line the message is referencing.</summary>
  298. public int line;
  299. /// <summary>Column the message is referencing.</summary>
  300. public int column;
  301. }
  302. }