using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Unity.VisualScripting; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.Callbacks; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; internal class LinkerCreator : IPreprocessBuildWithReport { private static string linkerPath => Path.Combine(BoltCore.Paths.persistentGenerated, "link.xml"); public int callbackOrder { get; } private static ManagedStrippingLevel GetManagedStrippingLevel(BuildTargetGroup buildTarget) { #if UNITY_2023_1_OR_NEWER var namedBuildTarget = UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(buildTarget); return PlayerSettings.GetManagedStrippingLevel(namedBuildTarget); #else return PlayerSettings.GetManagedStrippingLevel(buildTarget); #endif } public void OnPreprocessBuild(BuildReport report) { if (VSUsageUtility.isVisualScriptingUsed) { try { if (GetManagedStrippingLevel(EditorUserBuildSettings.selectedBuildTargetGroup) != ManagedStrippingLevel.Disabled) { GenerateLinker(); } } catch (Exception ex) { Debug.LogException(ex); DeleteLinker(); } } } [PostProcessBuild] private static void OnPostprocessBuild(BuildTarget buildTarget, string path) { if (VSUsageUtility.isVisualScriptingUsed) { DeleteLinker(); } } private static void DeleteLinker() { PathUtility.DeleteProjectFileIfExists(linkerPath, true); } // Automatically generates the link.xml file to prevent stripping. // Currently only used for plugin assemblies, because blanket preserving // all setting assemblies sometimes causes the IL2CPP process to fail. // For settings assemblies, the AOT stubs are good enough to fool // the static code analysis without needing this full coverage. // https://docs.unity3d.com/Manual/iphone-playerSizeOptimization.html // However, for FullSerializer, we need to preserve our custom assemblies. // This is mostly because IL2CPP will attempt to transform non-public // property setters used in deserialization into read-only accessors // that return false on PropertyInfo.CanWrite, but only in stripped builds. // Therefore, in stripped builds, FS will skip properties that should be // deserialized without any error (and that took hours of debugging to figure out). public static void GenerateLinker() { var linker = new XDocument(); var linkerNode = new XElement("linker"); if (!PluginContainer.initialized) PluginContainer.Initialize(); foreach (var pluginAssembly in PluginContainer.plugins .SelectMany(plugin => plugin.GetType() .GetAttributes() .Select(a => a.assemblyName)) .Distinct()) { var assemblyNode = new XElement("assembly"); var fullnameAttribute = new XAttribute("fullname", pluginAssembly); var preserveAttribute = new XAttribute("preserve", "all"); assemblyNode.Add(fullnameAttribute); assemblyNode.Add(preserveAttribute); linkerNode.Add(assemblyNode); } linker.Add(linkerNode); AddCustomNodesToLinker(linkerNode, PluginContainer.plugins); PathUtility.CreateDirectoryIfNeeded(BoltCore.Paths.transientGenerated); PathUtility.DeleteProjectFileIfExists(linkerPath, true); // Using ToString instead of Save to omit the declaration, // which doesn't appear in the Unity documentation page for the linker. File.WriteAllText(linkerPath, linker.ToString()); } private static void AddCustomNodesToLinker(XElement linkerNode, IEnumerable plugins) { var types = FindAllCustomTypes(); var customTypes = types.Where(t => !plugins.Any(p => t.Assembly == p.runtimeAssembly)); foreach (var type in customTypes) { var userAssembly = type.Assembly.GetName().Name; var assemblyNode = new XElement("assembly"); var fullnameAttribute = new XAttribute("fullname", userAssembly); var preserveAttribute = new XAttribute("preserve", "all"); assemblyNode.Add(fullnameAttribute); var customType = new XElement("type"); var fullnameType = new XAttribute("fullname", type.FullName); customType.Add(fullnameType); customType.Add(preserveAttribute); assemblyNode.Add(customType); linkerNode.Add(assemblyNode); } } private static void ProcessSubGraphs(HashSet types, SubgraphUnit subgraph) { foreach (var unit in subgraph.nest.graph.units) { AddTypeToHashSet(types, unit); } } private static void AddTypeToHashSet(HashSet types, IUnit unit) { if (unit.GetType() == typeof(SubgraphUnit)) { ProcessSubGraphs(types, (SubgraphUnit)unit); } else { types.Add(unit.GetType()); } } private static HashSet FindGraphsOnAssets() { var scriptGraphAssets = AssetUtility.GetAllAssetsOfType(); var types = new HashSet(); var index = 0; var total = scriptGraphAssets.Count(); foreach (var scriptGraphAsset in scriptGraphAssets) { if (EditorUtility.DisplayCancelableProgressBar($"Processing on assets {index}/{total}", $"Asset {scriptGraphAsset.name}", (float)index / (float)total)) { break; } index++; if (scriptGraphAsset.graph != null) { foreach (var unit in scriptGraphAsset.graph.units) { AddTypeToHashSet(types, unit); } } } EditorUtility.ClearProgressBar(); return types; } private static HashSet FindGraphsOnScenes(bool includeGraphAssets) { var activeScenePath = SceneManager.GetActiveScene().path; var scenePaths = EditorBuildSettings.scenes.Select(s => s.path).ToArray(); var index = 0; var total = scenePaths.Count(); var types = new HashSet(); foreach (var scenePath in scenePaths) { index++; if (EditorUtility.DisplayCancelableProgressBar($"Processing scenes {index} / {total}", $"Scene {scenePath}", (float)index / (float)total)) { break; } if (!string.IsNullOrEmpty(scenePath)) { EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); var scriptMachines = UnityObjectUtility.FindObjectsOfTypeIncludingInactive(); foreach (var scriptMachine in scriptMachines) { if (scriptMachine.nest != null && (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) || scriptMachine.nest.source == GraphSource.Embed) { foreach (var unit in scriptMachine.graph.units) { AddTypeToHashSet(types, unit); } } } } } if (!string.IsNullOrEmpty(activeScenePath)) { EditorSceneManager.OpenScene(activeScenePath); } GC.Collect(); EditorUtility.ClearProgressBar(); return types; } private static HashSet FindGraphsOnPrefabs(bool includeGraphAssets) { var types = new HashSet(); var files = System.IO.Directory.GetFiles(Application.dataPath, "*.prefab", System.IO.SearchOption.AllDirectories); var index = 0; var total = files.Count(); var currentScene = EditorSceneManager.GetActiveScene(); var scenePath = currentScene.path; EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); foreach (var file in files) { index++; if (EditorUtility.DisplayCancelableProgressBar($"Processing prefabs {index} / {total}", $"Prefab {file}", (float)index / (float)total)) { break; } var prefabPath = file.Replace(Application.dataPath, "Assets"); var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) as GameObject; if (prefab != null) { FindGraphInPrefab(types, prefab, includeGraphAssets); prefab = null; EditorUtility.UnloadUnusedAssetsImmediate(true); } } EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); EditorUtility.UnloadUnusedAssetsImmediate(true); GC.Collect(); EditorUtility.ClearProgressBar(); return types; } private static void FindGraphInPrefab(HashSet types, GameObject gameObject, bool includeGraphAssets) { var scriptMachines = gameObject.GetComponents(); foreach (var scriptMachine in scriptMachines) { if (scriptMachine.nest != null && (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) || scriptMachine.nest.source == GraphSource.Embed) { foreach (var unit in scriptMachine.graph.units) { AddTypeToHashSet(types, unit); } } } foreach (Transform child in gameObject.transform) { FindGraphInPrefab(types, child.gameObject, includeGraphAssets); } } private static HashSet FindAllCustomTypes() { var types = new HashSet(); var settings = (List)BoltCore.Configuration.GetMetadata("LinkerPropertyProviderSettings").value; if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets]) { types.AddRange(FindGraphsOnAssets()); } var includeGraphAssets = !settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets]; if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedSceneGraphs]) { types.AddRange(FindGraphsOnScenes(includeGraphAssets)); } if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedPrefabGraphs]) { types.AddRange(FindGraphsOnPrefabs(includeGraphAssets)); } return types; } }