Files
daluandou/.codely.packages/cn.tuanjie.codely.bridge@1.0.85-47f31100/Editor/Bridge/Helpers/CompilationHelper.cs
T
huadexuan 13d0c80335 ```
chore(bridge): 删除桥接包的提交记录和Git属性配置

删除了 .codely-bridge-commit 文件中存储的提交哈希值
移除了 .gitattributes 文件中的行结束符配置规则
```
2026-09-24 21:41:14 +08:00

352 lines
16 KiB
C#

using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
namespace UnityTcp.Editor.Helpers
{
/// <summary>
/// Helper class for Unity compilation status checking and error tracking
/// </summary>
public static class CompilationHelper
{
// Track last known compilation error/warning counts
// IMPORTANT: Keep these nullable. Returning 0 when counts are unknown is misleading
// (it can be interpreted as "validated: no errors/warnings").
private static int? _lastErrorCount = null;
private static int? _lastWarningCount = null;
/// <summary>
/// Helper to check compilation status across Unity versions
/// </summary>
public static bool IsCompiling()
{
if (EditorApplication.isCompiling)
{
return true;
}
try
{
System.Type pipeline = System.Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (prop != null)
{
return (bool)prop.GetValue(null);
}
}
catch { }
return false;
}
/// <summary>
/// Gets the count of compilation errors from the console.
/// This is an approximation based on console log entries.
/// </summary>
public static int? GetCompilationErrors()
{
try
{
// Try to get error count from LogEntries (internal API)
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
if (logEntriesType != null)
{
var getCountMethod = logEntriesType.GetMethod(
"GetCount",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
);
// Get count with error filter (mode = 1 for errors)
var getCountByTypeMethod = logEntriesType.GetMethod(
"GetCountsByType",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
);
if (getCountByTypeMethod != null)
{
// GetCountsByType(ref int errorCount, ref int warningCount, ref int logCount)
object[] args = new object[] { 0, 0, 0 };
getCountByTypeMethod.Invoke(null, args);
_lastErrorCount = (int)args[0]; // Errors
return _lastErrorCount;
}
}
}
catch (System.Exception e)
{
CodelyLogger.LogWarning($"[CompilationHelper] Failed to get error count: {e.Message}");
}
return _lastErrorCount;
}
/// <summary>
/// Gets the count of compilation warnings from the console.
/// This is an approximation based on console log entries.
/// </summary>
public static int? GetCompilationWarnings()
{
try
{
// Try to get warning count from LogEntries (internal API)
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
if (logEntriesType != null)
{
var getCountByTypeMethod = logEntriesType.GetMethod(
"GetCountsByType",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
);
if (getCountByTypeMethod != null)
{
// GetCountsByType(ref int errorCount, ref int warningCount, ref int logCount)
object[] args = new object[] { 0, 0, 0 };
getCountByTypeMethod.Invoke(null, args);
_lastWarningCount = (int)args[1]; // Warnings
return _lastWarningCount;
}
}
}
catch (System.Exception e)
{
CodelyLogger.LogWarning($"[CompilationHelper] Failed to get warning count: {e.Message}");
}
return _lastWarningCount;
}
/// <summary>
/// Resets tracked error/warning counts.
/// Should be called before starting a new compilation.
/// </summary>
public static void ResetCounts()
{
_lastErrorCount = null;
_lastWarningCount = null;
}
/// <summary>
/// Requests a script compilation after auto-saving any dirty scenes.
/// This is the single entry point that should be used instead of
/// <c>CompilationPipeline.RequestScriptCompilation()</c> everywhere in
/// the bridge, to prevent Unity's native "Save scene?" modal dialog
/// from blocking the main thread during domain reload.
/// </summary>
public static void RequestScriptCompilationSafe()
{
EnsureScenesSavedBeforeReload();
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
}
/// <summary>
/// Auto-saves any dirty scenes before a domain reload (triggered by
/// RequestScriptCompilation) to prevent Unity's native "Save scene?"
/// modal dialog from blocking the main thread.
///
/// - Dirty **named** scenes (have a path) are saved silently to their
/// existing file location.
/// - Dirty **unnamed** scenes (no path) are saved to
/// "Assets/Temp/CodelyAutosave/UnnamedScene_{timestamp}.unity" so
/// the dialog is never triggered. The autosave path is logged.
/// </summary>
internal static void EnsureScenesSavedBeforeReload()
{
int sceneCount = SceneManager.sceneCount;
for (int i = 0; i < sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
if (!scene.IsValid() || !scene.isDirty)
continue;
if (!string.IsNullOrEmpty(scene.path))
{
// Named scene — save silently to its existing path
bool ok = EditorSceneManager.SaveScene(scene);
CodelyLogger.Log(
$"[CompilationHelper] Auto-saved dirty scene '{scene.name}' → '{scene.path}' (ok={ok}) before compile");
}
else
{
// Unnamed scene — save to a temp autosave path to avoid the
// blocking "Save As" file dialog that hangs the editor
string dir = "Assets/Temp/CodelyAutosave";
string sceneName = !string.IsNullOrEmpty(scene.name) ? scene.name : "Untitled";
string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
string relPath = $"{dir}/{sceneName}_{timestamp}.unity";
// SaveScene fails if the target folder does not exist yet — create it
// first. Directory.CreateDirectory is a no-op if it already exists, and
// "Assets/..." resolves against Unity's project-root working directory.
System.IO.Directory.CreateDirectory(dir);
bool ok = EditorSceneManager.SaveScene(scene, relPath);
if (ok)
{
CodelyLogger.LogWarning(
$"[CompilationHelper] Unnamed dirty scene auto-saved to '{relPath}' before compile. " +
"Move or delete this file if not needed.");
}
else
{
CodelyLogger.LogError(
$"[CompilationHelper] Failed to auto-save unnamed dirty scene before compile. " +
"Domain reload may trigger a blocking dialog.");
}
}
}
}
/// <summary>
/// Starts a standard compilation pipeline:
/// 1. Clears console so a post-compile read returns only this build's output
/// 2. Requests compilation
///
/// This is the recommended pattern after any script modification.
/// </summary>
/// <param name="clearCache">
/// When true, attempts to clear the script assembly cache before compiling so that
/// stale intermediate build artefacts are discarded. Uses
/// <c>RequestScriptCompilationOptions.CleanBuildCache</c> (Unity 2019.3+) via
/// reflection; falls back to a normal recompile when that API is unavailable.
/// </param>
public static object StartCompilationPipeline(bool clearCache = false)
{
try
{
// Do not allow script compilation while in Play/Paused mode; recompiling during
// play mode is unsafe and Unity cannot cleanly reload the domain.
if (EditorApplication.isPlaying || EditorApplication.isPaused)
{
return Response.Error(
"compile_blocked_in_play_mode",
new
{
code = "compile_blocked_in_play_mode",
message = "Compilation is not allowed while the editor is in Play/Paused mode. Stop Play mode (unity_editor.stop) before requesting compilation.",
playMode = EditorApplication.isPlaying ? "playing" : "paused"
}
);
}
// Step 1: Clear console so a post-compile read returns only this build's output
UnityTcp.Editor.Tools.ReadConsole.HandleCommand(new Codely.Newtonsoft.Json.Linq.JObject
{
["action"] = "clear"
});
// Step 2: Reset error counts
ResetCounts();
string jobMessage = clearCache
? "Script compilation pipeline started (clean cache)"
: "Script compilation pipeline started";
// Auto-save dirty scenes before requesting compilation to prevent
// Unity's "Save scene?" modal dialog from blocking the main thread.
// Also covers the cache-clear reflection path below.
EnsureScenesSavedBeforeReload();
// Step 4: Detect externally-written/modified files before compiling.
// Files created via raw file writes (not the bridge's script tools, which call
// AssetDatabase.ImportAsset directly) are only picked up by Unity's AUTOMATIC
// refresh, which is gated on the editor having focus. When the editor runs
// unfocused/headless (agent-driven), those new .cs never import and therefore
// never compile. An explicit Refresh() forces detection + import regardless of
// focus (and regardless of the "Auto Refresh" preference), so RequestScriptCompilation
// then has the new scripts to compile.
AssetDatabase.Refresh();
// Step 5: Request compilation (optionally with cache clear).
//
// AssetDatabase.Refresh() is synchronous and, when it detects new/modified .cs
// files on disk, it starts script compilation (Bee) ITSELF. Issuing our own
// RequestScriptCompilation() on top of an already-running compile re-enters the
// pipeline and races Bee — the request can be dropped or corrupt the in-flight
// build. So only force a compile when Refresh() did NOT already start one.
//
// The explicit request is still required in the other path: bridge script tools
// import via AssetDatabase.ImportAsset directly, so there is no on-disk change for
// Refresh() to detect and it will not trigger a compile on its own.
bool cacheClearedViaFlag = false;
bool alreadyCompiling = IsCompiling();
if (!alreadyCompiling)
{
if (clearCache)
{
// Try RequestScriptCompilationOptions.CleanBuildCache (Unity 2019.3+) via
// reflection so this code remains compatible with older Unity versions.
try
{
var optionsType = System.Type.GetType(
"UnityEditor.Compilation.RequestScriptCompilationOptions, UnityEditor");
if (optionsType != null)
{
object cleanCacheValue = System.Enum.Parse(optionsType, "CleanBuildCache");
var requestWithOptions = typeof(UnityEditor.Compilation.CompilationPipeline).GetMethod(
"RequestScriptCompilation",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
null,
new[] { optionsType },
null);
if (requestWithOptions != null)
{
requestWithOptions.Invoke(null, new[] { cleanCacheValue });
cacheClearedViaFlag = true;
}
}
}
catch (System.Exception ex)
{
CodelyLogger.LogWarning($"[CompilationHelper] CleanBuildCache not available on this Unity version: {ex.Message}");
}
}
if (!cacheClearedViaFlag)
{
RequestScriptCompilationSafe();
}
}
// Step 6: Report what the caller needs to finish the job. This response is
// internal — CompilePipelineJob consumes it and then waits for the compile — so it
// carries no operation id; the tracker job above exists only to drive
// OnCompilationFinished and the editor-state snapshot.
var response = new System.Collections.Generic.Dictionary<string, object>
{
["success"] = true,
["message"] = jobMessage,
["pipeline_kind"] = "compile",
};
if (clearCache) response["cache_cleared"] = cacheClearedViaFlag;
return response;
}
catch (System.Exception e)
{
CodelyLogger.LogError($"[CompilationHelper] StartCompilationPipeline failed: {e}");
return Response.Error($"Failed to start compilation pipeline: {e.Message}");
}
}
/// <summary>
/// Gets a summary of the last compilation result.
/// </summary>
public static object GetCompilationSummary()
{
var errors = GetCompilationErrors();
var warnings = GetCompilationWarnings();
// Only include fields that are actually known; returning 0 is misleading.
var result = new System.Collections.Generic.Dictionary<string, object>
{
["isCompiling"] = IsCompiling()
};
if (errors.HasValue) result["errors"] = errors.Value;
if (warnings.HasValue) result["warnings"] = warnings.Value;
if (errors.HasValue) result["success"] = errors.Value == 0;
return result;
}
}
}