├── Markdown.meta ├── README.md.meta ├── Markdown ├── images.meta └── images │ ├── myregistries.png.meta │ ├── myregistries.png │ ├── dependency-browser.png │ └── dependency-browser.png.meta ├── Editor ├── DependencyBrowser.cs.meta ├── DependencyFolderNode.cs.meta ├── DependencyGuiUtilities.cs.meta ├── DependencyNodeBase.cs.meta ├── Ultraleap.DependencyToolkit.asmdef.meta ├── DependencyNode.cs.meta ├── DependencyTreeEditor.cs.meta ├── DependencyTree.cs.meta ├── Ultraleap.DependencyToolkit.asmdef ├── DependencyFolderNode.cs ├── DependencyNode.cs ├── DependencyNodeBase.cs ├── DependencyGuiUtilities.cs ├── DependencyBrowser.cs ├── DependencyTreeEditor.cs └── DependencyTree.cs ├── ThirdParty ├── LibGit2Sharp │ ├── LibGit2Sharp.dll │ ├── native │ │ ├── win-x64 │ │ │ ├── git2-106a5f2.dll │ │ │ └── git2-106a5f2.dll.meta │ │ └── win-x64.meta │ ├── native.meta │ └── LibGit2Sharp.dll.meta ├── Licenses.md └── LibGit2Sharp.meta ├── LICENSE.md.meta ├── Editor.meta ├── ThirdParty.meta ├── package.json.meta ├── package.json ├── .gitignore ├── README.md └── LICENSE.md /Markdown.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d449627b6ccb4589b4883bc4f1d66719 3 | timeCreated: 1656419450 -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5b9600c421e0408aa3d754859f5bde7d 3 | timeCreated: 1655914512 -------------------------------------------------------------------------------- /Markdown/images.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1e2ac35ab83f4a148e83e9a37b304601 3 | timeCreated: 1656419450 -------------------------------------------------------------------------------- /Editor/DependencyBrowser.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1302df1a5e2445468ce6e9e159d891e0 3 | timeCreated: 1653929211 -------------------------------------------------------------------------------- /Editor/DependencyFolderNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4770344557204de9b9e78b1207befa44 3 | timeCreated: 1644245220 -------------------------------------------------------------------------------- /Editor/DependencyGuiUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 534d1acc71814828a4c9e8e0a03d801d 3 | timeCreated: 1653929316 -------------------------------------------------------------------------------- /Editor/DependencyNodeBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 336732dbea3b48048ea473ef53a3e85a 3 | timeCreated: 1644245208 -------------------------------------------------------------------------------- /Markdown/images/myregistries.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ffb6d8dce2b402482358b2586912fcd 3 | timeCreated: 1656419296 -------------------------------------------------------------------------------- /Markdown/images/myregistries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultraleap/Unity-Dependency-Toolkit/HEAD/Markdown/images/myregistries.png -------------------------------------------------------------------------------- /Markdown/images/dependency-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultraleap/Unity-Dependency-Toolkit/HEAD/Markdown/images/dependency-browser.png -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/LibGit2Sharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultraleap/Unity-Dependency-Toolkit/HEAD/ThirdParty/LibGit2Sharp/LibGit2Sharp.dll -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/native/win-x64/git2-106a5f2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ultraleap/Unity-Dependency-Toolkit/HEAD/ThirdParty/LibGit2Sharp/native/win-x64/git2-106a5f2.dll -------------------------------------------------------------------------------- /ThirdParty/Licenses.md: -------------------------------------------------------------------------------- 1 | # Third Party Licenses 2 | 3 | ## LibGit2Sharp 4 | 5 | - https://github.com/libgit2/libgit2sharp 6 | - MIT Licensed 7 | - Binaries generated from a publish build. -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c9bbffdb82a4ff146b6770c368a83688 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9f31a238b59327469f9c91bee560525 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ThirdParty.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26fd1fd304c6f9a4486cce7a938838fb 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ededaee8ccd1a448ba86e99bf9a565d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Ultraleap.DependencyToolkit.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4665062a459165940bf460e1dba296a1 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/native.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af8b3f74f88344f4ea588d271ebd13ac 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/native/win-x64.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a520c528bbae85a489c2e795cd972169 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 96d527cb6dd37db4593ded35772a9056 3 | importerOverride: UnityEditor.CoreModule:UnityEditor:TextScriptImporter 4 | PackageManifestImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/DependencyNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c06b5184da1234508b7d4943f8168a85 3 | timeCreated: 1523548117 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/DependencyTreeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d72ac177eb4464a98bbdb2244fef254c 3 | timeCreated: 1523606640 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/DependencyTree.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59914db6b65ef4460948c132e0cfd9bf 3 | timeCreated: 1523533505 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {fileID: -5765324120231101433, guid: 0000000000000000d000000000000000, type: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/Ultraleap.DependencyToolkit.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ultraleap.DependencyToolkit", 3 | "rootNamespace": "Leap.Unity.Dependency", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/DependencyFolderNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Leap.Unity.Dependency 5 | { 6 | internal class DependencyFolderNode : DependencyNodeBase 7 | { 8 | public override List Children { get; } = new List(); 9 | public override long GetSize() => Children.Sum(c => c.GetSize()); 10 | public override bool DependsOn(DependencyNodeBase other) => Children.Any(c => c.DependsOnCached(other)); 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.ultraleap.dependencytoolkit", 3 | "version": "1.0.0-pre.2", 4 | "displayName": "Ultraleap Dependency Toolkit", 5 | "description": "Tools for discovering, navigating and analysing asset dependencies.", 6 | "unity": "2020.3", 7 | "license": "Apache-2.0", 8 | "keywords": [ 9 | "ultraleap", 10 | "leap motion" 11 | ], 12 | "author": { 13 | "name": "Ultraleap", 14 | "email": "contact@ultraleap.com", 15 | "url": "https://www.ultraleap.com" 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | 27 | # Unity3D generated meta files 28 | *.pidb.meta 29 | *.pdb.meta 30 | 31 | # Unity3D Generated File On Crash Reports 32 | sysinfo.txt 33 | 34 | # Builds 35 | *.apk 36 | *.unitypackage 37 | -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/LibGit2Sharp.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dded92f6a90d2204f9c8b08dd313b25c 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 1 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | Any: 16 | second: 17 | enabled: 1 18 | settings: {} 19 | - first: 20 | Editor: Editor 21 | second: 22 | enabled: 0 23 | settings: 24 | DefaultValueInitialized: true 25 | - first: 26 | Windows Store Apps: WindowsStoreApps 27 | second: 28 | enabled: 0 29 | settings: 30 | CPU: AnyCPU 31 | userData: 32 | assetBundleName: 33 | assetBundleVariant: 34 | -------------------------------------------------------------------------------- /Editor/DependencyNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace Leap.Unity.Dependency 7 | { 8 | internal class DependencyNode : DependencyNodeBase 9 | { 10 | public enum NodeKind 11 | { 12 | Default, 13 | Builtin, 14 | Missing, 15 | Unknown, 16 | } 17 | 18 | public string Guid; 19 | public long Size; 20 | public NodeKind Kind; 21 | 22 | public List Dependencies { get; } = new List(); 23 | public List Dependants { get; } = new List(); 24 | 25 | public override List Children => Array.Empty().ToList(); 26 | public override long GetSize() => Size; 27 | 28 | public override bool DependsOn(DependencyNodeBase other) 29 | { 30 | foreach (var d in Dependencies) { 31 | if (d == this) { 32 | Debug.Log($"Why does {Name} depend on itself?"); 33 | return false; 34 | } 35 | if (other == d || d.IsMyParent(other)) { 36 | return true; 37 | } 38 | } 39 | 40 | return Dependencies.Any(d => d.DependsOnCached(other)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ThirdParty/LibGit2Sharp/native/win-x64/git2-106a5f2.dll.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 32fb5cea0ba74ce4aa07b3e816a8d22b 3 | PluginImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | iconMap: {} 7 | executionOrder: {} 8 | defineConstraints: [] 9 | isPreloaded: 0 10 | isOverridable: 1 11 | isExplicitlyReferenced: 0 12 | validateReferences: 1 13 | platformData: 14 | - first: 15 | : Any 16 | second: 17 | enabled: 0 18 | settings: 19 | Exclude Android: 1 20 | Exclude Editor: 0 21 | Exclude Linux64: 1 22 | Exclude OSXUniversal: 1 23 | Exclude Win: 1 24 | Exclude Win64: 1 25 | - first: 26 | Android: Android 27 | second: 28 | enabled: 0 29 | settings: 30 | CPU: ARMv7 31 | - first: 32 | Any: 33 | second: 34 | enabled: 0 35 | settings: {} 36 | - first: 37 | Editor: Editor 38 | second: 39 | enabled: 1 40 | settings: 41 | CPU: x86_64 42 | DefaultValueInitialized: true 43 | OS: Windows 44 | - first: 45 | Standalone: Linux64 46 | second: 47 | enabled: 0 48 | settings: 49 | CPU: None 50 | - first: 51 | Standalone: OSXUniversal 52 | second: 53 | enabled: 0 54 | settings: 55 | CPU: None 56 | - first: 57 | Standalone: Win 58 | second: 59 | enabled: 0 60 | settings: 61 | CPU: None 62 | - first: 63 | Standalone: Win64 64 | second: 65 | enabled: 0 66 | settings: 67 | CPU: None 68 | userData: 69 | assetBundleName: 70 | assetBundleVariant: 71 | -------------------------------------------------------------------------------- /Editor/DependencyNodeBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor.PackageManager; 3 | 4 | namespace Leap.Unity.Dependency 5 | { 6 | /// 7 | /// Base class for nodes. 8 | /// Note that all nodes types will do caching based on the assumption that relations and fields are not mutated after 9 | /// creation. 10 | /// 11 | internal abstract class DependencyNodeBase 12 | { 13 | public string Name 14 | { 15 | get => _name; 16 | set 17 | { 18 | _name = value; 19 | _path = null; // Uncache path 20 | } 21 | } 22 | 23 | public string PackageName; 24 | public PackageSource PackageSource; 25 | 26 | public DependencyNodeBase Parent 27 | { 28 | get => _parent; 29 | set 30 | { 31 | _parent = value; 32 | _path = null; // Uncache path 33 | } 34 | } 35 | 36 | public abstract List Children { get; } 37 | public abstract long GetSize(); 38 | 39 | private readonly Dictionary _dependsCache = 40 | new Dictionary(); 41 | 42 | public bool DependsOnCached(DependencyNodeBase other) 43 | { 44 | if (other == null || other == this) return false; 45 | if (!_dependsCache.TryGetValue(other, out bool ret)) { 46 | _dependsCache.Add(other, false); 47 | ret = DependsOn(other); 48 | _dependsCache[other] = ret; 49 | } 50 | return ret; 51 | } 52 | 53 | public abstract bool DependsOn(DependencyNodeBase other); 54 | 55 | public bool IsMyParent(DependencyNodeBase other) => Parent != null && (Parent == other || Parent.IsMyParent(other)); 56 | 57 | public override string ToString() => Name; 58 | 59 | private string _path; 60 | private DependencyNodeBase _parent; 61 | private string _name; 62 | 63 | public string GetPath() 64 | { 65 | if (_path == null) 66 | { 67 | var parentPath = Parent?.GetPath(); 68 | _path = string.IsNullOrEmpty(parentPath) ? Name : $"{parentPath}/{Name}"; 69 | } 70 | 71 | return _path; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Markdown/images/dependency-browser.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85c788f5d3c18ca4698c90ed81b0fff8 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: 1 36 | aniso: 1 37 | mipBias: 0 38 | wrapU: 0 39 | wrapV: 0 40 | wrapW: 0 41 | nPOTScale: 1 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 0 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 0 54 | spriteTessellationDetail: -1 55 | textureType: 0 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | flipbookRows: 1 59 | flipbookColumns: 1 60 | maxTextureSizeSet: 0 61 | compressionQualitySet: 0 62 | textureFormatSet: 0 63 | ignorePngGamma: 0 64 | applyGammaDecoding: 0 65 | platformSettings: 66 | - serializedVersion: 3 67 | buildTarget: DefaultTexturePlatform 68 | maxTextureSize: 2048 69 | resizeAlgorithm: 0 70 | textureFormat: -1 71 | textureCompression: 1 72 | compressionQuality: 50 73 | crunchedCompression: 0 74 | allowsAlphaSplitting: 0 75 | overridden: 0 76 | androidETC2FallbackOverride: 0 77 | forceMaximumCompressionQuality_BC6H_BC7: 0 78 | spriteSheet: 79 | serializedVersion: 2 80 | sprites: [] 81 | outline: [] 82 | physicsShape: [] 83 | bones: [] 84 | spriteID: 85 | internalID: 0 86 | vertices: [] 87 | indices: 88 | edges: [] 89 | weights: [] 90 | secondaryTextures: [] 91 | spritePackingTag: 92 | pSDRemoveMatte: 0 93 | pSDShowRemoveMatteOption: 0 94 | userData: 95 | assetBundleName: 96 | assetBundleVariant: 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Dependency Toolkit 2 | 3 | Tools for discovering, navigating and analysing asset dependencies. 4 | 5 | ## Installing 6 | 7 | 1. In `Edit -> Project Settings -> Package Manager`, add a new scoped registry with the following details. Scoped registry setup only needs to be performed once per Unity project. 8 | 9 | ``` 10 | Name: Ultraleap 11 | URL: https://package.openupm.com 12 | Scope(s): com.ultraleap 13 | ``` 14 | 15 | 2. Open the Package Manager (`Window -> Package Manager`) and navigate to "My Registries" in the dropdown at the top left of the window. 16 | 17 | ![](Markdown/images/myregistries.png) 18 | 19 | 3. The `Ultraleap Dependency Toolkit` package (and other Ultraleap packages) will now be available from the list to install, update or remove. 20 | 21 | ## Dependency Browser 22 | 23 | ![](Markdown/images/dependency-browser.png) 24 | 25 | ### Features 26 | 27 | 1. Discover dependencies and references to an individual asset or folder 28 | 2. Filtering for unused (0 reference) assets 29 | 3. Filtering for missing references along with list of references to a specific missing asset 30 | 4. Lookup missing references in git repo history, recovering names for those found 31 | 5. Aggregate and navigate resource folders from all packages 32 | 6. Displays file sizes with option to sort by size 33 | 7. Supports both packages and the assets directory 34 | 35 | ### Limitations 36 | 37 | 1. Script dependency resolution is limited 38 | 1. Does not find script asset dependencies - they can be viewed through containing .asmdef 39 | 2. No support for dependencies/references between individual script files e.g. class references and such 40 | 2. Will not discover assets in hidden folders e.g. Samples~ in packages. 41 | 3. Missing references detection has some caveats 42 | 1. Does not display number of references missing on a specific asset 43 | 2. May pick up dead guids from an asset file that don't show in the inspector, indicating a non-issue 44 | 4. Will duplicate references everywhere that an asset has been copied by Unity, e.g. in scene or prefab files 45 | 1. For example if asset A references prefab B which in turn references prefab C, asset A will show as referencing prefab C 46 | 47 | ### Usage 48 | 49 | 1. `Assets -> Generate Dependency Tree` or right click context menu `Generate Dependency Tree`. 50 | 2. This will create a `Dependency Tree` asset in the root Assets folder. You can move this anywhere - note that using Generate again will create another asset if it has been moved. 51 | 3. On the inspector of `Dependency Tree`, use the `Refresh` button to scan the project. Use this any time the project has changed to update. 52 | 4. Use `Open Dependency Browser` open and look through the results. 53 | 54 | 55 | ### Known Issues 56 | 57 | - Dependency browser may break if it's open during recompilation. Reload the window to fix. 58 | - Refresh may break the dependency browser if it's open. Reload the window to fix. 59 | -------------------------------------------------------------------------------- /Editor/DependencyGuiUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using LibGit2Sharp; 6 | using UnityEngine; 7 | using Tree = LibGit2Sharp.Tree; 8 | 9 | namespace Leap.Unity.Dependency 10 | { 11 | internal static class DependencyGuiUtilities 12 | { 13 | public static IList FlattenNodes(this DependencyNodeBase node) 14 | where TNode : DependencyNodeBase 15 | { 16 | List list = new List(); 17 | 18 | void AddToList(DependencyNodeBase node) 19 | { 20 | if (node is TNode tNode) list.Add(tNode); 21 | foreach (var child in node.Children) 22 | { 23 | AddToList(child); 24 | } 25 | } 26 | 27 | AddToList(node); 28 | return list; 29 | } 30 | 31 | private static (bool isDependency, bool isDependant)? GetStatus(DependencyNodeBase node, DependencyNodeBase selected) => 32 | selected != null 33 | ? (selected.DependsOnCached(node), node.DependsOnCached(selected)) 34 | : ((bool, bool)?)null; 35 | 36 | public static Color GetColor(DependencyNodeBase node, DependencyNodeBase selected) 37 | { 38 | static Color ModColor(Color color) 39 | { 40 | const float tintLow = 0.6f; 41 | return new Color( 42 | Mathf.Lerp(tintLow, 1f, color.r), 43 | Mathf.Lerp(tintLow, 1f, color.g), 44 | Mathf.Lerp(tintLow, 1f, color.b), 45 | 1f 46 | ); 47 | } 48 | 49 | Color color; 50 | if (selected == null) color = Color.white; // Default color 51 | if (node == selected) color = Color.blue; // Selected 52 | color = GetStatus(node, selected) switch 53 | { 54 | (true, true) => Color.yellow, // Circular dependency 55 | (true, false) => Color.red, // Depends on selected 56 | (false, true) => Color.green, 57 | _ => Color.white // This also handles when GetStatus returns null (nothing selected) 58 | }; 59 | return ModColor(color); 60 | } 61 | 62 | public delegate bool DependencyNodeFilter((DependencyNodeBase node, DependencyNodeBase selected) pair); 63 | 64 | public static DependencyNodeFilter GetFilterPredicate(bool filterToDependencies, bool filterToDependants) => 65 | ((DependencyNodeBase node, DependencyNodeBase selected) t) => 66 | { 67 | // Note the filter function should return true for items that will not be displayed 68 | var nodeStatus = GetStatus(t.node, t.selected); 69 | var cyclicOnly = filterToDependencies && filterToDependants; 70 | if (nodeStatus == null) return false; 71 | if (cyclicOnly) return !(nodeStatus is (true, true)); 72 | if (filterToDependencies) return !(nodeStatus is (true, _)); 73 | if (filterToDependants) return !(nodeStatus is (_, true)); 74 | return false; // display everything 75 | }; 76 | 77 | public static string BytesToString(long byteCount) 78 | { 79 | string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB 80 | if (byteCount == 0) 81 | return "0" + suf[0]; 82 | long bytes = Math.Abs(byteCount); 83 | int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); 84 | double num = Math.Round(bytes / Math.Pow(1024, place), 1); 85 | return (Math.Sign(byteCount) * num) + suf[place]; 86 | } 87 | 88 | public static Dictionary> LookupMissingReferences(IReadOnlyList missingReferences, string repositoryDiscoverRootPath, int commitLimit) 89 | { 90 | if (!(Repository.Discover(repositoryDiscoverRootPath) is { } repoDirectory)) return null; 91 | var repo = new Repository(repoDirectory); 92 | var checkedBlobs = new HashSet(); 93 | var possibleFiles = new Dictionary>(); 94 | 95 | void RecurseTree(Tree commitTree) 96 | { 97 | foreach (var entry in commitTree) 98 | { 99 | switch (entry.Target) 100 | { 101 | case Blob blob: 102 | if (!entry.Name.EndsWith(".meta")) continue; // Only care about meta files 103 | if (blob.IsBinary) continue; // Why is it not text? Log this? 104 | if (!checkedBlobs.Add(blob.Sha)) continue; // Already checked this blob 105 | var filepath = $"{repo.Info.WorkingDirectory}{entry.Path}"; 106 | 107 | var content = blob.GetContentText(); 108 | foreach (var missingRef in missingReferences) 109 | { 110 | if (!content.Contains(missingRef.Guid)) continue; 111 | if (!possibleFiles.TryGetValue(missingRef, out var missingRefPossibleFiles)) 112 | { 113 | missingRefPossibleFiles = possibleFiles[missingRef] = new HashSet(); 114 | } 115 | 116 | missingRefPossibleFiles.Add(filepath); 117 | } 118 | 119 | break; 120 | case Tree t: 121 | RecurseTree(t); 122 | break; 123 | } 124 | } 125 | } 126 | 127 | foreach (var commit in repo.Commits.Take(commitLimit)) 128 | { 129 | RecurseTree(commit.Tree); 130 | } 131 | 132 | return possibleFiles.ToDictionary(pair => pair.Key, pair => pair.Value.ToList()); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Editor/DependencyBrowser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEditor.PackageManager; 6 | using UnityEngine; 7 | 8 | namespace Leap.Unity.Dependency 9 | { 10 | internal class DependencyBrowser : EditorWindow 11 | { 12 | private static DependencyBrowser window; 13 | private DependencyTreeEditor editor; 14 | 15 | // Resizing layout variables 16 | private float resizerSpacing; 17 | private bool resizingHorizontally; 18 | private float currentHorizontalDistance; 19 | private Rect horizontalResizeInteractionRect; 20 | private bool resizingVertically; 21 | private float currentVerticalDistance; 22 | private Rect verticalResizeInteractionRect; 23 | private Texture2D resizerTexture; 24 | 25 | // Browser view state 26 | private Vector2 windowSize; 27 | private Vector2 selectionScrollViewPosition = Vector2.zero; 28 | private List selectionNodesExpanded = new List(); 29 | private Vector2 dependenciesScrollViewPosition = Vector2.zero; 30 | private Vector2 dependantsScrollViewPosition = Vector2.zero; 31 | private bool isViewingCyclicDependencies; 32 | private Dictionary nodesUnusedOrHasUnusedChildren = new Dictionary(); 33 | private Dictionary nodeMissingRefCount = new Dictionary(); 34 | private Dictionary nodesMissingOrWithMissingChildren = new Dictionary(); 35 | 36 | private Func _selectionTreeViewDrawer; 37 | private Func _missingRefTreeViewDrawer; 38 | 39 | private Func _dependencyViewDrawer; 40 | private Func _dependantViewDrawer; 41 | private Func _cyclicDependencyViewDrawer; 42 | 43 | // Selection Options 44 | private string selectionFilterString; 45 | private bool filterToOnlyUnused; 46 | private bool filterToAssetsMissingRefs; 47 | private bool showExternalPackages; 48 | 49 | private int repositoryLookupCommitLimit; 50 | private string repositoryLookupCommitLimitKey = "UltraleapDependencyBrowserGitRepoLookupCommitLimit"; 51 | private string repositoryLookupRoot; 52 | private readonly string repositoryLookupRootKey = "UltraleapDependencyBrowserGitRepoLookupRoot"; 53 | 54 | private event Action resizeOccurred; 55 | 56 | public static void Open(DependencyTreeEditor dependencyTreeEditor) 57 | { 58 | if (window == null) 59 | { 60 | window = (DependencyBrowser)GetWindow(typeof(DependencyBrowser), 61 | true, "Dependency Browser", true); 62 | } 63 | window.editor = dependencyTreeEditor; 64 | window.Show(); 65 | 66 | 67 | ((DependencyTree)window.editor.target).OnRefresh += ClearCaches; 68 | 69 | void ClearCaches() 70 | { 71 | window.nodesUnusedOrHasUnusedChildren.Clear(); 72 | window.nodeMissingRefCount.Clear(); 73 | window.nodesMissingOrWithMissingChildren.Clear(); 74 | } 75 | } 76 | 77 | private void OnEnable() 78 | { 79 | this.position = new Rect(200, 200, 800, 600); 80 | resizerSpacing = 4f; 81 | windowSize = new Vector2(800, 600); 82 | resizerTexture = new Texture2D(1, 1); 83 | resizerTexture.SetPixel(0, 0, Color.black); 84 | resizerTexture.Apply(); 85 | currentHorizontalDistance = windowSize.x / 2f; 86 | currentVerticalDistance = windowSize.y / 2f; 87 | horizontalResizeInteractionRect = new Rect(currentHorizontalDistance, 0f, 8f, windowSize.y); 88 | verticalResizeInteractionRect = new Rect(currentHorizontalDistance + resizerSpacing, currentVerticalDistance, windowSize.x - currentHorizontalDistance - resizerSpacing, 8f); 89 | repositoryLookupRoot = EditorPrefs.GetString(repositoryLookupRootKey, Environment.CurrentDirectory); 90 | repositoryLookupCommitLimit = EditorPrefs.GetInt(repositoryLookupCommitLimitKey, 500); 91 | 92 | void UpdateResizeInteractionRects() 93 | { 94 | horizontalResizeInteractionRect.Set(currentHorizontalDistance, 0f, 8f, windowSize.y); 95 | verticalResizeInteractionRect.Set(currentHorizontalDistance + resizerSpacing, currentVerticalDistance, windowSize.x - currentHorizontalDistance - resizerSpacing, 8f); 96 | } 97 | 98 | resizeOccurred += UpdateResizeInteractionRects; 99 | } 100 | 101 | void OnGUI() 102 | { 103 | CheckForWindowResize(); 104 | GUILayout.BeginHorizontal(); 105 | DrawLeftColumn(); 106 | DrawResizer(ref resizingHorizontally, ref currentHorizontalDistance, ref horizontalResizeInteractionRect, resizeOccurred, resizerSpacing,false, resizerTexture); 107 | DrawRightColumn(); 108 | GUILayout.EndHorizontal(); 109 | Repaint(); 110 | } 111 | 112 | void DrawLeftColumn() 113 | { 114 | selectionScrollViewPosition = GUILayout.BeginScrollView(selectionScrollViewPosition, GUILayout.Height(position.height), GUILayout.Width(currentHorizontalDistance)); 115 | 116 | selectionFilterString = GUILayout.TextField(selectionFilterString); 117 | filterToOnlyUnused = GUILayout.Toggle(filterToOnlyUnused, "Filter to only unused"); 118 | filterToAssetsMissingRefs = GUILayout.Toggle(filterToAssetsMissingRefs, "Filter to assets with missing references"); 119 | showExternalPackages = GUILayout.Toggle(showExternalPackages, "Show external packages"); 120 | editor.DrawSortPopup(); 121 | 122 | repositoryLookupRoot = GUILayout.TextField(repositoryLookupRoot); 123 | EditorPrefs.SetString(repositoryLookupRootKey, repositoryLookupRoot); 124 | GUILayout.BeginHorizontal(); 125 | repositoryLookupCommitLimit = EditorGUILayout.IntField(repositoryLookupCommitLimit); 126 | EditorPrefs.SetInt(repositoryLookupCommitLimitKey, repositoryLookupCommitLimit); 127 | editor.DrawGitLookupMissingRefButton(repositoryLookupRoot, repositoryLookupCommitLimit); 128 | GUILayout.EndHorizontal(); 129 | 130 | GUILayout.Label($"Current selection: {editor.Selected}"); 131 | GUILayout.Label("Select an asset or folder"); 132 | _selectionTreeViewDrawer ??= editor.CreateNodeViewDrawer( 133 | editor.CreateSelectionTreeNodeViewDrawer(selectionNodesExpanded), false, false, 134 | SelectionTreeFilter); 135 | _selectionTreeViewDrawer(); 136 | 137 | GUILayout.EndScrollView(); 138 | } 139 | 140 | private bool SelectionTreeFilter((DependencyNodeBase node, DependencyNodeBase selected) pair) 141 | { 142 | if (filterToOnlyUnused) 143 | { 144 | bool HasUnusedChildrenOrIsUnusedCached(DependencyNodeBase node) 145 | { 146 | if (!nodesUnusedOrHasUnusedChildren.TryGetValue(node, out var unused)) 147 | { 148 | unused = node switch 149 | { 150 | DependencyNode n => n.Dependants.Count < 1, 151 | DependencyFolderNode folderNode => folderNode.Children.Any( 152 | HasUnusedChildrenOrIsUnusedCached), 153 | _ => throw new NotImplementedException($"Missing case for '{node.GetType()}'") 154 | }; 155 | 156 | nodesUnusedOrHasUnusedChildren[node] = unused; 157 | } 158 | 159 | return unused; 160 | } 161 | 162 | if (!HasUnusedChildrenOrIsUnusedCached(pair.node)) 163 | { 164 | return true; 165 | } 166 | } 167 | 168 | if (filterToAssetsMissingRefs) 169 | { 170 | var analysis = ((DependencyTree)editor.target).AnalysisResults; 171 | 172 | int MissingRefCountCached(DependencyNodeBase node) 173 | { 174 | if (!nodeMissingRefCount.TryGetValue(node, out var missingRefCount)) 175 | { 176 | missingRefCount = node switch 177 | { 178 | DependencyNode n => analysis.missingGuidReferences.Sum(guid => n.Dependencies.Count(dep => dep.Guid == guid)), 179 | DependencyFolderNode folderNode => folderNode.Children.Sum(MissingRefCountCached), 180 | _ => throw new NotImplementedException($"Missing case for '{node.GetType()}'") 181 | }; 182 | 183 | nodeMissingRefCount[node] = missingRefCount; 184 | } 185 | 186 | return missingRefCount; 187 | } 188 | 189 | bool IsMissingOrHasMissingChildrenCached(DependencyNodeBase node) 190 | { 191 | if (!nodesMissingOrWithMissingChildren.TryGetValue(node, out var missing)) 192 | { 193 | missing = node switch 194 | { 195 | DependencyNode n => analysis.missingGuidReferences.Contains(n.Guid), 196 | DependencyFolderNode folderNode => folderNode.Children.Any( 197 | IsMissingOrHasMissingChildrenCached), 198 | _ => throw new NotImplementedException($"Missing case for '{node.GetType()}'") 199 | }; 200 | } 201 | 202 | nodesMissingOrWithMissingChildren[node] = missing; 203 | return missing; 204 | } 205 | 206 | // Filters everything that is not itself a missing asset or has missing asset references 207 | if (MissingRefCountCached(pair.node) < 1 && !IsMissingOrHasMissingChildrenCached(pair.node)) 208 | { 209 | return true; 210 | } 211 | } 212 | 213 | if (!string.IsNullOrEmpty(selectionFilterString) && !pair.node.GetPath().Contains(selectionFilterString)) 214 | { 215 | return true; 216 | } 217 | 218 | if (!showExternalPackages && pair.node.PackageSource switch 219 | { 220 | // Only mutable package sources are considered non-external for filtering 221 | PackageSource.Embedded => false, 222 | PackageSource.Local => false, 223 | PackageSource.Unknown => false, // Don't filter when we don't know 224 | _ => true 225 | }) 226 | { 227 | return true; 228 | } 229 | 230 | return false; 231 | } 232 | 233 | void DrawDependencyAndDependantViews() 234 | { 235 | GUILayout.Label("Dependencies view - assets the selection references"); 236 | dependenciesScrollViewPosition = GUILayout.BeginScrollView(dependenciesScrollViewPosition, GUILayout.Height(currentVerticalDistance - EditorGUIUtility.singleLineHeight), GUILayout.Width(windowSize.x - currentHorizontalDistance - resizerSpacing)); 237 | _dependencyViewDrawer ??= editor.CreateNodeViewDrawer(editor.CreateListNodeViewDrawer(), true, false, 238 | guiFuncWhenNoSelection: () => GUILayout.Label("Select an asset to view dependencies.")); 239 | _dependencyViewDrawer(); 240 | GUILayout.EndScrollView(); 241 | 242 | DrawResizer(ref resizingVertically, ref currentVerticalDistance, ref verticalResizeInteractionRect, resizeOccurred, resizerSpacing, true, resizerTexture); 243 | 244 | GUILayout.BeginHorizontal(); 245 | GUILayout.Label("Dependants view - assets that reference the selection"); 246 | GUILayout.FlexibleSpace(); 247 | isViewingCyclicDependencies = GUILayout.Toggle(isViewingCyclicDependencies, new GUIContent("Filter to Cyclic Dependencies", "Show dependants that are also dependencies, these assets are tightly coupled to the selection.")); 248 | GUILayout.EndHorizontal(); 249 | 250 | dependantsScrollViewPosition = GUILayout.BeginScrollView(dependantsScrollViewPosition, GUILayout.Height(this.position.height - currentVerticalDistance - resizerSpacing), GUILayout.Width(windowSize.x - currentHorizontalDistance - resizerSpacing)); 251 | 252 | if (isViewingCyclicDependencies) 253 | { 254 | _cyclicDependencyViewDrawer ??= editor.CreateNodeViewDrawer(editor.CreateListNodeViewDrawer(), true, true, 255 | guiFuncWhenNoSelection: () => GUILayout.Label("Select an asset to view cyclic dependencies.")); 256 | _cyclicDependencyViewDrawer(); 257 | } 258 | else 259 | { 260 | _dependantViewDrawer ??= editor.CreateNodeViewDrawer(editor.CreateListNodeViewDrawer(), false, true, 261 | guiFuncWhenNoSelection: () => GUILayout.Label("Select an asset to view dependants.")); 262 | _dependantViewDrawer(); 263 | } 264 | 265 | GUILayout.EndScrollView(); 266 | } 267 | 268 | void DrawRightColumn() 269 | { 270 | GUILayout.BeginVertical(); 271 | DrawDependencyAndDependantViews(); 272 | GUILayout.EndVertical(); 273 | } 274 | 275 | private void CheckForWindowResize() 276 | { 277 | var size = new Vector2(this.position.width, this.position.height); 278 | if (size == windowSize) return; 279 | windowSize = size; 280 | resizeOccurred?.Invoke(); 281 | } 282 | 283 | private static void DrawResizer(ref bool resizing, ref float currentDistance, ref Rect cursorInteractionRect, Action resize, float resizerSpacing, bool isVerticalResizer, Texture2D resizerTexture) 284 | { 285 | var textureRect = new Rect(cursorInteractionRect); 286 | 287 | if (isVerticalResizer) 288 | { 289 | textureRect.height = cursorInteractionRect.height / 2f; 290 | textureRect.y += textureRect.height / 2f; 291 | } 292 | else 293 | { 294 | textureRect.width = cursorInteractionRect.width / 2f; 295 | textureRect.x += textureRect.width / 2f; 296 | } 297 | 298 | GUI.DrawTexture(textureRect, resizerTexture); 299 | EditorGUIUtility.AddCursorRect(cursorInteractionRect, isVerticalResizer ? MouseCursor.ResizeVertical : MouseCursor.ResizeHorizontal); 300 | 301 | if (Event.current.type == EventType.MouseDown && cursorInteractionRect.Contains(Event.current.mousePosition)) 302 | resizing = true; 303 | if (resizing) 304 | { 305 | if (isVerticalResizer) 306 | { 307 | currentDistance = Event.current.mousePosition.y; 308 | cursorInteractionRect.Set(cursorInteractionRect.x, currentDistance, cursorInteractionRect.width, cursorInteractionRect.height); 309 | } 310 | else 311 | { 312 | currentDistance = Event.current.mousePosition.x; 313 | cursorInteractionRect.Set(currentDistance, cursorInteractionRect.y, cursorInteractionRect.width, cursorInteractionRect.height); 314 | } 315 | resize?.Invoke(); 316 | } 317 | if (Event.current.type == EventType.MouseUp) 318 | resizing = false; 319 | 320 | GUILayout.Space(resizerSpacing); 321 | } 322 | } 323 | } -------------------------------------------------------------------------------- /Editor/DependencyTreeEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace Leap.Unity.Dependency 8 | { 9 | [CustomEditor(typeof(DependencyTree))] 10 | internal class DependencyTreeEditor : Editor 11 | { 12 | public enum SortMode 13 | { 14 | Directory, 15 | Name, 16 | Size 17 | } 18 | 19 | public SortMode Sort = SortMode.Directory; 20 | public string Selected; 21 | 22 | private SerializedProperty _knownBuiltinAssetFilePaths; 23 | 24 | private DependencyFolderNode _specialResourcesNode; 25 | private DependencyFolderNode _missingReferencesNode; 26 | private DependencyFolderNode _root; // Root is the project root folder, it has other project folders as children 27 | 28 | private const string SpecialResourcesNodeName = "[All Resources]"; 29 | private const string MissingReferencesNodeName = "[Missing references]"; 30 | 31 | public delegate bool DrawNodeViewFunc(IList nodes, Predicate filter); 32 | 33 | private void OnEnable() 34 | { 35 | _knownBuiltinAssetFilePaths = serializedObject.FindProperty(nameof(DependencyTree.KnownBuiltinAssetFilePaths)); 36 | } 37 | 38 | public override void OnInspectorGUI() 39 | { 40 | serializedObject.Update(); 41 | EditorGUILayout.PropertyField(_knownBuiltinAssetFilePaths); 42 | serializedObject.ApplyModifiedProperties(); 43 | 44 | var dependencyTree = ((DependencyTree)target); 45 | var rootNode = dependencyTree.GetRootNode(); 46 | if (_specialResourcesNode == null || _missingReferencesNode == null || _root != rootNode && rootNode != null) 47 | { 48 | _root = rootNode; 49 | _specialResourcesNode = new DependencyFolderNode 50 | { 51 | Parent = _root, 52 | Name = SpecialResourcesNodeName 53 | }; 54 | 55 | _specialResourcesNode.Children.AddRange( 56 | rootNode.FlattenNodes() 57 | .Where(f => f.Name == "Resources") 58 | .Select(n => { 59 | var newFolderNode = new DependencyFolderNode{ 60 | Name = n.GetPath(), 61 | Parent = _specialResourcesNode // New parent will be included in the path after assignment 62 | }; 63 | newFolderNode.Children.AddRange(n.Children); 64 | foreach (var child in n.Children) { 65 | child.Parent = newFolderNode; 66 | } 67 | return newFolderNode; 68 | }) 69 | ); 70 | 71 | _missingReferencesNode = new DependencyFolderNode { Parent = _root, Name = MissingReferencesNodeName }; 72 | foreach (var missingGuid in dependencyTree.AnalysisResults.missingGuidReferences) 73 | { 74 | var missingRefNode = dependencyTree.NodesByGuid[missingGuid]; 75 | missingRefNode.Parent = _missingReferencesNode; // New parent will be included in the path after assignment 76 | _missingReferencesNode.Children.Add(missingRefNode); 77 | } 78 | } 79 | 80 | DrawRefreshButton(); 81 | 82 | if (GUILayout.Button("Open Dependency Browser", GUILayout.MaxWidth(300f), GUILayout.Height(32f))) 83 | { 84 | DependencyBrowser.Open(this); 85 | } 86 | EditorUtility.SetDirty(this); 87 | } 88 | 89 | private DependencyNodeBase GetNodeFromPath(string nodePath) 90 | { 91 | if (string.IsNullOrEmpty(nodePath)) return null; 92 | 93 | static DependencyNodeBase GetNamedChild(DependencyNodeBase node, string name) => node.Children.FirstOrDefault(o => o.Name == name); 94 | 95 | static DependencyNodeBase LookupNodeFromPath(DependencyNodeBase root, string path) 96 | { 97 | DependencyNodeBase current = root; 98 | foreach (var pathSegment in path.Split(new []{'/'}, StringSplitOptions.RemoveEmptyEntries)) 99 | { 100 | current = GetNamedChild(current, pathSegment); 101 | if (current == null) break; // Didn't find a named child, result will be null 102 | } 103 | 104 | return current; 105 | } 106 | 107 | if (nodePath.StartsWith(SpecialResourcesNodeName)) 108 | { 109 | var pathSplitAtResources = nodePath.Substring(SpecialResourcesNodeName.Length+1).Split(new [] { "Resources" }, 2, StringSplitOptions.RemoveEmptyEntries); 110 | var root = _specialResourcesNode.Children.FirstOrDefault(c => c.Name == $"{pathSplitAtResources[0]}Resources"); 111 | return LookupNodeFromPath(root, pathSplitAtResources[1]); 112 | } else if (nodePath.StartsWith(MissingReferencesNodeName)) { 113 | return LookupNodeFromPath(_missingReferencesNode, nodePath.Substring(MissingReferencesNodeName.Length)); 114 | } else { 115 | return LookupNodeFromPath(((DependencyTree)target).GetRootNode(), nodePath); 116 | } 117 | } 118 | 119 | public void DrawRefreshButton() 120 | { 121 | if (GUILayout.Button("Refresh (2-3 minutes)", GUILayout.MaxWidth(300f), GUILayout.Height(32f))) 122 | { 123 | ((DependencyTree)target).Refresh(); 124 | } 125 | } 126 | 127 | public void DrawSortPopup() => Sort = (SortMode)EditorGUILayout.EnumPopup("Sort by", Sort); 128 | 129 | public Func CreateNodeViewDrawer(DrawNodeViewFunc drawNodeViewFunc, bool filterToDependencies, bool filterToDependants, DependencyGuiUtilities.DependencyNodeFilter extraFilter = null, Action guiFuncWhenNoSelection = null) => 130 | () => 131 | { 132 | var nodeRelationFilter = DependencyGuiUtilities.GetFilterPredicate(filterToDependencies, filterToDependants); 133 | 134 | // Combine the relation filter with the extra filter 135 | bool Filter((DependencyNodeBase node, DependencyNodeBase selected) pair) => 136 | nodeRelationFilter(pair) 137 | || (extraFilter?.Invoke(pair) ?? 138 | false); // Use extra filter if non-null else do no filtering with constant false 139 | 140 | var selectedNode = GetNodeFromPath(Selected); 141 | if (selectedNode == null && guiFuncWhenNoSelection != null) 142 | { 143 | guiFuncWhenNoSelection.Invoke(); 144 | return false; 145 | } 146 | 147 | bool ShouldFilter(DependencyNodeBase node) => Filter((node, selectedNode)); 148 | 149 | return drawNodeViewFunc(new[] { _specialResourcesNode, _missingReferencesNode }.Concat(_root?.Children ?? Enumerable.Empty()).ToArray(), 150 | ShouldFilter); 151 | }; 152 | 153 | private static bool DrawToggle(string nodePath, ref string selectedNodePath, DependencyNodeBase node, 154 | ref DependencyNodeBase selectedNode) 155 | { 156 | if (!EditorGUILayout.Toggle(selectedNodePath == nodePath, EditorStyles.radioButton, 157 | GUILayout.Width(20f)) || selectedNodePath == nodePath) 158 | { 159 | return false; 160 | } 161 | 162 | selectedNodePath = nodePath; 163 | selectedNode = node; 164 | return true; 165 | } 166 | 167 | public DrawNodeViewFunc CreateListNodeViewDrawer() 168 | { 169 | DependencyNodeBase previousSelectedNode = null; 170 | IEnumerable nodesCached = null; 171 | SortMode previousSortMode = Sort; 172 | 173 | return DrawNodeViewFunc; 174 | 175 | bool DrawNodeViewFunc(IList nodes, Predicate filter) 176 | { 177 | var selectedNode = GetNodeFromPath(Selected); 178 | var selectionChanged = previousSelectedNode != selectedNode; 179 | 180 | void RefreshCache() 181 | { 182 | previousSortMode = Sort; 183 | previousSelectedNode = selectedNode; 184 | 185 | var flattened = nodes.SelectMany(n => n.FlattenNodes()); 186 | nodesCached = (Sort switch 187 | { 188 | SortMode.Name => flattened.OrderBy(c => c.Name), 189 | SortMode.Size => flattened.OrderByDescending(c => c.GetSize()), 190 | SortMode.Directory => flattened, 191 | _ => Enumerable.Empty() 192 | }).OfType().ToArray(); // only display leaf nodes, force enumeration now with ToArray to avoid repeated enumeration later 193 | } 194 | 195 | if (nodesCached == null || selectionChanged || previousSortMode != Sort) 196 | { 197 | RefreshCache(); 198 | } 199 | 200 | foreach (var node in nodesCached) 201 | { 202 | if (filter(node)) continue; 203 | EditorGUILayout.BeginHorizontal(); 204 | 205 | var nodePath = node.GetPath(); 206 | 207 | if (DrawToggle(nodePath, ref Selected, node, ref selectedNode)) 208 | { 209 | selectionChanged = true; 210 | RefreshCache(); 211 | } 212 | 213 | if (GUILayout.Button("Go To", GUILayout.Width(50f))) 214 | { 215 | var objectToSelect = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(node.Guid)); 216 | Selection.activeObject = objectToSelect; 217 | EditorGUIUtility.PingObject(objectToSelect); 218 | } 219 | 220 | var nodeText = $"{node.GetPath()} ({DependencyGuiUtilities.BytesToString(node.GetSize())}) {($"[{node.Dependants.Count} refs, {node.Dependencies.Count} deps]")}"; 221 | 222 | GUI.color = DependencyGuiUtilities.GetColor(node, selectedNode); 223 | EditorGUILayout.LabelField(nodeText); 224 | GUI.color = Color.white; 225 | 226 | EditorGUILayout.EndHorizontal(); 227 | } 228 | 229 | return selectionChanged; 230 | } 231 | } 232 | 233 | public DrawNodeViewFunc CreateSelectionTreeNodeViewDrawer(List expandedNodes) 234 | { 235 | int indent = 0; 236 | DependencyNodeBase selectedNode; 237 | 238 | return DrawNodeViewFunc; 239 | 240 | bool DrawNodeViewFunc(IList nodes, Predicate filter) 241 | { 242 | selectedNode = GetNodeFromPath(Selected); 243 | bool selectionChanged = false; 244 | foreach (var node in nodes) 245 | { 246 | selectionChanged |= DrawNode(node, filter); 247 | } 248 | 249 | return selectionChanged; 250 | } 251 | 252 | bool DrawNode(DependencyNodeBase node, Predicate filter) 253 | { 254 | var nodePath = node.GetPath(); 255 | bool selectionChanged = false; 256 | 257 | EditorGUILayout.BeginHorizontal(); 258 | 259 | if (DrawToggle(nodePath, ref Selected, node, ref selectedNode)) 260 | { 261 | selectionChanged = true; 262 | } 263 | 264 | var dependencyNode = node as DependencyNode; 265 | 266 | GUILayout.Space(4 + (12 * indent)); 267 | var nodeText = $"{node.Name} ({DependencyGuiUtilities.BytesToString(node.GetSize())}){(dependencyNode != null ? $" [{dependencyNode.Dependants.Count} refs, {dependencyNode.Dependencies.Count} deps]" : string.Empty)}"; 268 | 269 | void DrawGoToButton() 270 | { 271 | if (dependencyNode != null && GUILayout.Button("Go To", GUILayout.Width(50f))) 272 | { 273 | var objectToSelect = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(dependencyNode.Guid)); 274 | Selection.activeObject = objectToSelect; 275 | EditorGUIUtility.PingObject(objectToSelect); 276 | } 277 | } 278 | 279 | IEnumerable children = Enumerable.Empty(); 280 | if (node is DependencyFolderNode folderNode) 281 | { 282 | var wasFoldedOut = expandedNodes.Contains(nodePath); 283 | GUI.color = DependencyGuiUtilities.GetColor(folderNode, selectedNode); 284 | var foldedOut = EditorGUILayout.Foldout(wasFoldedOut, nodeText); 285 | GUI.color = Color.white; 286 | if (foldedOut && !wasFoldedOut) 287 | { 288 | expandedNodes.Add(nodePath); 289 | } 290 | else if (!foldedOut && wasFoldedOut) 291 | { 292 | expandedNodes.Remove(nodePath); 293 | } 294 | 295 | if (foldedOut) 296 | { 297 | children = folderNode.Children; 298 | } 299 | DrawGoToButton(); 300 | EditorGUILayout.EndHorizontal(); 301 | } 302 | else if (dependencyNode is { Kind: DependencyNode.NodeKind.Missing }) 303 | { 304 | GUI.color = DependencyGuiUtilities.GetColor(node, selectedNode); 305 | EditorGUILayout.LabelField(nodeText); 306 | DrawGoToButton(); 307 | EditorGUILayout.EndHorizontal(); 308 | var missingRefDetails = ((DependencyTree)target).MissingReferenceLookups.FirstOrDefault(lookup => lookup.guid == dependencyNode.Guid); 309 | foreach (var foundFile in missingRefDetails?.foundFiles ?? Enumerable.Empty()) 310 | { 311 | EditorGUILayout.BeginHorizontal(); 312 | GUILayout.Space(20 + 4 + (12 * indent)); 313 | EditorGUILayout.LabelField(foundFile); 314 | EditorGUILayout.EndHorizontal(); 315 | } 316 | GUI.color = Color.white; 317 | } 318 | else 319 | { 320 | GUI.color = DependencyGuiUtilities.GetColor(node, selectedNode); 321 | EditorGUILayout.LabelField(nodeText); 322 | GUI.color = Color.white; 323 | DrawGoToButton(); 324 | EditorGUILayout.EndHorizontal(); 325 | } 326 | 327 | foreach (var child in (Sort switch 328 | { 329 | SortMode.Name => children.OrderBy(c => c.Name), 330 | SortMode.Size => children.OrderByDescending(c => c.GetSize()), 331 | SortMode.Directory => children, 332 | _ => Enumerable.Empty() 333 | })) 334 | { 335 | if (filter(child)) continue; 336 | indent++; 337 | selectionChanged |= DrawNode(child, filter); 338 | indent--; 339 | } 340 | 341 | return selectionChanged; 342 | } 343 | } 344 | 345 | public void DrawGitLookupMissingRefButton(string lookupRoot, int commitLimit) 346 | { 347 | if (GUILayout.Button("Git Lookup Missing Refs")) 348 | { 349 | var missingRefNodes = _missingReferencesNode.Children.Cast().ToArray(); 350 | var lookup = DependencyGuiUtilities.LookupMissingReferences(missingRefNodes, lookupRoot, commitLimit); 351 | ((DependencyTree)target).MissingReferenceLookups = lookup.Select(pair => new DependencyTree.MissingReferenceLookup 352 | { 353 | guid = pair.Key.Guid, 354 | foundFiles = pair.Value 355 | }).ToList(); 356 | } 357 | } 358 | } 359 | } -------------------------------------------------------------------------------- /Editor/DependencyTree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using UnityEditor; 7 | using UnityEditor.Compilation; 8 | using UnityEditor.PackageManager; 9 | using UnityEngine; 10 | 11 | namespace Leap.Unity.Dependency 12 | { 13 | internal class DependencyTree : ScriptableObject 14 | { 15 | [Serializable] 16 | public class AssetReference 17 | { 18 | public string guid; 19 | public string path; 20 | public long size; 21 | public List references = new List(); 22 | public List referencedBy = new List(); 23 | public string packageName; 24 | public PackageSource packageSource; 25 | public string asmdefName; 26 | } 27 | 28 | [Serializable] 29 | public class StaticAnalysisResults 30 | { 31 | public List assetsWithoutGuids = new List(); 32 | public List duplicatedGuids = new List(); 33 | public List missingGuidReferences = new List(); 34 | } 35 | 36 | [Serializable] 37 | public class MissingReferenceLookup 38 | { 39 | public string guid; 40 | public List foundFiles; 41 | } 42 | 43 | /// 44 | /// This class is used to bundle up information required for tree generation 45 | /// 46 | private class GenerationContext 47 | { 48 | public PackageCollection PackageCollection; 49 | public List KnownBuiltinAssetFilePaths; 50 | 51 | public GenerationContext(PackageCollection packageCollection, List knownBuiltinAssetFilePaths) 52 | { 53 | PackageCollection = packageCollection; 54 | KnownBuiltinAssetFilePaths = knownBuiltinAssetFilePaths; 55 | } 56 | } 57 | 58 | public event Action OnRefresh; 59 | 60 | private static readonly HashSet YamlFileExtensions 61 | = new HashSet(StringComparer.OrdinalIgnoreCase) 62 | { 63 | ".asset", 64 | ".unity", 65 | ".prefab", 66 | ".mat", 67 | ".controller", 68 | ".anim", 69 | ".asmdef" 70 | }; 71 | 72 | // GUID format prefix label is slightly different in asmdef files 73 | private static readonly Regex GuidRegex = new Regex("(?:GUID:|guid: )([0-9a-f]{32})", 74 | RegexOptions.CultureInvariant); 75 | 76 | private static (bool Success, List AssetReferences, StaticAnalysisResults AnalysisResults) GenerateTree(GenerationContext generationContext) 77 | { 78 | var intermediateGuidsList = new List(); 79 | var filesMissingMetas = new List(); 80 | var assetsByGuid = new Dictionary(); 81 | var analysis = new StaticAnalysisResults(); 82 | var packages = generationContext.PackageCollection.ToArray(); 83 | 84 | static void UpdateAssetReferenceFromFilePath(AssetReference assetReference, string filePath, GenerationContext generationContext) 85 | { 86 | var isBuiltinAsset = generationContext.KnownBuiltinAssetFilePaths.Any(path => path.Equals(filePath)); 87 | 88 | assetReference.path = filePath; 89 | 90 | if (!isBuiltinAsset) 91 | { 92 | try 93 | { 94 | assetReference.size = new FileInfo(filePath).Length; 95 | } 96 | catch (Exception e) 97 | { 98 | Debug.LogError($"Skipping file due to exception. See exception trace in following message."); 99 | Debug.LogException(e); 100 | } 101 | 102 | var package = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(filePath); 103 | if (package != null) 104 | { 105 | assetReference.packageName = package.name; 106 | assetReference.packageSource = package.source; 107 | } 108 | assetReference.asmdefName = CompilationPipeline.GetAssemblyNameFromScriptPath(filePath); 109 | } 110 | else 111 | { 112 | assetReference.packageName = ""; 113 | } 114 | } 115 | 116 | void ParseDirectory(string directoryPath) 117 | { 118 | // Adds all GUIDs referenced by the asset to the list. 119 | // The first added element is always its own GUID. 120 | // Returns whether or not the GUID of this asset was found and is first in the references list 121 | static bool ParseAssetFile(string path, List references, ICollection missingMetaFiles) 122 | { 123 | // Add GUIDs in a file to the references list 124 | static void ParseFileContent(string content, List references) => 125 | references.AddRange(GuidRegex.Matches(content) 126 | .Cast() 127 | .Select(match => match.Groups[1].Value)); 128 | 129 | var metaFile = $"{path}.meta"; 130 | var guidFound = true; 131 | if (File.Exists(metaFile)) { 132 | ParseFileContent(File.ReadAllText(metaFile), references); 133 | } 134 | else 135 | { 136 | missingMetaFiles.Add(path); 137 | guidFound = false; 138 | // We don't return here on this error case, if it's a Yaml file there is still useful data within 139 | } 140 | if (YamlFileExtensions.Contains(Path.GetExtension(path) ?? string.Empty)) { 141 | ParseFileContent(File.ReadAllText(path), references); 142 | } 143 | 144 | return guidFound; 145 | } 146 | 147 | var directoryAssets = AssetDatabase.FindAssets("", new []{directoryPath}) 148 | .Select(AssetDatabase.GUIDToAssetPath) 149 | .Where(p => !Directory.Exists(p)) // Exclude folders for now, Unity treats them as Assets but we don't care about them 150 | .ToArray(); 151 | 152 | foreach (var filePath in directoryAssets) 153 | { 154 | if (filePath.Equals(DependencyTreePath)) continue; // Don't parse the dependency tree itself as it will be cyclic 155 | var ext = Path.GetExtension(filePath); 156 | if (ext.Equals(".meta", StringComparison.OrdinalIgnoreCase)) { 157 | // Skip .meta files on their own; we'll find them later 158 | continue; 159 | } 160 | 161 | intermediateGuidsList.Clear(); 162 | 163 | string thisAssetsGuid = ParseAssetFile(filePath, intermediateGuidsList, filesMissingMetas) 164 | ? intermediateGuidsList[0] 165 | : null; 166 | 167 | // Add every new guid to the map 168 | foreach (var guid in intermediateGuidsList) { 169 | if (!assetsByGuid.ContainsKey(guid)) { 170 | assetsByGuid.Add(guid, new AssetReference{guid = guid, packageName = ""}); 171 | } 172 | } 173 | 174 | // Get this asset to fill in the details 175 | // It might already be in the map if some other asset references it and was processed first 176 | AssetReference thisAsset; 177 | if (thisAssetsGuid == null) 178 | { 179 | // If there's no discovered guid it won't be in the map at all, so add it to the list of assets without guids 180 | thisAsset = new AssetReference { guid = string.Empty }; 181 | analysis.assetsWithoutGuids.Add(filePath); 182 | } 183 | else 184 | { 185 | thisAsset = assetsByGuid[thisAssetsGuid]; 186 | } 187 | 188 | if (!string.IsNullOrEmpty(thisAsset.path) && !thisAsset.path.Equals(filePath)) analysis.duplicatedGuids.Add(thisAsset.guid); 189 | thisAsset.references.AddRange(intermediateGuidsList.Skip(1).Distinct()); // TODO: Count references instead of throwing away count? 190 | foreach (var referencedAsset in intermediateGuidsList.Skip(1).Select(guid => assetsByGuid[guid])) 191 | { 192 | referencedAsset.referencedBy.Add(thisAsset.guid); 193 | } 194 | 195 | UpdateAssetReferenceFromFilePath(thisAsset, filePath, generationContext); 196 | } 197 | } 198 | 199 | var directoryCount = packages.Length + 1; // Packages + Assets 200 | 201 | if (EditorUtility.DisplayCancelableProgressBar("Parsing Assets directory", $"(1/{directoryCount}): Assets", (float)0 / directoryCount)) 202 | { 203 | EditorUtility.ClearProgressBar(); 204 | return default; 205 | } 206 | 207 | ParseDirectory("Assets"); 208 | 209 | for (var i = 0; i < packages.Length; i++) 210 | { 211 | var directoryIdx = i + 2; // +1 for indexing from 0, +1 for including assets directory 212 | var package = packages[i]; 213 | if (EditorUtility.DisplayCancelableProgressBar("Parsing all packages", $"({directoryIdx}/{directoryCount}): {package.name}", (float)i+1.0f / directoryCount)) 214 | { 215 | EditorUtility.ClearProgressBar(); 216 | return default; 217 | } 218 | 219 | ParseDirectory(package.assetPath); 220 | } 221 | 222 | EditorUtility.ClearProgressBar(); 223 | 224 | // Fill in details for assets that were not in the directories we scanned 225 | foreach (var tuple in assetsByGuid) 226 | { 227 | var (guid, assetRef) = (tuple.Key, tuple.Value); 228 | if (!string.IsNullOrEmpty(assetRef.path)) continue; 229 | var assetPath = AssetDatabase.GUIDToAssetPath(guid); 230 | if (string.IsNullOrEmpty(assetPath)) analysis.missingGuidReferences.Add(guid); 231 | else UpdateAssetReferenceFromFilePath(assetRef, assetPath, generationContext); 232 | } 233 | 234 | var allAssetReferences = assetsByGuid.Values.ToList(); 235 | 236 | return (true, allAssetReferences, analysis); 237 | } 238 | 239 | private static (DependencyFolderNode, Dictionary)? BuildFolderTree(List leafAssetReferences, StaticAnalysisResults analysis, List knownBuiltinAssets) 240 | { 241 | var sep = new char[]{'\\', '/'}; 242 | var root = new DependencyFolderNode(); 243 | var guidMap = new Dictionary(); 244 | 245 | if (leafAssetReferences == null) return null; 246 | // Build folder structure 247 | foreach (var asset in leafAssetReferences) 248 | { 249 | string nodeName = asset.guid; // Use guid by default, not guaranteed we'll know the real name 250 | DependencyNode.NodeKind nodeKind = DependencyNode.NodeKind.Default; 251 | DependencyFolderNode parent = null; 252 | 253 | var isMissingAsset = analysis.missingGuidReferences.Contains(asset.guid); 254 | var isBuiltinAsset = knownBuiltinAssets.Any(path => path.Equals(asset.path)); 255 | 256 | switch (isMissingAsset, isBuiltinAsset) 257 | { 258 | case (true, false): 259 | nodeKind = DependencyNode.NodeKind.Missing; 260 | break; 261 | case (false, true): 262 | nodeKind = DependencyNode.NodeKind.Builtin; 263 | break; 264 | case (true, true): 265 | Debug.LogWarning($"Unknown asset kind: {asset.guid}"); 266 | nodeKind = DependencyNode.NodeKind.Unknown; 267 | break; 268 | } 269 | 270 | if (!string.IsNullOrEmpty(asset.path)) 271 | { 272 | // Go through and add in folder nodes that are missing to reach this node 273 | var path = asset.path.Split(sep, StringSplitOptions.RemoveEmptyEntries); 274 | parent = root; 275 | for (int i = 0; i < (path.Length - 1); i++) 276 | { 277 | var childFolderNode = parent.Children.FirstOrDefault(c => c.Name == path[i]) as DependencyFolderNode; 278 | if (childFolderNode == null) { 279 | childFolderNode = new DependencyFolderNode 280 | { 281 | Name = path[i], 282 | PackageName = asset.packageName, 283 | PackageSource = asset.packageSource, 284 | Parent = parent 285 | }; 286 | parent.Children.Add(childFolderNode); 287 | } 288 | parent = childFolderNode; 289 | } 290 | 291 | nodeName = path[path.Length - 1]; 292 | } 293 | 294 | var leafNode = new DependencyNode 295 | { 296 | Name = nodeName, 297 | PackageName = asset.packageName, 298 | PackageSource = asset.packageSource, 299 | Guid = asset.guid, 300 | Size = asset.size, 301 | Kind = nodeKind, 302 | Parent = parent 303 | }; 304 | 305 | guidMap.Add(asset.guid, leafNode); 306 | parent?.Children.Add(leafNode); 307 | } 308 | 309 | // Add dependencies 310 | foreach (var asset in leafAssetReferences) 311 | { 312 | var assetNode = guidMap[asset.guid]; 313 | foreach (var guid in asset.references) 314 | { 315 | if (guidMap.TryGetValue(guid, out DependencyNode dependencyNode)) { 316 | assetNode.Dependencies.Add(dependencyNode); 317 | } 318 | } 319 | 320 | foreach (var guid in asset.referencedBy) 321 | { 322 | if (guidMap.TryGetValue(guid, out DependencyNode dependantNode)) { 323 | assetNode.Dependants.Add(dependantNode); 324 | } 325 | } 326 | } 327 | return (root, guidMap); 328 | } 329 | 330 | public List KnownBuiltinAssetFilePaths = new List 331 | { 332 | "Resources/unity_builtin_extra", 333 | "Library/unity default resources", 334 | "Library/unity editor resources" 335 | }; 336 | public List RawTree = new List(); 337 | public StaticAnalysisResults AnalysisResults; 338 | public List MissingReferenceLookups = new List(); 339 | [NonSerialized] public DependencyFolderNode RootNode; 340 | [NonSerialized] public Dictionary NodesByGuid; 341 | 342 | public void Refresh() 343 | { 344 | // TODO: Only rebuild what's changed instead of everything 345 | var listRequest = Client.List(); 346 | EditorApplication.update += OnComplete; 347 | 348 | void OnComplete() 349 | { 350 | if (!listRequest.IsCompleted) return; 351 | EditorApplication.update -= OnComplete; 352 | 353 | if (listRequest.Status == StatusCode.Failure) 354 | { 355 | Debug.Log(listRequest.Error.message); 356 | return; 357 | } 358 | 359 | var generationContext = new GenerationContext(listRequest.Result, KnownBuiltinAssetFilePaths); 360 | var (succeeded, rawTree, analysisResults) = GenerateTree(generationContext); 361 | if (!succeeded) return; 362 | 363 | RawTree = rawTree; 364 | AnalysisResults = analysisResults; 365 | EditorUtility.SetDirty(this); 366 | AssetDatabase.SaveAssets(); 367 | RootNode = null; 368 | OnRefresh?.Invoke(); 369 | } 370 | } 371 | 372 | public DependencyFolderNode GetRootNode() 373 | { 374 | if (RootNode == null || NodesByGuid == null) { 375 | (RootNode, NodesByGuid) = BuildFolderTree(RawTree, AnalysisResults, KnownBuiltinAssetFilePaths) ?? (null, null); 376 | } 377 | return RootNode; 378 | } 379 | 380 | private static string DependencyTreePath = "Assets/DependencyTree.asset"; 381 | 382 | [MenuItem("Assets/Generate Dependency Tree")] 383 | public static void Generate() 384 | { 385 | // TODO: Put the asset somewhere less annoying to users 386 | if (File.Exists(DependencyTreePath)) 387 | { 388 | Selection.activeObject = AssetDatabase.LoadAssetAtPath(DependencyTreePath); 389 | } 390 | else 391 | { 392 | var holder = CreateInstance(); 393 | Selection.activeObject = holder; 394 | AssetDatabase.CreateAsset(holder, DependencyTreePath); 395 | } 396 | } 397 | } 398 | } 399 | --------------------------------------------------------------------------------