using UnityEditor; using UnityEngine; using System.IO; using System.Linq; using Ubisoft.Hotel.Extensions.UnityEngine.Editor; namespace Ubisoft { /** * An editor utility for easily creating symlinks in your project. * * Adds a Menu item under `Assets/Create/Folder (Symlink)`, and * draws a small indicator in the Project view for folders that are * symlinks. * * Based on https://github.com/karl-/unity-symlink-utility * * Instructions to update it: * 1)Change namespace from Paradox to Ubisoft.Hotel.Extensions.UnityEditor.Editor * 2)Comment out SymlinkRelative() and rename 'Assets/Create/Folder (Absolute Symlink)' for 'Assets/Create/Folder (Symlink)', * in order to make it less confusing for users * 3)Fix compilation errors that have to do with UnityEngine.Debug by deleting UnityEngine. * 4)Move the code that creates the command to run to create the symbolic link to a method called SymLink with * void SymLink(string sourcePath, string targetPath, bool refreshDatabase, SymlinkType linkType = SymlinkType.Junction) * as a signature and call this method instead. * 5)Delete ExecuteCmdCommand() and ExecuteBashCommand() methods. Use OSUtlity.ExecuteCommand() instead. * 6)Keep SimlinkViaCodeExample() * 7)Make sure that macOS implementation is also used on Linux. */ [InitializeOnLoad] public static class HtSymlinkUtility { public enum SymlinkType { Junction, Absolute, Relative } // FileAttributes that match a junction folder. const FileAttributes FOLDER_SYMLINK_ATTRIBS = FileAttributes.Directory | FileAttributes.ReparsePoint; // Style used to draw the symlink indicator in the project view. private static GUIStyle s_symlinkMarkerStyle = null; private static GUIStyle SymlinkMarkerStyle { get { if (s_symlinkMarkerStyle == null) { s_symlinkMarkerStyle = new GUIStyle(EditorStyles.label); s_symlinkMarkerStyle.normal.textColor = new Color(.2f, .8f, .2f, .8f); s_symlinkMarkerStyle.alignment = TextAnchor.MiddleRight; } return s_symlinkMarkerStyle; } } /** * Static constructor subscribes to projectWindowItemOnGUI delegate. */ static HtSymlinkUtility() { EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI; } /** * Draw a little indicator if folder is a symlink */ private static void OnProjectWindowItemGUI(string guid, Rect r) { try { string path = AssetDatabase.GUIDToAssetPath(guid); if (!string.IsNullOrEmpty(path)) { FileAttributes attribs = File.GetAttributes(path); if ((attribs & FOLDER_SYMLINK_ATTRIBS) == FOLDER_SYMLINK_ATTRIBS) { GUI.Label(r, "<=>", SymlinkMarkerStyle); } } } catch { } } [MenuItem(UnityEditorMenu.SAMPLES + "Symlink via code")] internal static void ViaCodeSample() { string directoryName = "SymlinkSample"; // // 1. dst // // We need to process dst before processing src so this sample can be run more than once. // When run as a second time we need to unlink dst before preparing src so changes in src // don't affect dst // string dstPath = $"Assets/{directoryName}"; string platformDstPath = HtAssetDatabase.UnityPathToPlatformPath(dstPath, true); if (HtAssetDatabase.ExistsFolder(dstPath)) { // Delete dstPath to avoid infinite loops just in case this sample is being run more than once HtAssetDatabase.DeleteFolder(dstPath, true); } // // 2. src // string srcPath = $"Assets/../{directoryName}"; string platformSrcPath = HtAssetDatabase.UnityPathToPlatformPath(srcPath, true); // Delete src folder if (Directory.Exists(platformSrcPath)) { Directory.Delete(platformSrcPath, true); } _ = Directory.CreateDirectory(platformSrcPath); // Create a file inside this folder so it can be seen from the folder linked to this folder in Unity string platformFilePath = HtAssetDatabase.PlatformPathCombine(platformSrcPath, "README.txt"); File.WriteAllText(platformFilePath, "This file will be visible from Unity Editor after SymLink is done"); // 3. Symlink Symlink(platformSrcPath, platformDstPath, true); } // // Add a menu item in the Assets/Create category to add symlinks to directories. // #if UNITY_EDITOR_WIN // Create an absolute junction [MenuItem("Assets/Create/Folder (Junction)", false, 20)] internal static void Junction() { Symlink(SymlinkType.Junction); } #endif // Create an absolute symbolic link [MenuItem("Assets/Create/Folder (Symlink)", false, 21)] internal static void SymlinkAbsolute() { Symlink(SymlinkType.Absolute); } /* // Create a relative symbolic link [MenuItem("Assets/Create/Folder (Relative Symlink)", false, 22)] internal static void SymlinkRelative() { Symlink(SymlinkType.Relative); } */ public static bool IsASymlink(string directoryPlatformPath) { DirectoryInfo info = new DirectoryInfo(directoryPlatformPath); return info != null && info.Attributes.HasFlag(FileAttributes.ReparsePoint); } public static void DeleteSymlink(string directoryPlatformPath) { #if UNITY_EDITOR_WIN string command = string.Format("rmdir \"{0}\"", directoryPlatformPath); HtOSUtility.ExecuteCommand(command); #else directoryPlatformPath = directoryPlatformPath.Replace(" ", "\\ "); string command = string.Format("rm {0}", directoryPlatformPath); HtOSUtility.ExecuteCommand(command); #endif } private static void Symlink(SymlinkType linkType) { string sourceFolderPath = EditorUtility.OpenFolderPanel("Select Folder Source", "", ""); // Cancelled dialog if (string.IsNullOrEmpty(sourceFolderPath)) { return; } if (sourceFolderPath.Contains(Application.dataPath)) { Debug.EditorLogWarning("Cannot create a symlink to folder in your project!"); return; } string sourceFolderName = sourceFolderPath.Split(new char[] { '/', '\\' }).LastOrDefault(); if (string.IsNullOrEmpty(sourceFolderName)) { Debug.EditorLogWarning("Couldn't deduce the folder name?"); return; } Object uobject = Selection.activeObject; string targetPath = uobject != null ? AssetDatabase.GetAssetPath(uobject) : null; if (string.IsNullOrEmpty(targetPath)) { targetPath = "Assets"; } FileAttributes attribs = File.GetAttributes(targetPath); if ((attribs & FileAttributes.Directory) != FileAttributes.Directory) { targetPath = Path.GetDirectoryName(targetPath); } // Get path to project. string pathToProject = Application.dataPath.Split(new string[1] { "/Assets" }, System.StringSplitOptions.None)[0]; targetPath = string.Format("{0}/{1}/{2}", pathToProject, targetPath, sourceFolderName); if (Directory.Exists(targetPath)) { Debug.EditorLogWarning(string.Format("A folder already exists at this location, aborting link.\n{0} -> {1}", sourceFolderPath, targetPath)); return; } // Use absolute path or relative path? string sourcePath = linkType == SymlinkType.Relative ? GetRelativePath(sourceFolderPath, targetPath) : sourceFolderPath; InternalSymlink(linkType, sourcePath, targetPath, true); } private static void InternalSymlink(SymlinkType linkType, string sourcePath, string targetPath, bool refreshDatabase) { if (Directory.Exists(sourcePath)) { #if UNITY_EDITOR_WIN string linkOption = linkType == SymlinkType.Junction ? "/J" : "/D"; string command = string.Format("mklink {0} \"{1}\" \"{2}\"", linkOption, targetPath, sourcePath); HtOSUtility.ExecuteCommand(command, linkType != SymlinkType.Junction); // Symlinks require admin privilege on windows, junctions do not. #else // For some reason, OSX doesn't want to create a symlink with quotes around the paths, so escape the spaces instead. sourcePath = sourcePath.Replace(" ", "\\ "); targetPath = targetPath.Replace(" ", "\\ "); string command = string.Format("ln -s {0} {1}", sourcePath, targetPath); HtOSUtility.ExecuteCommand(command); #endif //Debug.EditorLog(string.Format("Created symlink: {0} <=> {1}", targetPath, sourceFolderPath)); if (refreshDatabase) { AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); } } else { Debug.EditorLogError($"Can't create a symbolic link when source path {Debug.FormatTextInUserContext(sourcePath)} doesn't exist"); } } /// /// Create a symbolic link between the platform absolute paths passed as parameters. /// Since Junction type is used in Windows to avoid problems with lack of permissions. /// /// Have a look at ViaCodeSample() for a practical sample. /// /// Platform absolute path to the source folder. /// Platform absolute path to the target folder. /// When true Unity assets database is refreshed to reflect changes immediately. public static void Symlink(string platformSrcPath, string platformTargetPath, bool refreshDatabase = true) { SymlinkType linkType = SymlinkType.Absolute; #if UNITY_EDITOR_WIN linkType = SymlinkType.Junction; #endif InternalSymlink(linkType, platformSrcPath, platformTargetPath, refreshDatabase); } static string GetRelativePath(string sourcePath, string outputPath) { if (string.IsNullOrEmpty(outputPath)) { return sourcePath; } if (sourcePath == null) { sourcePath = string.Empty; } var splitOutput = outputPath.Split(new char[1] { '/' }, System.StringSplitOptions.RemoveEmptyEntries); var splitSource = sourcePath.Split(new char[1] { '/' }, System.StringSplitOptions.RemoveEmptyEntries); int max = Mathf.Min(splitOutput.Length, splitSource.Length); int i = 0; while (i < max) { if (splitOutput[i] != splitSource[i]) { break; } ++i; } int hopUpCount = splitOutput.Length - i - 1; int newSplitCount = hopUpCount + splitSource.Length - i; string[] newSplitTarget = new string[newSplitCount]; int j = 0; for (; j < hopUpCount; ++j) { newSplitTarget[j] = ".."; } for (max = newSplitTarget.Length; j < max; ++j, ++i) { newSplitTarget[j] = splitSource[i]; } return string.Join(Path.DirectorySeparatorChar.ToString(), newSplitTarget); } } }