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 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 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; } /// /// 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. /// 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(); } } }