From 2e49e4c1208fac8efa2a72069b78aee0966e3ee8 Mon Sep 17 00:00:00 2001 From: mob-sakai Date: Wed, 10 Nov 2021 21:33:15 +0900 Subject: [PATCH] feat: add custom compiler interface --- Plugins/CSharpCompilerSettings/Core.cs | 201 +++++------------- .../CustomCompiler_Legacy.cs | 137 ++++++++++++ .../CustomCompiler_Legacy.cs.meta | 11 + Plugins/CSharpCompilerSettings/Logger.cs | 1 + .../ReflectionExtensions.cs | 54 ++++- Plugins/Recompiler/CSharpCompilerSettings.rsp | 1 + 6 files changed, 256 insertions(+), 149 deletions(-) create mode 100644 Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs create mode 100644 Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs.meta diff --git a/Plugins/CSharpCompilerSettings/Core.cs b/Plugins/CSharpCompilerSettings/Core.cs index 60dba82..2b992f8 100644 --- a/Plugins/CSharpCompilerSettings/Core.cs +++ b/Plugins/CSharpCompilerSettings/Core.cs @@ -1,21 +1,60 @@ using System; -using System.Collections; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; -using UnityEditor.Compilation; using Assembly = System.Reflection.Assembly; namespace Coffee.CSharpCompilerSettings { + internal interface ICustomCompiler : IDisposable + { + bool IsValid(); + void Register(); + } + internal static class Core { private static bool IsGlobal { get; set; } + private static bool IsDevelopAssembly + { + get { return typeof(Core).Assembly.GetName().Name == "CSharpCompilerSettings_"; } + } + + private static readonly ICustomCompiler[] customCompilers = new ICustomCompiler[] + { + CustomCompiler_Legacy.instance, + }; + + private static ICustomCompiler currentCustomCompiler { get; set; } + + public static bool ShouldToRecompile(string assemblyName, string asmdef) + { + if (assemblyName == typeof(Core).Assembly.GetName().Name) + { + // Logger.LogWarning(" Assembly '{0}' requires default csc.", assemblyName); + return false; + } + else if (IsGlobal && GetPortableDllPath(asmdef) != null) + { + // Logger.LogWarning(" Local CSharpCompilerSettings.*.dll for '{0}' is found.", assemblyName); + return false; + } + else if (!IsGlobal && !IsInSameDirectory(asmdef)) + { + // Logger.LogWarning(" Assembly '{0}' is not target.", assemblyName); + return false; + } + else if (!GetSettings().ShouldToRecompile(asmdef)) + { + // Logger.LogWarning(" Assembly '{0}' does not need to be recompiled.", assemblyName); + return false; + } + return true; + } + public static string GetAssemblyName(string asmdefPath) { if (string.IsNullOrEmpty(asmdefPath)) return null; @@ -68,8 +107,8 @@ public static CscSettingsAsset GetSettings() public static string ModifyResponseFile(CscSettingsAsset setting, string assemblyName, string asmdefPath, string text) { var asmdefDir = string.IsNullOrEmpty(asmdefPath) ? null : Path.GetDirectoryName(asmdefPath); - text = Regex.Replace(text, "[\r\n]+", "\n"); - text = Regex.Replace(text, "^-", "/"); + text = Regex.Replace(text, "[\r\n]+", "\n", RegexOptions.Multiline); + text = Regex.Replace(text, "^-", "/", RegexOptions.Multiline); text = Regex.Replace(text, "\n/debug\n", "\n/debug:portable\n"); text += "\n/preferreduilang:en-US"; @@ -139,127 +178,6 @@ public static string ModifyResponseFile(CscSettingsAsset setting, string assembl return text; } - private static void ChangeCompilerProcess(object compiler, object scriptAssembly, CscSettingsAsset setting) - { - if (IsDevelopAssembly) - return; - - var tProgram = Type.GetType("UnityEditor.Utils.Program, UnityEditor"); - var tScriptCompilerBase = Type.GetType("UnityEditor.Scripting.Compilers.ScriptCompilerBase, UnityEditor"); - var fiProcess = tScriptCompilerBase.GetField("process", BindingFlags.NonPublic | BindingFlags.Instance); - var psi = compiler.Get("process", fiProcess).Call("GetProcessStartInfo") as ProcessStartInfo; - var oldCommand = (psi.FileName + " " + psi.Arguments).Replace('\\', '/'); - var command = oldCommand.Replace(EditorApplication.applicationContentsPath.Replace('\\', '/'), "@APP_CONTENTS@"); - var isDefaultCsc = Regex.IsMatch(command, "@APP_CONTENTS@/[^ ]*(mcs|csc)"); - var assemblyName = Path.GetFileNameWithoutExtension(scriptAssembly.Get("Filename") as string); - var asmdefDir = scriptAssembly.Get("OriginPath") as string; - var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : FindAsmdef(asmdefDir); - - // csc is not Unity default. It is already modified. - if (!isDefaultCsc) - { - Logger.LogWarning(" current csc is not Unity default. It is already modified."); - return; - } - - // Kill current process. - compiler.Call("Dispose"); - - // Response file. - var responseFile = Regex.Replace(psi.Arguments, "^.*@(.+)$", "$1"); - - // Change to custom compiler. - if (setting.ShouldToUseCustomCompiler(asmdefPath)) - { - var compilerInfo = CompilerInfo.GetInstalledInfo(setting.CompilerPackage.PackageId); - - // csc is not installed. Restart current process. - if (!compilerInfo.IsValid) - { - Logger.LogWarning(" C# compiler '{0}' is not installed. Restart compiler process: {1}", compilerInfo.Path, oldCommand); - - var currentProgram = tProgram.New(psi); - currentProgram.Call("Start"); - compiler.Set("process", currentProgram, fiProcess); - return; - } - - // Change exe file path. - compilerInfo.Setup(psi, responseFile, Application.platform); - } - - // Modify response file. - var text = File.ReadAllText(responseFile); - text = ModifyResponseFile(setting, assemblyName, asmdefPath, text); - File.WriteAllText(responseFile, text); - - // Logging - if (CscSettingsAsset.instance.EnableDebugLog) - 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."); - - // Restart compiler process. - Logger.LogDebug("Restart compiler process: {0} {1}\n old command = {2}", psi.FileName, psi.Arguments, oldCommand); - var program = tProgram.New(psi); - program.Call("Start"); - compiler.Set("process", program, fiProcess); - } - - public static void OnAssemblyCompilationStarted(string name) - { - try - { - var assemblyName = Path.GetFileNameWithoutExtension(name); - if (assemblyName == typeof(Core).Assembly.GetName().Name) - { - Logger.LogWarning(" Assembly '{0}' requires default csc.", assemblyName); - return; - } - - var tEditorCompilationInterface = Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor"); - var compilerTasks = tEditorCompilationInterface.Get("Instance").Get("compilationTask").Get("compilerTasks") as IDictionary; - var scriptAssembly = compilerTasks.Keys.Cast().FirstOrDefault(x => (x.Get("Filename") as string) == assemblyName + ".dll"); - if (scriptAssembly == null) - { - Logger.LogWarning(" scriptAssembly '{0}' is not found.", assemblyName); - return; - } - - var asmdefDir = scriptAssembly.Get("OriginPath") as string; - var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : FindAsmdef(asmdefDir); - if (IsGlobal && GetPortableDllPath(asmdefPath) != null) - { - Logger.LogWarning(" Local CSharpCompilerSettings.*.dll for '{0}' is found.", assemblyName); - return; - } - - if (!IsGlobal && !IsInSameDirectory(asmdefPath)) - { - Logger.LogWarning(" Assembly '{0}' is not target.", assemblyName); - return; - } - - var globalSettings = CscSettingsAsset.instance; - var settings = GetSettings(); - if (!globalSettings.ShouldToRecompile(asmdefPath)) - { - Logger.LogWarning(" Assembly '{0}' does not need to be recompiled.", assemblyName); - return; - } - - // Create new compiler to recompile. - Logger.LogDebug("Assembly compilation started: {0} should be recompiled.\nsettings = {1}", assemblyName, JsonUtility.ToJson(settings)); - ChangeCompilerProcess(compilerTasks[scriptAssembly], scriptAssembly, settings); - } - catch (Exception e) - { - Logger.LogException(e); - } - } - - static bool IsDevelopAssembly - { - get { return typeof(Core).Assembly.GetName().Name == "CSharpCompilerSettings_"; } - } [InitializeOnLoadMethod] public static void Initialize() @@ -270,7 +188,9 @@ public static void Initialize() if (IsGlobal) { Logger.Setup( - "[CscSettings] ", + IsDevelopAssembly + ? "[CscSettings(dev)] " + : "[CscSettings] ", () => CscSettingsAsset.instance.EnableDebugLog ); } @@ -286,27 +206,18 @@ public static void Initialize() Logger.LogException("Target assembly is not found. {0}", typeof(Core).Assembly.Location.Replace(Environment.CurrentDirectory, ".")); } - // Dump loaded assemblies - if (CscSettingsAsset.instance.EnableDebugLog) + // This is global assembly, but the dev assembly is found: do nothing. + if (IsGlobal && !IsDevelopAssembly && (Type.GetType("UnityEditor.EditorAssemblies, UnityEditor").Get("loadedAssemblies") as Assembly[]).Any(asm => asm.GetName().Name == "CSharpCompilerSettings_")) { - var sb = new System.Text.StringBuilder("InitializeOnLoad, the loaded assemblies:\n"); - foreach (var asm in Type.GetType("UnityEditor.EditorAssemblies, UnityEditor").Get("loadedAssemblies") as Assembly[]) - { - var name = asm.GetName().Name; - var path = asm.Location; - if (path.Contains(Path.GetDirectoryName(EditorApplication.applicationPath))) - sb.AppendFormat(" > {0}:\t{1}\n", name, "APP_PATH/.../" + Path.GetFileName(path)); - else - sb.AppendFormat(" > {0}:\t{1}\n", name, path.Replace(Environment.CurrentDirectory, ".")); - } - - Logger.LogDebug(sb.ToString()); + Logger.LogWarning("This is global assembly, but the dev assembly is found: ignored."); + return; } // Register callback. - Logger.LogDebug("InitializeOnLoad: start watching assembly compilation."); - CompilationPipeline.assemblyCompilationStarted -= OnAssemblyCompilationStarted; - CompilationPipeline.assemblyCompilationStarted += OnAssemblyCompilationStarted; + currentCustomCompiler?.Dispose(); + currentCustomCompiler = customCompilers.FirstOrDefault(c => c.IsValid()); + currentCustomCompiler?.Register(); + Logger.LogDebug("InitializeOnLoad: A custom compiler registered: {0}", currentCustomCompiler); // Install custom compiler package before compilation. var settings = GetSettings(); diff --git a/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs b/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs new file mode 100644 index 0000000..7e2a856 --- /dev/null +++ b/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEngine; +using UnityEditor.Compilation; + +namespace Coffee.CSharpCompilerSettings +{ + internal class CustomCompiler_Legacy : ScriptableSingleton, ICustomCompiler + { + [SerializeField] private bool isInitialized; + + public bool IsValid() + { + var unityVersions = Application.unityVersion.Split('.'); + return int.Parse(unityVersions[0]) <= 2020; + } + + public void Dispose() + { + typeof(CompilationPipeline).RemoveEvent("assemblyCompilationStarted", OnAssemblyCompilationStarted); + } + + public void Register() + { + typeof(CompilationPipeline).AddEvent("assemblyCompilationStarted", OnAssemblyCompilationStarted); + + // Request recompilation at once. + if (!isInitialized) + { + isInitialized = true; + if (Regex.IsMatch(Application.unityVersion, "2019.(1|2)")) + { + Logger.LogInfo("This is first compilation. Request script compilation again."); + Utils.RequestCompilation(); + } + } + } + + private static void ChangeCompilerProcess(object compiler, object scriptAssembly, CscSettingsAsset setting) + { + var tProgram = Type.GetType("UnityEditor.Utils.Program, UnityEditor"); + var tScriptCompilerBase = Type.GetType("UnityEditor.Scripting.Compilers.ScriptCompilerBase, UnityEditor"); + var fiProcess = tScriptCompilerBase.GetField("process", BindingFlags.NonPublic | BindingFlags.Instance); + var psi = compiler.Get("process", fiProcess).Call("GetProcessStartInfo") as ProcessStartInfo; + var oldCommand = (psi.FileName + " " + psi.Arguments).Replace('\\', '/'); + var command = oldCommand.Replace(EditorApplication.applicationContentsPath.Replace('\\', '/'), "@APP_CONTENTS@"); + var isDefaultCsc = Regex.IsMatch(command, "@APP_CONTENTS@/[^ ]*(mcs|csc)"); + var assemblyName = Path.GetFileNameWithoutExtension(scriptAssembly.Get("Filename") as string); + var asmdefDir = scriptAssembly.Get("OriginPath") as string; + var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : Core.FindAsmdef(asmdefDir); + + // csc is not Unity default. It is already modified. + if (!isDefaultCsc) + { + Logger.LogWarning(" current csc is not Unity default. It is already modified."); + return; + } + + // Kill current process. + compiler.Call("Dispose"); + + // Response file. + var responseFile = Regex.Replace(psi.Arguments, "^.*@(.+)$", "$1"); + + // Change to custom compiler. + if (setting.ShouldToUseCustomCompiler(asmdefPath)) + { + var compilerInfo = CompilerInfo.GetInstalledInfo(setting.CompilerPackage.PackageId); + + // csc is not installed. Restart current process. + if (!compilerInfo.IsValid) + { + Logger.LogWarning(" C# compiler '{0}' is not installed. Restart compiler process: {1}", compilerInfo.Path, oldCommand); + + var currentProgram = tProgram.New(psi); + currentProgram.Call("Start"); + compiler.Set("process", currentProgram, fiProcess); + return; + } + + // Change exe file path. + compilerInfo.Setup(psi, responseFile, Application.platform); + } + + // Modify response file. + var text = File.ReadAllText(responseFile); + text = Core.ModifyResponseFile(setting, assemblyName, asmdefPath, text); + File.WriteAllText(responseFile, text); + + // Logging + if (CscSettingsAsset.instance.EnableDebugLog) + 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."); + + // Restart compiler process. + Logger.LogDebug("Restart compiler process: {0} {1}\n old command = {2}", psi.FileName, psi.Arguments, oldCommand); + var program = tProgram.New(psi); + program.Call("Start"); + compiler.Set("process", program, fiProcess); + } + + private void OnAssemblyCompilationStarted(string name) + { + try + { + var assemblyName = Path.GetFileNameWithoutExtension(name); + var tEditorCompilationInterface = Type.GetType("UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface, UnityEditor"); + var compilerTasks = tEditorCompilationInterface.Get("Instance").Get("compilationTask").Get("compilerTasks") as IDictionary; + var scriptAssembly = compilerTasks.Keys.Cast().FirstOrDefault(x => (x.Get("Filename") as string) == assemblyName + ".dll"); + if (scriptAssembly == null) + { + Logger.LogWarning(" scriptAssembly '{0}' is not found.", assemblyName); + return; + } + + var asmdefDir = scriptAssembly.Get("OriginPath") as string; + var asmdefPath = string.IsNullOrEmpty(asmdefDir) ? "" : Core.FindAsmdef(asmdefDir); + if (!Core.ShouldToRecompile(assemblyName, asmdefPath)) return; + + var settings = Core.GetSettings(); + + // Create new compiler to recompile. + Logger.LogDebug("Assembly compilation started: {0} should be recompiled.\nsettings = {1}", assemblyName, JsonUtility.ToJson(settings)); + ChangeCompilerProcess(compilerTasks[scriptAssembly], scriptAssembly, settings); + } + catch (Exception e) + { + Logger.LogException(e); + } + } + } +} diff --git a/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs.meta b/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs.meta new file mode 100644 index 0000000..1e02fe4 --- /dev/null +++ b/Plugins/CSharpCompilerSettings/CustomCompiler_Legacy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef34d8e058245435797af21b0fa315d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Plugins/CSharpCompilerSettings/Logger.cs b/Plugins/CSharpCompilerSettings/Logger.cs index 4b47bcc..f12a3d5 100644 --- a/Plugins/CSharpCompilerSettings/Logger.cs +++ b/Plugins/CSharpCompilerSettings/Logger.cs @@ -11,6 +11,7 @@ public static void Setup(string header, Func logLevelFunc) { s_LogHeader = header; s_EnableLogFunc = logLevelFunc; + LogDebug("Setup"); } public static void LogInfo(string format, params object[] args) diff --git a/Plugins/CSharpCompilerSettings/ReflectionExtensions.cs b/Plugins/CSharpCompilerSettings/ReflectionExtensions.cs index 965dc2d..4488ecd 100644 --- a/Plugins/CSharpCompilerSettings/ReflectionExtensions.cs +++ b/Plugins/CSharpCompilerSettings/ReflectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Text; using System; using System.Linq; using System.Reflection; @@ -6,7 +7,8 @@ namespace Coffee.CSharpCompilerSettings { internal static class ReflectionExtensions { - const BindingFlags FLAGS = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + const BindingFlags FLAGS = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase; + static readonly StringBuilder _sb = new StringBuilder(1024); private static object Inst(this object self) { @@ -44,20 +46,64 @@ public static object Call(this object self, Type[] genericTypes, string methodNa public static object Get(this object self, string memberName, MemberInfo mi = null) { - mi = mi ?? self.Type().GetMember(memberName, FLAGS)[0]; + mi = mi ?? self.Type().GetField(memberName, FLAGS) ?? self.Type().GetProperty(memberName, FLAGS) as MemberInfo; if (mi is PropertyInfo) return (mi as PropertyInfo).GetValue(self.Inst(), new object[0]); - else + else if (mi is FieldInfo) return (mi as FieldInfo).GetValue(self.Inst()); + else + throw new Exception(string.Format("Reflection not found: {0} in {1}", memberName, self.Type())); } public static void Set(this object self, string memberName, object value, MemberInfo mi = null) { - mi = mi ?? self.Type().GetMember(memberName, FLAGS)[0]; + mi = mi ?? self.Type().GetField(memberName, FLAGS) ?? self.Type().GetProperty(memberName, FLAGS) as MemberInfo ?? self.Type().GetField(memberName, FLAGS); if (mi is PropertyInfo) (mi as PropertyInfo).SetValue(self.Inst(), value, new object[0]); else (mi as FieldInfo).SetValue(self.Inst(), value); } + + public static void AddEvent(this object self, string memberName, Action callback) + { + var ev = self.Get(memberName) as Action; + ev += callback; + self.Set(memberName, ev); + } + + public static void AddEvent(this object self, string memberName, Action callback) + { + var ev = self.Get(memberName) as Action; + ev += callback; + self.Set(memberName, ev); + } + public static void RemoveEvent(this object self, string memberName, Action callback) + { + var ev = self.Get(memberName) as Action; + ev -= callback; + self.Set(memberName, ev); + } + + public static void RemoveEvent(this object self, string memberName, Action callback) + { + var ev = self.Get(memberName) as Action; + ev -= callback; + self.Set(memberName, ev); + } + + public static string DumpEvent(this object self, string memberName) + { + _sb.Length = 0; + var fi = self.Type().GetField(memberName, FLAGS); + var handler = fi.GetValue(self.Inst()) as MulticastDelegate; + var invocationList = handler != null ? handler.GetInvocationList() : new Delegate[0]; + _sb.AppendFormat("Dump {0}.{1} event ({2} callbacks):\n", fi.DeclaringType, fi.Name, invocationList.Length); + for (var i = 0; i < invocationList.Length; i++) + { + var m = invocationList[i].Method; + _sb.AppendFormat(" -> [{0}({1})] {2}\n", m.DeclaringType, m.DeclaringType.Assembly.GetName().Name, m); + } + return _sb.ToString(); + } } } diff --git a/Plugins/Recompiler/CSharpCompilerSettings.rsp b/Plugins/Recompiler/CSharpCompilerSettings.rsp index a5a1627..e63ca34 100644 --- a/Plugins/Recompiler/CSharpCompilerSettings.rsp +++ b/Plugins/Recompiler/CSharpCompilerSettings.rsp @@ -17,6 +17,7 @@ "${PLUGIN_SOURCE}/AssemblyInfo.cs" "${PLUGIN_SOURCE}/Core.cs" +"${PLUGIN_SOURCE}/CustomCompiler_Legacy.cs" "${PLUGIN_SOURCE}/CscSettingsAsset.cs" "${PLUGIN_SOURCE}/CSharpLanguageVersion.cs" "${PLUGIN_SOURCE}/ReflectionExtensions.cs"