175 lines
7.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|