using AssetStoreTools.Uploader.Utility; using AssetStoreTools.Utility; using AssetStoreTools.Utility.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; namespace AssetStoreTools.Exporter { internal class PackageExporterDefault : PackageExporter { private const string TemporaryExportPathName = "CustomExport"; private DefaultExporterSettings _exportSettings; private PackageExporterDefault(DefaultExporterSettings exportSettings) { _exportSettings = exportSettings; } public static ExportResult ExportPackage(DefaultExporterSettings exportSettings) { var exporter = new PackageExporterDefault(exportSettings); return exporter.ExportPackage(); } private ExportResult ExportPackage() { ASDebug.Log("Using custom package exporter"); // Save assets before exporting EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepSavingAssets, 0.1f); AssetDatabase.SaveAssets(); try { // Create a temporary export path var temporaryExportPath = GetTemporaryExportPath(); if (!Directory.Exists(temporaryExportPath)) Directory.CreateDirectory(temporaryExportPath); // Construct an unzipped package structure CreateTempPackageStructure(temporaryExportPath); // Build a .unitypackage file from the temporary folder CreateUnityPackage(temporaryExportPath, _exportSettings.OutputFilename); EditorUtility.RevealInFinder(_exportSettings.OutputFilename); ASDebug.Log($"Package file has been created at {_exportSettings.OutputFilename}"); return new ExportResult() { Success = true, ExportedPath = _exportSettings.OutputFilename }; } catch (Exception e) { return new ExportResult() { Success = false, Error = ASError.GetGenericError(e) }; } finally { PostExportCleanup(); } } private string GetTemporaryExportPath() { return $"{AssetStoreCache.TempCachePath}/{TemporaryExportPathName}"; } private void CreateTempPackageStructure(string tempOutputPath) { EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepGatheringFiles, 0.4f); var pathGuidPairs = GetPathGuidPairs(_exportSettings.ExportPaths); // Caching asset previews takes time, so we'll start doing it as we // iterate through assets and only retrieve them after generating the rest // of the package structure AssetPreview.SetPreviewTextureCacheSize(pathGuidPairs.Count + 100); var pathObjectPairs = new Dictionary(); foreach (var pair in pathGuidPairs) { var originalAssetPath = pair.Key; var outputAssetPath = $"{tempOutputPath}/{pair.Value}"; if (Directory.Exists(outputAssetPath)) { var path1 = File.ReadAllText($"{outputAssetPath}/pathname"); var path2 = originalAssetPath; throw new InvalidOperationException($"Multiple assets with guid {pair.Value} have been detected " + $"when exporting the package. Please resolve the guid conflicts and try again:\n{path1}\n{path2}"); } Directory.CreateDirectory(outputAssetPath); // Every exported asset has a pathname file using (StreamWriter writer = new StreamWriter($"{outputAssetPath}/pathname")) writer.Write(originalAssetPath); // Only files (not folders) have an asset file if (File.Exists(originalAssetPath)) File.Copy(originalAssetPath, $"{outputAssetPath}/asset"); // Most files and folders have an asset.meta file (but ProjectSettings folder assets do not) if (File.Exists($"{originalAssetPath}.meta")) File.Copy($"{originalAssetPath}.meta", $"{outputAssetPath}/asset.meta"); // To-do: handle previews in hidden folders as they are not part of the AssetDatabase var previewObject = AssetDatabase.LoadAssetAtPath(originalAssetPath); if (previewObject == null) continue; // Start caching the asset preview AssetPreview.GetAssetPreview(previewObject); pathObjectPairs.Add(outputAssetPath, previewObject); } WritePreviewTextures(pathObjectPairs); if (_exportSettings.Dependencies == null || _exportSettings.Dependencies.Length == 0) return; var exportDependenciesDict = JsonValue.NewDict(); var allRegistryPackages = PackageUtility.GetAllRegistryPackages(); foreach(var exportDependency in _exportSettings.Dependencies) { var registryPackage = allRegistryPackages.FirstOrDefault(x => x.name == exportDependency); if (registryPackage == null) { // Package is either not from a registry source or does not exist in the project UnityEngine.Debug.LogWarning($"Found an unsupported Package Manager dependency: {exportDependency}.\n" + "This dependency is not supported in the project's manifest.json and will be skipped."); continue; } exportDependenciesDict[registryPackage.name] = registryPackage.version; } var exportManifestJson = JsonValue.NewDict(); exportManifestJson["dependencies"] = exportDependenciesDict; var tempManifestDirectoryPath = $"{tempOutputPath}/packagemanagermanifest"; Directory.CreateDirectory(tempManifestDirectoryPath); var tempManifestFilePath = $"{tempManifestDirectoryPath}/asset"; File.WriteAllText(tempManifestFilePath, exportManifestJson.ToString()); } private Dictionary GetPathGuidPairs(string[] exportPaths) { var pathGuidPairs = new Dictionary(); foreach (var exportPath in exportPaths) { var assetPaths = GetAssetPaths(exportPath); foreach (var assetPath in assetPaths) { var guid = GetAssetGuid(assetPath, true, true); if (string.IsNullOrEmpty(guid)) continue; pathGuidPairs.Add(assetPath, guid); } } return pathGuidPairs; } private void WritePreviewTextures(Dictionary pathObjectPairs) { foreach (var kvp in pathObjectPairs) { var obj = kvp.Value; var queuePreview = false; switch (obj) { case Material _: case TerrainLayer _: case AudioClip _: case Mesh _: case Texture _: case UnityEngine.Tilemaps.Tile _: case GameObject _: queuePreview = true; break; } if (!queuePreview) continue; AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _); var preview = GetAssetPreviewFromGuid(guid); if (!preview) continue; var thumbnailWidth = Mathf.Min(preview.width, 128); var thumbnailHeight = Mathf.Min(preview.height, 128); var rt = RenderTexture.GetTemporary(thumbnailWidth, thumbnailHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.sRGB); var copy = new Texture2D(rt.width, rt.height, TextureFormat.ARGB32, false); RenderTexture.active = rt; GL.Clear(true, true, new Color(0, 0, 0, 0)); Graphics.Blit(preview, rt); copy.ReadPixels(new Rect(0, 0, copy.width, copy.height), 0, 0, false); copy.Apply(); RenderTexture.active = null; var bytes = copy.EncodeToPNG(); if (bytes != null && bytes.Length > 0) { File.WriteAllBytes(kvp.Key + "/preview.png", bytes); } RenderTexture.ReleaseTemporary(rt); } } private Texture2D GetAssetPreviewFromGuid(string guid) { var method = typeof(AssetPreview).GetMethod("GetAssetPreviewFromGUID", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string) }, null); var args = new object[] { guid }; return method?.Invoke(null, args) as Texture2D; } private void CreateUnityPackage(string pathToArchive, string outputPath) { if (Directory.GetDirectories(pathToArchive).Length == 0) throw new InvalidOperationException("Unable to export package. The specified path is empty"); EditorUtility.DisplayProgressBar(ProgressBarTitle, ProgressBarStepCompressingPackage, 0.5f); // Archiving process working path will be set to the // temporary package path so adjust the output path accordingly if (!Path.IsPathRooted(outputPath)) outputPath = $"{Application.dataPath.Substring(0, Application.dataPath.Length - "/Assets".Length)}/{outputPath}"; #if UNITY_EDITOR_WIN CreateUnityPackageUniversal(pathToArchive, outputPath); #elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX CreateUnityPackageOsxLinux(pathToArchive, outputPath); #endif } private void CreateUnityPackageUniversal(string pathToArchive, string outputPath) { var _7zPath = EditorApplication.applicationContentsPath; #if UNITY_EDITOR_WIN _7zPath = Path.Combine(_7zPath, "Tools", "7z.exe"); #elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX _7zPath = Path.Combine(_7zPath, "Tools", "7za"); #endif if (!File.Exists(_7zPath)) throw new FileNotFoundException("Archiving utility was not found in your Unity installation directory"); var argumentsTar = $"a -r -ttar -y -bd archtemp.tar ."; var result = StartProcess(_7zPath, argumentsTar, pathToArchive); if (result != 0) throw new Exception("Failed to compress the package"); // Create a GZIP archive var argumentsGzip = $"a -tgzip -bd -y \"{outputPath}\" archtemp.tar"; result = StartProcess(_7zPath, argumentsGzip, pathToArchive); if (result != 0) throw new Exception("Failed to compress the package"); } private void CreateUnityPackageOsxLinux(string pathToArchive, string outputPath) { var tarPath = "/usr/bin/tar"; if (!File.Exists(tarPath)) { // Fallback to the universal export method ASDebug.LogWarning("'/usr/bin/tar' executable not found. Falling back to 7za"); CreateUnityPackageUniversal(pathToArchive, outputPath); return; } // Create a TAR archive var arguments = $"-czpf \"{outputPath}\" ."; var result = StartProcess(tarPath, arguments, pathToArchive); if (result != 0) throw new Exception("Failed to compress the package"); } private int StartProcess(string processPath, string arguments, string workingDirectory) { var info = new ProcessStartInfo() { FileName = processPath, Arguments = arguments, WorkingDirectory = workingDirectory, CreateNoWindow = true, UseShellExecute = false }; #if UNITY_EDITOR_OSX // Prevent OSX-specific archive pollution info.EnvironmentVariables.Add("COPYFILE_DISABLE", "1"); #endif using (Process process = Process.Start(info)) { process.WaitForExit(); return process.ExitCode; } } protected override void PostExportCleanup() { base.PostExportCleanup(); var tempExportPath = GetTemporaryExportPath(); if (Directory.Exists(tempExportPath)) Directory.Delete(tempExportPath, true); } } }