Skip to content

Commit 5595c7b

Browse files
authored
Merge pull request #1300 from kpkhxlgy0/codex/codedom-assembly-dedup
fix: deduplicate CodeDom assembly references
2 parents 84dfa76 + d2247e9 commit 5595c7b

2 files changed

Lines changed: 270 additions & 6 deletions

File tree

MCPForUnity/Editor/Tools/ExecuteCode.cs

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ public static class ExecuteCode
3030

3131
private static readonly List<HistoryEntry> _history = new List<HistoryEntry>();
3232
private static string[] _cachedAssemblyPaths;
33+
private static string[] _cachedCodeDomAssemblyPaths;
3334

3435
[UnityEditor.InitializeOnLoadMethod]
3536
private static void OnDomainReload()
3637
{
3738
_cachedAssemblyPaths = null;
39+
_cachedCodeDomAssemblyPaths = null;
3840
RoslynCompiler.ResetCache();
3941
}
4042

@@ -366,16 +368,110 @@ private static Assembly CodeDomCompile(string source, string[] assemblyPaths, ou
366368
"System.Collections",
367369
};
368370

369-
private static string[] FilterAssemblyPathsForCodeDom(string[] allPaths)
371+
internal static string[] FilterAssemblyPathsForCodeDom(string[] allPaths)
370372
{
371-
bool hasNetstandard = allPaths.Any(p =>
373+
var useCache = ReferenceEquals(allPaths, _cachedAssemblyPaths);
374+
if (useCache && _cachedCodeDomAssemblyPaths != null)
375+
return _cachedCodeDomAssemblyPaths;
376+
377+
var hasNetstandard = allPaths.Any(p =>
372378
string.Equals(Path.GetFileNameWithoutExtension(p), "netstandard", StringComparison.OrdinalIgnoreCase));
373379

374-
if (!hasNetstandard)
375-
return allPaths;
380+
var filtered = hasNetstandard
381+
? allPaths.Where(p =>
382+
!_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray()
383+
: allPaths;
384+
385+
var result = DeduplicateAssemblyPathsForCodeDom(filtered);
386+
if (useCache)
387+
_cachedCodeDomAssemblyPaths = result;
388+
return result;
389+
}
390+
391+
private static string[] DeduplicateAssemblyPathsForCodeDom(string[] paths)
392+
{
393+
var candidates = new List<CodeDomAssemblyCandidate>();
394+
var unresolvedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
395+
396+
foreach (var path in paths)
397+
{
398+
try
399+
{
400+
candidates.Add(new CodeDomAssemblyCandidate(path, AssemblyName.GetAssemblyName(path)));
401+
}
402+
catch
403+
{
404+
unresolvedPaths.Add(path);
405+
}
406+
}
407+
408+
var groups = candidates
409+
.GroupBy(candidate => candidate.AssemblyName.Name, StringComparer.OrdinalIgnoreCase)
410+
.ToArray();
411+
412+
if (groups.All(group => group.Count() == 1))
413+
return paths;
414+
415+
var referenceCounts = GetLoadedAssemblyReferenceCounts();
416+
var selectedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
417+
418+
foreach (var group in groups)
419+
{
420+
var selected = group
421+
.OrderByDescending(candidate => GetReferenceCount(referenceCounts, candidate.AssemblyName.FullName))
422+
.ThenByDescending(candidate => candidate.AssemblyName.Version)
423+
.ThenBy(candidate => candidate.Path, StringComparer.OrdinalIgnoreCase)
424+
.First();
425+
selectedPaths.Add(selected.Path);
426+
}
427+
428+
return paths.Where(path => unresolvedPaths.Contains(path) || selectedPaths.Contains(path)).ToArray();
429+
}
430+
431+
private static Dictionary<string, int> GetLoadedAssemblyReferenceCounts()
432+
{
433+
var referenceCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
434+
435+
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
436+
{
437+
if (assembly.IsDynamic) continue;
438+
439+
AssemblyName[] referencedAssemblies;
440+
try
441+
{
442+
referencedAssemblies = assembly.GetReferencedAssemblies();
443+
}
444+
catch (NotSupportedException)
445+
{
446+
continue;
447+
}
448+
449+
foreach (var referencedAssembly in referencedAssemblies)
450+
{
451+
var fullName = referencedAssembly.FullName;
452+
referenceCounts.TryGetValue(fullName, out var count);
453+
referenceCounts[fullName] = count + 1;
454+
}
455+
}
456+
457+
return referenceCounts;
458+
}
459+
460+
private static int GetReferenceCount(Dictionary<string, int> referenceCounts, string fullName)
461+
{
462+
return referenceCounts.TryGetValue(fullName, out var count) ? count : 0;
463+
}
464+
465+
private sealed class CodeDomAssemblyCandidate
466+
{
467+
public CodeDomAssemblyCandidate(string path, AssemblyName assemblyName)
468+
{
469+
Path = path;
470+
AssemblyName = assemblyName;
471+
}
376472

377-
return allPaths.Where(p =>
378-
!_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray();
473+
public string Path { get; }
474+
public AssemblyName AssemblyName { get; }
379475
}
380476

381477
// ──────────────────── Shared helpers ────────────────────

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
using System;
2+
using System.CodeDom.Compiler;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection;
6+
using Microsoft.CSharp;
17
using Newtonsoft.Json.Linq;
28
using NUnit.Framework;
39
using MCPForUnity.Editor.Tools;
@@ -367,8 +373,170 @@ public void Execute_CodedomBackend_ResolvesUnityTypes()
367373
Assert.IsNotNull(result["data"]["result"]);
368374
}
369375

376+
[Test]
377+
public void FilterAssemblyPathsForCodeDom_WithNetstandard_PreservesSystemSecurity()
378+
{
379+
var tempRoot = CreateTempDirectory();
380+
try
381+
{
382+
var netstandardPath = CompileVersionedAssembly(tempRoot, "netstandard", "2.0.0.0");
383+
var securityFixturePath = CompileVersionedAssembly(tempRoot, "SystemSecurityFixture", "4.0.0.0");
384+
var systemSecurityPath = Path.Combine(
385+
Path.GetDirectoryName(securityFixturePath),
386+
"System.Security.dll");
387+
File.Copy(securityFixturePath, systemSecurityPath);
388+
389+
var filtered = ExecuteCode.FilterAssemblyPathsForCodeDom(new[]
390+
{
391+
netstandardPath,
392+
systemSecurityPath,
393+
});
394+
395+
CollectionAssert.Contains(filtered, systemSecurityPath);
396+
}
397+
finally
398+
{
399+
Directory.Delete(tempRoot, true);
400+
}
401+
}
402+
403+
[Test]
404+
public void FilterAssemblyPathsForCodeDom_DuplicateNames_PrefersReferencedVersion()
405+
{
406+
var tempRoot = CreateTempDirectory();
407+
try
408+
{
409+
var assemblyName = "McpCodeDomDuplicate" + Guid.NewGuid().ToString("N");
410+
var referencedPath = CompileVersionedAssembly(tempRoot, assemblyName, "1.0.0.0");
411+
var newerPath = CompileVersionedAssembly(tempRoot, assemblyName, "2.0.0.0");
412+
LoadAssemblyReferencing(referencedPath);
413+
414+
var filtered = ExecuteCode.FilterAssemblyPathsForCodeDom(new[]
415+
{
416+
newerPath,
417+
referencedPath,
418+
});
419+
420+
Assert.AreEqual(1, filtered.Length);
421+
Assert.AreEqual(referencedPath, filtered[0]);
422+
}
423+
finally
424+
{
425+
Directory.Delete(tempRoot, true);
426+
}
427+
}
428+
429+
[Test]
430+
public void FilterAssemblyPathsForCodeDom_CachedAssemblyPaths_ReusesResultUntilDomainReload()
431+
{
432+
var tempRoot = CreateTempDirectory();
433+
var cachedAssemblyPathsField = typeof(ExecuteCode).GetField(
434+
"_cachedAssemblyPaths",
435+
BindingFlags.NonPublic | BindingFlags.Static);
436+
var cachedCodeDomAssemblyPathsField = typeof(ExecuteCode).GetField(
437+
"_cachedCodeDomAssemblyPaths",
438+
BindingFlags.NonPublic | BindingFlags.Static);
439+
var onDomainReload = typeof(ExecuteCode).GetMethod(
440+
"OnDomainReload",
441+
BindingFlags.NonPublic | BindingFlags.Static);
442+
Assert.IsNotNull(cachedAssemblyPathsField);
443+
Assert.IsNotNull(cachedCodeDomAssemblyPathsField);
444+
Assert.IsNotNull(onDomainReload);
445+
446+
try
447+
{
448+
onDomainReload.Invoke(null, null);
449+
var assemblyName = "McpCodeDomCache" + Guid.NewGuid().ToString("N");
450+
var olderPath = CompileVersionedAssembly(tempRoot, assemblyName, "1.0.0.0");
451+
var newerPath = CompileVersionedAssembly(tempRoot, assemblyName, "2.0.0.0");
452+
var cachedAssemblyPaths = new[] { olderPath, newerPath };
453+
cachedAssemblyPathsField.SetValue(null, cachedAssemblyPaths);
454+
455+
var first = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths);
456+
Assert.AreEqual(1, first.Length);
457+
458+
File.WriteAllText(olderPath, "invalidated");
459+
File.WriteAllText(newerPath, "invalidated");
460+
var second = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths);
461+
Assert.AreSame(first, second);
462+
463+
onDomainReload.Invoke(null, null);
464+
cachedAssemblyPathsField.SetValue(null, cachedAssemblyPaths);
465+
var afterReload = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths);
466+
Assert.AreNotSame(first, afterReload);
467+
Assert.AreEqual(2, afterReload.Length);
468+
}
469+
finally
470+
{
471+
onDomainReload.Invoke(null, null);
472+
Directory.Delete(tempRoot, true);
473+
}
474+
}
475+
370476
// ──────────────────── Helpers ────────────────────
371477

478+
private static string CreateTempDirectory()
479+
{
480+
var path = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N"));
481+
Directory.CreateDirectory(path);
482+
return path;
483+
}
484+
485+
private static string CompileVersionedAssembly(string tempRoot, string assemblyName, string version)
486+
{
487+
var outputDirectory = Path.Combine(tempRoot, version);
488+
Directory.CreateDirectory(outputDirectory);
489+
var outputPath = Path.Combine(outputDirectory, assemblyName + ".dll");
490+
var source =
491+
"using System.Reflection;\n" +
492+
"[assembly: AssemblyVersion(\"" + version + "\")]\n" +
493+
"public sealed class VersionMarker { }";
494+
495+
using (var provider = new CSharpCodeProvider())
496+
{
497+
var parameters = new CompilerParameters
498+
{
499+
GenerateExecutable = false,
500+
GenerateInMemory = false,
501+
OutputAssembly = outputPath,
502+
};
503+
var results = provider.CompileAssemblyFromSource(parameters, source);
504+
AssertCompilerSuccess(results);
505+
}
506+
507+
return outputPath;
508+
}
509+
510+
private static void LoadAssemblyReferencing(string referencedAssemblyPath)
511+
{
512+
using (var provider = new CSharpCodeProvider())
513+
{
514+
var parameters = new CompilerParameters
515+
{
516+
GenerateExecutable = false,
517+
GenerateInMemory = true,
518+
};
519+
parameters.ReferencedAssemblies.Add(referencedAssemblyPath);
520+
521+
var results = provider.CompileAssemblyFromSource(
522+
parameters,
523+
"public static class ReferenceHolder { " +
524+
"public static System.Type Get() { return typeof(VersionMarker); } }");
525+
AssertCompilerSuccess(results);
526+
Assert.IsNotNull(results.CompiledAssembly);
527+
}
528+
}
529+
530+
private static void AssertCompilerSuccess(CompilerResults results)
531+
{
532+
var errors = results.Errors
533+
.Cast<CompilerError>()
534+
.Where(error => !error.IsWarning)
535+
.Select(error => error.ToString())
536+
.ToArray();
537+
Assert.IsFalse(results.Errors.HasErrors, string.Join("\n", errors));
538+
}
539+
372540
private static JObject Execute(string code)
373541
{
374542
return ToJObject(ExecuteCode.HandleCommand(new JObject

0 commit comments

Comments
 (0)