/* +------------------------------------------------------------------+ | Author: Ivan Murzak (https://github.com/IvanMurzak) | | Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) | | Copyright (c) 2025 Ivan Murzak | | Licensed under the Apache License, Version 2.0. | | See the LICENSE file in the project root for more information. | +------------------------------------------------------------------+ */ #nullable enable using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEngine; namespace com.IvanMurzak.Unity.MCP.Editor.DependencyResolver { /// /// Operations that gate Unity's next compile pass to include only the /// resolver itself: wiping Library/ScriptAssemblies/ (forces a /// full rebuild) and toggling /// (main plugin asmdefs gate on it, so stripping it makes them skip /// compile until the resolver re-adds it on a healthy install). /// public static class RecompileGate { const string Tag = "[RecompileGate]"; /// /// Belt-and-braces reset before swapping plugin source under the /// editor: forces a clean recompile () AND blocks /// the main plugin asmdefs from compiling until the resolver /// re-validates the DLL set (). Use /// this when an unrelated user-asmdef compile error could otherwise /// keep the OLD plugin AppDomain loaded and prevent the new /// resolver code from running. /// public static void Reset() { Wipe(); RemoveReadyDefine(); } /// /// Best-effort recursive delete of Library/ScriptAssemblies/; /// falls back to per-file delete when the atomic call hits a locked /// file (loaded plugin DLLs are mmapped on Windows). /// public static void Wipe() { var path = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Library", "ScriptAssemblies")); if (!Directory.Exists(path)) return; try { Directory.Delete(path, recursive: true); Debug.Log($"{Tag} Wiped {path} — Unity will recompile every assembly on the next compile."); return; } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { } var locked = 0; foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) { try { File.Delete(file); } catch { locked++; } } Debug.LogWarning($"{Tag} Could not fully wipe {path} ({locked} locked file(s) survived); Unity will recompile what it can on the next compile."); } /// /// Adds to every supported /// build target group. Applying it only to the active group would /// let target switching reintroduce compile failures in assemblies /// gated by defineConstraints. /// public static void EnsureReadyDefine() { var changed = ForEachTarget(TryAddReadyDefine); if (changed.Count > 0) Debug.Log($"{Tag} Added '{NuGetConfig.ReadyDefine}' for: {string.Join(", ", changed)}."); } /// Strips from every supported build target group. public static void RemoveReadyDefine() { var changed = ForEachTarget(TryRemoveReadyDefine); if (changed.Count > 0) Debug.Log($"{Tag} Removed '{NuGetConfig.ReadyDefine}' from: {string.Join(", ", changed)}; resolver will re-add it once the DLL set is healthy."); } /// /// Applies to every supported build target /// group plus (a distinct /// target not reachable via ). /// Returns the names of targets the modifier reported as changed. /// static List ForEachTarget(Func modify) { var changed = new List(); foreach (BuildTargetGroup group in Enum.GetValues(typeof(BuildTargetGroup))) { if (group == BuildTargetGroup.Unknown) continue; NamedBuildTarget target; try { target = NamedBuildTarget.FromBuildTargetGroup(group); } catch { continue; } if (modify(target)) changed.Add(target.TargetName); } if (modify(NamedBuildTarget.Server)) changed.Add(NamedBuildTarget.Server.TargetName); return changed; } static bool TryAddReadyDefine(NamedBuildTarget target) { try { PlayerSettings.GetScriptingDefineSymbols(target, out var defines); if (Array.IndexOf(defines, NuGetConfig.ReadyDefine) >= 0) return false; var newDefines = new string[defines.Length + 1]; Array.Copy(defines, newDefines, defines.Length); newDefines[defines.Length] = NuGetConfig.ReadyDefine; PlayerSettings.SetScriptingDefineSymbols(target, newDefines); return true; } catch { return false; } } static bool TryRemoveReadyDefine(NamedBuildTarget target) { try { PlayerSettings.GetScriptingDefineSymbols(target, out var defines); var index = Array.IndexOf(defines, NuGetConfig.ReadyDefine); if (index < 0) return false; var newDefines = new string[defines.Length - 1]; Array.Copy(defines, 0, newDefines, 0, index); Array.Copy(defines, index + 1, newDefines, index, defines.Length - index - 1); PlayerSettings.SetScriptingDefineSymbols(target, newDefines); return true; } catch { return false; } } } }