235 lines
9.0 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|