TH1/Unity/Assets/Scripts/TH1_Logic/Editor/TH1MigrationCommandLine.cs
2026-06-29 11:07:07 +08:00

543 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TH1_Logic.Editor.HybridCLR;
using TH1_Logic.Editor.YooAssetTools;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
using DiagnosticsProcess = System.Diagnostics.Process;
using DiagnosticsProcessStartInfo = System.Diagnostics.ProcessStartInfo;
namespace TH1_Logic.Editor
{
public static class TH1MigrationCommandLine
{
private const string BuildDirArg = "-th1SmokeBuildDir";
private const string DefaultBuildSubDirectory = "TH1/CodexSmoke";
private const string ProductName = "TOHOTOPIA";
private const string ExeName = "TOHOTOPIA.exe";
private static readonly string[] BaseSharedDefines =
{
"UNITY",
"ENABLE_VIEW",
"NODECANVAS"
};
private static readonly string[] ControlledDefines =
{
"UNITY",
"ENABLE_VIEW",
"NODECANVAS",
"STEAMWORKS_NET",
"STEAM_CHANNEL",
"USE_INPUT",
"ENABLE_SPEEDUP",
"GAME_AUTO_DEBUG",
"CHECK_ACTIONDEFFERENCE",
"STEAM_TEST",
"TH1_PLATFORM_PC",
"TH1_PLATFORM_IOS"
};
[MenuItem("Tools/TH1/iOS Migration/Command Line/Prepare And Build Windows Smoke")]
public static void PrepareAndBuildWindowsSmoke()
{
RunBatchAction(() =>
{
ConfigureWindowsIl2CppSmokeSettings();
PrepareCurrentPlatform(true);
var exePath = BuildWindowsSmokePlayer();
Debug.Log($"[TH1.Migration.CLI] Windows smoke build OK: {exePath}");
});
}
[MenuItem("Tools/TH1/iOS Migration/Command Line/Build Windows Debug Hotfix Obfuscation Smoke")]
public static void BuildWindowsDebugHotfixObfuscationSmoke()
{
RunBatchAction(() =>
{
ConfigureWindowsIl2CppDebugHotfixObfuscationSettings();
PrepareWindowsDebugHotfixObfuscation();
var exePath = BuildWindowsSmokePlayer(true);
Debug.Log($"[TH1.Migration.CLI] Windows debug hotfix-obfuscation smoke build OK: {exePath}");
});
}
[MenuItem("Tools/TH1/iOS Migration/Command Line/Prepare Current Platform")]
public static void PrepareCurrentPlatformMenu()
{
RunBatchAction(() => PrepareCurrentPlatform(true));
}
public static void PrepareCurrentPlatform(bool developmentBuild)
{
AssetDatabase.SaveAssets();
TH1HybridCLRBuildTools.ConfigureHotfixSettings();
TH1YooAssetBuildTools.ConfigureDefaultPackageCollector();
TH1HybridCLRBuildTools.GenerateAll();
if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(developmentBuild))
{
throw new BuildFailedException("[TH1.Migration.CLI] Build hotfix dll failed.");
}
TH1YooAssetBuildTools.BuildBuiltinDefaultPackage();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var blockers = TH1MigrationBuildStatus.GetBuildBlockingMessages(EditorUserBuildSettings.activeBuildTarget);
if (blockers.Count > 0)
{
throw new BuildFailedException(
"[TH1.Migration.CLI] Build preparation still has blocking errors:\n" +
string.Join("\n", blockers));
}
Debug.Log("[TH1.Migration.CLI] Prepare current platform OK.");
}
private static void PrepareWindowsDebugHotfixObfuscation()
{
AssetDatabase.SaveAssets();
TH1HybridCLRBuildTools.ConfigureHotfixSettings();
TH1YooAssetBuildTools.ConfigureDefaultPackageCollector();
TH1HybridCLRBuildTools.SetOpsObfuscationEnabled(false);
TH1HybridCLRBuildTools.GenerateAll();
if (!TH1HybridCLRBuildTools.BuildAndCopyHotfixArtifacts(true, true))
{
throw new BuildFailedException("[TH1.Migration.CLI] Build obfuscated hotfix dll failed.");
}
TH1YooAssetBuildTools.BuildBuiltinDefaultPackage();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
var blockers = TH1MigrationBuildStatus.GetBuildBlockingMessages(
EditorUserBuildSettings.activeBuildTarget,
true,
true);
if (blockers.Count > 0)
{
throw new BuildFailedException(
"[TH1.Migration.CLI] Debug hotfix-obfuscation preparation still has blocking errors:\n" +
string.Join("\n", blockers));
}
Debug.Log("[TH1.Migration.CLI] Windows debug hotfix-obfuscation preparation OK.");
}
private static void ConfigureWindowsIl2CppDebugHotfixObfuscationSettings()
{
const BuildTargetGroup group = BuildTargetGroup.Standalone;
const BuildTarget target = BuildTarget.StandaloneWindows64;
if (EditorUserBuildSettings.activeBuildTarget != target)
{
EditorUserBuildSettings.SwitchActiveBuildTarget(group, target);
}
SetWindowsDebugDefines();
PlayerSettings.productName = ProductName;
PlayerSettings.SetScriptingBackend(group, ScriptingImplementation.IL2CPP);
EditorUserBuildSettings.development = true;
EditorUserBuildSettings.allowDebugging = true;
PlayerSettings.usePlayerLog = true;
PlayerSettings.enableInternalProfiler = true;
PlayerSettings.SetStackTraceLogType(LogType.Log, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Warning, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Error, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Assert, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Exception, StackTraceLogType.ScriptOnly);
Debug.Log("[TH1.Migration.CLI] Configured Windows IL2CPP debug hotfix-obfuscation build settings.");
}
private static void SetWindowsDebugDefines()
{
const BuildTargetGroup group = BuildTargetGroup.Standalone;
var defines = new HashSet<string>(
PlayerSettings.GetScriptingDefineSymbolsForGroup(group)
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(symbol => symbol.Trim())
.Where(symbol => !string.IsNullOrEmpty(symbol)),
StringComparer.Ordinal);
foreach (var symbol in ControlledDefines)
{
defines.Remove(symbol);
}
foreach (var symbol in BaseSharedDefines)
{
defines.Add(symbol);
}
defines.Add("TH1_PLATFORM_PC");
defines.Add("STEAMWORKS_NET");
defines.Add("STEAM_CHANNEL");
defines.Add("USE_INPUT");
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, string.Join(";", defines.OrderBy(symbol => symbol)));
}
private static void ConfigureWindowsIl2CppSmokeSettings()
{
const BuildTargetGroup group = BuildTargetGroup.Standalone;
const BuildTarget target = BuildTarget.StandaloneWindows64;
if (EditorUserBuildSettings.activeBuildTarget != target)
{
EditorUserBuildSettings.SwitchActiveBuildTarget(group, target);
}
PlayerSettings.SetScriptingBackend(group, ScriptingImplementation.IL2CPP);
EditorUserBuildSettings.development = true;
EditorUserBuildSettings.allowDebugging = false;
PlayerSettings.usePlayerLog = true;
PlayerSettings.SetStackTraceLogType(LogType.Log, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Warning, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Error, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Assert, StackTraceLogType.ScriptOnly);
PlayerSettings.SetStackTraceLogType(LogType.Exception, StackTraceLogType.ScriptOnly);
Debug.Log("[TH1.Migration.CLI] Configured Windows IL2CPP smoke build settings.");
}
private static string BuildWindowsSmokePlayer(bool validateDebugHotfixObfuscation = false)
{
var outputRoot = GetSmokeBuildDirectory();
CleanSmokeBuildDirectory(outputRoot);
var outputExe = Path.Combine(outputRoot, ExeName);
var scenes = EditorBuildSettings.scenes
.Where(scene => scene.enabled)
.Select(scene => scene.path)
.ToArray();
if (scenes.Length == 0)
{
throw new BuildFailedException("[TH1.Migration.CLI] No enabled scenes in EditorBuildSettings.");
}
var options = new BuildPlayerOptions
{
scenes = scenes,
locationPathName = outputExe,
target = BuildTarget.StandaloneWindows64,
targetGroup = BuildTargetGroup.Standalone,
options = BuildOptions.Development | BuildOptions.AllowDebugging
};
BuildReport report;
using (TH1MigrationBuildValidationGate.Suppress())
{
report = BuildPipeline.BuildPlayer(options);
}
var summary = report.summary;
if (summary.result != BuildResult.Succeeded)
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Build failed: {summary.result}, errors={summary.totalErrors}, warnings={summary.totalWarnings}");
}
var playerExe = FindPlayerExecutable(outputRoot, outputExe);
if (string.IsNullOrEmpty(playerExe))
{
BuildExportedVisualStudioSolution(outputRoot);
playerExe = FindPlayerExecutable(outputRoot, outputExe);
}
if (string.IsNullOrEmpty(playerExe))
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Build succeeded but no runnable player exe was found under {outputRoot}.");
}
if (validateDebugHotfixObfuscation)
{
ValidateIl2CppDebugHotfixObfuscationOutput(outputRoot, playerExe);
}
return playerExe;
}
private static void ValidateIl2CppDebugHotfixObfuscationOutput(string outputRoot, string playerExe)
{
var gameAssembly = Directory.GetFiles(outputRoot, "GameAssembly.dll", SearchOption.AllDirectories)
.FirstOrDefault();
if (string.IsNullOrEmpty(gameAssembly))
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Windows Debug hotfix-obfuscation build must use IL2CPP, but GameAssembly.dll was not found under {outputRoot}.");
}
var playerName = Path.GetFileNameWithoutExtension(playerExe);
var dataDirectory = Path.Combine(Path.GetDirectoryName(playerExe) ?? outputRoot, playerName + "_Data");
var managedDirectory = Path.Combine(dataDirectory, "Managed");
var embeddedHotfixDll = Path.Combine(managedDirectory, "TH1.Hotfix.dll");
if (File.Exists(embeddedHotfixDll))
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] TH1.Hotfix.dll should not be embedded in IL2CPP player Managed directory: {embeddedHotfixDll}");
}
var streamingHotfixDll = Directory
.GetFiles(dataDirectory, "TH1.Hotfix.dll.bytes", SearchOption.AllDirectories)
.FirstOrDefault(path => path.IndexOf("HybridCLR", StringComparison.OrdinalIgnoreCase) >= 0);
if (string.IsNullOrEmpty(streamingHotfixDll))
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Obfuscated hotfix dll bytes were not copied into player StreamingAssets under {dataDirectory}.");
}
Debug.Log($"[TH1.Migration.CLI] Windows IL2CPP debug hotfix-obfuscation output verified: gameAssembly={gameAssembly}, streaming={streamingHotfixDll}");
}
private static string GetSmokeBuildDirectory()
{
var argValue = GetCommandLineValue(BuildDirArg);
if (!string.IsNullOrEmpty(argValue))
{
return Path.GetFullPath(argValue);
}
return Path.GetFullPath(Path.Combine(GetProjectRoot(), DefaultBuildSubDirectory));
}
private static string FindPlayerExecutable(string outputRoot, string expectedExe)
{
if (File.Exists(expectedExe)) return expectedExe;
if (!Directory.Exists(outputRoot)) return string.Empty;
var exeFiles = Directory.GetFiles(outputRoot, "*.exe", SearchOption.AllDirectories)
.Where(path =>
{
if (path.IndexOf("Il2CppOutputProject", StringComparison.OrdinalIgnoreCase) >= 0) return false;
var fileName = Path.GetFileName(path);
if (fileName.IndexOf("CrashHandler", StringComparison.OrdinalIgnoreCase) >= 0) return false;
if (fileName.Equals("il2cpp.exe", StringComparison.OrdinalIgnoreCase)) return false;
if (fileName.Equals("UnityLinker.exe", StringComparison.OrdinalIgnoreCase)) return false;
if (fileName.Equals("bee_backend.exe", StringComparison.OrdinalIgnoreCase)) return false;
if (fileName.Equals("Analytics.exe", StringComparison.OrdinalIgnoreCase)) return false;
if (fileName.Equals("createdump.exe", StringComparison.OrdinalIgnoreCase)) return false;
return true;
})
.OrderBy(path => path.Length)
.ToArray();
return exeFiles.FirstOrDefault() ?? string.Empty;
}
private static void BuildExportedVisualStudioSolution(string outputRoot)
{
var solutionPath = Directory.GetFiles(outputRoot, "*.sln", SearchOption.TopDirectoryOnly)
.OrderBy(path => path.Length)
.FirstOrDefault();
if (string.IsNullOrEmpty(solutionPath))
{
return;
}
var msBuildPath = FindMSBuildPath();
if (string.IsNullOrEmpty(msBuildPath))
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Unity exported a Visual Studio solution but MSBuild was not found: {solutionPath}");
}
Debug.Log($"[TH1.Migration.CLI] Unity exported Visual Studio solution, building it with MSBuild: {solutionPath}");
var windowsSdkVersion = FindLatestWindowsSdkVersion();
var platformToolset = FindLatestPlatformToolset(msBuildPath);
var retargetArgs = string.Empty;
if (!string.IsNullOrEmpty(windowsSdkVersion))
{
retargetArgs += $" /p:WindowsTargetPlatformVersion={windowsSdkVersion}";
}
if (!string.IsNullOrEmpty(platformToolset))
{
retargetArgs += $" /p:PlatformToolset={platformToolset}";
}
RunProcess(
msBuildPath,
$"\"{solutionPath}\" /m /p:Configuration=Debug /p:Platform=x64{retargetArgs} /verbosity:minimal",
outputRoot);
}
private static string FindMSBuildPath()
{
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
var vsWherePath = Path.Combine(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
if (File.Exists(vsWherePath))
{
var output = RunProcess(vsWherePath, "-latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\\**\\Bin\\MSBuild.exe", GetProjectRoot(), false);
var path = output
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var candidates = new[]
{
Path.Combine(programFiles, "Microsoft Visual Studio", "2022", "Community", "MSBuild", "Current", "Bin", "MSBuild.exe"),
Path.Combine(programFiles, "Microsoft Visual Studio", "2022", "Professional", "MSBuild", "Current", "Bin", "MSBuild.exe"),
Path.Combine(programFiles, "Microsoft Visual Studio", "2022", "Enterprise", "MSBuild", "Current", "Bin", "MSBuild.exe"),
Path.Combine(programFilesX86, "Microsoft Visual Studio", "2022", "BuildTools", "MSBuild", "Current", "Bin", "MSBuild.exe")
};
return candidates.FirstOrDefault(File.Exists) ?? string.Empty;
}
private static string FindLatestWindowsSdkVersion()
{
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
var sdkLibRoot = Path.Combine(programFilesX86, "Windows Kits", "10", "Lib");
if (!Directory.Exists(sdkLibRoot))
{
return string.Empty;
}
return Directory.GetDirectories(sdkLibRoot)
.Select(Path.GetFileName)
.Where(name => Version.TryParse(name?.TrimEnd('.'), out _))
.OrderByDescending(name => Version.Parse(name.TrimEnd('.')))
.FirstOrDefault() ?? string.Empty;
}
private static string FindLatestPlatformToolset(string msBuildPath)
{
var current = new FileInfo(msBuildPath).Directory;
while (current != null)
{
var toolsetRoot = Path.Combine(current.FullName, "Microsoft", "VC", "v170", "Platforms", "x64", "PlatformToolsets");
if (Directory.Exists(toolsetRoot))
{
return Directory.GetDirectories(toolsetRoot)
.Select(Path.GetFileName)
.Where(name => name != null && name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(name => name)
.FirstOrDefault() ?? string.Empty;
}
current = current.Parent;
}
return string.Empty;
}
private static string RunProcess(string fileName, string arguments, string workingDirectory, bool throwOnError = true)
{
var process = new DiagnosticsProcess
{
StartInfo = new DiagnosticsProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrWhiteSpace(output))
{
Debug.Log(output);
}
if (!string.IsNullOrWhiteSpace(error))
{
Debug.LogWarning(error);
}
if (throwOnError && process.ExitCode != 0)
{
throw new BuildFailedException(
$"[TH1.Migration.CLI] Process failed ({process.ExitCode}): {fileName} {arguments}\n{output}\n{error}");
}
return output;
}
private static string GetCommandLineValue(string name)
{
var args = Environment.GetCommandLineArgs();
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return string.Empty;
}
private static string GetProjectRoot()
{
return Directory.GetParent(Application.dataPath)?.FullName ?? Application.dataPath;
}
private static void CleanSmokeBuildDirectory(string outputRoot)
{
var fullOutputRoot = Path.GetFullPath(outputRoot);
var defaultSafeRoot = Path.GetFullPath(Path.Combine(GetProjectRoot(), "TH1"));
if (!fullOutputRoot.StartsWith(defaultSafeRoot, StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrEmpty(GetCommandLineValue(BuildDirArg)))
{
throw new BuildFailedException($"[TH1.Migration.CLI] Refuse to clean unexpected smoke build dir: {fullOutputRoot}");
}
if (Directory.Exists(fullOutputRoot))
{
Directory.Delete(fullOutputRoot, true);
}
Directory.CreateDirectory(fullOutputRoot);
}
private static void RunBatchAction(System.Action action)
{
try
{
action();
if (Application.isBatchMode)
{
EditorApplication.Exit(0);
}
}
catch (Exception e)
{
Debug.LogError($"[TH1.Migration.CLI] Failed:\n{e}");
if (Application.isBatchMode)
{
EditorApplication.Exit(1);
return;
}
throw;
}
}
}
}