using System; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEditor.PackageManager; using UnityEditor.PackageManager.Requests; namespace Ubisoft.Hotel.Package.Editor { /// /// Class responsible for performing some basic tasks required to install Hotel packages even though Hotel PackageManager package is not /// installed which may be convenient while developing Hotel. /// public class PackageManagerHelper : IPreprocessBuildWithReport { private static readonly string PACKAGE_LINK_XML_FILENAME = "link.xml"; private static readonly string PACKAGE_PLUGINS_FOLDER = "Plugins"; private static readonly string PACKAGE_TOOLS_FOLDER = "Tools"; /// /// The Verbatim folder is meant to store stuff that needs to be copied as it is over to app space exactly. It typically contains meta files. /// private static readonly string PACKAGE_VERBATIM_FOLDER = "Verbatim"; private static readonly string HOTEL = "Hotel"; public static readonly string ASSETS_HOTEL_ROOT_PATH = $"Assets/{HOTEL}"; public static readonly string ASSETS_HOTEL_EDITOR_ROOT_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/Editor"; public static readonly string ASSETS_HOTEL_RUNTIME_ROOT_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/Runtime"; public static readonly string ASSETS_HOTEL_STREAMING_ASSETS_ROOT_PATH = $"Assets/StreamingAssets/{HOTEL}"; public static readonly string ASSETS_HOTEL_EDITOR_ASSETS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/Assets"; public static readonly string ASSETS_HOTEL_EDITOR_SCRIPTS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/Scripts"; /// /// Unity path to the temporary folder where all packages link.xml files are stored before building. These files are only required before building to /// make sure that code that is not supposed to be stripped out is included in the build /// private static readonly string ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/PackageLinks"; public static readonly string ASSETS_HOTEL_PLUGINS_PATH = $"{ASSETS_HOTEL_ROOT_PATH}/{PACKAGE_PLUGINS_FOLDER}"; public static readonly string ASSETS_HOTEL_EDITOR_TOOLS_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/{PACKAGE_TOOLS_FOLDER}"; public static readonly string ASSETS_HOTEL_EDITOR_VERBATIM_PATH = $"{ASSETS_HOTEL_EDITOR_ROOT_PATH}/{PACKAGE_VERBATIM_FOLDER}"; public static readonly string ASSETS_HOTEL_RUNTIME_VERBATIM_PATH = $"{ASSETS_HOTEL_RUNTIME_ROOT_PATH}/{PACKAGE_VERBATIM_FOLDER}"; private static readonly string PACKAGE_EDITOR_ROOT_PATH = "Editor"; private static readonly string PACKAGE_RUNTIME_ROOT_PATH = "Runtime"; /// /// Path inside the package to link.xml file /// private static readonly string PACKAGE_LINK_XML_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_LINK_XML_FILENAME}"; /// /// Path inside the package to Plugins folder /// private static readonly string PACKAGE_PLUGINS_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_PLUGINS_FOLDER}"; /// /// Path inside the package to Tools folder /// private static readonly string PACKAGE_TOOLS_PATH = $"{PACKAGE_EDITOR_ROOT_PATH}/{PACKAGE_TOOLS_FOLDER}"; private static readonly string RESOURCES_DIRECTORYNAME = "Resources"; /// /// Directory where to put files that this package needs to move to Hotel/Resources in App space /// private static readonly string RESOURCES_PATH = "Editor/" + RESOURCES_DIRECTORYNAME; private static ListRequest PackagesRequest { get; set; } private static List>> OnProcessPackagesDoneCallbacks { get; set; } #if !HT_PACKAGE_PACKAGEMANAGER_PROD [MenuItem("Hotel/Core/Package/Update app space with packages configuration")] internal static void UpdatePackages() { Patch(true); } #endif [InitializeOnLoadMethod] #pragma warning disable IDE0051 // Called by Unity static void OnLoad() #pragma warning restore IDE0051 { #if !HT_PACKAGE_PACKAGEMANAGER_PROD Init(); const string projectOpenedKey = "ProjectOpened"; if (!SessionState.GetBool(projectOpenedKey, false) && EditorApplication.isPlayingOrWillChangePlaymode == false) { SessionState.SetBool(projectOpenedKey, true); Patch(true); } #endif } public static void Init() { } public static void ProcessPackages(Action> onDone) { if (onDone != null) { if (OnProcessPackagesDoneCallbacks == null) { OnProcessPackagesDoneCallbacks = new List>>(); } OnProcessPackagesDoneCallbacks.Add(onDone); } if (PackagesRequest == null) { PackagesRequest = Client.List(); // List packages installed for the project EditorApplication.update += UpdatePackagesRequest; } } private static void UpdatePackagesRequest() { if (PackagesRequest.IsCompleted) { EditorApplication.update -= UpdatePackagesRequest; HashSet packageNames = new HashSet(); if (PackagesRequest.Status == StatusCode.Success) { foreach (var package in PackagesRequest.Result) { _ = packageNames.Add(package.name); foreach (var dependency in package.resolvedDependencies) { _ = packageNames.Add(dependency.name); } } DoProcessPackages(packageNames, GetHotelPackages()); } else if (PackagesRequest.Status >= StatusCode.Failure) { Debug.EditorLogError("Error when retrieving packages: " + PackagesRequest.Error.message); } PackagesRequest = null; if (OnProcessPackagesDoneCallbacks != null) { foreach (Action> callback in OnProcessPackagesDoneCallbacks) { callback(packageNames); } OnProcessPackagesDoneCallbacks.Clear(); } } } public static List GetHotelPackages() { List returnValue = new List(); Type packageType = typeof(Package); foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) { foreach (Type t in asm.GetTypes()) { if (packageType != t && packageType.IsAssignableFrom(t)) { returnValue.Add((Package)Activator.CreateInstance(t)); } } } return returnValue; } public static void PrunePackages() { string unityPath = ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH; if (HtAssetDatabase.ExistsFolder(unityPath)) { HtAssetDatabase.DeleteFolder(unityPath); } // Plugins unityPath = ASSETS_HOTEL_PLUGINS_PATH; if (HtAssetDatabase.ExistsFolder(unityPath)) { HtAssetDatabase.DeleteFolder(unityPath); } // Editor Verbatim unityPath = ASSETS_HOTEL_EDITOR_VERBATIM_PATH; if (HtAssetDatabase.ExistsFolder(unityPath)) { HtAssetDatabase.DeleteFolder(unityPath); } HtAssetDatabase.CreateFolder(unityPath); // Runtime Verbatim unityPath = ASSETS_HOTEL_RUNTIME_VERBATIM_PATH; if (HtAssetDatabase.ExistsFolder(unityPath)) { HtAssetDatabase.DeleteFolder(unityPath); } HtAssetDatabase.CreateFolder(unityPath); // Tools unityPath = ASSETS_HOTEL_EDITOR_TOOLS_PATH; if (HtAssetDatabase.ExistsFolder(unityPath)) { HtAssetDatabase.DeleteFolder(unityPath); } } private static void DoProcessPackages(HashSet packageNames, List hotelPackages) { PrunePackages(); HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH); HtAssetDatabase.CreateFolder(ASSETS_HOTEL_PLUGINS_PATH); HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_VERBATIM_PATH); HtAssetDatabase.CreateFolder(ASSETS_HOTEL_RUNTIME_VERBATIM_PATH); HtAssetDatabase.CreateFolder(ASSETS_HOTEL_EDITOR_TOOLS_PATH); // Store some information that might be required by some tools, namely: // -)Unity Editor path // -)Project path string editorPath = EditorApplication.applicationPath; #if UNITY_EDITOR_WIN // Use quotes just in case the path contains a whitespace editorPath = $"\"{editorPath}\""; #else editorPath = $"{editorPath}/Contents/MacOS/Unity"; #endif WriteSettingsFile(ASSETS_HOTEL_EDITOR_TOOLS_PATH, "unity_editor_path.txt", editorPath); // Application.dataPath contains "Asset" folder so we need to take that into account WriteSettingsFile(ASSETS_HOTEL_EDITOR_TOOLS_PATH, "project_root_path.txt", HtAssetDatabase.PlatformPathCombine(UnityEngine.Application.dataPath, "..")); if (packageNames != null) { // Packages link.xml files are copied into this package right before building, this way we make sure that only packages that are included in // the build are taken into consideration string platformDstLinkPath = HtAssetDatabase.UnityPathToPlatformPath(ASSETS_HOTEL_EDITOR_PACKAGE_LINKS_PATH, true); string unityResourcesPath = AssetManager.HOTEL_ASSETS_RESOURCES_PATH; string platformDstResourcesPath = HtAssetDatabase.UnityPathToPlatformPath(unityResourcesPath, true); // Loop through all dependencies and copy link.xml file, if exists, into the Hotel folder dedicated to store all packages link.xml files foreach (var packageName in packageNames) { if (UnityBuildProperty.IsAHotelPackage(packageName)) { // Package link.xml ProcessPackageLinkXml(packageName, platformDstLinkPath); // Package Resources ProcessPackageResources(packageName, platformDstResourcesPath); // Package Plugins ProcessPackagePlugins(packageName, ASSETS_HOTEL_PLUGINS_PATH); // Package Tools ProcessPackageTools(packageName, ASSETS_HOTEL_EDITOR_TOOLS_PATH); // Package Editor Verbatim folder ProcessPackageVerbatim(packageName, ASSETS_HOTEL_EDITOR_VERBATIM_PATH, true); // Package Runtime Verbatim folder ProcessPackageVerbatim(packageName, ASSETS_HOTEL_RUNTIME_VERBATIM_PATH, false); } } } // Every hotel packages implementing Package class has its chance to apply its settings foreach (Package package in hotelPackages) { // Make sure package name is in packageNames if (packageNames.Contains(package.Name)) { package.OnInstallOrUpdate(); } } } private static void WriteSettingsFile(string platformFolder, string fileName, string value) { string platformPath = HtAssetDatabase.PlatformPathCombine(platformFolder, fileName); StreamWriter sw = File.CreateText(platformPath); sw.WriteLine(value); sw.Close(); } private static void ProcessPackageLinkXml(string packageName, string platformRootDestPath) { string unityAssetPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_LINK_XML_PATH); string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityAssetPath, true); if (File.Exists(platformPath)) { string platformDestPath = $"{platformRootDestPath}{Path.DirectorySeparatorChar}{packageName}"; if (!Directory.Exists(platformDestPath)) { _ = Directory.CreateDirectory(platformDestPath); } platformDestPath += Path.DirectorySeparatorChar + PACKAGE_LINK_XML_FILENAME; File.Copy(platformPath, platformDestPath); } } private static void ProcessPackageResources(string packageName, string platformRootDestPath) { // Check if the dst folder already contains the files required by this package. If so then no files need to be moved into app space // because those files may have been changed by the consumer string platformDestPath = $"{platformRootDestPath}{Path.DirectorySeparatorChar}{packageName}"; string unityAssetPath = HtAssetUtility.GetAssetInPackagePath(packageName, RESOURCES_PATH); string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityAssetPath, true); if (Directory.Exists(platformPath)) { IEnumerable appFilePaths = Directory.Exists(platformDestPath) ? HtSystemIO.GetAllFiles(platformDestPath, true) : new string[0]; IEnumerable packageFilePaths = HtSystemIO.GetAllFiles(platformPath, true); HashSet filesToCreate = new HashSet(packageFilePaths); HashSet filesToDelete = new HashSet(); // Loops through all files in app space: // -)Those files in package space that also exist in app space mustn't be created again because the consumer might have changed them // -)Those files in app space that don't exist in package space must be deleted because the package don't support them anymore string filePath; foreach (string path in appFilePaths) { filePath = path.Replace(platformDestPath, platformPath); // If the file already exists in app space then the one in app space has precedence // If it's actually a directory then it shouldn't be copied because we're copying files one by one if (filesToCreate.Contains(filePath) || HtSystemIO.Directory_Exists(filePath)) { _ = filesToCreate.Remove(filePath); } else { _ = filesToDelete.Add(path); } } if (filesToCreate.Count > 0) { if (!Directory.Exists(platformDestPath)) { _ = Directory.CreateDirectory(platformDestPath); } string newPath; string platformFileDestPath; foreach (string path in filesToCreate) { newPath = path.Replace(platformPath, ""); platformFileDestPath = platformDestPath; if (!newPath.StartsWith("" + Path.DirectorySeparatorChar)) { platformFileDestPath += Path.DirectorySeparatorChar; } platformFileDestPath += newPath; // A directory is not copied because we're copying files one by one and if we copied // the directory then its files would be copied twice // // A meta file is not copied either to avoid duplicates. Their meta files will be created by Unity when they're copied // inside Assets folder if (HtSystemIO.Directory_Exists(path)) { _ = Directory.CreateDirectory(platformFileDestPath); } else if (!HtAssetDatabase.IsAMetaFile(HtAssetDatabase.PlatformPathToUnityPath(path))) { File.Copy(path, platformFileDestPath); } } } if (filesToDelete.Count > 0) { foreach (string path in filesToDelete) { if (HtSystemIO.Directory_Exists(path)) { Directory.Delete(path, true); } else if (File.Exists(path)) { File.Delete(path); } } if (HtSystemIO.Directory_FileCount(platformPath) == 0) { Directory.Delete(platformPath); } } } } private static void ProcessPackagePlugins(string packageName, string rootDstUnityPath) { string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_PLUGINS_PATH); if (HtAssetDatabase.ExistsFolder(srcUnityPath)) { // Android string thisSrcUnityPath = $"{srcUnityPath}/Android"; if (HtAssetDatabase.ExistsFolder(thisSrcUnityPath)) { string dstUnityPath = $"{rootDstUnityPath}/Android/{packageName}.androidlib"; if (!HtAssetDatabase.ExistsFolder(dstUnityPath)) { HtAssetDatabase.CreateFolder(dstUnityPath); } HtAssetDatabase.CopyFolder(thisSrcUnityPath, dstUnityPath); } // iOS thisSrcUnityPath = $"{srcUnityPath}/iOS"; if (HtAssetDatabase.ExistsFolder(thisSrcUnityPath)) { string dstUnityPath = $"{rootDstUnityPath}/iOS/{packageName}"; if (!HtAssetDatabase.ExistsFolder(dstUnityPath)) { HtAssetDatabase.CreateFolder(dstUnityPath); } HtAssetDatabase.CopyFolder(thisSrcUnityPath, dstUnityPath); } } } private static void ProcessPackageTools(string packageName, string rootDstUnityPath) { string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, PACKAGE_TOOLS_PATH); if (HtAssetDatabase.ExistsFolder(srcUnityPath)) { string dstUnityPath = $"{rootDstUnityPath}/{packageName}"; if (!HtAssetDatabase.ExistsFolder(dstUnityPath)) { HtAssetDatabase.CreateFolder(dstUnityPath); } HtAssetDatabase.CopyFolder(srcUnityPath, dstUnityPath); } } /// /// Copy the content of Verbatim folder over to app space. Add stuff to this folder when you need that stuff to /// by copied verbatim, typically when you need metas to preserve. /// /// Name of the package which verbatim content needs to be copied to app space. /// Unity path to the destination folder. private static void ProcessPackageVerbatim(string packageName, string rootDstUnityPath, bool isEditor) { string srcUnityPath = HtAssetUtility.GetAssetInPackagePath(packageName, ""); if (!srcUnityPath.EndsWith("/")) { srcUnityPath += "/"; } string srcPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(srcUnityPath, true); string srcPlatformFolder = isEditor ? PACKAGE_EDITOR_ROOT_PATH : PACKAGE_RUNTIME_ROOT_PATH; string token = HtAssetDatabase.PlatformPathCombine(srcPlatformFolder, $"{PACKAGE_VERBATIM_FOLDER}~"); // Loop through all frist level subdirectories because a package might have some modules inside. Each submodule // could have a Verbatim~ folder. string subdirectoryName; string candidatePlatformPath; string[] directories = Directory.GetDirectories(srcPlatformPath); int count = directories.Length; for (int i = 0; i < count; i++) { subdirectoryName = directories[i].Replace($"{srcPlatformPath}", ""); if (subdirectoryName == PACKAGE_EDITOR_ROOT_PATH || subdirectoryName == PACKAGE_RUNTIME_ROOT_PATH) { candidatePlatformPath = subdirectoryName == srcPlatformFolder ? srcPlatformPath : null; subdirectoryName = null; } else { candidatePlatformPath = directories[i]; } if (!string.IsNullOrEmpty(candidatePlatformPath)) { if (!candidatePlatformPath.EndsWith("" + Path.DirectorySeparatorChar)) { candidatePlatformPath += Path.DirectorySeparatorChar; } candidatePlatformPath += token; if (Directory.Exists(candidatePlatformPath)) { CopyPackageVerbatim(candidatePlatformPath, rootDstUnityPath, packageName, subdirectoryName); } } } } private static void CopyPackageVerbatim(string srcPlatformPath, string rootDstUnityPath, string packageName, string subdirectory) { if (Directory.Exists(srcPlatformPath)) { string dstUnityPath = $"{rootDstUnityPath}/{packageName}"; if (!string.IsNullOrEmpty(subdirectory)) { dstUnityPath += $"/{subdirectory}"; } string dstPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(dstUnityPath); HtSystemIO.CopyDirectory(srcPlatformPath, dstPlatformPath); } } public static void PrunePackagesResources(Dictionary packagesHandledByConsumer, Dependencies dependencies, string platformDstResourcesPath) { if (Directory.Exists(platformDstResourcesPath)) { string[] directories = Directory.GetDirectories(platformDstResourcesPath); string pathToReplace = $"{platformDstResourcesPath}{Path.DirectorySeparatorChar}"; string packageName; foreach (string path in directories) { packageName = path.Replace(pathToReplace, ""); // If package name is not handled by the consumer anymore (it's not in the manifest or in the list of packages handled by the consumer) then // it means that the consumer doesn't need that package anymore and its resources are deleted from the app space if (!dependencies.ContainsDependency(packageName) && (packagesHandledByConsumer == null || !packagesHandledByConsumer.ContainsKey(packageName))) { Directory.Delete(path, true); } } } } static void Patch(bool debugEnabled) { // if HT_PACKAGE_PACKAGEMANAGER_PROD is disabled then the consumer is not using the built-in system to deal enable/disable logging based on // consumer's configuration. This situation typically happens when the consumer is a contributor developing a Hotel package. // In this situation we want logging to be enabled #if !HT_PACKAGE_PACKAGEMANAGER_PROD Manifest manifest = GetManifest(); if (!manifest.Dependencies.ContainsDependency("com.ubisoft.hotel.packagemanager")) { ProcessPackages(null); PatchDefineSymbols(true); } #endif } #if !HT_PACKAGE_PACKAGEMANAGER_PROD private static Manifest GetManifest() { string json = UpmBuildProperty.ReadManifestJson(); return Manifest.Parse(json); } static void PatchDefineSymbols(bool debugEnabled) { BuildTargetGroup buildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget); HashSet symbols = HtPlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup); // Make sure Logger works if Unity development is enabled if (debugEnabled) { // Add Logger symbols so Debug is enabled foreach (string symbol in Debug.HT_LOGGER_SYMBOLS) { _ = symbols.Add(symbol); } } _ = symbols.Add("HT_PACKAGE_CORE_PROD"); HtPlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, symbols); } #endif // It needs to be higher than Hotel PackagaManager.BuildProcessor's one to give it the chance to define HT_PACKAGE_PACKAGEMANAGER_PROD, public int callbackOrder { get { return 1; } } public void OnPreprocessBuild(BuildReport report) { Patch(EditorUserBuildSettings.development); } } }