Files
2026-07-13 12:49:17 +08:00

235 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.Build;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools.Build
{
public static class BuildRunner
{
public static object ScheduleBuild(BuildJob job, BuildPlayerOptions options)
{
job.State = BuildJobState.Pending;
BuildJobStore.AddBuildJob(job);
ScheduleOnNextUpdate(() =>
RunBuildCore(job, () => BuildPipeline.BuildPlayer(options)));
return new PendingResponse(
$"Build scheduled for {job.Target}. Polling for completion...",
pollIntervalSeconds: 5.0,
data: new { job_id = job.JobId, platform = job.Target.ToString() }
);
}
#if UNITY_6000_0_OR_NEWER
public static object ScheduleProfileBuild(BuildJob job, BuildPlayerWithProfileOptions options)
{
job.State = BuildJobState.Pending;
BuildJobStore.AddBuildJob(job);
ScheduleOnNextUpdate(() =>
RunBuildCore(job, () => BuildPipeline.BuildPlayer(options)));
return new PendingResponse(
$"Profile build scheduled for {job.Target}. Polling for completion...",
pollIntervalSeconds: 5.0,
data: new { job_id = job.JobId, platform = job.Target.ToString() }
);
}
#endif
public static BuildPlayerOptions CreateBuildOptions(
BuildTarget target,
string outputPath,
string[] scenes,
BuildOptions buildOptions,
int subtarget)
{
var options = new BuildPlayerOptions
{
target = target,
targetGroup = BuildTargetMapping.GetTargetGroup(target),
locationPathName = outputPath,
scenes = scenes ?? GetDefaultScenes(),
options = buildOptions,
};
// Subtarget is only meaningful for Standalone (Server vs Player).
// On Android/iOS it maps to MobileTextureSubtarget (texture compression format).
// Passing StandaloneBuildSubtarget.Player (0) for mobile platforms forces
// a specific texture format — confirmed Unity bug IN-102413 where value 0
// on Unity 6000+ triggers PVRTC, ignoring Player Settings.
// Leave at platform default (0) so Unity respects Player Settings.
if (target == BuildTarget.StandaloneWindows
|| target == BuildTarget.StandaloneWindows64
|| target == BuildTarget.StandaloneOSX
|| target == BuildTarget.StandaloneLinux64)
{
options.subtarget = subtarget;
}
return options;
}
public static BuildOptions ParseBuildOptions(string[] optionNames, bool development)
{
var opts = BuildOptions.None;
if (development)
opts |= BuildOptions.Development;
if (optionNames == null) return opts;
foreach (var name in optionNames)
{
switch (name.ToLowerInvariant())
{
case "clean_build": opts |= BuildOptions.CleanBuildCache; break;
case "auto_run": opts |= BuildOptions.AutoRunPlayer; break;
case "deep_profiling": opts |= BuildOptions.EnableDeepProfilingSupport; break;
case "compress_lz4": opts |= BuildOptions.CompressWithLz4; break;
case "strict_mode": opts |= BuildOptions.StrictMode; break;
case "detailed_report": opts |= BuildOptions.DetailedBuildReport; break;
case "allow_debugging": opts |= BuildOptions.AllowDebugging; break;
case "connect_profiler": opts |= BuildOptions.ConnectWithProfiler; break;
case "scripts_only": opts |= BuildOptions.BuildScriptsOnly; break;
case "show_player": opts |= BuildOptions.ShowBuiltPlayer; break;
case "include_tests": opts |= BuildOptions.IncludeTestAssemblies; break;
}
}
return opts;
}
private static void RunBuildCore(BuildJob job, Func<BuildReport> buildFunc)
{
job.State = BuildJobState.Building;
job.StartedAt = DateTime.UtcNow;
try
{
BuildReport report = buildFunc();
job.CompletedAt = DateTime.UtcNow;
// Extract summary data immediately, then let the BuildReport be GC'd
var summary = report.summary;
job.TotalSizeMb = Math.Round(summary.totalSize / (1024.0 * 1024.0), 2);
job.TotalErrors = summary.totalErrors;
job.TotalWarnings = summary.totalWarnings;
if (summary.result == BuildResult.Succeeded)
{
job.State = BuildJobState.Succeeded;
}
else
{
job.State = BuildJobState.Failed;
#if UNITY_2023_1_OR_NEWER
job.ErrorMessage = report.SummarizeErrors();
#else
job.ErrorMessage = $"Build failed with result: {summary.result}";
#endif
}
}
catch (Exception ex)
{
job.State = BuildJobState.Failed;
job.CompletedAt = DateTime.UtcNow;
job.ErrorMessage = ex.Message;
}
BuildJobStore.SetLastCompleted(job);
// Emit console signal so the build result is visible in Unity's Console
if (job.State == BuildJobState.Succeeded)
{
UnityEngine.Debug.Log(
$"[MCP Build] Build succeeded: {job.Target} → {job.OutputPath} " +
$"({job.TotalSizeMb} MB, {(job.CompletedAt.Value - job.StartedAt).TotalSeconds:F1}s)");
}
else
{
UnityEngine.Debug.LogError(
$"[MCP Build] ✗ Build failed: {job.Target} — {job.ErrorMessage}");
}
}
public static void ScheduleNextBatchBuild(BatchJob batch, Func<int, BuildJob> createChildBuild)
{
batch.CurrentIndex++;
if (batch.State == BuildJobState.Cancelled)
{
for (int i = batch.CurrentIndex; i < batch.Children.Count; i++)
batch.Children[i].State = BuildJobState.Skipped;
return;
}
if (batch.CurrentIndex >= batch.Children.Count)
{
bool anyFailed = batch.Children.Any(c => c.State == BuildJobState.Failed);
batch.State = anyFailed ? BuildJobState.Failed : BuildJobState.Succeeded;
return;
}
var child = batch.Children[batch.CurrentIndex];
createChildBuild(batch.CurrentIndex);
// Safety timeout: if child never transitions out of Building/Pending after 2 hours,
// unregister the delegate to prevent an orphaned update loop
var watchStart = DateTime.UtcNow;
var maxWait = TimeSpan.FromHours(2);
void WaitForCompletion()
{
if (DateTime.UtcNow - watchStart > maxWait)
{
EditorApplication.update -= WaitForCompletion;
if (child.State == BuildJobState.Building || child.State == BuildJobState.Pending)
{
child.State = BuildJobState.Failed;
child.ErrorMessage = "Build timed out after 2 hours.";
}
ScheduleNextBatchBuild(batch, createChildBuild);
return;
}
if (child.State == BuildJobState.Building || child.State == BuildJobState.Pending)
return;
EditorApplication.update -= WaitForCompletion;
ScheduleNextBatchBuild(batch, createChildBuild);
}
EditorApplication.update += WaitForCompletion;
}
/// <summary>
/// Schedule an action to run on the next EditorApplication.update tick.
/// Unlike delayCall, update callbacks are guaranteed to fire since the
/// TransportCommandDispatcher processes commands through the same mechanism.
/// </summary>
private static void ScheduleOnNextUpdate(Action action)
{
bool executed = false;
void RunOnce()
{
if (executed) return;
executed = true;
EditorApplication.update -= RunOnce;
action();
}
EditorApplication.update += RunOnce;
}
private static string[] GetDefaultScenes()
{
return EditorBuildSettings.scenes
.Where(s => s.enabled)
.Select(s => s.path)
.ToArray();
}
}
}