343 lines
14 KiB
C#
343 lines
14 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using TH1_Logic.Hotfix;
|
||
using UnityEditor;
|
||
using UnityEditor.Build;
|
||
using UnityEngine;
|
||
|
||
namespace TH1_Logic.Editor.HybridCLR
|
||
{
|
||
public static class TH1HybridCLRBuildTools
|
||
{
|
||
private const string HybridClrInstallerMenu = "HybridCLR/Installer...";
|
||
private const string HybridClrGenerateAllMenu = "HybridCLR/Generate/All";
|
||
public const long MinimumHotfixDllSize = 64 * 1024;
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/1. Run HybridCLR Installer")]
|
||
public static void RunHybridClrInstaller()
|
||
{
|
||
ExecuteHybridClrMenu(HybridClrInstallerMenu);
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/2. Configure TH1 Hotfix Settings")]
|
||
public static void ConfigureHotfixSettings()
|
||
{
|
||
var settingsType = FindType("HybridCLR.Editor.Settings.HybridCLRSettings");
|
||
if (settingsType == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] HybridCLR package is not available yet. Let Unity resolve packages, then run this menu again.");
|
||
return;
|
||
}
|
||
|
||
var instance = settingsType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
||
if (instance == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] HybridCLRSettings.Instance is unavailable.");
|
||
return;
|
||
}
|
||
|
||
SetBoolField(settingsType, instance, "enable", true);
|
||
AddStringToArrayField(settingsType, instance, "hotUpdateAssemblies", HotfixManifest.HotfixAssemblyName);
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "mscorlib");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System.Core");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "MemoryPack");
|
||
AddStringToArrayField(settingsType, instance, "patchAOTAssemblies", "System.Runtime.CompilerServices.Unsafe");
|
||
|
||
settingsType.GetMethod("Save", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
|
||
Debug.Log("[TH1.HybridCLR] Configured HybridCLRSettings for TH1.Hotfix.");
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/3. Generate All")]
|
||
public static void GenerateAll()
|
||
{
|
||
using (TH1MigrationBuildValidationGate.Suppress())
|
||
{
|
||
EnsureHybridClrInstalled();
|
||
InvokeHybridClrGenerateAll();
|
||
EnsureAotMetadataSourceFiles(EditorUserBuildSettings.activeBuildTarget);
|
||
}
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/4. Compile Hotfix Dll")]
|
||
public static void CompileHotfixDll()
|
||
{
|
||
TryCompileHotfixDll(EditorUserBuildSettings.development);
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/5. Copy Hotfix Artifacts To StreamingAssets")]
|
||
public static void CopyHotfixArtifactsToStreamingAssets()
|
||
{
|
||
BuildAndCopyHotfixArtifacts(EditorUserBuildSettings.development);
|
||
}
|
||
|
||
public static bool BuildAndCopyHotfixArtifacts(bool developmentBuild)
|
||
{
|
||
if (!TryCompileHotfixDll(developmentBuild))
|
||
{
|
||
Debug.LogError("[TH1.HybridCLR] Compile hotfix dll failed. Skip copying hotfix artifacts.");
|
||
return false;
|
||
}
|
||
|
||
var copiedHotfixDll = CopyHotfixDll();
|
||
var copiedAotMetadata = CopyAotMetadataDlls();
|
||
AssetDatabase.Refresh();
|
||
return copiedHotfixDll && copiedAotMetadata;
|
||
}
|
||
|
||
[MenuItem("Tools/TH1/iOS Migration/HybridCLR/6. Test Load StreamingAssets Hotfix In Editor")]
|
||
public static void TestLoadHotfixInEditor()
|
||
{
|
||
var loaded = HotfixBootstrap.InitializeFromStreamingAssets(true);
|
||
Debug.Log($"[TH1.HybridCLR] Test load result: {loaded}, assembly: {HotfixBootstrap.LoadedAssemblyFullName}");
|
||
}
|
||
|
||
public static string GetHotfixDllSourcePath(BuildTarget buildTarget)
|
||
{
|
||
return Path.Combine("HybridCLRData", "HotUpdateDlls", buildTarget.ToString(), HotfixManifest.HotfixAssemblyName + ".dll");
|
||
}
|
||
|
||
public static string GetStreamingHotfixDllPath()
|
||
{
|
||
return Path.Combine(
|
||
Application.streamingAssetsPath,
|
||
HotfixManifest.RootFolderName,
|
||
HotfixManifest.HotfixDllFolderName,
|
||
HotfixManifest.HotfixAssemblyFileName);
|
||
}
|
||
|
||
public static string GetAotMetadataSourceDir(BuildTarget buildTarget)
|
||
{
|
||
return Path.Combine("HybridCLRData", "AssembliesPostIl2CppStrip", buildTarget.ToString());
|
||
}
|
||
|
||
public static string GetStreamingAotMetadataDir()
|
||
{
|
||
return Path.Combine(Application.streamingAssetsPath, HotfixManifest.RootFolderName, HotfixManifest.AotMetadataFolderName);
|
||
}
|
||
|
||
public static bool IsUsableHotfixDll(string path, out string message)
|
||
{
|
||
if (!File.Exists(path))
|
||
{
|
||
message = $"缺失: {path}";
|
||
return false;
|
||
}
|
||
|
||
var file = new FileInfo(path);
|
||
if (file.Length < MinimumHotfixDllSize)
|
||
{
|
||
message = $"文件过小,通常表示还是旧的空壳 DLL({FormatBytes(file.Length)}): {path}";
|
||
return false;
|
||
}
|
||
|
||
message = $"OK({FormatBytes(file.Length)},{file.LastWriteTime:yyyy-MM-dd HH:mm:ss})";
|
||
return true;
|
||
}
|
||
|
||
private static bool CopyHotfixDll()
|
||
{
|
||
var source = GetHotfixDllSourcePath(EditorUserBuildSettings.activeBuildTarget);
|
||
if (File.Exists(source) && new FileInfo(source).Length < MinimumHotfixDllSize)
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] hotfix dll is too small and looks stale: {source}");
|
||
return false;
|
||
}
|
||
|
||
var destination = GetStreamingHotfixDllPath();
|
||
|
||
return CopyFile(source, destination, "hotfix dll");
|
||
}
|
||
|
||
private static bool CopyAotMetadataDlls()
|
||
{
|
||
var sourceDir = GetAotMetadataSourceDir(EditorUserBuildSettings.activeBuildTarget);
|
||
var destinationDir = GetStreamingAotMetadataDir();
|
||
|
||
if (!Directory.Exists(sourceDir))
|
||
{
|
||
Debug.LogError($"[TH1.HybridCLR] AOT metadata source missing: {sourceDir}. Run HybridCLR Installer, then HybridCLR/Generate/All after an IL2CPP target is selected.");
|
||
return false;
|
||
}
|
||
|
||
var copiedAll = true;
|
||
Directory.CreateDirectory(destinationDir);
|
||
foreach (var fileName in HotfixManifest.AotMetadataAssemblyFileNames)
|
||
{
|
||
var source = Path.Combine(sourceDir, fileName.Replace(".bytes", ""));
|
||
var destination = Path.Combine(destinationDir, fileName);
|
||
if (!File.Exists(source))
|
||
{
|
||
copiedAll = false;
|
||
Debug.LogError($"[TH1.HybridCLR] AOT metadata dll missing: {source}");
|
||
continue;
|
||
}
|
||
|
||
File.Copy(source, destination, true);
|
||
Debug.Log($"[TH1.HybridCLR] Copied AOT metadata: {destination}");
|
||
}
|
||
|
||
return copiedAll;
|
||
}
|
||
|
||
private static bool ExecuteHybridClrMenu(string menuPath)
|
||
{
|
||
if (EditorApplication.ExecuteMenuItem(menuPath))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
Debug.LogWarning($"[TH1.HybridCLR] Unity menu not found: {menuPath}. Make sure the HybridCLR package has been resolved.");
|
||
return false;
|
||
}
|
||
|
||
private static void EnsureHybridClrInstalled()
|
||
{
|
||
var installerType = FindType("HybridCLR.Editor.Installer.InstallerController");
|
||
if (installerType == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] InstallerController was not found. Make sure the HybridCLR package has been resolved.");
|
||
}
|
||
|
||
var controller = Activator.CreateInstance(installerType);
|
||
var method = installerType.GetMethod("HasInstalledHybridCLR", BindingFlags.Public | BindingFlags.Instance);
|
||
if (method == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] InstallerController.HasInstalledHybridCLR was not found.");
|
||
}
|
||
|
||
var installed = method.Invoke(controller, null) is bool value && value;
|
||
if (!installed)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] HybridCLR is not initialized. Open Tools/TH1/iOS Migration/HybridCLR/1. Run HybridCLR Installer, click Install in the installer window, then run Prepare Player Build again.");
|
||
}
|
||
}
|
||
|
||
private static void InvokeHybridClrGenerateAll()
|
||
{
|
||
var commandType = FindType("HybridCLR.Editor.Commands.PrebuildCommand");
|
||
if (commandType == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] PrebuildCommand was not found. Make sure the HybridCLR package has been resolved.");
|
||
}
|
||
|
||
var method = commandType.GetMethod("GenerateAll", BindingFlags.Public | BindingFlags.Static);
|
||
if (method == null)
|
||
{
|
||
throw new BuildFailedException("[TH1.HybridCLR] PrebuildCommand.GenerateAll was not found.");
|
||
}
|
||
|
||
try
|
||
{
|
||
method.Invoke(null, null);
|
||
}
|
||
catch (TargetInvocationException e)
|
||
{
|
||
var root = e.InnerException ?? e;
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All failed: {root.Message}\n{root}");
|
||
}
|
||
}
|
||
|
||
private static void EnsureAotMetadataSourceFiles(BuildTarget buildTarget)
|
||
{
|
||
var sourceDir = GetAotMetadataSourceDir(buildTarget);
|
||
if (!Directory.Exists(sourceDir))
|
||
{
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All did not create AOT metadata directory: {sourceDir}");
|
||
}
|
||
|
||
var missing = HotfixManifest.AotMetadataAssemblyFileNames
|
||
.Select(fileName => fileName.Replace(".bytes", ""))
|
||
.Where(fileName => !File.Exists(Path.Combine(sourceDir, fileName)))
|
||
.ToArray();
|
||
|
||
if (missing.Length > 0)
|
||
{
|
||
throw new BuildFailedException($"[TH1.HybridCLR] Generate All did not create required AOT metadata files under {sourceDir}: {string.Join(", ", missing)}");
|
||
}
|
||
}
|
||
|
||
private static bool CopyFile(string source, string destination, string label)
|
||
{
|
||
if (!File.Exists(source))
|
||
{
|
||
Debug.LogWarning($"[TH1.HybridCLR] {label} source missing: {source}");
|
||
return false;
|
||
}
|
||
|
||
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
||
File.Copy(source, destination, true);
|
||
Debug.Log($"[TH1.HybridCLR] Copied {label}: {destination}");
|
||
return true;
|
||
}
|
||
|
||
private static bool TryCompileHotfixDll(bool developmentBuild)
|
||
{
|
||
var compileCommandType = FindType("HybridCLR.Editor.Commands.CompileDllCommand");
|
||
if (compileCommandType == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] CompileDllCommand was not found. Make sure the HybridCLR package has been resolved.");
|
||
return false;
|
||
}
|
||
|
||
var compileMethod = compileCommandType.GetMethod(
|
||
"CompileDll",
|
||
BindingFlags.Public | BindingFlags.Static,
|
||
null,
|
||
new[] { typeof(BuildTarget), typeof(bool) },
|
||
null);
|
||
if (compileMethod == null)
|
||
{
|
||
Debug.LogWarning("[TH1.HybridCLR] CompileDll(BuildTarget, bool) was not found.");
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
compileMethod.Invoke(null, new object[] { EditorUserBuildSettings.activeBuildTarget, developmentBuild });
|
||
return true;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
var root = e is TargetInvocationException && e.InnerException != null ? e.InnerException : e;
|
||
Debug.LogError($"[TH1.HybridCLR] Compile hotfix dll failed:\n{root}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static string FormatBytes(long length)
|
||
{
|
||
if (length >= 1024 * 1024) return $"{length / 1024f / 1024f:F2} MB";
|
||
if (length >= 1024) return $"{length / 1024f:F1} KB";
|
||
return $"{length} B";
|
||
}
|
||
|
||
private static Type FindType(string fullName)
|
||
{
|
||
return AppDomain.CurrentDomain.GetAssemblies()
|
||
.Select(assembly => assembly.GetType(fullName, false))
|
||
.FirstOrDefault(type => type != null);
|
||
}
|
||
|
||
private static void SetBoolField(Type type, object instance, string fieldName, bool value)
|
||
{
|
||
type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance)?.SetValue(instance, value);
|
||
}
|
||
|
||
private static void AddStringToArrayField(Type type, object instance, string fieldName, string value)
|
||
{
|
||
var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
|
||
if (field == null) return;
|
||
|
||
var values = ((string[])field.GetValue(instance) ?? Array.Empty<string>()).ToList();
|
||
if (!values.Contains(value))
|
||
{
|
||
values.Add(value);
|
||
field.SetValue(instance, values.ToArray());
|
||
}
|
||
}
|
||
}
|
||
}
|