-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathDotnetExeCompilator.cs
More file actions
384 lines (330 loc) · 18.3 KB
/
DotnetExeCompilator.cs
File metadata and controls
384 lines (330 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using FastScriptReload.Editor.AssemblyPostProcess;
using HarmonyLib;
using ImmersiveVRTools.Editor.Common.Cache;
using ImmersiveVRTools.Editor.Common.Utilities;
using ImmersiveVRTools.Runtime.Common;
using ImmersiveVrToolsCommon.Runtime.Logging;
using UnityEditor;
namespace FastScriptReload.Editor.Compilation
{
[InitializeOnLoad]
public class DotnetExeDynamicCompilation : DynamicCompilationBase
{
private static string _dotnetExePath;
private static string _cscDll;
private static string _tempFolder;
private static string ApplicationContentsPath = EditorApplication.applicationContentsPath;
private static readonly List<string> _createdFilesToCleanUp = new List<string>();
private static readonly Dictionary<string, Assembly> _typeNameAssemblyCache = new Dictionary<string, Assembly>(16);
private static readonly List<string> _analyzers = new List<string>();
static DotnetExeDynamicCompilation()
{
const string DefaultUnityProjectFilePath = "Assembly-CSharp.csproj";
#if UNITY_EDITOR_WIN
const string dotnetExecutablePath = "dotnet.exe";
#else
const string dotnetExecutablePath = "dotnet"; //mac and linux, no extension
#endif
_dotnetExePath = FindFileOrThrow(dotnetExecutablePath);
_cscDll = FindFileOrThrow("csc.dll"); //even on mac/linux need to find dll and use, not no extension one
_tempFolder = Path.GetTempPath();
if (File.Exists(DefaultUnityProjectFilePath))
{
try
{
var xmlCSProj = File.ReadAllText(DefaultUnityProjectFilePath);
var xdoc = XDocument.Parse(xmlCSProj);
var analyzers = xdoc.Root.Elements("ItemGroup")
.Elements("Analyzer")
.Select(e => e.Attribute("Include").Value)
.Where(File.Exists)
.ToArray()
;
foreach (var analyzer in analyzers)
{
_analyzers.Add(analyzer);
}
}
catch (Exception e)
{
LoggerScoped.LogWarning($"Unable to collect roslyn analyzers. {e}");
}
}
else
{
LoggerScoped.LogWarning($"{DefaultUnityProjectFilePath} does not exists, hence analyzers in project are ignored.");
}
EditorApplication.playModeStateChanged += obj =>
{
if (obj == PlayModeStateChange.ExitingPlayMode && _createdFilesToCleanUp.Any())
{
LoggerScoped.LogDebug($"Removing temporary files: [{string.Join(",", _createdFilesToCleanUp)}]");
foreach (var fileToCleanup in _createdFilesToCleanUp)
{
new FileInfo(fileToCleanup).IsReadOnly = false;
File.Delete(fileToCleanup);
}
_createdFilesToCleanUp.Clear();
}
};
}
private static string FindFileOrThrow(string fileName)
{
return SessionStateCache.GetOrCreateString($"FSR:FilePath_{fileName}", () =>
{
var foundFile = Directory
.GetFiles(ApplicationContentsPath, fileName, SearchOption.AllDirectories)
.FirstOrDefault();
if (foundFile == null)
{
throw new Exception($"Unable to find '{fileName}', make sure Editor version supports it. You can also add preprocessor directive 'FastScriptReload_CompileViaMCS' which will use Mono compiler instead");
}
return foundFile;
});
}
public static CompileResult Compile(List<string> filePathsWithSourceCode, UnityMainThreadDispatcher unityMainThreadDispatcher)
{
var sourceCodeCombinedFilePath = string.Empty;
try
{
var asmName = Guid.NewGuid().ToString().Replace("-", "");
var rspFile = _tempFolder + $"{asmName}.rsp";
var assemblyAttributeFilePath = _tempFolder + $"{asmName}.DynamicallyCreatedAssemblyAttribute.cs";
sourceCodeCombinedFilePath = _tempFolder + $"{asmName}.SourceCodeCombined.cs";
var outLibraryPath = $"{_tempFolder}{asmName}.dll";
var createSourceCodeCombinedResult = CreateSourceCodeCombinedContents(filePathsWithSourceCode, ActiveScriptCompilationDefines.ToList());
CreateFileAndTrackAsCleanup(sourceCodeCombinedFilePath, createSourceCodeCombinedResult.SourceCode, _createdFilesToCleanUp);
#if UNITY_EDITOR
unityMainThreadDispatcher.Enqueue(() =>
{
if ((bool)FastScriptReloadPreference.IsAutoOpenGeneratedSourceFileOnChangeEnabled.GetEditorPersistedValueOrDefault())
{
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(sourceCodeCombinedFilePath, 0);
}
});
#endif
var originalAssemblyPathToAsmWithInternalsVisibleToCompiled = PerfMeasure.Elapsed(
() => CreateAssemblyCopiesWithInternalsVisibleTo(createSourceCodeCombinedResult, asmName),
out var createInternalVisibleToAsmElapsedMilliseconds);
var shouldAddUnsafeFlag = createSourceCodeCombinedResult.SourceCode.Contains("unsafe"); //TODO: not ideal as 'unsafe' can be part of comment, not code. But compiling with that flag in more cases shouldn't cause issues
var rspFileContent = GenerateCompilerArgsRspFileContents(outLibraryPath, sourceCodeCombinedFilePath, assemblyAttributeFilePath,
originalAssemblyPathToAsmWithInternalsVisibleToCompiled, shouldAddUnsafeFlag);
CreateFileAndTrackAsCleanup(rspFile, rspFileContent, _createdFilesToCleanUp);
CreateFileAndTrackAsCleanup(assemblyAttributeFilePath, DynamicallyCreatedAssemblyAttributeSourceCode, _createdFilesToCleanUp);
var exitCode = ExecuteDotnetExeCompilation(_dotnetExePath, _cscDll, rspFile, outLibraryPath, out var outputMessages);
var compiledAssembly = Assembly.LoadFrom(outLibraryPath);
return new CompileResult(outLibraryPath, outputMessages, exitCode, compiledAssembly, createSourceCodeCombinedResult.SourceCode,
sourceCodeCombinedFilePath, createInternalVisibleToAsmElapsedMilliseconds);
}
catch (SourceCodeHasErrorsException e)
{
// FastScriptReloadManager has a special case for reporting SourceCodeHasErrorsException.
// Just pass it through.
throw e;
}
catch (Exception e)
{
LoggerScoped.LogError($"Compilation error: temporary files were not removed so they can be inspected: "
+ string.Join(", ", _createdFilesToCleanUp
.Select(f => $"<a href=\"{f}\" line=\"1\">{f}</a>")));
if (LogHowToFixMessageOnCompilationError)
{
LoggerScoped.LogWarning($@"HOW TO FIX - INSTRUCTIONS:
1) Open file that caused issue by looking at error log starting with: 'FSR: Compilation error: temporary files were not removed so they can be inspected: '. And click on file path to open.
2) Look up other error in the console, which will be like 'Error When updating files:' - this one contains exact line that failed to compile (in XXX_SourceCodeGenerated.cs file). Those are same compilation errors as you see in Unity/IDE when developing.
3) Read compiler error message as it'll help understand the issue
Error could be caused by a normal compilation issue that you created in source file (eg typo), in that case please fix and it'll recompile.
It's possible compilation fails due to existing limitation, in that case:
<b><color='orange'>You can quickly specify custom script rewrite override for part of code that's failing.</color></b>
Please use project panel to:
1) Right-click on the original file that has compilation issue
2) Click Fast Script Reload -> Add / Open User Script Rewrite Override
3) Read top comment in opened file and it'll explain how to create overrides
I'm continuously working on mitigating limitations.
If you could please get in touch with me via 'support@immersivevrtools.com' and include error you see in the console as well as created files (from paths in previous error). This way I can get it fixed for you.
You can also:
1) Look at 'limitation' section in the docs - which will explain bit more around limitations and workarounds
2) Move some of the code that you want to work on to different file - compilation happens on whole file, if you have multiple types there it could increase the chance of issues
3) Have a look at compilation error, it shows error line (in the '*.SourceCodeCombined.cs' file, it's going to be something that compiler does not accept, likely easy to spot. To workaround you can change that part of code in original file. It's specific patterns that'll break it.
*If you want to prevent that message from reappearing please go to Window -> Fast Script Reload -> Start Screen -> Logging -> tick off 'Log how to fix message on compilation error'*");
}
throw new HotReloadCompilationException(e.Message, e, sourceCodeCombinedFilePath);
}
}
private static Dictionary<string, string> CreateAssemblyCopiesWithInternalsVisibleTo(CreateSourceCodeCombinedContentsResult createSourceCodeCombinedResult, string asmName)
{
var originalAssemblyPathToAsmWithInternalsVisibleToCompiled = new Dictionary<string, string>();
try
{
var assembliesForTypesInCombinedFile = createSourceCodeCombinedResult.TypeNamesDefinitions
.Select(GetAssemblyByTypeName)
.Where(t => t != null)
.Distinct();
foreach (var assemblyForTypesInCombinedFile in assembliesForTypesInCombinedFile)
{
var createdAssemblyWithInternalsVisibleToNewlyCompiled = AddInternalsVisibleToForAllUserAssembliesPostProcess.CreateAssemblyWithInternalsContentsVisibleTo(
assemblyForTypesInCombinedFile, asmName
);
originalAssemblyPathToAsmWithInternalsVisibleToCompiled.Add(assemblyForTypesInCombinedFile.Location, createdAssemblyWithInternalsVisibleToNewlyCompiled);
}
}
catch (Exception e)
{
LoggerScoped.LogWarning($"Unable to create assembly with '{nameof(InternalsVisibleToAttribute)}' for dynamically recompiled code. {e}");
}
return originalAssemblyPathToAsmWithInternalsVisibleToCompiled;
}
private static void CreateFileAndTrackAsCleanup(string filePath, string contents, List<string> createdFilesToCleanUp)
{
File.WriteAllText(filePath, contents);
new FileInfo(filePath).IsReadOnly = true;
createdFilesToCleanUp.Add(filePath);
}
private static Assembly GetAssemblyByTypeName(string typeName)
{
// This cache is barely worth it on my machine - it's ~1ms without, ~0ms with.
// However, the number of assemblies to search is technically unbounded
// - so this might be more important for somebody else.
if (_typeNameAssemblyCache.TryGetValue(typeName, out var assembly)) return assembly;
// FSR (via Harmony) originally did this search by enumerating assembly.GetTypes().
// I can't see anything in the documentation suggesting the assembly.GetType(typeName) version misses any cases.
// It's much faster.
assembly = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(asm => asm.GetType(typeName, false) != null);
if (assembly != null) _typeNameAssemblyCache.Add(typeName, assembly);
return assembly;
}
private static string GenerateCompilerArgsRspFileContents(string outLibraryPath, string sourceCodeCombinedFilePath, string assemblyAttributeFilePath,
Dictionary<string, string> originalAssemblyPathToAsmWithInternalsVisibleToCompiled, bool addUnsafeFlag)
{
var rspContents = new StringBuilder();
rspContents.AppendLine("-target:library");
rspContents.AppendLine($"-out:\"{outLibraryPath}\"");
// rspContents.AppendLine($"-refout:\"{tempFolder}{asmName}.ref.dll\""); //reference assembly for linking, not needed
foreach (var symbol in ActiveScriptCompilationDefines)
{
rspContents.AppendLine($"-define:{symbol}");
}
foreach (var referenceToAdd in ResolveReferencesToAdd(new List<string>()))
{
if (originalAssemblyPathToAsmWithInternalsVisibleToCompiled.TryGetValue(referenceToAdd, out var asmWithInternalsVisibleTo))
{
//Changed assembly have InternalsVisibleTo added to it to avoid any issues where types are defined internal
rspContents.AppendLine($"-r:\"{asmWithInternalsVisibleTo}\"");
}
else
{
rspContents.AppendLine($"-r:\"{referenceToAdd}\"");
}
}
foreach (var analyzer in _analyzers)
{
rspContents.AppendLine($"-analyzer:\"{analyzer}\"");
}
rspContents.AppendLine($"\"{sourceCodeCombinedFilePath}\"");
rspContents.AppendLine($"\"{assemblyAttributeFilePath}\"");
rspContents.AppendLine($"-langversion:latest");
rspContents.AppendLine("/deterministic");
rspContents.AppendLine("/optimize-");
rspContents.AppendLine("/debug:portable");
rspContents.AppendLine("/nologo");
rspContents.AppendLine("/RuntimeMetadataVersion:v4.0.30319");
if (addUnsafeFlag)
{
rspContents.AppendLine("/unsafe");
}
rspContents.AppendLine("/nowarn:0169");
rspContents.AppendLine("/nowarn:0649");
rspContents.AppendLine("/nowarn:1701");
rspContents.AppendLine("/nowarn:1702");
rspContents.AppendLine("/utf8output");
rspContents.AppendLine("/preferreduilang:en-US");
var rspContentsString = rspContents.ToString();
return rspContentsString;
}
private static int ExecuteDotnetExeCompilation(string dotnetExePath, string cscDll, string rspFile,
string outLibraryPath, out List<string> outputMessages)
{
var process = new Process();
process.StartInfo.FileName = dotnetExePath;
process.StartInfo.Arguments = $"exec \"{cscDll}\" /nostdlib /noconfig /shared \"@{rspFile}\"";
var outMessages = new List<string>();
var stderr_completed = new ManualResetEvent(false);
var stdout_completed = new ManualResetEvent(false);
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
outMessages.Add(args.Data);
else
stderr_completed.Set();
};
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outMessages.Add(args.Data);
return;
}
stdout_completed.Set();
};
process.StartInfo.StandardOutputEncoding = process.StartInfo.StandardErrorEncoding = Encoding.UTF8;
try
{
process.Start();
}
catch (Exception ex)
{
if (ex is Win32Exception win32Exception)
throw new SystemException(string.Format("Error running {0}: {1}", process.StartInfo.FileName,
typeof(Win32Exception)
.GetMethod("GetErrorMessage", BindingFlags.Static | BindingFlags.NonPublic)?
.Invoke(null, new object[] { win32Exception.NativeErrorCode }) ??
$"<Unable to resolve GetErrorMessage function>, NativeErrorCode: {win32Exception.NativeErrorCode}"));
throw;
}
int exitCode = -1;
try
{
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
exitCode = process.ExitCode;
}
finally
{
stderr_completed.WaitOne(TimeSpan.FromSeconds(30.0));
stdout_completed.WaitOne(TimeSpan.FromSeconds(30.0));
process.Close();
}
if (!File.Exists(outLibraryPath))
throw new Exception("Compiler failed to produce the assembly. Output: '" +
string.Join(Environment.NewLine + Environment.NewLine, outMessages) + "'");
outputMessages = new List<string>();
outputMessages.AddRange(outMessages);
return exitCode;
}
}
public class HotReloadCompilationException : Exception
{
public string SourceCodeCombinedFileCreated { get; }
public HotReloadCompilationException(string message, Exception innerException, string sourceCodeCombinedFileCreated) : base(message, innerException)
{
SourceCodeCombinedFileCreated = sourceCodeCombinedFileCreated;
}
}
}