|
| 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 |
0 commit comments