Files
2026-07-13 12:49:17 +08:00

175 lines
7.6 KiB
C#

using System;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
namespace MCPForUnity.Editor.Setup
{
public static class RoslynInstaller
{
private const string PluginsRelPath = "Plugins/Roslyn";
private static readonly (string packageId, string version, string dllPath, string dllName)[] NuGetEntries =
{
("microsoft.codeanalysis.common", "4.12.0", "lib/netstandard2.0/Microsoft.CodeAnalysis.dll", "Microsoft.CodeAnalysis.dll"),
("microsoft.codeanalysis.csharp", "4.12.0", "lib/netstandard2.0/Microsoft.CodeAnalysis.CSharp.dll", "Microsoft.CodeAnalysis.CSharp.dll"),
("system.collections.immutable", "8.0.0", "lib/netstandard2.0/System.Collections.Immutable.dll", "System.Collections.Immutable.dll"),
("system.reflection.metadata", "8.0.0", "lib/netstandard2.0/System.Reflection.Metadata.dll", "System.Reflection.Metadata.dll"),
// Transitive dep of Microsoft.CodeAnalysis.* on netstandard2.0. Without it, Roslyn's StringTable
// static cctor throws FileNotFoundException for v6.0.0.0 and every Roslyn entry point fails to
// initialize. Unity ships a v4.x of this assembly which does NOT satisfy the v6 reference.
("system.runtime.compilerservices.unsafe","6.0.0", "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll", "System.Runtime.CompilerServices.Unsafe.dll"),
};
public static bool IsInstalled()
{
string folder = Path.Combine(Application.dataPath, PluginsRelPath);
foreach (var entry in NuGetEntries)
{
string path = Path.Combine(folder, entry.dllName);
if (!File.Exists(path))
return false;
// Defense-in-depth: a stale DLL whose assembly version is older than what
// Roslyn references (e.g. a v4.x System.Runtime.CompilerServices.Unsafe
// shadowing the v6 we actually need) would still satisfy file-existence but
// leave Roslyn unable to load. Compare the on-disk assembly version against
// each entry's declared NuGet version, treating "older or unreadable" as not
// installed so Install() can rewrite it.
if (Version.TryParse(entry.version, out var requiredVersion))
{
try
{
var actual = AssemblyName.GetAssemblyName(path).Version;
if (actual == null || actual < requiredVersion)
return false;
}
catch
{
return false;
}
}
}
return true;
}
public static void Install(bool interactive = true)
{
if (IsInstalled() && interactive)
{
if (!EditorUtility.DisplayDialog(
"Roslyn Already Installed",
$"Roslyn DLLs are already present in Assets/{PluginsRelPath}.\nReinstall?",
"Reinstall", "Cancel"))
return;
}
string destFolder = Path.Combine(Application.dataPath, PluginsRelPath);
try
{
Directory.CreateDirectory(destFolder);
for (int i = 0; i < NuGetEntries.Length; i++)
{
var (packageId, pkgVersion, dllPathInZip, dllName) = NuGetEntries[i];
if (interactive)
{
EditorUtility.DisplayProgressBar(
"Installing Roslyn",
$"Downloading {packageId} v{pkgVersion}...",
(float)i / NuGetEntries.Length);
}
string url =
$"https://api.nuget.org/v3-flatcontainer/{packageId}/{pkgVersion}/{packageId}.{pkgVersion}.nupkg";
using (var request = UnityWebRequest.Get(url))
{
request.timeout = 30;
request.SendWebRequest();
while (!request.isDone)
System.Threading.Thread.Sleep(50);
if (request.result != UnityWebRequest.Result.Success)
throw new Exception($"Failed to download {packageId}: {request.error}");
byte[] nupkgBytes = request.downloadHandler.data;
byte[] dllBytes = ExtractFileFromZip(nupkgBytes, dllPathInZip);
if (dllBytes == null)
{
Debug.LogError($"[MCP] Could not find {dllPathInZip} in {packageId}.{pkgVersion}.nupkg");
continue;
}
string destPath = Path.Combine(destFolder, dllName);
File.WriteAllBytes(destPath, dllBytes);
Debug.Log($"[MCP] Extracted {dllName} ({dllBytes.Length / 1024}KB) → Assets/{PluginsRelPath}/{dllName}");
}
}
if (interactive)
EditorUtility.DisplayProgressBar("Installing Roslyn", "Refreshing assets...", 0.95f);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
if (interactive)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog(
"Roslyn Installed",
$"Roslyn DLLs and dependencies installed to Assets/{PluginsRelPath}/.\n\n" +
"The runtime_compilation tool is now available via MCP.",
"OK");
}
Debug.Log($"[MCP] Roslyn installation complete ({NuGetEntries.Length} DLLs). runtime_compilation is now available.");
}
catch (Exception e)
{
if (interactive) EditorUtility.ClearProgressBar();
Debug.LogError($"[MCP] Failed to install Roslyn: {e}");
if (interactive)
{
EditorUtility.DisplayDialog(
"Installation Failed",
$"Could not download Roslyn DLLs:\n{e.Message}\n\n" +
"You can manually download Microsoft.CodeAnalysis.CSharp from NuGet " +
"and place the DLLs in Assets/Plugins/Roslyn/.",
"OK");
}
}
}
private static byte[] ExtractFileFromZip(byte[] zipBytes, string entryPath)
{
entryPath = entryPath.Replace('\\', '/');
using (var stream = new MemoryStream(zipBytes))
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
{
foreach (var entry in archive.Entries)
{
if (entry.FullName.Replace('\\', '/').Equals(entryPath, StringComparison.OrdinalIgnoreCase))
{
using (var entryStream = entry.Open())
using (var output = new MemoryStream())
{
entryStream.CopyTo(output);
return output.ToArray();
}
}
}
}
return null;
}
}
}