using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using Unity.Pipeline.Commands; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; namespace Unity.Pipeline.Editor.Commands { /// /// Forces a script recompile that works even when the editor is unfocused or minimized. /// /// Why this is non-trivial: /// - Unity only runs script compilation while the editor is the active OS application. Pass /// focus=true to bring it to the foreground (editor_focus) before AssetDatabase.Refresh(); /// it is off by default because the server keeps the editor ticking while unfocused, so /// compilation still proceeds without stealing the user's foreground window. /// - A successful compile triggers a domain reload, which destroys the managed AppDomain /// (HTTP server, in-flight requests, statics). The triggering request therefore cannot stay /// open and return when done. /// /// Pattern (mirrors the test runner): completion is reported via a status file that survives the /// domain reload. Call "recompile" to trigger, then poll "recompile_status" until status is /// "completed" or "up_to_date". The client must tolerate connection errors during the reload. /// [InitializeOnLoad] public static class RecompileCommand { const string StatusFile = "Temp/pipeline_recompile_status.json"; static readonly List s_Errors = new List(); // Focus action, indirected so tests can observe whether focus was performed without // actually stealing the OS foreground window. internal static Action s_FocusAction = () => FocusEditorCommand.FocusEditor(); static RecompileCommand() { CompilationPipeline.compilationStarted += OnCompilationStarted; CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished; CompilationPipeline.compilationFinished += OnCompilationFinished; } [CliCommand("recompile", "Force a script recompile (works while unfocused/minimized). Poll recompile_status for completion.", MainThreadRequired = true)] public static object Recompile( [CliArg("focus", "If true, bring the Editor to the foreground before compiling. Off by default.")] bool focus = false) { // Unity compiles only while it is the active application. Optionally bring it to the // foreground first; off by default (the server keeps the editor ticking while unfocused, // so compilation still proceeds). if (focus) s_FocusAction(); WriteStatus("triggered", false, null); // Trigger asset import + compilation. AssetDatabase.Refresh(); if (EditorApplication.isCompiling) { // compilationStarted has written "compiling"; a domain reload will follow on success. return new { status = "compiling", message = "Recompilation started. Poll recompile_status until completed." }; } // Nothing needed recompilation. WriteStatus("up_to_date", false, null); return new { status = "up_to_date", message = "No scripts needed recompilation." }; } [CliCommand("recompile_status", "Get the status of the last recompile: idle | triggered | compiling | completed | up_to_date.", MainThreadRequired = false)] public static string RecompileStatus() { if (File.Exists(StatusFile)) return File.ReadAllText(StatusFile); return "{\"status\":\"idle\"}"; } static void OnCompilationStarted(object _) { s_Errors.Clear(); WriteStatus("compiling", false, null); } static void OnAssemblyCompilationFinished(string assembly, CompilerMessage[] messages) { if (messages == null) return; foreach (var m in messages) if (m.type == CompilerMessageType.Error) s_Errors.Add(m.message); } static void OnCompilationFinished(object _) { // Written before the domain reload, so it persists for pollers after the reload. WriteStatus("completed", s_Errors.Count > 0, s_Errors.ToArray()); } static void WriteStatus(string status, bool failed, string[] errors) { try { var payload = new { status, failed, errors = errors ?? Array.Empty() }; File.WriteAllText(StatusFile, JsonConvert.SerializeObject(payload)); } catch (Exception ex) { Debug.LogError($"[Recompile] Failed to write status file: {ex.Message}"); } } } }