TH1/Unity/Assets/Scripts/TH1_Logic/Editor/TH1MigrationCommandLine.cs

378 lines
15 KiB
C#

using System;
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 ExeName = "TOHOTOPIA.exe";
[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/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 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 = true;
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()
{
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
};
var 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}.");
}
return playerExe;
}
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;
}
}
}
}