Skip to content

Commit 2e49e4c

Browse files
committed
feat: add custom compiler interface
1 parent 06b6d25 commit 2e49e4c

File tree

6 files changed

+256
-149
lines changed

6 files changed

+256
-149
lines changed

Plugins/CSharpCompilerSettings/Core.cs

Lines changed: 56 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,60 @@
11
using System;
2-
using System.Collections;
3-
using System.Diagnostics;
42
using System.IO;
53
using System.Linq;
6-
using System.Reflection;
74
using System.Text.RegularExpressions;
85
using UnityEditor;
96
using UnityEngine;
10-
using UnityEditor.Compilation;
117
using Assembly = System.Reflection.Assembly;
128

139
namespace Coffee.CSharpCompilerSettings
1410
{
11+
internal interface ICustomCompiler : IDisposable
12+
{
13+
bool IsValid();
14+
void Register();
15+
}
16+
1517
internal static class Core
1618
{
1719
private static bool IsGlobal { get; set; }
1820

21+
private static bool IsDevelopAssembly
22+
{
23+
get { return typeof(Core).Assembly.GetName().Name == "CSharpCompilerSettings_"; }
24+
}
25+
26+
private static readonly ICustomCompiler[] customCompilers = new ICustomCompiler[]
27+
{
28+
CustomCompiler_Legacy.instance,
29+
};
30+
31+
private static ICustomCompiler currentCustomCompiler { get; set; }
32+
33+
public static bool ShouldToRecompile(string assemblyName, string asmdef)
34+
{
35+
if (assemblyName == typeof(Core).Assembly.GetName().Name)
36+
{
37+
// Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> requires default csc.</color>", assemblyName);
38+
return false;
39+
}
40+
else if (IsGlobal && GetPortableDllPath(asmdef) != null)
41+
{
42+
// Logger.LogWarning(" <color=#bbbb44><Skipped> Local CSharpCompilerSettings.*.dll for <b>'{0}'</b> is found.</color>", assemblyName);
43+
return false;
44+
}
45+
else if (!IsGlobal && !IsInSameDirectory(asmdef))
46+
{
47+
// Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> is not target.</color>", assemblyName);
48+
return false;
49+
}
50+
else if (!GetSettings().ShouldToRecompile(asmdef))
51+
{
52+
// Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> does not need to be recompiled.</color>", assemblyName);
53+
return false;
54+
}
55+
return true;
56+
}
57+
1958
public static string GetAssemblyName(string asmdefPath)
2059
{
2160
if (string.IsNullOrEmpty(asmdefPath)) return null;
@@ -68,8 +107,8 @@ public static CscSettingsAsset GetSettings()
68107
public static string ModifyResponseFile(CscSettingsAsset setting, string assemblyName, string asmdefPath, string text)
69108
{
70109
var asmdefDir = string.IsNullOrEmpty(asmdefPath) ? null : Path.GetDirectoryName(asmdefPath);
71-
text = Regex.Replace(text, "[\r\n]+", "\n");
72-
text = Regex.Replace(text, "^-", "/");
110+
text = Regex.Replace(text, "[\r\n]+", "\n", RegexOptions.Multiline);
111+
text = Regex.Replace(text, "^-", "/", RegexOptions.Multiline);
73112
text = Regex.Replace(text, "\n/debug\n", "\n/debug:portable\n");
74113
text += "\n/preferreduilang:en-US";
75114

@@ -139,127 +178,6 @@ public static string ModifyResponseFile(CscSettingsAsset setting, string assembl
139178
return text;
140179
}
141180

142-
private static void ChangeCompilerProcess(object compiler, object scriptAssembly, CscSettingsAsset setting)
143-
{
144-
if (IsDevelopAssembly)
145-
return;
146-
147-
var tProgram = Type.GetType("UnityEditor.Utils.Program, UnityEditor");
148-
var tScriptCompilerBase = Type.GetType("UnityEditor.Scripting.Compilers.ScriptCompilerBase, UnityEditor");
149-
var fiProcess = tScriptCompilerBase.GetField("process", BindingFlags.NonPublic | BindingFlags.Instance);
150-
var psi = compiler.Get("process", fiProcess).Call("GetProcessStartInfo") as ProcessStartInfo;
151-
var oldCommand = (psi.FileName + " " + psi.Arguments).Replace('\\', '/');
152-
var command = oldCommand.Replace(EditorApplication.applicationContentsPath.Replace('\\', '/'), "@APP_CONTENTS@");
153-
var isDefaultCsc = Regex.IsMatch(command, "@APP_CONTENTS@/[^ ]*(mcs|csc)");
154-
var assemblyName = Path.GetFileNameWithoutExtension(scriptAssembly.Get("Filename") as string);
155-
var asmdefDir = scriptAssembly.Get("OriginPath") as string;
156-
var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : FindAsmdef(asmdefDir);
157-
158-
// csc is not Unity default. It is already modified.
159-
if (!isDefaultCsc)
160-
{
161-
Logger.LogWarning(" <color=#bbbb44><Skipped> current csc is not Unity default. It is already modified.</color>");
162-
return;
163-
}
164-
165-
// Kill current process.
166-
compiler.Call("Dispose");
167-
168-
// Response file.
169-
var responseFile = Regex.Replace(psi.Arguments, "^.*@(.+)$", "$1");
170-
171-
// Change to custom compiler.
172-
if (setting.ShouldToUseCustomCompiler(asmdefPath))
173-
{
174-
var compilerInfo = CompilerInfo.GetInstalledInfo(setting.CompilerPackage.PackageId);
175-
176-
// csc is not installed. Restart current process.
177-
if (!compilerInfo.IsValid)
178-
{
179-
Logger.LogWarning(" <color=#bbbb44><Skipped> C# compiler '{0}' is not installed. Restart compiler process: {1}</color>", compilerInfo.Path, oldCommand);
180-
181-
var currentProgram = tProgram.New(psi);
182-
currentProgram.Call("Start");
183-
compiler.Set("process", currentProgram, fiProcess);
184-
return;
185-
}
186-
187-
// Change exe file path.
188-
compilerInfo.Setup(psi, responseFile, Application.platform);
189-
}
190-
191-
// Modify response file.
192-
var text = File.ReadAllText(responseFile);
193-
text = ModifyResponseFile(setting, assemblyName, asmdefPath, text);
194-
File.WriteAllText(responseFile, text);
195-
196-
// Logging
197-
if (CscSettingsAsset.instance.EnableDebugLog)
198-
Logger.LogDebug("Response file '{0}' has been modified:\n{1}", responseFile, Regex.Replace(text, "\n/reference.*", "") + "\n\n* The references are skipped because it was too long.");
199-
200-
// Restart compiler process.
201-
Logger.LogDebug("Restart compiler process: {0} {1}\n old command = {2}", psi.FileName, psi.Arguments, oldCommand);
202-
var program = tProgram.New(psi);
203-
program.Call("Start");
204-
compiler.Set("process", program, fiProcess);
205-
}
206-
207-
public static void OnAssemblyCompilationStarted(string name)
208-
{
209-
try
210-
{
211-
var assemblyName = Path.GetFileNameWithoutExtension(name);
212-
if (assemblyName == typeof(Core).Assembly.GetName().Name)
213-
{
214-
Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> requires default csc.</color>", assemblyName);
215-
return;
216-
}
217-
218-
var tEditorCompilationInterface = Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor");
219-
var compilerTasks = tEditorCompilationInterface.Get("Instance").Get("compilationTask").Get("compilerTasks") as IDictionary;
220-
var scriptAssembly = compilerTasks.Keys.Cast<object>().FirstOrDefault(x => (x.Get("Filename") as string) == assemblyName + ".dll");
221-
if (scriptAssembly == null)
222-
{
223-
Logger.LogWarning(" <color=#bbbb44><Skipped> scriptAssembly <b>'{0}'</b> is not found.</color>", assemblyName);
224-
return;
225-
}
226-
227-
var asmdefDir = scriptAssembly.Get("OriginPath") as string;
228-
var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : FindAsmdef(asmdefDir);
229-
if (IsGlobal && GetPortableDllPath(asmdefPath) != null)
230-
{
231-
Logger.LogWarning(" <color=#bbbb44><Skipped> Local CSharpCompilerSettings.*.dll for <b>'{0}'</b> is found.</color>", assemblyName);
232-
return;
233-
}
234-
235-
if (!IsGlobal && !IsInSameDirectory(asmdefPath))
236-
{
237-
Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> is not target.</color>", assemblyName);
238-
return;
239-
}
240-
241-
var globalSettings = CscSettingsAsset.instance;
242-
var settings = GetSettings();
243-
if (!globalSettings.ShouldToRecompile(asmdefPath))
244-
{
245-
Logger.LogWarning(" <color=#bbbb44><Skipped> Assembly <b>'{0}'</b> does not need to be recompiled.</color>", assemblyName);
246-
return;
247-
}
248-
249-
// Create new compiler to recompile.
250-
Logger.LogDebug("<color=#22aa22>Assembly compilation started: <b>{0} should be recompiled.</b></color>\nsettings = {1}", assemblyName, JsonUtility.ToJson(settings));
251-
ChangeCompilerProcess(compilerTasks[scriptAssembly], scriptAssembly, settings);
252-
}
253-
catch (Exception e)
254-
{
255-
Logger.LogException(e);
256-
}
257-
}
258-
259-
static bool IsDevelopAssembly
260-
{
261-
get { return typeof(Core).Assembly.GetName().Name == "CSharpCompilerSettings_"; }
262-
}
263181

264182
[InitializeOnLoadMethod]
265183
public static void Initialize()
@@ -270,7 +188,9 @@ public static void Initialize()
270188
if (IsGlobal)
271189
{
272190
Logger.Setup(
273-
"<b><color=#bb4444>[CscSettings]</color></b> ",
191+
IsDevelopAssembly
192+
? "<b><color=#bb4444>[CscSettings(dev)]</color></b> "
193+
: "<b><color=#bb4444>[CscSettings]</color></b> ",
274194
() => CscSettingsAsset.instance.EnableDebugLog
275195
);
276196
}
@@ -286,27 +206,18 @@ public static void Initialize()
286206
Logger.LogException("Target assembly is not found. {0}", typeof(Core).Assembly.Location.Replace(Environment.CurrentDirectory, "."));
287207
}
288208

289-
// Dump loaded assemblies
290-
if (CscSettingsAsset.instance.EnableDebugLog)
209+
// This is global assembly, but the dev assembly is found: do nothing.
210+
if (IsGlobal && !IsDevelopAssembly && (Type.GetType("UnityEditor.EditorAssemblies, UnityEditor").Get("loadedAssemblies") as Assembly[]).Any(asm => asm.GetName().Name == "CSharpCompilerSettings_"))
291211
{
292-
var sb = new System.Text.StringBuilder("<color=#22aa22><b>InitializeOnLoad,</b></color> the loaded assemblies:\n");
293-
foreach (var asm in Type.GetType("UnityEditor.EditorAssemblies, UnityEditor").Get("loadedAssemblies") as Assembly[])
294-
{
295-
var name = asm.GetName().Name;
296-
var path = asm.Location;
297-
if (path.Contains(Path.GetDirectoryName(EditorApplication.applicationPath)))
298-
sb.AppendFormat(" > {0}:\t{1}\n", name, "APP_PATH/.../" + Path.GetFileName(path));
299-
else
300-
sb.AppendFormat(" > <color=#22aa22><b>{0}</b></color>:\t{1}\n", name, path.Replace(Environment.CurrentDirectory, "."));
301-
}
302-
303-
Logger.LogDebug(sb.ToString());
212+
Logger.LogWarning("This is global assembly, but the dev assembly is found: ignored.");
213+
return;
304214
}
305215

306216
// Register callback.
307-
Logger.LogDebug("<color=#22aa22><b>InitializeOnLoad:</b></color> start watching assembly compilation.");
308-
CompilationPipeline.assemblyCompilationStarted -= OnAssemblyCompilationStarted;
309-
CompilationPipeline.assemblyCompilationStarted += OnAssemblyCompilationStarted;
217+
currentCustomCompiler?.Dispose();
218+
currentCustomCompiler = customCompilers.FirstOrDefault(c => c.IsValid());
219+
currentCustomCompiler?.Register();
220+
Logger.LogDebug("<color=#22aa22><b>InitializeOnLoad:</b></color> A custom compiler registered: {0}", currentCustomCompiler);
310221

311222
// Install custom compiler package before compilation.
312223
var settings = GetSettings();
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.Collections;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Text.RegularExpressions;
8+
using UnityEditor;
9+
using UnityEngine;
10+
using UnityEditor.Compilation;
11+
12+
namespace Coffee.CSharpCompilerSettings
13+
{
14+
internal class CustomCompiler_Legacy : ScriptableSingleton<CustomCompiler_Legacy>, ICustomCompiler
15+
{
16+
[SerializeField] private bool isInitialized;
17+
18+
public bool IsValid()
19+
{
20+
var unityVersions = Application.unityVersion.Split('.');
21+
return int.Parse(unityVersions[0]) <= 2020;
22+
}
23+
24+
public void Dispose()
25+
{
26+
typeof(CompilationPipeline).RemoveEvent<string>("assemblyCompilationStarted", OnAssemblyCompilationStarted);
27+
}
28+
29+
public void Register()
30+
{
31+
typeof(CompilationPipeline).AddEvent<string>("assemblyCompilationStarted", OnAssemblyCompilationStarted);
32+
33+
// Request recompilation at once.
34+
if (!isInitialized)
35+
{
36+
isInitialized = true;
37+
if (Regex.IsMatch(Application.unityVersion, "2019.(1|2)"))
38+
{
39+
Logger.LogInfo("This is first compilation. Request script compilation again.");
40+
Utils.RequestCompilation();
41+
}
42+
}
43+
}
44+
45+
private static void ChangeCompilerProcess(object compiler, object scriptAssembly, CscSettingsAsset setting)
46+
{
47+
var tProgram = Type.GetType("UnityEditor.Utils.Program, UnityEditor");
48+
var tScriptCompilerBase = Type.GetType("UnityEditor.Scripting.Compilers.ScriptCompilerBase, UnityEditor");
49+
var fiProcess = tScriptCompilerBase.GetField("process", BindingFlags.NonPublic | BindingFlags.Instance);
50+
var psi = compiler.Get("process", fiProcess).Call("GetProcessStartInfo") as ProcessStartInfo;
51+
var oldCommand = (psi.FileName + " " + psi.Arguments).Replace('\\', '/');
52+
var command = oldCommand.Replace(EditorApplication.applicationContentsPath.Replace('\\', '/'), "@APP_CONTENTS@");
53+
var isDefaultCsc = Regex.IsMatch(command, "@APP_CONTENTS@/[^ ]*(mcs|csc)");
54+
var assemblyName = Path.GetFileNameWithoutExtension(scriptAssembly.Get("Filename") as string);
55+
var asmdefDir = scriptAssembly.Get("OriginPath") as string;
56+
var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : Core.FindAsmdef(asmdefDir);
57+
58+
// csc is not Unity default. It is already modified.
59+
if (!isDefaultCsc)
60+
{
61+
Logger.LogWarning(" <color=#bbbb44><Skipped> current csc is not Unity default. It is already modified.</color>");
62+
return;
63+
}
64+
65+
// Kill current process.
66+
compiler.Call("Dispose");
67+
68+
// Response file.
69+
var responseFile = Regex.Replace(psi.Arguments, "^.*@(.+)$", "$1");
70+
71+
// Change to custom compiler.
72+
if (setting.ShouldToUseCustomCompiler(asmdefPath))
73+
{
74+
var compilerInfo = CompilerInfo.GetInstalledInfo(setting.CompilerPackage.PackageId);
75+
76+
// csc is not installed. Restart current process.
77+
if (!compilerInfo.IsValid)
78+
{
79+
Logger.LogWarning(" <color=#bbbb44><Skipped> C# compiler '{0}' is not installed. Restart compiler process: {1}</color>", compilerInfo.Path, oldCommand);
80+
81+
var currentProgram = tProgram.New(psi);
82+
currentProgram.Call("Start");
83+
compiler.Set("process", currentProgram, fiProcess);
84+
return;
85+
}
86+
87+
// Change exe file path.
88+
compilerInfo.Setup(psi, responseFile, Application.platform);
89+
}
90+
91+
// Modify response file.
92+
var text = File.ReadAllText(responseFile);
93+
text = Core.ModifyResponseFile(setting, assemblyName, asmdefPath, text);
94+
File.WriteAllText(responseFile, text);
95+
96+
// Logging
97+
if (CscSettingsAsset.instance.EnableDebugLog)
98+
Logger.LogDebug("Response file '{0}' has been modified:\n{1}", responseFile, Regex.Replace(text, "\n/reference.*", "") + "\n\n* The references are skipped because it was too long.");
99+
100+
// Restart compiler process.
101+
Logger.LogDebug("Restart compiler process: {0} {1}\n old command = {2}", psi.FileName, psi.Arguments, oldCommand);
102+
var program = tProgram.New(psi);
103+
program.Call("Start");
104+
compiler.Set("process", program, fiProcess);
105+
}
106+
107+
private void OnAssemblyCompilationStarted(string name)
108+
{
109+
try
110+
{
111+
var assemblyName = Path.GetFileNameWithoutExtension(name);
112+
var tEditorCompilationInterface = Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor");
113+
var compilerTasks = tEditorCompilationInterface.Get("Instance").Get("compilationTask").Get("compilerTasks") as IDictionary;
114+
var scriptAssembly = compilerTasks.Keys.Cast<object>().FirstOrDefault(x => (x.Get("Filename") as string) == assemblyName + ".dll");
115+
if (scriptAssembly == null)
116+
{
117+
Logger.LogWarning(" <color=#bbbb44><Skipped> scriptAssembly <b>'{0}'</b> is not found.</color>", assemblyName);
118+
return;
119+
}
120+
121+
var asmdefDir = scriptAssembly.Get("OriginPath") as string;
122+
var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : Core.FindAsmdef(asmdefDir);
123+
if (!Core.ShouldToRecompile(assemblyName, asmdefPath)) return;
124+
125+
var settings = Core.GetSettings();
126+
127+
// Create new compiler to recompile.
128+
Logger.LogDebug("<color=#22aa22>Assembly compilation started: <b>{0} should be recompiled.</b></color>\nsettings = {1}", assemblyName, JsonUtility.ToJson(settings));
129+
ChangeCompilerProcess(compilerTasks[scriptAssembly], scriptAssembly, settings);
130+
}
131+
catch (Exception e)
132+
{
133+
Logger.LogException(e);
134+
}
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)