/* +------------------------------------------------------------------+ | 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.IO; using UnityEditor; using UnityEngine; namespace com.IvanMurzak.Unity.MCP.Editor.DependencyResolver { /// /// Configures PluginImporter settings for NuGet DLLs. /// /// Handles four cases: /// 1. Unity provides the DLL + we need it in builds → include in builds, exclude from editor /// 2. Unity provides the DLL + editor-only → disable entirely /// 3. We provide the DLL + we need it in builds → include everywhere /// 4. We provide the DLL + editor-only → editor only /// /// Case 1 is critical: assemblies like System.Diagnostics.DiagnosticSource are /// available in the Unity Editor but NOT included in player builds automatically. /// Our NuGet copy must be included in builds while excluded from editor to avoid duplicates. /// /// Since #733: DLLs sit FLAT under with the /// versioned filename pattern {stem}.{packageVersion}.dll. The package → DLL /// owner mapping comes from (we can't infer the /// package ID from the on-disk path anymore — every DLL sits in the same folder). /// static class NuGetPluginConfigurator { const string Tag = NuGetConfig.LogTag; /// /// Configures PluginImporter for every DLL recorded in the NuGet install manifest. /// Called after packages are installed/restored. /// public static void ConfigureAll() { if (!Directory.Exists(NuGetConfig.InstallPath)) return; var manifest = NuGetInstallManifest.Load(NuGetConfig.InstallPath); // Batch importer changes so Unity performs a single reimport pass at the end // instead of one reimport per DLL (which was dominating editor startup time // on projects with many NuGet packages). AssetDatabase.StartAssetEditing(); try { foreach (var (packageId, entry) in manifest.Packages) { var includeInBuild = ShouldIncludeInBuild(packageId); foreach (var dll in entry.Dlls) { var dllPath = Path.Combine(NuGetConfig.InstallPath, dll); if (!File.Exists(dllPath)) continue; // Convert to Unity asset path (forward slashes, relative to project) var assetPath = dllPath.Replace('\\', '/'); ConfigureDll(assetPath, includeInBuild); } } } finally { AssetDatabase.StopAssetEditing(); } } /// /// Configures a single DLL's PluginImporter settings. /// public static void ConfigureDll(string assetPath, bool includeInBuild) { var importer = AssetImporter.GetAtPath(assetPath) as PluginImporter; if (importer == null) return; // The DLL filename carries the package version as a tail segment // (e.g. "System.Memory.10.0.3.dll"). Strip it so the assembly- // resolver lookup matches the assembly's manifest name, not the // on-disk filename. var fileName = Path.GetFileName(assetPath); var assemblyName = NuGetInstallManifest.TryParseInstalledDllName(fileName, out var stem, out _) && stem != null ? stem : Path.GetFileNameWithoutExtension(assetPath); var unityProvidesIt = UnityAssemblyResolver.IsAlreadyImported(assemblyName); bool anyPlatform; bool excludeEditor; bool editorOnly; if (unityProvidesIt && includeInBuild) { // Unity provides this DLL in the editor, but builds need our copy. anyPlatform = true; excludeEditor = true; editorOnly = false; } else if (unityProvidesIt) { // Unity provides it and we don't need it in builds — disable entirely. anyPlatform = false; excludeEditor = false; editorOnly = false; } else if (includeInBuild) { // Runtime DLL not provided by Unity: include everywhere. anyPlatform = true; excludeEditor = false; editorOnly = false; } else { // Editor-only DLL not provided by Unity. anyPlatform = false; excludeEditor = false; editorOnly = true; } // Check if settings need to change var currentAnyPlatform = importer.GetCompatibleWithAnyPlatform(); var currentEditor = importer.GetCompatibleWithEditor(); var currentExcludeEditor = importer.GetExcludeEditorFromAnyPlatform(); // When Any Platform is on, the Editor flag must also track !excludeEditor — // otherwise a stale Editor=0 left over from Unity's initial import silently // survives (this was the cause of "Unloading broken assembly" on startup). var expectedEditor = anyPlatform ? !excludeEditor : editorOnly; var needsChange = currentAnyPlatform != anyPlatform || currentExcludeEditor != excludeEditor || currentEditor != expectedEditor; if (!needsChange) return; if (anyPlatform) { importer.SetCompatibleWithAnyPlatform(true); importer.SetExcludeEditorFromAnyPlatform(excludeEditor); // Explicitly sync the individual Editor platform flag. Unity's initial import // sometimes leaves Editor at enabled=0 even when Any Platform is on without // Exclude Editor; without this call, the stale 0 persists in the .meta and // Editor-side loading fails (e.g., "Unloading broken assembly ..." for DLLs // whose transitive deps are also editor-disabled). importer.SetCompatibleWithEditor(!excludeEditor); } else { importer.SetCompatibleWithAnyPlatform(false); importer.SetCompatibleWithEditor(editorOnly); } importer.SaveAndReimport(); Debug.Log($"{Tag} Configured '{assemblyName}': anyPlatform={anyPlatform}, excludeEditor={excludeEditor}, editorOnly={editorOnly}"); } /// /// Sets the file's compatibility to "no /// platforms" so Unity unloads the DLL on the next domain reload, /// freeing the OS file handle. Used by the legacy-folder migration /// and the stale-flat sweep to recover from the case where Unity has /// the assembly loaded into the editor AppDomain and the file is /// therefore locked. Safe to call on non-DLL files or paths Unity /// hasn't imported (no-op). /// public static void DisableImporter(string filePath) { if (!filePath.EndsWith(".dll", System.StringComparison.OrdinalIgnoreCase)) return; // Asset paths use forward slashes and are project-relative; // Windows Path APIs may have inserted '\' so normalise here. var assetPath = filePath.Replace('\\', '/'); try { if (!(AssetImporter.GetAtPath(assetPath) is PluginImporter importer)) return; importer.SetCompatibleWithAnyPlatform(false); importer.SetCompatibleWithEditor(false); importer.SaveAndReimport(); Debug.Log($"{Tag} Disabled PluginImporter for locked '{assetPath}'; deletion will be retried after the next domain reload."); } catch (Exception ex) { Debug.LogWarning($"{Tag} Could not disable PluginImporter for '{assetPath}': {ex.Message}"); } } /// /// Best-effort that swallows missing-file /// races and logs a warning on any other failure. Shared between the /// migration and per-package install paths so the two surfaces don't /// drift on what counts as "best effort". /// internal static void TryDeleteFile(string path) { if (!File.Exists(path)) return; try { File.Delete(path); } catch (Exception ex) { Debug.LogWarning($"{Tag} Failed to delete '{path}': {ex.Message}"); } } /// /// Determines if a DLL should be included in game builds based on its /// owning package ID. Configured packages use their IncludeInBuild flag. /// Transitive dependencies default to included (runtime packages depend on them). /// static bool ShouldIncludeInBuild(string packageId) { foreach (var package in NuGetConfig.Packages) { if (string.Equals(packageId, package.Id, System.StringComparison.OrdinalIgnoreCase)) return package.IncludeInBuild; } // Transitive dependency — include in builds by default. return true; } } }