Skip to content

Commit 8f10c0d

Browse files
committed
feat: change the C# compiler (csc) used in project
1 parent 0cb5e9a commit 8f10c0d

File tree

5 files changed

+393
-0
lines changed

5 files changed

+393
-0
lines changed

Assets/CSharpCompilerSettings.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
#if UNITY_EDITOR && !PUBLISH_AS_DLL
2+
using System;
3+
using System.Collections;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Reflection;
9+
using System.Text;
10+
using System.Text.RegularExpressions;
11+
using System.Threading;
12+
using UnityEditor;
13+
using UnityEngine;
14+
using UnityEditor.Compilation;
15+
using Assembly = System.Reflection.Assembly;
16+
17+
namespace Coffee.CSharpCompilierSettings
18+
{
19+
internal static class ReflectionExtensions
20+
{
21+
const BindingFlags FLAGS = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
22+
23+
private static object Inst(this object self)
24+
{
25+
return (self is Type) ? null : self;
26+
}
27+
28+
private static Type Type(this object self)
29+
{
30+
return (self as Type) ?? self.GetType();
31+
}
32+
33+
public static object New(this Type self, params object[] args)
34+
{
35+
var types = args.Select(x => x.GetType()).ToArray();
36+
return self.Type().GetConstructor(types)
37+
.Invoke(args);
38+
}
39+
40+
public static object Call(this object self, string methodName, params object[] args)
41+
{
42+
var types = args.Select(x => x.GetType()).ToArray();
43+
return self.Type().GetMethod(methodName, types)
44+
.Invoke(self.Inst(), args);
45+
}
46+
47+
public static object Call(this object self, Type[] genericTypes, string methodName, params object[] args)
48+
{
49+
return self.Type().GetMethod(methodName, FLAGS)
50+
.MakeGenericMethod(genericTypes)
51+
.Invoke(self.Inst(), args);
52+
}
53+
54+
public static object Get(this object self, string memberName, MemberInfo mi = null)
55+
{
56+
mi = mi ?? self.Type().GetMember(memberName, FLAGS)[0];
57+
return mi is PropertyInfo
58+
? (mi as PropertyInfo).GetValue(self.Inst(), new object[0])
59+
: (mi as FieldInfo).GetValue(self.Inst());
60+
}
61+
62+
public static void Set(this object self, string memberName, object value, MemberInfo mi = null)
63+
{
64+
mi = mi ?? self.Type().GetMember(memberName, FLAGS)[0];
65+
if (mi is PropertyInfo)
66+
(mi as PropertyInfo).SetValue(self.Inst(), value, new object[0]);
67+
else
68+
(mi as FieldInfo).SetValue(self.Inst(), value);
69+
}
70+
}
71+
72+
internal static class CustomCompiler
73+
{
74+
static string s_InstallPath;
75+
76+
public static string GetInstalledPath(string packageId)
77+
{
78+
if (!string.IsNullOrEmpty(s_InstallPath) && File.Exists(s_InstallPath))
79+
return s_InstallPath;
80+
81+
try
82+
{
83+
s_InstallPath = Install(packageId);
84+
}
85+
catch (Exception ex)
86+
{
87+
Core.LogExeption(ex);
88+
}
89+
90+
return s_InstallPath;
91+
}
92+
93+
private static string Install(string packageId)
94+
{
95+
var sep = Path.DirectorySeparatorChar;
96+
var url = "https://globalcdn.nuget.org/packages/" + packageId.ToLower() + ".nupkg";
97+
var downloadPath = ("Temp/" + Path.GetFileName(Path.GetTempFileName())).Replace('/', sep);
98+
var installPath = ("Library/" + packageId).Replace('/', sep);
99+
var cscToolExe = (installPath + "/tools/csc.exe").Replace('/', sep);
100+
101+
// C# compiler is already installed.
102+
if (File.Exists(cscToolExe))
103+
{
104+
Core.LogDebug("C# compiler '{0}' is already installed at {1}", packageId, cscToolExe);
105+
return cscToolExe;
106+
}
107+
108+
if (Directory.Exists(installPath))
109+
Directory.Delete(installPath, true);
110+
111+
var cb = ServicePointManager.ServerCertificateValidationCallback;
112+
ServicePointManager.ServerCertificateValidationCallback = (_, __, ___, ____) => true;
113+
try
114+
{
115+
Core.LogInfo("Install C# compiler '{0}'", packageId);
116+
117+
// Download C# compiler package from nuget.
118+
{
119+
Core.LogInfo("Download {0} from nuget: {1}", packageId, url);
120+
EditorUtility.DisplayProgressBar("C# Compiler Installer", string.Format("Download {0} from nuget", packageId), 0.2f);
121+
122+
using (var client = new WebClient())
123+
{
124+
client.DownloadFile(url, downloadPath);
125+
}
126+
}
127+
128+
// Extract nuget package (unzip).
129+
{
130+
Core.LogInfo("Extract {0} to {1} with 7z", downloadPath, installPath);
131+
EditorUtility.DisplayProgressBar("C# Compiler Installer", string.Format("Extract {0}", downloadPath), 0.4f);
132+
133+
var appPath = EditorApplication.applicationContentsPath.Replace('/', sep);
134+
var args = string.Format("x {0} -o{1}", downloadPath, installPath);
135+
136+
switch (Application.platform)
137+
{
138+
case RuntimePlatform.WindowsEditor:
139+
ExecuteCommand(appPath + "\\Tools\\7z.exe", args);
140+
break;
141+
case RuntimePlatform.OSXEditor:
142+
case RuntimePlatform.LinuxEditor:
143+
ExecuteCommand(appPath + "/Tools/7za", args);
144+
break;
145+
}
146+
}
147+
148+
Core.LogInfo("C# compiler '{0}' has been installed at {1}.", packageId, installPath);
149+
}
150+
catch
151+
{
152+
throw new Exception(string.Format("C# compiler '{0}' installation failed.", packageId));
153+
}
154+
finally
155+
{
156+
ServicePointManager.ServerCertificateValidationCallback = cb;
157+
EditorUtility.ClearProgressBar();
158+
}
159+
160+
if (File.Exists(cscToolExe))
161+
return cscToolExe;
162+
163+
throw new FileNotFoundException(string.Format("C# compiler '{0}' is not found at {1}", packageId, cscToolExe));
164+
}
165+
166+
private static void ExecuteCommand(string exe, string args)
167+
{
168+
Core.LogInfo("Execute command: {0} {1}", exe, args);
169+
170+
var p = Process.Start(new ProcessStartInfo
171+
{
172+
FileName = exe,
173+
Arguments = args,
174+
CreateNoWindow = true,
175+
UseShellExecute = false,
176+
RedirectStandardError = true,
177+
});
178+
179+
// Don't consume 100% of CPU while waiting for process to exit
180+
if (Application.platform == RuntimePlatform.OSXEditor)
181+
while (!p.HasExited)
182+
Thread.Sleep(100);
183+
else
184+
p.WaitForExit();
185+
186+
if (p.ExitCode != 0)
187+
{
188+
var ex = new Exception(p.StandardError.ReadToEnd());
189+
Core.LogExeption(ex);
190+
throw ex;
191+
}
192+
}
193+
}
194+
195+
[InitializeOnLoad]
196+
internal static class Core
197+
{
198+
private const string k_LogHeader = "<b><color=#aa2222>[CscSettings]</color></b> ";
199+
private static bool LogDebugEnabled { get; }
200+
201+
public static void LogDebug(string format, params object[] args)
202+
{
203+
if (LogDebugEnabled)
204+
LogInfo(format, args);
205+
}
206+
207+
public static void LogInfo(string format, params object[] args)
208+
{
209+
if (args == null || args.Length == 0)
210+
UnityEngine.Debug.Log(k_LogHeader + format);
211+
else
212+
UnityEngine.Debug.LogFormat(k_LogHeader + format, args);
213+
}
214+
215+
public static void LogExeption(Exception e)
216+
{
217+
UnityEngine.Debug.LogException(new Exception(k_LogHeader + e.Message, e.InnerException));
218+
}
219+
220+
public static void RequestScriptCompilation()
221+
{
222+
Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor")
223+
.Call("DirtyAllScripts");
224+
}
225+
226+
private static void ChangeCompilerProcess(object compiler, object scriptAssembly, CscSettings setting)
227+
{
228+
var tProgram = Type.GetType("UnityEditor.Utils.Program, UnityEditor");
229+
var tScriptCompilerBase = Type.GetType("UnityEditor.Scripting.Compilers.ScriptCompilerBase, UnityEditor");
230+
var fiProcess = tScriptCompilerBase.GetField("process", BindingFlags.NonPublic | BindingFlags.Instance);
231+
var psi = compiler.Get("process", fiProcess).Call("GetProcessStartInfo") as ProcessStartInfo;
232+
var isDefaultCsc = (psi.FileName + " " + psi.Arguments).Replace('\\', '/')
233+
.Split(' ')
234+
.FirstOrDefault(x => x.EndsWith("/csc.exe") || x.EndsWith("/csc.dll") || x.EndsWith("/csc") || x.EndsWith("/unity_csc.sh") || x.EndsWith("/unity_csc.bat"))
235+
.StartsWith(EditorApplication.applicationContentsPath.Replace('\\', '/'));
236+
237+
// csc is not Unity default. It is already modified.
238+
if (!isDefaultCsc)
239+
{
240+
LogDebug(" <color=#bbbb44><Skipped> current csc is not Unity default. It is already modified.</color>");
241+
return;
242+
}
243+
244+
var cscToolExe = CustomCompiler.GetInstalledPath(setting.PackageId);
245+
246+
// csc is not installed.
247+
if (string.IsNullOrEmpty(cscToolExe))
248+
{
249+
LogDebug(" <color=#bbbb44><Skipped> custom csc is not installed.</color>");
250+
return;
251+
}
252+
253+
// Kill current process.
254+
compiler.Call("Dispose");
255+
256+
var responseFile = Regex.Replace(psi.Arguments, "^.*@(.+)$", "$1");
257+
var text = File.ReadAllText(responseFile);
258+
text = Regex.Replace(text, "[\r\n]+", "\n");
259+
text = Regex.Replace(text, "^-", "/");
260+
text = Regex.Replace(text, "\n/langversion:[^\n]+\n", "\n/langversion:" + setting.LanguageVersion + "\n");
261+
text = Regex.Replace(text, "\n/debug\n", "\n/debug:portable\n");
262+
text += "\n/preferreduilang:en-US";
263+
264+
// Change exe file path.
265+
LogDebug("Change csc to {0}", cscToolExe);
266+
if (Application.platform == RuntimePlatform.WindowsEditor)
267+
{
268+
psi.FileName = Path.GetFullPath(cscToolExe);
269+
psi.Arguments = "/shared /noconfig @" + responseFile;
270+
}
271+
else
272+
{
273+
psi.FileName = Path.Combine(EditorApplication.applicationContentsPath, "MonoBleedingEdge/bin/mono");
274+
psi.Arguments = cscToolExe + " /noconfig @" + responseFile;
275+
}
276+
277+
text = Regex.Replace(text, "\n", Environment.NewLine);
278+
File.WriteAllText(responseFile, text);
279+
280+
LogDebug("Restart compiler process: {0} {1}", psi.FileName, psi.Arguments);
281+
var program = tProgram.New(psi);
282+
program.Call("Start");
283+
compiler.Set("process", program, fiProcess);
284+
}
285+
286+
private static void OnAssemblyCompilationStarted(string name)
287+
{
288+
try
289+
{
290+
LogDebug("Assembly compilation started: {0}", name);
291+
if (Path.GetFileNameWithoutExtension(name) == "CSharpCompilerSettings")
292+
{
293+
LogDebug(" <color=#bbbb44><Skipped> Assembly 'CSharpCompilerSettings' requires default csc.</color>");
294+
return;
295+
}
296+
297+
var settings = CscSettings.instance;
298+
if (settings.UseDefaultCompiler)
299+
return;
300+
301+
var assemblyName = Path.GetFileNameWithoutExtension(name);
302+
if (assemblyName == typeof(Core).Assembly.GetName().Name)
303+
return;
304+
305+
var tEditorCompilationInterface = Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor");
306+
var compilerTasks = tEditorCompilationInterface.Get("Instance").Get("compilationTask").Get("compilerTasks") as IDictionary;
307+
var scriptAssembly = compilerTasks.Keys.Cast<object>().FirstOrDefault(x => (x.Get("Filename") as string) == assemblyName + ".dll");
308+
if (scriptAssembly == null)
309+
return;
310+
311+
// Create new compiler to recompile.
312+
LogDebug("Assembly compilation started: <b>{0} should be recompiled.</b>", assemblyName);
313+
ChangeCompilerProcess(compilerTasks[scriptAssembly], scriptAssembly, settings);
314+
}
315+
catch (Exception e)
316+
{
317+
LogExeption(e);
318+
}
319+
}
320+
321+
static Core()
322+
{
323+
LogDebugEnabled = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup)
324+
.Split(';', ',')
325+
.Any(x => x == "CSC_SETTINGS_DEBUG");
326+
327+
if (LogDebugEnabled)
328+
{
329+
var sb = new StringBuilder("<b>InitializeOnLoad</b>. Loaded assemblies:\n");
330+
foreach (var asm in Type.GetType("UnityEditor.EditorAssemblies, UnityEditor").Get("loadedAssemblies") as Assembly[])
331+
{
332+
var name = asm.GetName().Name;
333+
var path = asm.Location;
334+
if (path.Contains(Path.GetDirectoryName(EditorApplication.applicationPath)))
335+
sb.AppendFormat(" > {0}:\t{1}\n", name, "APP_PATH/.../" + Path.GetFileName(path));
336+
else
337+
sb.AppendFormat(" > <color=#22aa22><b>{0}</b></color>:\t{1}\n", name, path.Replace(Environment.CurrentDirectory, "."));
338+
}
339+
340+
LogDebug(sb.ToString());
341+
}
342+
343+
var assembly = typeof(Core).Assembly;
344+
var assemblyName = assembly.GetName().Name;
345+
var location = assembly.Location.Replace(Environment.CurrentDirectory, ".");
346+
LogInfo("Start watching assembly compilation: assembly = {0} ({1})", assemblyName, location);
347+
CompilationPipeline.assemblyCompilationStarted += OnAssemblyCompilationStarted;
348+
}
349+
}
350+
}
351+
#endif

Assets/CSharpCompilerSettings/AsmdefEx.Core.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "CSharpCompilerSettings",
3+
"references": [],
4+
"optionalUnityReferences": [],
5+
"includePlatforms": [
6+
"Editor"
7+
],
8+
"excludePlatforms": [],
9+
"allowUnsafeCode": false,
10+
"overrideReferences": false,
11+
"precompiledReferences": [],
12+
"autoReferenced": false,
13+
"defineConstraints": [
14+
"CSC_SETTINGS_DEVELOP"
15+
]
16+
}

Assets/CSharpCompilerSettings/CSharpCompilerSettings.asmdef.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)