unity-mcp/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs

304 lines
12 KiB
C#
Raw Normal View History

2025-04-08 19:22:24 +08:00
using System;
using System.IO;
using System.Linq;
2025-04-08 22:33:14 +08:00
using System.Net;
using System.Runtime.InteropServices;
2025-04-08 19:22:24 +08:00
using UnityEngine;
namespace UnityMcpBridge.Editor.Helpers
{
public static class ServerInstaller
{
2025-04-08 23:41:14 +08:00
private const string RootFolder = "UnityMCP";
private const string ServerFolder = "UnityMcpServer";
2025-04-08 22:33:14 +08:00
private const string BranchName = "feature/install-overhaul"; // Adjust branch as needed
private const string GitUrl = "https://github.com/justinpbarnett/unity-mcp.git";
private const string PyprojectUrl =
2025-04-08 20:59:26 +08:00
"https://raw.githubusercontent.com/justinpbarnett/unity-mcp/"
+ BranchName
+ "/UnityMcpServer/pyproject.toml";
2025-04-08 22:33:14 +08:00
/// <summary>
/// Ensures the unity-mcp-server is installed and up to date.
/// </summary>
public static void EnsureServerInstalled()
2025-04-08 19:22:24 +08:00
{
try
{
2025-04-08 22:33:14 +08:00
string saveLocation = GetSaveLocation();
Debug.Log($"Server save location: {saveLocation}");
if (!IsServerInstalled(saveLocation))
{
2025-04-08 22:33:14 +08:00
Debug.Log("Server not found. Installing...");
InstallServer(saveLocation);
}
2025-04-08 22:33:14 +08:00
else
2025-04-08 19:22:24 +08:00
{
2025-04-08 22:33:14 +08:00
Debug.Log("Server is installed. Checking version...");
string installedVersion = GetInstalledVersion(saveLocation);
string latestVersion = GetLatestVersion();
if (IsNewerVersion(latestVersion, installedVersion))
2025-04-08 19:22:24 +08:00
{
Debug.Log(
2025-04-08 22:33:14 +08:00
$"Newer version available ({latestVersion} > {installedVersion}). Updating..."
2025-04-08 19:22:24 +08:00
);
2025-04-08 22:33:14 +08:00
UpdateServer(saveLocation);
2025-04-08 19:22:24 +08:00
}
else
{
2025-04-08 22:33:14 +08:00
Debug.Log("Server is up to date.");
2025-04-08 19:22:24 +08:00
}
}
}
catch (Exception ex)
{
2025-04-08 22:33:14 +08:00
Debug.LogError($"Failed to ensure server installation: {ex.Message}");
}
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Gets the platform-specific save location for the server.
/// </summary>
private static string GetSaveLocation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
2025-04-08 22:33:14 +08:00
// Use a user-specific program directory under %USERPROFILE%\AppData\Local\Programs
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"AppData",
"Local",
"Programs",
2025-04-08 23:41:14 +08:00
RootFolder
2025-04-08 22:33:14 +08:00
);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
2025-04-08 22:33:14 +08:00
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"bin",
2025-04-08 23:41:14 +08:00
RootFolder
2025-04-08 22:33:14 +08:00
);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
2025-04-08 22:33:14 +08:00
string path = "/usr/local/bin";
if (!Directory.Exists(path) || !IsDirectoryWritable(path))
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Applications",
2025-04-08 23:41:14 +08:00
RootFolder
2025-04-08 22:33:14 +08:00
);
}
2025-04-08 23:41:14 +08:00
return Path.Combine(path, RootFolder);
}
2025-04-08 22:33:14 +08:00
throw new Exception("Unsupported operating system.");
}
2025-04-08 22:33:14 +08:00
private static bool IsDirectoryWritable(string path)
{
try
{
2025-04-08 22:33:14 +08:00
File.Create(Path.Combine(path, "test.txt")).Dispose();
File.Delete(Path.Combine(path, "test.txt"));
return true;
}
catch
{
2025-04-08 22:33:14 +08:00
return false;
}
2025-04-08 22:33:14 +08:00
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Checks if the server is installed at the specified location.
/// </summary>
private static bool IsServerInstalled(string location)
{
return Directory.Exists(location) && File.Exists(Path.Combine(location, "version.txt"));
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Installs the server by cloning only the UnityMcpServer folder from the repository and setting up dependencies.
2025-04-08 22:33:14 +08:00
/// </summary>
private static void InstallServer(string location)
{
// Create the src directory where the server code will reside
2025-04-08 23:41:14 +08:00
Directory.CreateDirectory(location);
// Initialize git repo in the src directory
2025-04-08 23:41:14 +08:00
RunCommand("git", $"init", workingDirectory: location);
// Add remote
2025-04-08 23:41:14 +08:00
RunCommand("git", $"remote add origin {GitUrl}", workingDirectory: location);
// Configure sparse checkout
2025-04-08 23:41:14 +08:00
RunCommand("git", "config core.sparseCheckout true", workingDirectory: location);
// Set sparse checkout path to only include UnityMcpServer folder
2025-04-08 23:41:14 +08:00
string sparseCheckoutPath = Path.Combine(location, ".git", "info", "sparse-checkout");
File.WriteAllText(sparseCheckoutPath, "UnityMcpServer/");
// Fetch and checkout the branch
2025-04-08 23:41:14 +08:00
RunCommand("git", $"fetch --depth=1 origin {BranchName}", workingDirectory: location);
RunCommand("git", $"checkout {BranchName}", workingDirectory: location);
// Create version.txt file based on pyproject.toml, stored at the root level
2025-04-08 23:44:12 +08:00
string pyprojectPath = Path.Combine(
location,
"UnityMcpServer",
"src",
"pyproject.toml"
);
if (File.Exists(pyprojectPath))
{
string pyprojectContent = File.ReadAllText(pyprojectPath);
string version = ParseVersionFromPyproject(pyprojectContent);
File.WriteAllText(Path.Combine(location, "version.txt"), version);
}
else
{
throw new Exception("Failed to find pyproject.toml after checkout");
}
2025-04-08 22:33:14 +08:00
// Set up virtual environment at the root level
2025-04-08 22:33:14 +08:00
string venvPath = Path.Combine(location, "venv");
RunCommand("python", $"-m venv \"{venvPath}\"");
2025-04-08 22:39:14 +08:00
// Determine the path to the virtual environment's Python interpreter
string pythonPath = Path.Combine(
venvPath,
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "Scripts\\python.exe"
: "bin/python"
);
// Install uv into the virtual environment
RunCommand(pythonPath, "-m pip install uv");
// Use uv to install dependencies from the UnityMcpServer subdirectory in src
2025-04-08 22:33:14 +08:00
string uvPath = Path.Combine(
venvPath,
2025-04-08 22:39:14 +08:00
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Scripts\\uv.exe" : "bin/uv"
2025-04-08 22:33:14 +08:00
);
RunCommand(uvPath, "pip install ./src/UnityMcpServer", workingDirectory: location);
2025-04-08 19:22:24 +08:00
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Retrieves the installed server version from version.txt.
/// </summary>
private static string GetInstalledVersion(string location)
2025-04-08 19:22:24 +08:00
{
2025-04-08 22:33:14 +08:00
string versionFile = Path.Combine(location, "version.txt");
return File.ReadAllText(versionFile).Trim();
2025-04-08 19:22:24 +08:00
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Fetches the latest version from the GitHub pyproject.toml file.
/// </summary>
private static string GetLatestVersion()
{
2025-04-08 22:33:14 +08:00
using var webClient = new WebClient();
string pyprojectContent = webClient.DownloadString(PyprojectUrl);
return ParseVersionFromPyproject(pyprojectContent);
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Updates the server by pulling the latest changes for the UnityMcpServer folder only.
2025-04-08 22:33:14 +08:00
/// </summary>
private static void UpdateServer(string location)
{
// Pull latest changes in the src directory
2025-04-08 23:41:14 +08:00
string serverDir = Path.Combine(location, ServerFolder, "src");
RunCommand("git", $"pull origin {BranchName}", workingDirectory: serverDir);
// Update version.txt file based on pyproject.toml in src
string pyprojectPath = Path.Combine(serverDir, "UnityMcpServer", "pyproject.toml");
if (File.Exists(pyprojectPath))
{
string pyprojectContent = File.ReadAllText(pyprojectPath);
string version = ParseVersionFromPyproject(pyprojectContent);
File.WriteAllText(Path.Combine(location, "version.txt"), version);
}
// Reinstall dependencies to ensure they're up to date
string venvPath = Path.Combine(location, "venv");
string uvPath = Path.Combine(
venvPath,
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Scripts\\uv.exe" : "bin/uv"
);
RunCommand(uvPath, "pip install -U ./src/UnityMcpServer", workingDirectory: location);
2025-04-08 22:33:14 +08:00
}
/// <summary>
/// Parses the version number from pyproject.toml content.
/// </summary>
private static string ParseVersionFromPyproject(string content)
{
foreach (var line in content.Split('\n'))
{
2025-04-08 22:33:14 +08:00
if (line.Trim().StartsWith("version ="))
{
var parts = line.Split('=');
if (parts.Length == 2)
return parts[1].Trim().Trim('"');
}
}
2025-04-08 22:33:14 +08:00
throw new Exception("Version not found in pyproject.toml");
}
2025-04-08 22:33:14 +08:00
/// <summary>
/// Compares two version strings to determine if the latest is newer.
/// </summary>
private static bool IsNewerVersion(string latest, string installed)
2025-04-08 19:22:24 +08:00
{
2025-04-08 22:33:14 +08:00
var latestParts = latest.Split('.').Select(int.Parse).ToArray();
var installedParts = installed.Split('.').Select(int.Parse).ToArray();
for (int i = 0; i < Math.Min(latestParts.Length, installedParts.Length); i++)
{
2025-04-08 22:33:14 +08:00
if (latestParts[i] > installedParts[i])
return true;
if (latestParts[i] < installedParts[i])
return false;
}
2025-04-08 22:33:14 +08:00
return latestParts.Length > installedParts.Length;
}
/// <summary>
/// Runs a command-line process and handles output/errors.
/// </summary>
private static void RunCommand(
string command,
string arguments,
string workingDirectory = null
)
2025-04-08 22:33:14 +08:00
{
var process = new System.Diagnostics.Process
{
2025-04-08 22:33:14 +08:00
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory ?? string.Empty,
2025-04-08 22:33:14 +08:00
},
};
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
2025-04-08 22:33:14 +08:00
throw new Exception(
$"Command failed: {command} {arguments}\nOutput: {output}\nError: {error}"
);
}
2025-04-08 19:22:24 +08:00
}
}
}