TH1/Unity/Assets/Scripts/TH1_Logic/Editor/TH1MigrationBuildPanel.cs
2026-06-12 23:35:09 +08:00

572 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using TH1_Logic.Editor.HybridCLR;
using TH1_Logic.Editor.YooAssetTools;
using TH1_Logic.Hotfix;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace TH1_Logic.Editor
{
public sealed class TH1MigrationBuildPanel : EditorWindow
{
private Vector2 _scroll;
private bool _developmentBuild;
private bool _runGenerateAll = true;
private bool _showAdvancedActions;
private List<MigrationStatusItem> _statusItems = new List<MigrationStatusItem>();
private string _lastAction = "Ready";
[MenuItem("Tools/TH1/iOS Migration/Build Panel")]
public static void Open()
{
var window = GetWindow<TH1MigrationBuildPanel>("TH1 Build Panel");
window.minSize = new Vector2(620, 520);
window.RefreshStatus();
window.Show();
}
private void OnEnable()
{
_developmentBuild = EditorUserBuildSettings.development;
RefreshStatus();
}
private void OnGUI()
{
DrawHeader();
DrawGuide();
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("刷新状态", GUILayout.Height(26)))
{
RefreshStatus();
}
if (GUILayout.Button("打开 StreamingAssets", GUILayout.Height(26)))
{
EditorUtility.RevealInFinder(Application.streamingAssetsPath);
}
}
_scroll = EditorGUILayout.BeginScrollView(_scroll);
DrawStatus();
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(8);
DrawActions();
}
private void DrawHeader()
{
EditorGUILayout.LabelField("TH1 iOS / HybridCLR / YooAsset 出包面板", EditorStyles.boldLabel);
EditorGUILayout.LabelField("当前平台", EditorUserBuildSettings.activeBuildTarget.ToString());
EditorGUILayout.LabelField("脚本后端", GetScriptingBackend(EditorUserBuildSettings.activeBuildTarget).ToString());
EditorGUILayout.LabelField("上次操作", _lastAction);
_developmentBuild = EditorGUILayout.ToggleLeft("热更 DLL 使用 Development 编译PC Smoke Test 建议开启;正式包可关闭)", _developmentBuild);
_runGenerateAll = EditorGUILayout.ToggleLeft("一键准备前先执行 HybridCLR Generate All迁移阶段建议保持开启", _runGenerateAll);
if (EditorApplication.isCompiling)
{
EditorGUILayout.HelpBox("Unity 正在编译脚本,等编译完成后再执行构建动作。", MessageType.Warning);
}
}
private void DrawGuide()
{
var errorCount = CountStatus(MigrationStatusLevel.Error);
var warningCount = CountStatus(MigrationStatusLevel.Warning);
if (errorCount > 0)
{
EditorGUILayout.HelpBox(
$"现在不能直接出包:还有 {errorCount} 个阻断项。你现在只需要点下面的大按钮“新手只点这个:准备当前平台出包”。",
MessageType.Error);
}
else if (warningCount > 0)
{
EditorGUILayout.HelpBox(
$"基础产物已经可用,但还有 {warningCount} 个提示项。建议点一次大按钮刷新所有产物后再出包。",
MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox(
"当前平台出包准备完成:热更 DLL、AOT Metadata、YooAsset AB 都可用。现在可以 Build Player。",
MessageType.Info);
}
EditorGUILayout.HelpBox(
"日常规则:改了代码或资源后,打 PC/iOS 包前都点一次大按钮。编辑器内 Play 测试一般不用点。",
MessageType.None);
}
private void DrawStatus()
{
foreach (var item in _statusItems)
{
EditorGUILayout.LabelField(item.Title, EditorStyles.boldLabel);
EditorGUILayout.HelpBox(item.Message, ToMessageType(item.Level));
}
}
private void DrawActions()
{
using (new EditorGUI.DisabledScope(EditorApplication.isCompiling || EditorApplication.isPlayingOrWillChangePlaymode))
{
if (GUILayout.Button("新手只点这个:准备当前平台出包(热更 DLL + AOT + AB", GUILayout.Height(44)))
{
RunAction("Prepare Player Build", PreparePlayerBuild);
}
EditorGUILayout.Space(8);
_showAdvancedActions = EditorGUILayout.Foldout(_showAdvancedActions, "高级/单项操作(平时不用)", true);
if (!_showAdvancedActions) return;
EditorGUILayout.LabelField("HybridCLR", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("配置 HybridCLR", GUILayout.Height(28)))
{
RunAction("Configure HybridCLR", TH1HybridCLRBuildTools.ConfigureHotfixSettings);
}
if (GUILayout.Button("Generate All", GUILayout.Height(28)))
{
RunAction("HybridCLR Generate All", TH1HybridCLRBuildTools.GenerateAll);
}
}
if (GUILayout.Button("构建热更 DLL 并写入 StreamingAssets", GUILayout.Height(32)))
{
RunAction("Build Hotfix DLL", () =>
{
if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild))
{
throw new Exception("Build hotfix dll failed. See Console for details.");
}
});
}
if (GUILayout.Button("测试 Hotfix 混淆", GUILayout.Height(32)))
{
RunAction("Test Hotfix Obfuscation", () =>
{
if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild, true))
{
throw new Exception("Test hotfix obfuscation failed. See Console for details.");
}
});
}
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("YooAsset", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("配置 Collector", GUILayout.Height(28)))
{
RunAction("Configure YooAsset Collector", TH1YooAssetBuildTools.ConfigureDefaultPackageCollector);
}
if (GUILayout.Button("验证 Collector 地址", GUILayout.Height(28)))
{
RunAction("Validate YooAsset Collector", TH1YooAssetBuildTools.ValidateCollectorAddresses);
}
}
if (GUILayout.Button("构建 YooAsset 内置 AB", GUILayout.Height(32)))
{
RunAction("Build YooAsset AB", TH1YooAssetBuildTools.BuildBuiltinDefaultPackage);
}
EditorGUILayout.Space(8);
EditorGUILayout.HelpBox("上面的单项按钮用于排查问题。正常出包只点顶部的大按钮。", MessageType.None);
}
}
private void PreparePlayerBuild()
{
TH1HybridCLRBuildTools.ConfigureHotfixSettings();
TH1YooAssetBuildTools.ConfigureDefaultPackageCollector();
if (_runGenerateAll)
{
TH1HybridCLRBuildTools.GenerateAll();
}
if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(_developmentBuild))
{
throw new Exception("Build hotfix dll failed. See Console for details.");
}
TH1YooAssetBuildTools.BuildBuiltinDefaultPackage();
}
private void RunAction(string title, System.Action action)
{
try
{
_lastAction = $"{title} running...";
Repaint();
AssetDatabase.SaveAssets();
action();
AssetDatabase.Refresh();
_lastAction = $"{title} OK";
Debug.Log($"[TH1.Migration] {title} OK");
}
catch (Exception e)
{
var root = e is TargetInvocationException && e.InnerException != null ? e.InnerException : e;
_lastAction = $"{title} failed: {root.Message}";
Debug.LogError($"[TH1.Migration] {title} failed:\n{root}");
}
finally
{
RefreshStatus();
Repaint();
}
}
private void RefreshStatus()
{
_statusItems = TH1MigrationBuildStatus.Collect(EditorUserBuildSettings.activeBuildTarget);
}
private int CountStatus(MigrationStatusLevel level)
{
var count = 0;
foreach (var item in _statusItems)
{
if (item.Level == level) count++;
}
return count;
}
private static MessageType ToMessageType(MigrationStatusLevel level)
{
return level switch
{
MigrationStatusLevel.Ok => MessageType.Info,
MigrationStatusLevel.Warning => MessageType.Warning,
MigrationStatusLevel.Error => MessageType.Error,
_ => MessageType.None
};
}
private static ScriptingImplementation GetScriptingBackend(BuildTarget target)
{
return PlayerSettings.GetScriptingBackend(BuildPipeline.GetBuildTargetGroup(target));
}
}
public sealed class TH1MigrationBuildPreprocessor : IPreprocessBuildWithReport
{
public int callbackOrder => -1000;
public void OnPreprocessBuild(BuildReport report)
{
if (TH1MigrationBuildValidationGate.IsSuppressed) return;
var errors = TH1MigrationBuildStatus.GetBuildBlockingMessages(report.summary.platform);
if (errors.Count == 0) return;
throw new BuildFailedException(
"[TH1.Migration] 出包前准备未完成,请打开 Tools/TH1/iOS Migration/Build Panel 执行一键准备出包。\n" +
string.Join("\n", errors));
}
}
internal static class TH1MigrationBuildValidationGate
{
private static int _suppressCount;
public static bool IsSuppressed => _suppressCount > 0;
public static IDisposable Suppress()
{
_suppressCount++;
return new SuppressScope();
}
private sealed class SuppressScope : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_suppressCount = Math.Max(0, _suppressCount - 1);
}
}
}
internal static class TH1MigrationBuildStatus
{
public static List<MigrationStatusItem> Collect(BuildTarget target)
{
return Collect(target, TH1HybridCLRBuildTools.IsHotfixObfuscationEnabled(), true);
}
public static List<MigrationStatusItem> Collect(BuildTarget target, bool expectObfuscatedHotfix)
{
return Collect(target, expectObfuscatedHotfix, true);
}
public static List<MigrationStatusItem> Collect(BuildTarget target, bool expectObfuscatedHotfix, bool checkYooAssetAb)
{
var items = new List<MigrationStatusItem>();
AddHotfixStatus(items, target, expectObfuscatedHotfix);
AddAotMetadataStatus(items, target);
if (checkYooAssetAb)
{
AddYooAssetStatus(items);
}
else
{
items.Add(new MigrationStatusItem(
"4. YooAsset 内置 AB",
MigrationStatusLevel.Warning,
"已按一体化出包选项跳过检查,将沿用 StreamingAssets 中已有的 YooAsset AB。"));
}
return items;
}
public static List<string> GetBuildBlockingMessages(BuildTarget target)
{
return GetBuildBlockingMessages(target, TH1HybridCLRBuildTools.IsHotfixObfuscationEnabled(), true);
}
public static List<string> GetBuildBlockingMessages(BuildTarget target, bool expectObfuscatedHotfix)
{
return GetBuildBlockingMessages(target, expectObfuscatedHotfix, true);
}
public static List<string> GetBuildBlockingMessages(BuildTarget target, bool expectObfuscatedHotfix, bool checkYooAssetAb)
{
var errors = new List<string>();
foreach (var item in Collect(target, expectObfuscatedHotfix, checkYooAssetAb))
{
if (item.Level == MigrationStatusLevel.Error)
{
errors.Add($"- {item.Title}: {item.Message}");
}
}
return errors;
}
private static void AddHotfixStatus(List<MigrationStatusItem> items, BuildTarget target, bool expectObfuscatedHotfix)
{
var source = TH1HybridCLRBuildTools.GetHotfixDllSourcePath(target);
var obfuscated = TH1HybridCLRBuildTools.GetObfuscatedHotfixDllPath(target);
var streaming = TH1HybridCLRBuildTools.GetStreamingHotfixDllPath();
var sourceOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(source, out var sourceMessage);
items.Add(new MigrationStatusItem(
"1. 代码热更 DLL 输出",
sourceOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error,
$"{sourceMessage}\n这是 HybridCLR 针对当前平台编出来的 TH1.Hotfix.dll。"));
var expected = source;
var expectedOk = sourceOk;
var expectedLabel = "未混淆输出";
if (expectObfuscatedHotfix)
{
var obfuscatedOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(obfuscated, out var obfuscatedMessage);
items.Add(new MigrationStatusItem(
"1.5 OPS 混淆后 DLL 输出",
obfuscatedOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error,
$"{obfuscatedMessage}\n这是打包会优先拷贝的 TH1.Hotfix.dll。"));
expected = obfuscated;
expectedOk = obfuscatedOk;
expectedLabel = "混淆输出";
}
var streamingOk = TH1HybridCLRBuildTools.IsUsableHotfixDll(streaming, out var streamingMessage);
var streamingLevel = streamingOk ? MigrationStatusLevel.Ok : MigrationStatusLevel.Error;
var streamingDetails = $"{streamingMessage}\n{streaming}";
if (streamingOk && FileContainsAscii(streaming, "UnityEditor"))
{
streamingLevel = MigrationStatusLevel.Error;
streamingDetails += "\n该 DLL 疑似引用 UnityEditor不能进 PC/iOS 包。请用面板重新构建目标平台热更 DLL。";
}
else if (streamingOk && expectedOk)
{
var sourceInfo = new FileInfo(expected);
var streamingInfo = new FileInfo(streaming);
if (streamingInfo.LastWriteTimeUtc.AddSeconds(1) < sourceInfo.LastWriteTimeUtc ||
streamingInfo.Length != sourceInfo.Length)
{
streamingLevel = MigrationStatusLevel.Error;
streamingDetails += $"\n它和{expectedLabel}不一致,请重新执行大按钮。";
}
}
items.Add(new MigrationStatusItem("2. 打包会携带的热更 DLL", streamingLevel, streamingDetails));
}
private static void AddAotMetadataStatus(List<MigrationStatusItem> items, BuildTarget target)
{
var streamingDir = TH1HybridCLRBuildTools.GetStreamingAotMetadataDir();
var missing = new List<string>();
foreach (var fileName in HotfixManifest.AotMetadataAssemblyFileNames)
{
var path = Path.Combine(streamingDir, fileName);
if (!File.Exists(path) || new FileInfo(path).Length == 0)
{
missing.Add(fileName);
}
}
var backend = PlayerSettings.GetScriptingBackend(BuildPipeline.GetBuildTargetGroup(target));
if (missing.Count > 0)
{
var level = backend == ScriptingImplementation.IL2CPP ? MigrationStatusLevel.Error : MigrationStatusLevel.Warning;
items.Add(new MigrationStatusItem(
"3. AOT Metadata",
level,
$"缺少: {string.Join(", ", missing)}\n{streamingDir}"));
return;
}
items.Add(new MigrationStatusItem("3. AOT Metadata", MigrationStatusLevel.Ok, $"OK\n{streamingDir}"));
}
private static void AddYooAssetStatus(List<MigrationStatusItem> items)
{
var packageRoot = TH1YooAssetBuildTools.GetStreamingPackageRoot();
var manifestVersion = TH1YooAssetBuildTools.GetStreamingManifestVersionPath();
if (!File.Exists(manifestVersion))
{
items.Add(new MigrationStatusItem(
"4. YooAsset 内置 AB",
MigrationStatusLevel.Error,
$"缺少 DefaultPackage manifest请构建 YooAsset 内置 AB。\n{manifestVersion}"));
return;
}
var packageFileCount = CountFiles(packageRoot);
var latestPackageWrite = GetLatestWriteTimeUtc(packageRoot);
var resourceRoot = ToAbsoluteProjectPath(TH1YooAssetBuildTools.BundleResourceRoot);
var latestResourceWrite = GetLatestWriteTimeUtc(resourceRoot);
var level = MigrationStatusLevel.Ok;
var message = $"OK, files={packageFileCount}, latest={FormatTime(latestPackageWrite)}\n{packageRoot}";
if (packageFileCount == 0)
{
level = MigrationStatusLevel.Error;
message = $"manifest 存在,但包内没有可用文件,请重新构建 YooAsset 内置 AB。\n{packageRoot}";
}
if (latestResourceWrite.HasValue && latestPackageWrite.HasValue &&
latestPackageWrite.Value.AddSeconds(1) < latestResourceWrite.Value)
{
level = MigrationStatusLevel.Error;
message += $"\nBundleResources 最近变更时间 {FormatTime(latestResourceWrite)} 晚于 AB请重新构建 YooAsset 内置 AB。";
}
items.Add(new MigrationStatusItem("4. YooAsset 内置 AB", level, message));
}
private static int CountFiles(string root)
{
if (!Directory.Exists(root)) return 0;
var count = 0;
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) continue;
count++;
}
return count;
}
private static DateTime? GetLatestWriteTimeUtc(string root)
{
if (!Directory.Exists(root)) return null;
DateTime? latest = Directory.GetLastWriteTimeUtc(root);
foreach (var directory in Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories))
{
var writeTime = Directory.GetLastWriteTimeUtc(directory);
if (!latest.HasValue || writeTime > latest.Value) latest = writeTime;
}
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
if (file.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) continue;
var writeTime = File.GetLastWriteTimeUtc(file);
if (!latest.HasValue || writeTime > latest.Value) latest = writeTime;
}
return latest;
}
private static string ToAbsoluteProjectPath(string projectRelativePath)
{
var projectRoot = Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath;
return Path.Combine(projectRoot, projectRelativePath.Replace('/', Path.DirectorySeparatorChar));
}
private static string FormatTime(DateTime? utcTime)
{
return utcTime.HasValue ? utcTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") : "missing";
}
private static bool FileContainsAscii(string path, string marker)
{
var bytes = File.ReadAllBytes(path);
var markerBytes = Encoding.ASCII.GetBytes(marker);
for (var i = 0; i <= bytes.Length - markerBytes.Length; i++)
{
var matched = true;
for (var j = 0; j < markerBytes.Length; j++)
{
if (bytes[i + j] == markerBytes[j]) continue;
matched = false;
break;
}
if (matched) return true;
}
return false;
}
}
internal enum MigrationStatusLevel
{
Ok,
Warning,
Error
}
internal readonly struct MigrationStatusItem
{
public readonly string Title;
public readonly MigrationStatusLevel Level;
public readonly string Message;
public MigrationStatusItem(string title, MigrationStatusLevel level, string message)
{
Title = title;
Level = level;
Message = message;
}
}
}