using CaoCao.XAsset; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEngine; namespace CaoCao.Editor { public static class Builder { public static IBuildPipeline BuildPipeline { get; set; } = new DefaultBuildPipeline(); public static Action PreprocessBuildBundles { get; set; } = null; public static Action PostprocessBuildBundles { get; set; } = null; //打包前所有文件 public static List BuildBeforeAllFiles = new List(); //打包后所有文件 public static List BuildAfterAllFiles = new List(); public static void BuildBundles(params Build[] builds) { FindBuildBeforeAllFiles(); BuildBundlesInternal(false, false, builds); } public static void BuildHotfix(params Build[] builds) { FindBuildBeforeAllFiles(); BuildBundlesInternal(false, true, builds); } public static void BuildBundlesWithLastBuild(params Build[] builds) { BuildBundlesInternal(true, false, builds); } private static void BuildBundlesInternal(bool withLastBuild, bool isHotfix, params Build[] builds) { var settings = Settings.GetDefaultSettings(); if (builds.Length == 0) builds = Settings.FindAssets(); PreprocessBuildBundles?.Invoke(builds, settings); if (settings.bundle.checkReference && FindReferences()) return; CreateDirectories(); //获取到所有Build配置 var assets = Array.ConvertAll(builds, AssetDatabase.GetAssetPath); if (assets.Length == 0) { XAsset.Logger.I("Nothing to build."); return; } var watch = new Stopwatch(); watch.Start(); var changes = new List(); foreach (var asset in assets) { var build = AssetDatabase.LoadAssetAtPath(asset); var parameters = build.parameters; parameters.name = build.name; ////只打hotfix 只收集 //if (isHotfix && build.name != "HotfixB") //{ // var taskHotfix = BuildJob.StartNew(parameters, new LoadBuildAssets()); // continue; //} var tmps = build.name.Split('_'); #if UNITY_ANDROID //HotfixB_Android if (tmps.Length == 2) { if (tmps[1] != "Android") continue; } #elif UNITY_IOS //HotfixB_IOS if (tmps.Length == 2) { if (tmps[1] != "IOS") continue; } #else //HotfixB_Win if (tmps.Length == 2) { if (tmps[1] != "Win") continue; } #endif //只打hotfix if (isHotfix && tmps[0] != "HotfixB") { //var taskHotfix = BuildJob.StartNew(parameters, new LoadBuildAssets()); continue; } var task = withLastBuild ? BuildJob.StartNew(parameters, new LoadBuildAssets(), new BuildBundles(), new BuildVersions()) : parameters.optimizeDependentAssets ? BuildJob.StartNew(parameters, new CollectAssets(), new ClearDuplicateAssets(), new OptimizeDependentAssets(), new SaveBuildAssets(), new BuildBundles(), new BuildVersions()) : BuildJob.StartNew(parameters, new CollectAssets(), new ClearDuplicateAssets(), new SaveBuildAssets(), new BuildBundles(), new BuildVersions()); if (!string.IsNullOrEmpty(task.error)) { XAsset.Logger.E(task.error); } else { if (task.changes.Count <= 0) continue; foreach (var change in task.changes) changes.Add(Settings.GetDataPath(change)); } } watch.Stop(); XAsset.Logger.I($"Finish {nameof(BuildBundles)} with {watch.ElapsedMilliseconds / 1000f}s."); if (changes.Count <= 0) return; PostprocessBuildBundles?.Invoke(changes.ToArray()); SaveVersions(changes); } private static void SaveVersions(List changes) { var versions = Settings.GetDefaultVersions(); var path = Settings.GetDataPath(versions.GetFilename()); var configs = Settings.FindAssets(); var packs = new List(); foreach (var config in configs) if (config.deliveryMode == AssetPack.DeliveryMode.InstallTime || config.deliveryMode == AssetPack.DeliveryMode.FastFollow) packs.Add(config.name); versions.preloadPacks = packs.ToArray(); versions.Save(Settings.GetCachePath(Versions.Filename)); versions.Save(path); changes.Add(path); PostprocessBuildBundles?.Invoke(changes.ToArray()); var file = new FileInfo(path); BuildUpdateInfo(versions, XAsset.Utility.ComputeHash(path), file.Length); // updateInfo. var size = GetChanges(changes.ToArray(), versions.GetFilename()); var savePath = Settings.GetCachePath(BuildRecords.Filename); var records = XAsset.Utility.LoadFromFile(savePath); records.Set(versions.GetFilename(), changes.ToArray(), size); var json = JsonUtility.ToJson(records); File.WriteAllText(savePath, json); CopyDifferenceFiles($"versions_v{versions.ToString()}"); } public static void BuildUpdateInfo(Versions versions, string hash, long size) { var settings = Settings.GetDefaultSettings(); var downloadURL = $"{settings.downloadURL}{Assets.Bundles}/{Settings.Platform}/"; var updateInfoPath = Settings.GetCachePath(UpdateInfo.Filename); var updateInfo = CaoCao.XAsset.Utility.LoadFromFile(updateInfoPath); updateInfo.hash = hash; updateInfo.size = (ulong) size; updateInfo.file = versions.GetFilename(); updateInfo.downloadURL = downloadURL; //updateInfo.bigVersion = settings.BigVersion; File.WriteAllText(updateInfoPath, JsonUtility.ToJson(updateInfo)); } private static void CreateDirectories() { var directories = new List { Settings.PlatformCachePath, Settings.PlatformDataPath }; foreach (var directory in directories) if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); } public static bool FindReferences() { var builds = Settings.FindAssets(); if (builds.Length == 0) { XAsset.Logger.I("Nothing to build."); return false; } var assets = new List(); foreach (var build in builds) { var item = build.parameters; item.name = build.name; var task = item.optimizeDependentAssets ? BuildJob.StartNew(item, new CollectAssets(), new OptimizeDependentAssets()) : BuildJob.StartNew(item, new CollectAssets()); if (!string.IsNullOrEmpty(task.error)) return true; foreach (var asset in task.bundledAssets) assets.Add(asset); foreach (var asset in task.rawAssets) assets.Add(asset); } var assetWithGroups = new Dictionary>(); foreach (var asset in assets) { if (!assetWithGroups.TryGetValue(asset.path, out var refs)) { refs = new HashSet(); assetWithGroups.Add(asset.path, refs); } refs.Add($"{asset.group.build}-{asset.group.name}"); } var sb = new StringBuilder(); foreach (var pair in assetWithGroups.Where(pair => pair.Value.Count > 1)) { sb.AppendLine(pair.Key); foreach (var s in pair.Value) sb.AppendLine(" - " + s); } var content = sb.ToString(); if (string.IsNullOrEmpty(content)) { XAsset.Logger.I("Checking completed, Everything is ok."); return false; } const string filename = "MultipleReferences.txt"; File.WriteAllText(filename, content); if (EditorUtility.DisplayDialog("提示", "检测到多重引用关系,是否打开查看?", "确定")) EditorUtility.OpenWithDefaultApp(filename); return true; } public static void RemoveBundles(params string[] assetPaths) { var bundles = new HashSet(); var versions = Settings.GetDefaultVersions(); foreach (var version in versions.data) { var manifest = CaoCao.XAsset.Utility.LoadFromFile(Settings.GetDataPath(version.file)); foreach (var assetPath in assetPaths) { var bundle = manifest.GetBundle(assetPath); if (bundle != null) bundles.Add(bundle); } } var files = new List(); foreach (var bundle in bundles) { files.Add(Settings.GetCachePath(bundle.name)); files.Add(Settings.GetCachePath(bundle.name + ".manifest")); files.Add(Settings.GetDataPath(bundle.file)); } var sb = new StringBuilder(); sb.AppendLine("Delete Files:"); foreach (var file in files) { if (!File.Exists(file)) continue; File.Delete(file); sb.AppendLine($"Delete: {file}"); } CaoCao.XAsset.Logger.I(sb.ToString()); } public static ManifestBundle[] GetBundlesInBuild(Settings settings, Versions versions) { var set = new HashSet(); var assets = new HashSet(); //只有Settings中PlayerAssetsSplitMode设置为SplitByAssetPacksWithInstallTime或IncludeAllAssets才算 switch (settings.playerAssetsSplitMode) { case PlayerAssetsSplitMode.SplitByAssetPacksWithInstallTime: var assetPackConfigs = Settings.FindAssets(); var assetPacks = new List(); //AssetPack中交付模式设置为InstallTime才算 foreach (var assetPack in assetPackConfigs) { if (assetPack.deliveryMode == AssetPack.DeliveryMode.InstallTime) assetPacks.Add(assetPack); } foreach (var config in assetPacks) { //找到这个AssetPack中所有的asset Settings.Collect(config, assets); //只有在versions.data中的bundle才算 foreach (var version in versions.data) { var manifest = XAsset.Utility.LoadFromFile(Settings.GetDataPath(version.file)); foreach (var path in assets) { var bundle = manifest.GetBundle(path); if (bundle == null) continue; set.Add(bundle); set.UnionWith(manifest.GetDependencies(bundle)); } } } break; case PlayerAssetsSplitMode.ExcludeAllAssets: break; case PlayerAssetsSplitMode.IncludeAllAssets: foreach (var version in versions.data) { var path = Settings.GetDataPath(version.file); var manifest = XAsset.Utility.LoadFromFile(path); set.UnionWith(manifest.bundles); } break; } return set.ToArray(); } public static ulong GetChanges(IEnumerable changes, string name) { var sb = new StringBuilder(); var size = 0UL; var files = new List(); foreach (var change in changes) { var file = new FileInfo(change); if (!file.Exists) continue; size += (ulong) file.Length; files.Add(file); } files.Sort((a, b) => b.Length.CompareTo(a.Length)); foreach (var file in files) sb.AppendLine($"{file.FullName}({CaoCao.XAsset.Utility.FormatBytes((ulong) file.Length)})"); XAsset.Logger.I(size > 0 ? $"GetChanges from {name} with following files({CaoCao.XAsset.Utility.FormatBytes(size)}):\n {sb}" : "Nothing changed."); return size; } public static void BuildReferences() { var builds = Settings.FindAssets(); var assets = new List(); foreach (var build in builds) { var job = BuildJob.StartNew(build.parameters, new CollectAssets(), new ClearDuplicateAssets(), new OptimizeDependentAssets(), new SaveBuildAssets()); assets.AddRange(job.bundledAssets); assets.AddRange(job.rawAssets); } var cache = Settings.GetBuildAssetCache(); var newFiles = assets.Count - cache.data.Count; XAsset.Logger.I($"Build Asset References with {newFiles} files."); cache.data = assets; cache.BuildReferences(); EditorUtility.SetDirty(cache); AssetDatabase.SaveAssets(); Selection.activeObject = cache; EditorUtility.FocusProjectWindow(); } //-------------------------------------------------------比对差异文件----------------------------------------------------- public static void FindBuildBeforeAllFiles() { string folderPath = Settings.PlatformDataPath + "/"; UnityEngine.Debug.Log($"热更资源文件夹 = {folderPath}"); if (!Directory.Exists(folderPath)) return; BuildBeforeAllFiles = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories).ToList(); UnityEngine.Debug.Log($"打包前:文件数量 = {BuildBeforeAllFiles.Count}"); } public static void CopyDifferenceFiles(string dirName) { string newFolder = $"{Environment.CurrentDirectory.Replace('\\', '/')}/{Assets.Bundles}/{dirName}"; if (!Directory.Exists(newFolder)) Directory.CreateDirectory(newFolder); string folderPath = Settings.PlatformDataPath + "/"; BuildAfterAllFiles = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories).ToList(); UnityEngine.Debug.Log($"打包后:文件数量 = {BuildAfterAllFiles.Count}"); foreach(var fileName in BuildAfterAllFiles) { if (BuildBeforeAllFiles.Contains(fileName)) continue; //分层建立文件夹 F:/HxMobileProject/Bundles/iOS/assetspackb/xxx.bundle string t = fileName.Replace('\\', '/'); //assetspackb/xxx.bundle string tmp = t.Replace(folderPath, ""); string[] tmps = tmp.Split('/'); foreach(var ss in tmps) { //xxx.bundle if (ss.EndsWith(".bundle") || ss.EndsWith(".json")) continue; //ss = assetspackb //F:/HxMobileProject/Bundles/iOS/assetspackb string tmpDir = newFolder + "/" + ss; if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); } string dest = newFolder + "/" + tmp; File.Copy(fileName, dest); } //复制updateinfo var updateInfoPath = Settings.GetCachePath(UpdateInfo.Filename); string destPath = newFolder + "/" + UpdateInfo.Filename; File.Copy(updateInfoPath, destPath); } } }