AssemblyOmission.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.IO;
  5. using UnityEditor;
  6. using System.Linq;
  7. using System.Runtime.CompilerServices;
  8. using SingularityGroup.HotReload.Newtonsoft.Json;
  9. using UnityEditor.Compilation;
  10. [assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorTests")]
  11. namespace SingularityGroup.HotReload.Editor {
  12. internal static class AssemblyOmission {
  13. // [MenuItem("Window/Hot Reload Dev/List omitted projects")]
  14. private static void Check() {
  15. Log.Info("To compile C# files same as a Player build, we must omit projects which aren't part of the selected Player build.");
  16. var omitted = GetOmittedProjects(EditorUserBuildSettings.activeScriptCompilationDefines);
  17. Log.Info("---------");
  18. foreach (var name in omitted) {
  19. Log.Info("omitted editor/other project named: {0}", name);
  20. }
  21. }
  22. [JsonObject(MemberSerialization.Fields)]
  23. private class AssemblyDefinitionJson {
  24. public string name;
  25. public string[] defineConstraints;
  26. }
  27. // scripts in Assets/ (with no asmdef) are always compiled into Assembly-CSharp
  28. private static readonly string alwaysIncluded = "Assembly-CSharp";
  29. private class Cache : AssetPostprocessor {
  30. public static string[] ommitedProjects;
  31. private static void OnPostprocessAllAssets(string[] importedAssets,
  32. string[] deletedAssets,
  33. string[] movedAssets,
  34. string[] movedFromAssetPaths) {
  35. ommitedProjects = null;
  36. }
  37. }
  38. // main thread only
  39. public static string[] GetOmittedProjects(string allDefineSymbols, bool verboseLogs = false) {
  40. if (Cache.ommitedProjects != null) {
  41. return Cache.ommitedProjects;
  42. }
  43. var arr = allDefineSymbols.Split(';');
  44. var omitted = GetOmittedProjects(arr, verboseLogs);
  45. Cache.ommitedProjects = omitted;
  46. return omitted;
  47. }
  48. // must be deterministic (return projects in same order each time)
  49. private static string[] GetOmittedProjects(string[] allDefineSymbols, bool verboseLogs = false) {
  50. // HotReload uses names of assemblies.
  51. var editorAssemblies = GetEditorAssemblies();
  52. editorAssemblies.Remove(alwaysIncluded);
  53. var omittedByConstraint = DefineConstraints.GetOmittedAssemblies(allDefineSymbols);
  54. editorAssemblies.AddRange(omittedByConstraint);
  55. // Note: other platform player assemblies are also returned here, but I haven't seen it cause issues
  56. // when using Hot Reload with IdleGame Android build.
  57. var playerAssemblies = GetPlayerAssemblies().ToArray();
  58. if (verboseLogs) {
  59. foreach (var name in editorAssemblies) {
  60. Log.Info("found project named {0}", name);
  61. }
  62. foreach (var playerAssemblyName in playerAssemblies) {
  63. Log.Debug("player assembly named {0}", playerAssemblyName);
  64. }
  65. }
  66. // leaves the editor assemblies that are not built into player assemblies (e.g. editor and test assemblies)
  67. var toOmit = editorAssemblies.Except(playerAssemblies.Select(asm => asm.name));
  68. var unique = new HashSet<string>(toOmit);
  69. return unique.OrderBy(s => s).ToArray();
  70. }
  71. // main thread only
  72. public static List<string> GetEditorAssemblies() {
  73. return CompilationPipeline
  74. .GetAssemblies(AssembliesType.Editor)
  75. .Select(asm => asm.name)
  76. .ToList();
  77. }
  78. public static Assembly[] GetPlayerAssemblies() {
  79. var playerAssemblyNames = CompilationPipeline
  80. #if UNITY_2019_3_OR_NEWER
  81. .GetAssemblies(AssembliesType.PlayerWithoutTestAssemblies) // since Unity 2019.3
  82. #else
  83. .GetAssemblies(AssembliesType.Player)
  84. #endif
  85. .ToArray();
  86. return playerAssemblyNames;
  87. }
  88. internal static class DefineConstraints {
  89. /// <summary>
  90. /// When define constraints evaluate to false, we need
  91. /// </summary>
  92. /// <param name="defineSymbols"></param>
  93. /// <returns></returns>
  94. /// <remarks>
  95. /// Not aware of a Unity api to read defineConstraints, so we do it ourselves.<br/>
  96. /// Find any asmdef files where the define constraints evaluate to false.
  97. /// </remarks>
  98. public static string[] GetOmittedAssemblies(string[] defineSymbols) {
  99. var guids = AssetDatabase.FindAssets("t:asmdef");
  100. var asmdefFiles = guids.Select(AssetDatabase.GUIDToAssetPath);
  101. var shouldOmit = new List<string>();
  102. foreach (var asmdefFile in asmdefFiles) {
  103. var asmdef = ReadDefineConstraints(asmdefFile);
  104. if (asmdef == null) continue;
  105. if (asmdef.defineConstraints == null || asmdef.defineConstraints.Length == 0) {
  106. // Hot Reload already handles assemblies correctly if they have no define symbols.
  107. continue;
  108. }
  109. var allPass = asmdef.defineConstraints.All(constraint => EvaluateDefineConstraint(constraint, defineSymbols));
  110. if (!allPass) {
  111. shouldOmit.Add(asmdef.name);
  112. }
  113. }
  114. return shouldOmit.ToArray();
  115. }
  116. static AssemblyDefinitionJson ReadDefineConstraints(string path) {
  117. try {
  118. var json = File.ReadAllText(path);
  119. var asmdef = JsonConvert.DeserializeObject<AssemblyDefinitionJson>(json);
  120. return asmdef;
  121. } catch (Exception) {
  122. // ignore malformed asmdef
  123. return null;
  124. }
  125. }
  126. // Unity Define Constraints syntax is described in the docs https://docs.unity3d.com/Manual/class-AssemblyDefinitionImporter.html
  127. static readonly Dictionary<string, string> syntaxMap = new Dictionary<string, string> {
  128. { "OR", "||" },
  129. { "AND", "&&" },
  130. { "NOT", "!" }
  131. };
  132. /// <summary>
  133. /// Evaluate a define constraint like 'UNITY_ANDROID || UNITY_IOS'
  134. /// </summary>
  135. /// <param name="input"></param>
  136. /// <param name="defineSymbols"></param>
  137. /// <returns></returns>
  138. public static bool EvaluateDefineConstraint(string input, string[] defineSymbols) {
  139. // map Unity defineConstraints syntax to DataTable syntax (unity supports both)
  140. foreach (var item in syntaxMap) {
  141. // surround with space because || may not have spaces around it
  142. input = input.Replace(item.Value, $" {item.Key} ");
  143. }
  144. // remove any extra spaces we just created
  145. input = input.Replace(" ", " ");
  146. var tokens = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  147. foreach (var token in tokens) {
  148. if (!syntaxMap.ContainsKey(token) && token != "false" && token != "true") {
  149. var index = input.IndexOf(token, StringComparison.Ordinal);
  150. // replace symbols with true or false depending if they are in the array or not.
  151. input = input.Substring(0, index) + defineSymbols.Contains(token) + input.Substring(index + token.Length);
  152. }
  153. }
  154. var dt = new DataTable();
  155. return (bool)dt.Compute(input, "");
  156. }
  157. }
  158. }
  159. }