Files
AIGame/AIClient/Packages/com.unity.pipeline/Editor/Commands/RecompileCommand.cs
T

212 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Unity.Pipeline.Commands;
using Unity.Pipeline.Console;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
#if UNITY_6000_5_OR_NEWER
using Unity.Scripting.LifecycleManagement;
#endif
namespace Unity.Pipeline.Editor.Commands
{
/// <summary>
/// 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.
///
/// The status file is not sufficient on its own: a failed editor compile suppresses the domain
/// reload entirely, and the file lives under Temp/, which Unity deletes at every editor launch.
/// Both commands therefore also consult the compile-failure flag Unity keeps natively.
/// </summary>
[InitializeOnLoad]
#if UNITY_6000_5_OR_NEWER
[NoAutoStaticsCleanup]
#endif
static class RecompileCommand
{
const string StatusFile = "Temp/pipeline_recompile_status.json";
// How stale a ground-truth sample may be and still describe the present. Sampling runs on
// the main thread, so it stops while the Editor is blocked and across a domain reload.
const double MaxGroundTruthAgeSeconds = 2.0;
static readonly List<string> s_Errors = new List<string>();
// 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();
// Indirected for the same reason: a test cannot make Unity's native compile-failure flag
// true, but it can point this at a stub.
internal static Func<bool> s_CompilationFailedProbe = () => EditorUtility.scriptCompilationFailed;
/// <summary>The status file's contents. Shared by the writer and both readers.</summary>
[Serializable]
class RecompileStatusPayload
{
[JsonProperty("status")] public string Status;
[JsonProperty("failed")] public bool Failed;
[JsonProperty("errors")] public string[] Errors;
/// <summary>
/// Whether the project currently has editor compile errors, as Unity reports it
/// natively. Filled when the status is read rather than written: it outlives both the
/// status file and any domain reload, so it is the authority when the two disagree.
/// </summary>
[JsonProperty("compilationFailed")] public bool CompilationFailed;
}
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, Tags = new[] { "scripts/compile" })]
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();
var previous = ReadStatusOrIdle();
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 was imported and nothing compiled. That is only "up to date" if the code
// actually builds: a failed editor compile leaves the errors standing and suppresses the
// domain reload, so a repeat call with nothing changed lands here with the failure still
// true. Reporting "up_to_date" would erase it.
if (s_CompilationFailedProbe())
{
if (previous.Failed)
WriteStatus(previous.Status, true, previous.Errors);
else
WriteStatus("completed", true, Array.Empty<string>());
return new { status = "failed", message = "Scripts still have compile errors; nothing was recompiled. Read them with the console command." };
}
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, Tags = new[] { "scripts/compile" })]
public static string RecompileStatus()
{
var status = ReadStatusOrIdle();
var truth = ConsoleLogCapture.GroundTruth;
var fresh = truth != null
&& (DateTime.UtcNow - truth.SampledUtc).TotalSeconds <= MaxGroundTruthAgeSeconds;
status.CompilationFailed = fresh && truth.CompilationFailed;
// Only a status that carries no evidence of its own defers to the native flag. "idle" is
// what the file reads after an editor launch, since Temp/ is deleted then, and
// "up_to_date" means nothing compiled — in both cases errors can be standing with
// nothing in the file to say so.
//
// A "completed" status is not overridden: it was written by the compile that just ran and
// lists exactly what that compile produced. The native flag still reads true for a moment
// after a fixing compile finishes — it clears with the domain reload that follows — so
// trusting it there would report a build that has just been fixed as broken.
var carriesNoEvidence = status.Status == "idle" || status.Status == "up_to_date";
if (status.CompilationFailed && carriesNoEvidence && !status.Failed)
{
status.Status = "completed";
status.Failed = true;
}
return JsonConvert.SerializeObject(status);
}
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 RecompileStatusPayload ReadStatusOrIdle()
{
try
{
if (File.Exists(StatusFile))
{
var payload = JsonConvert.DeserializeObject<RecompileStatusPayload>(File.ReadAllText(StatusFile));
if (payload != null)
{
payload.Errors = payload.Errors ?? Array.Empty<string>();
return payload;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[Recompile] Failed to read status file: {ex.Message}");
}
return new RecompileStatusPayload { Status = "idle", Errors = Array.Empty<string>() };
}
static void WriteStatus(string status, bool failed, string[] errors)
{
try
{
var payload = new RecompileStatusPayload
{
Status = status,
Failed = failed,
Errors = errors ?? Array.Empty<string>()
};
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(payload));
}
catch (Exception ex)
{
Debug.LogError($"[Recompile] Failed to write status file: {ex.Message}");
}
}
}
}