updated server install process

main
Justin Barnett 2025-04-08 10:33:14 -04:00
parent 441c0aac57
commit 2f6d748607
2 changed files with 169 additions and 189 deletions

View File

@ -1,9 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using UnityEngine; using UnityEngine;
namespace UnityMcpBridge.Editor.Helpers namespace UnityMcpBridge.Editor.Helpers
@ -11,240 +10,222 @@ namespace UnityMcpBridge.Editor.Helpers
public static class ServerInstaller public static class ServerInstaller
{ {
private const string PackageName = "unity-mcp-server"; private const string PackageName = "unity-mcp-server";
private const string BranchName = "feature/install-overhaul"; private const string BranchName = "feature/install-overhaul"; // Adjust branch as needed
private const string GitUrl = private const string GitUrl = "https://github.com/justinpbarnett/unity-mcp.git";
"git+https://github.com/justinpbarnett/unity-mcp.git@"
+ BranchName
+ "#subdirectory=UnityMcpServer";
private const string PyprojectUrl = private const string PyprojectUrl =
"https://raw.githubusercontent.com/justinpbarnett/unity-mcp/" "https://raw.githubusercontent.com/justinpbarnett/unity-mcp/"
+ BranchName + BranchName
+ "/UnityMcpServer/pyproject.toml"; + "/UnityMcpServer/pyproject.toml";
// Typical uv installation paths per OS /// <summary>
private static readonly string[] WindowsUvPaths = new[] /// Ensures the unity-mcp-server is installed and up to date.
{ /// </summary>
@"C:\Users\$USER$\.local\bin\uv.exe",
@"C:\Program Files\uv\uv.exe",
@"C:\Users\$USER$\AppData\Local\Programs\uv\uv.exe",
};
private static readonly string[] LinuxUvPaths = new[]
{
"/home/$USER$/.local/bin/uv",
"/usr/local/bin/uv",
"/usr/bin/uv",
};
private static readonly string[] MacUvPaths = new[]
{
"/Users/$USER$/.local/bin/uv",
"/usr/local/bin/uv",
"/opt/homebrew/bin/uv",
};
public static void EnsureServerInstalled() public static void EnsureServerInstalled()
{ {
try try
{ {
string uvPath = FindUvExecutable(); string saveLocation = GetSaveLocation();
if (string.IsNullOrEmpty(uvPath)) Debug.Log($"Server save location: {saveLocation}");
{
throw new Exception(
"Could not find 'uv' executable. Please ensure it is installed."
);
}
// Check if the package is installed if (!IsServerInstalled(saveLocation))
System.Diagnostics.Process process = new();
process.StartInfo.FileName = uvPath;
process.StartInfo.Arguments = "pip show " + PackageName;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode == 0)
{ {
// Package is installed, check version Debug.Log("Server not found. Installing...");
string installedVersion = GetVersionFromPipShow(output); InstallServer(saveLocation);
string latestVersion = GetLatestVersionFromGitHub();
if (new Version(installedVersion) < new Version(latestVersion))
{
Debug.Log(
$"Updating {PackageName} from {installedVersion} to {latestVersion}..."
);
RunCommand(uvPath, "pip install --upgrade " + GitUrl);
Debug.Log($"{PackageName} updated successfully.");
}
else
{
Debug.Log($"{PackageName} is up to date (version {installedVersion}).");
}
}
else if (process.ExitCode == 1 && output.Contains("Package(s) not found"))
{
// Package not found, install it from GitHub
Debug.Log("Installing " + PackageName + "...");
RunCommand(uvPath, "pip install " + GitUrl);
Debug.Log(PackageName + " installed successfully.");
} }
else else
{ {
throw new Exception( Debug.Log("Server is installed. Checking version...");
$"Command 'uv pip show {PackageName}' failed with exit code {process.ExitCode}. Output: {output} Error: {error}" string installedVersion = GetInstalledVersion(saveLocation);
); string latestVersion = GetLatestVersion();
if (IsNewerVersion(latestVersion, installedVersion))
{
Debug.Log(
$"Newer version available ({latestVersion} > {installedVersion}). Updating..."
);
UpdateServer(saveLocation);
}
else
{
Debug.Log("Server is up to date.");
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError($"Failed to ensure {PackageName} is installed: {ex.Message}"); Debug.LogError($"Failed to ensure server installation: {ex.Message}");
Debug.LogWarning(
"Please install "
+ PackageName
+ " manually using 'uv pip install "
+ GitUrl
+ "'."
);
} }
} }
private static void RunCommand(string uvPath, string arguments) /// <summary>
/// Gets the platform-specific save location for the server.
/// </summary>
private static string GetSaveLocation()
{ {
System.Diagnostics.Process installProcess = new();
installProcess.StartInfo.FileName = uvPath;
installProcess.StartInfo.Arguments = arguments;
installProcess.StartInfo.UseShellExecute = false;
installProcess.StartInfo.RedirectStandardOutput = true;
installProcess.StartInfo.RedirectStandardError = true;
installProcess.Start();
string installOutput = installProcess.StandardOutput.ReadToEnd();
string installError = installProcess.StandardError.ReadToEnd();
installProcess.WaitForExit();
if (installProcess.ExitCode != 0)
{
throw new Exception(
$"Command '{uvPath} {arguments}' failed. Output: {installOutput} Error: {installError}"
);
}
}
private static string FindUvExecutable()
{
string username = Environment.UserName;
string[] uvPaths;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
uvPaths = WindowsUvPaths.Select(p => p.Replace("$USER$", username)).ToArray(); // Use a user-specific program directory under %USERPROFILE%\AppData\Local\Programs
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"AppData",
"Local",
"Programs",
PackageName
);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{ {
uvPaths = LinuxUvPaths.Select(p => p.Replace("$USER$", username)).ToArray(); return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"bin",
PackageName
);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
uvPaths = MacUvPaths.Select(p => p.Replace("$USER$", username)).ToArray(); string path = "/usr/local/bin";
} if (!Directory.Exists(path) || !IsDirectoryWritable(path))
else {
{ return Path.Combine(
throw new PlatformNotSupportedException("Unsupported operating system."); Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Applications",
PackageName
);
}
return Path.Combine(path, PackageName);
} }
throw new Exception("Unsupported operating system.");
}
// First, try 'uv' directly from PATH private static bool IsDirectoryWritable(string path)
{
try try
{ {
System.Diagnostics.Process process = new(); File.Create(Path.Combine(path, "test.txt")).Dispose();
process.StartInfo.FileName = "uv"; File.Delete(Path.Combine(path, "test.txt"));
process.StartInfo.Arguments = "--version"; return true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
process.WaitForExit(2000); // Wait up to 2 seconds
if (process.ExitCode == 0)
{
return "uv"; // Found in PATH
}
} }
catch catch
{ {
// Not in PATH, proceed to check specific locations return false;
} }
}
// Check specific paths /// <summary>
foreach (string path in uvPaths) /// 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"));
}
/// <summary>
/// Installs the server by cloning the repository and setting up dependencies.
/// </summary>
private static void InstallServer(string location)
{
// Clone the repository
RunCommand("git", $"clone -b {BranchName} {GitUrl} \"{location}\"");
// Set up virtual environment and install dependencies
string venvPath = Path.Combine(location, "venv");
RunCommand("python", $"-m venv \"{venvPath}\"");
string uvPath = Path.Combine(
venvPath,
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Scripts/uv.exe" : "bin/uv"
);
RunCommand(uvPath, "install");
}
/// <summary>
/// Retrieves the installed server version from version.txt.
/// </summary>
private static string GetInstalledVersion(string location)
{
string versionFile = Path.Combine(location, "version.txt");
return File.ReadAllText(versionFile).Trim();
}
/// <summary>
/// Fetches the latest version from the GitHub pyproject.toml file.
/// </summary>
private static string GetLatestVersion()
{
using var webClient = new WebClient();
string pyprojectContent = webClient.DownloadString(PyprojectUrl);
return ParseVersionFromPyproject(pyprojectContent);
}
/// <summary>
/// Updates the server by pulling the latest changes.
/// </summary>
private static void UpdateServer(string location)
{
RunCommand("git", $"-C \"{location}\" pull");
// Optionally reinstall dependencies if requirements changed
}
/// <summary>
/// Parses the version number from pyproject.toml content.
/// </summary>
private static string ParseVersionFromPyproject(string content)
{
foreach (var line in content.Split('\n'))
{ {
if (File.Exists(path)) if (line.Trim().StartsWith("version ="))
{ {
return path; var parts = line.Split('=');
if (parts.Length == 2)
return parts[1].Trim().Trim('"');
} }
} }
throw new Exception("Version not found in pyproject.toml");
return null; // Not found
} }
private static string GetVersionFromPipShow(string output) /// <summary>
/// Compares two version strings to determine if the latest is newer.
/// </summary>
private static bool IsNewerVersion(string latest, string installed)
{ {
string[] lines = output.Split('\n'); var latestParts = latest.Split('.').Select(int.Parse).ToArray();
foreach (string line in lines) var installedParts = installed.Split('.').Select(int.Parse).ToArray();
for (int i = 0; i < Math.Min(latestParts.Length, installedParts.Length); i++)
{ {
if (line.StartsWith("Version:")) if (latestParts[i] > installedParts[i])
return true;
if (latestParts[i] < installedParts[i])
return false;
}
return latestParts.Length > installedParts.Length;
}
/// <summary>
/// Runs a command-line process and handles output/errors.
/// </summary>
private static void RunCommand(string command, string arguments)
{
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{ {
return line["Version:".Length..].Trim(); FileName = command,
} Arguments = arguments,
} RedirectStandardOutput = true,
throw new Exception("Version not found in pip show output"); RedirectStandardError = true,
} UseShellExecute = false,
CreateNoWindow = true,
private static string GetLatestVersionFromGitHub() },
{ };
using HttpClient client = new(); process.Start();
client.DefaultRequestHeaders.Add("User-Agent", "UnityMcpBridge"); string output = process.StandardOutput.ReadToEnd();
string content = client.GetStringAsync(PyprojectUrl).Result; string error = process.StandardError.ReadToEnd();
string pattern = @"version\s*=\s*""(.*?)"""; process.WaitForExit();
Match match = Regex.Match(content, pattern); if (process.ExitCode != 0)
return match.Success
? match.Groups[1].Value
: throw new Exception("Could not find version in pyproject.toml");
}
private static string GetUvNotFoundMessage()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
return "uv not found in PATH or typical Windows locations."; throw new Exception(
$"Command failed: {command} {arguments}\nOutput: {output}\nError: {error}"
);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "uv not found in PATH or typical Linux locations.";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "uv not found in PATH or typical macOS locations.";
}
return "uv not found on this platform.";
}
private static string GetInstallInstructions()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "Install uv with: powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\" and ensure it's in your PATH.";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "Install uv with: curl -LsSf https://astral.sh/uv/install.sh | sh and ensure it's in your PATH.";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Install uv with: brew install uv or curl -LsSf https://astral.sh/uv/install.sh | sh and ensure it's in your PATH.";
}
return "Install uv following platform-specific instructions and add it to your PATH.";
} }
} }
} }

View File

@ -56,6 +56,8 @@ namespace UnityMcpBridge.Editor
public static void Start() public static void Start()
{ {
Stop();
try try
{ {
ServerInstaller.EnsureServerInstalled(); ServerInstaller.EnsureServerInstalled();
@ -70,9 +72,6 @@ namespace UnityMcpBridge.Editor
return; return;
} }
// Stop any existing listener to free the port
Stop();
try try
{ {
listener = new TcpListener(IPAddress.Loopback, unityPort); listener = new TcpListener(IPAddress.Loopback, unityPort);