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);
}
}
}