123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEditor;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.IO;
- using UnityProjectCloner;
- namespace UnityProjectCloner
- {
- /// <summary>
- /// Contains all required methods for creating a linked clone of the Unity project.
- /// </summary>
- public class ProjectCloner
- {
- /// <summary>
- /// Name used for an identifying file created in the clone project directory.
- /// </summary>
- /// <remarks>
- /// (!) Do not change this after the clone was created, because then connection will be lost.
- /// </remarks>
- public const string CloneFileName = ".clone";
- /// <summary>
- /// Suffix added to the end of the project clone name when it is created.
- /// </summary>
- /// <remarks>
- /// (!) Do not change this after the clone was created, because then connection will be lost.
- /// </remarks>
- public const string CloneNameSuffix = "_clone";
- public const int MaxCloneProjectCount = 10;
- #region Managing clones
- /// <summary>
- /// Creates clone from the project currently open in Unity Editor.
- /// </summary>
- /// <returns></returns>
- public static Project CreateCloneFromCurrent()
- {
- if (IsClone())
- {
- Debug.LogError("This project is already a clone. Cannot clone it.");
- return null;
- }
- string currentProjectPath = ProjectCloner.GetCurrentProjectPath();
- return ProjectCloner.CreateCloneFromPath(currentProjectPath);
- }
- /// <summary>
- /// Creates clone of the project located at the given path.
- /// </summary>
- /// <param name="sourceProjectPath"></param>
- /// <returns></returns>
- public static Project CreateCloneFromPath(string sourceProjectPath)
- {
- Project sourceProject = new Project(sourceProjectPath);
- string cloneProjectPath = null;
- //Find available clone suffix id
- for (int i = 0; i < MaxCloneProjectCount; i++)
- {
- string originalProjectPath = ProjectCloner.GetCurrentProject().projectPath;
- string possibleCloneProjectPath = originalProjectPath + ProjectCloner.CloneNameSuffix + "_" + i;
- if (!Directory.Exists(possibleCloneProjectPath))
- {
- cloneProjectPath = possibleCloneProjectPath;
- break;
- }
- }
- if (string.IsNullOrEmpty(cloneProjectPath))
- {
- Debug.LogError("The number of cloned projects has reach its limit. Limit: " + MaxCloneProjectCount);
- return null;
- }
- Project cloneProject = new Project(cloneProjectPath);
- Debug.Log("Start project name: " + sourceProject);
- Debug.Log("Clone project name: " + cloneProject);
- ProjectCloner.CreateProjectFolder(cloneProject);
- ProjectCloner.CopyLibraryFolder(sourceProject, cloneProject);
- ProjectCloner.LinkFolders(sourceProject.assetPath, cloneProject.assetPath);
- ProjectCloner.LinkFolders(sourceProject.projectSettingsPath, cloneProject.projectSettingsPath);
- ProjectCloner.LinkFolders(sourceProject.packagesPath, cloneProject.packagesPath);
- ProjectCloner.LinkFolders(sourceProject.autoBuildPath, cloneProject.autoBuildPath);
- ProjectCloner.RegisterClone(cloneProject);
- return cloneProject;
- }
- /// <summary>
- /// Registers a clone by placing an identifying ".clone" file in its root directory.
- /// </summary>
- /// <param name="cloneProject"></param>
- private static void RegisterClone(Project cloneProject)
- {
- /// Add clone identifier file.
- string identifierFile = Path.Combine(cloneProject.projectPath, ProjectCloner.CloneFileName);
- File.Create(identifierFile).Dispose();
- /// Add collabignore.txt to stop the clone from messing with Unity Collaborate if it's enabled. Just in case.
- string collabignoreFile = Path.Combine(cloneProject.projectPath, "collabignore.txt");
- File.WriteAllText(collabignoreFile, "*"); /// Make it ignore ALL files in the clone.
- }
- /// <summary>
- /// Opens a project located at the given path (if one exists).
- /// </summary>
- /// <param name="projectPath"></param>
- public static void OpenProject(string projectPath)
- {
- if (!Directory.Exists(projectPath))
- {
- Debug.LogError("Cannot open the project - provided folder (" + projectPath + ") does not exist.");
- return;
- }
- if (projectPath == ProjectCloner.GetCurrentProjectPath())
- {
- Debug.LogError("Cannot open the project - it is already open.");
- return;
- }
- string fileName = EditorApplication.applicationPath;
- string args = "-projectPath \"" + projectPath + "\"";
- Debug.Log("Opening project \"" + fileName + " " + args + "\"");
- ProjectCloner.StartHiddenConsoleProcess(fileName, args);
- }
- /// <summary>
- /// Deletes the clone of the currently open project, if such exists.
- /// </summary>
- public static void DeleteClone(string cloneProjectPath)
- {
- /// Clone won't be able to delete itself.
- if (ProjectCloner.IsClone()) return;
- ///Extra precautions.
- if (cloneProjectPath == string.Empty) return;
- if (cloneProjectPath == ProjectCloner.GetOriginalProjectPath()) return;
- if (!cloneProjectPath.EndsWith(ProjectCloner.CloneNameSuffix)) return;
- //Check what OS is
- switch (Application.platform)
- {
- case (RuntimePlatform.WindowsEditor):
- Debug.Log("Attempting to delete folder \"" + cloneProjectPath + "\"");
- string args = "/c " + @"rmdir /s/q " + string.Format("\"{0}\"", cloneProjectPath);
- StartHiddenConsoleProcess("cmd.exe", args);
-
- break;
- case (RuntimePlatform.OSXEditor):
- throw new System.NotImplementedException("No Mac function implement yet :(");
- //break;
- case (RuntimePlatform.LinuxEditor):
- throw new System.NotImplementedException("No linux support yet :(");
- //break;
- default:
- Debug.LogWarning("Not in a known editor. Where are you!?");
- break;
- }
- }
- #endregion
- #region Creating project folders
- /// <summary>
- /// Creates an empty folder using data in the given Project object
- /// </summary>
- /// <param name="project"></param>
- public static void CreateProjectFolder(Project project)
- {
- string path = project.projectPath;
- Debug.Log("Creating new empty folder at: " + path);
- Directory.CreateDirectory(path);
- }
- /// <summary>
- /// Copies the full contents of the unity library. We want to do this to avoid the lengthy reserialization of the whole project when it opens up the clone.
- /// </summary>
- /// <param name="sourceProject"></param>
- /// <param name="destinationProject"></param>
- public static void CopyLibraryFolder(Project sourceProject, Project destinationProject)
- {
- if (Directory.Exists(destinationProject.libraryPath))
- {
- Debug.LogWarning("Library copy: destination path already exists! ");
- return;
- }
- Debug.Log("Library copy: " + destinationProject.libraryPath);
- ProjectCloner.CopyDirectoryWithProgressBar(sourceProject.libraryPath, destinationProject.libraryPath, "Cloning project '" + sourceProject.name + "'. ");
- }
- #endregion
- #region Creating symlinks
- /// <summary>
- /// Creates a symlink between destinationPath and sourcePath (Mac version).
- /// </summary>
- /// <param name="sourcePath"></param>
- /// <param name="destinationPath"></param>
- private static void CreateLinkMac(string sourcePath, string destinationPath)
- {
- // Debug.LogWarning("This hasn't been tested yet! I am mac-less :( Please chime in on the github if it works for you.");
- string cmd = "-s " + string.Format("\"{0}\" \"{1}\"", sourcePath, destinationPath);
- Debug.Log("Mac hard link " + cmd);
- // ProjectCloner.StartHiddenConsoleProcess("/bin/zsh", cmd);
- StartProcessWithShell("ln", cmd);
- }
- /// <summary>
- /// Creates a symlink between destinationPath and sourcePath (Windows version).
- /// </summary>
- /// <param name="sourcePath"></param>
- /// <param name="destinationPath"></param>
- private static void CreateLinkWin(string sourcePath, string destinationPath)
- {
- string cmd = "/C mklink /J " + string.Format("\"{0}\" \"{1}\"", destinationPath, sourcePath);
- Debug.Log("Windows junction: " + cmd);
- ProjectCloner.StartHiddenConsoleProcess("cmd.exe", cmd);
- }
- /// <summary>
- /// Creates a symlink between destinationPath and sourcePath (Linux version).
- /// </summary>
- /// <param name="sourcePath"></param>
- /// <param name="destinationPath"></param>
- private static void CreateLinkLunux(string sourcePath, string destinationPath)
- {
- string cmd = string.Format("-c \"ln -s {0} {1}\"", sourcePath, destinationPath);
- Debug.Log("Linux junction: " + cmd);
- ProjectCloner.StartHiddenConsoleProcess("/bin/bash", cmd);
- }
- //TODO avoid terminal calls and use proper api stuff. See below for windows!
- ////https://docs.microsoft.com/en-us/windows/desktop/api/ioapiset/nf-ioapiset-deviceiocontrol
- //[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- //private static extern bool DeviceIoControl(System.IntPtr hDevice, uint dwIoControlCode,
- // System.IntPtr InBuffer, int nInBufferSize,
- // System.IntPtr OutBuffer, int nOutBufferSize,
- // out int pBytesReturned, System.IntPtr lpOverlapped);
- /// <summary>
- /// Create a link / junction from the real project to it's clone.
- /// </summary>
- /// <param name="sourcePath"></param>
- /// <param name="destinationPath"></param>
- public static void LinkFolders(string sourcePath, string destinationPath)
- {
- if ((Directory.Exists(destinationPath) == false) && (Directory.Exists(sourcePath) == true))
- {
- switch (Application.platform)
- {
- case (RuntimePlatform.WindowsEditor):
- CreateLinkWin(sourcePath, destinationPath);
- break;
- case (RuntimePlatform.OSXEditor):
- CreateLinkMac(sourcePath, destinationPath);
- break;
- case (RuntimePlatform.LinuxEditor):
- CreateLinkLunux(sourcePath, destinationPath);
- break;
- default:
- Debug.LogWarning("Not in a known editor. Where are you!?");
- break;
- }
- }
- else
- {
- Debug.LogWarning("Skipping Asset link, it already exists: " + destinationPath);
- }
- }
- #endregion
- #region Utility methods
- /// <summary>
- /// Returns true is the project currently open in Unity Editor is a clone.
- /// </summary>
- /// <returns></returns>
- public static bool IsClone()
- {
- /// The project is a clone if its root directory contains an empty file named ".clone".
- string cloneFilePath = Path.Combine(ProjectCloner.GetCurrentProjectPath(), ProjectCloner.CloneFileName);
- bool isClone = File.Exists(cloneFilePath);
- return isClone;
- }
- /// <summary>
- /// Get the path to the current unityEditor project folder's info
- /// </summary>
- /// <returns></returns>
- public static string GetCurrentProjectPath()
- {
- return Application.dataPath.Replace("/Assets", "");
- }
- /// <summary>
- /// Return a project object that describes all the paths we need to clone it.
- /// </summary>
- /// <returns></returns>
- public static Project GetCurrentProject()
- {
- string pathString = ProjectCloner.GetCurrentProjectPath();
- return new Project(pathString);
- }
- /// <summary>
- /// Returns the path to the original project.
- /// If currently open project is the original, returns its own path.
- /// If the original project folder cannot be found, retuns an empty string.
- /// </summary>
- /// <returns></returns>
- public static string GetOriginalProjectPath()
- {
- if (IsClone())
- {
- /// If this is a clone...
- /// Original project path can be deduced by removing the suffix from the clone's path.
- string cloneProjectPath = ProjectCloner.GetCurrentProject().projectPath;
- int index = cloneProjectPath.LastIndexOf(ProjectCloner.CloneNameSuffix);
- if (index > 0)
- {
- string originalProjectPath = cloneProjectPath.Substring(0, index);
- if (Directory.Exists(originalProjectPath)) return originalProjectPath;
- }
- return string.Empty;
- }
- else
- {
- /// If this is the original, we return its own path.
- return ProjectCloner.GetCurrentProjectPath();
- }
- }
- /// <summary>
- /// Returns all clone projects path.
- /// </summary>
- /// <returns></returns>
- public static List<string> GetCloneProjectsPath()
- {
- List<string> projectsPath = new List<string>();
- for (int i = 0; i < MaxCloneProjectCount; i++)
- {
- string originalProjectPath = ProjectCloner.GetCurrentProject().projectPath;
- string cloneProjectPath = originalProjectPath + ProjectCloner.CloneNameSuffix + "_" + i;
- if (Directory.Exists(cloneProjectPath))
- projectsPath.Add(cloneProjectPath);
- }
- return projectsPath;
- }
- /// <summary>
- /// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
- /// </summary>
- /// <param name="source">Directory to be copied.</param>
- /// <param name="destination">Destination directory (created automatically if needed).</param>
- /// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
- public static void CopyDirectoryWithProgressBar(string sourcePath, string destinationPath, string progressBarPrefix = "")
- {
- var source = new DirectoryInfo(sourcePath);
- var destination = new DirectoryInfo(destinationPath);
- long totalBytes = 0;
- long copiedBytes = 0;
- ProjectCloner.CopyDirectoryWithProgressBarRecursive(source, destination, ref totalBytes, ref copiedBytes, progressBarPrefix);
- EditorUtility.ClearProgressBar();
- }
- /// <summary>
- /// Copies directory located at sourcePath to destinationPath. Displays a progress bar.
- /// Same as the previous method, but uses recursion to copy all nested folders as well.
- /// </summary>
- /// <param name="source">Directory to be copied.</param>
- /// <param name="destination">Destination directory (created automatically if needed).</param>
- /// <param name="totalBytes">Total bytes to be copied. Calculated automatically, initialize at 0.</param>
- /// <param name="copiedBytes">To track already copied bytes. Calculated automatically, initialize at 0.</param>
- /// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
- private static void CopyDirectoryWithProgressBarRecursive(DirectoryInfo source, DirectoryInfo destination, ref long totalBytes, ref long copiedBytes, string progressBarPrefix = "")
- {
- /// Directory cannot be copied into itself.
- if (source.FullName.ToLower() == destination.FullName.ToLower())
- {
- Debug.LogError("Cannot copy directory into itself.");
- return;
- }
- /// Calculate total bytes, if required.
- if (totalBytes == 0)
- {
- totalBytes = ProjectCloner.GetDirectorySize(source, true, progressBarPrefix);
- }
- /// Create destination directory, if required.
- if (!Directory.Exists(destination.FullName))
- {
- Directory.CreateDirectory(destination.FullName);
- }
- /// Copy all files from the source.
- foreach (FileInfo file in source.GetFiles())
- {
- try
- {
- file.CopyTo(Path.Combine(destination.ToString(), file.Name), true);
- }
- catch (IOException)
- {
- /// Some files may throw IOException if they are currently open in Unity editor.
- /// Just ignore them in such case.
- }
- /// Account the copied file size.
- copiedBytes += file.Length;
- /// Display the progress bar.
- float progress = (float)copiedBytes / (float)totalBytes;
- bool cancelCopy = EditorUtility.DisplayCancelableProgressBar(
- progressBarPrefix + "Copying '" + source.FullName + "' to '" + destination.FullName + "'...",
- "(" + (progress * 100f).ToString("F2") + "%) Copying file '" + file.Name + "'...",
- progress);
- if (cancelCopy) return;
- }
- /// Copy all nested directories from the source.
- foreach (DirectoryInfo sourceNestedDir in source.GetDirectories())
- {
- DirectoryInfo nextDestingationNestedDir = destination.CreateSubdirectory(sourceNestedDir.Name);
- ProjectCloner.CopyDirectoryWithProgressBarRecursive(sourceNestedDir, nextDestingationNestedDir, ref totalBytes, ref copiedBytes, progressBarPrefix);
- }
- }
- /// <summary>
- /// Calculates the size of the given directory. Displays a progress bar.
- /// </summary>
- /// <param name="directory">Directory, which size has to be calculated.</param>
- /// <param name="includeNested">If true, size will include all nested directories.</param>
- /// <param name="progressBarPrefix">Optional string added to the beginning of the progress bar window header.</param>
- /// <returns>Size of the directory in bytes.</returns>
- private static long GetDirectorySize(DirectoryInfo directory, bool includeNested = false, string progressBarPrefix = "")
- {
- EditorUtility.DisplayProgressBar(progressBarPrefix + "Calculating size of directories...", "Scanning '" + directory.FullName + "'...", 0f);
- /// Calculate size of all files in directory.
- long filesSize = directory.EnumerateFiles().Sum((FileInfo file) => file.Length);
- /// Calculate size of all nested directories.
- long directoriesSize = 0;
- if (includeNested)
- {
- IEnumerable<DirectoryInfo> nestedDirectories = directory.EnumerateDirectories();
- foreach (DirectoryInfo nestedDir in nestedDirectories)
- {
- directoriesSize += ProjectCloner.GetDirectorySize(nestedDir, true, progressBarPrefix);
- }
- }
- return filesSize + directoriesSize;
- }
- /// <summary>
- /// Starts process in the system console, taking the given fileName and args.
- /// </summary>
- /// <param name="fileName"></param>
- /// <param name="args"></param>
- private static void StartHiddenConsoleProcess(string fileName, string args)
- {
- var process = new System.Diagnostics.Process();
- //process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
- process.StartInfo.FileName = fileName;
- process.StartInfo.Arguments = args;
- process.Start();
- }
- /// <summary>
- /// starts process with the system shell
- /// </summary>
- /// <param name="cmdAndArgs"></param>
- /// <returns></returns>
- private static void StartProcessWithShell(string cmdName, string Args)
- {
- var process = new System.Diagnostics.Process();
- process.StartInfo.UseShellExecute = true;
- process.StartInfo.FileName = cmdName;
- // process.StartInfo.RedirectStandardError = true;
- process.StartInfo.Arguments = Args;
- process.Start();
- process.WaitForExit(-1);
- Debug.Log($"cmd process exiting with code :{process.ExitCode}");
- }
- #endregion
- }
- }
|