2025-04-08 19:22:24 +08:00
|
|
|
using System;
|
2025-04-08 19:52:44 +08:00
|
|
|
using System.IO;
|
|
|
|
|
using System.Runtime.InteropServices;
|
2025-08-13 02:56:46 +08:00
|
|
|
using System.Text;
|
2025-08-12 23:32:51 +08:00
|
|
|
using System.Reflection;
|
2025-08-08 23:08:30 +08:00
|
|
|
using UnityEditor;
|
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 19:52:44 +08:00
|
|
|
|
2025-04-08 22:33:14 +08:00
|
|
|
/// <summary>
|
2025-08-08 23:08:30 +08:00
|
|
|
/// Ensures the unity-mcp-server is installed locally by copying from the embedded package source.
|
|
|
|
|
/// No network calls or Git operations are performed.
|
2025-04-08 22:33:14 +08:00
|
|
|
/// </summary>
|
2025-04-08 19:52:44 +08:00
|
|
|
public static void EnsureServerInstalled()
|
2025-04-08 19:22:24 +08:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-04-08 22:33:14 +08:00
|
|
|
string saveLocation = GetSaveLocation();
|
2025-08-08 23:08:30 +08:00
|
|
|
string destRoot = Path.Combine(saveLocation, ServerFolder);
|
|
|
|
|
string destSrc = Path.Combine(destRoot, "src");
|
2025-04-08 22:33:14 +08:00
|
|
|
|
2025-08-08 23:08:30 +08:00
|
|
|
if (File.Exists(Path.Combine(destSrc, "server.py")))
|
2025-04-08 19:52:44 +08:00
|
|
|
{
|
2025-08-08 23:08:30 +08:00
|
|
|
return; // Already installed
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
2025-04-08 22:33:14 +08:00
|
|
|
|
2025-08-08 23:08:30 +08:00
|
|
|
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
|
2025-04-08 19:22:24 +08:00
|
|
|
}
|
2025-08-08 23:08:30 +08:00
|
|
|
|
|
|
|
|
// Ensure destination exists
|
|
|
|
|
Directory.CreateDirectory(destRoot);
|
|
|
|
|
|
|
|
|
|
// Copy the entire UnityMcpServer folder (parent of src)
|
|
|
|
|
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
|
|
|
|
|
CopyDirectoryRecursive(embeddedRoot, destRoot);
|
2025-04-08 19:22:24 +08:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
// If a usable server is already present (installed or embedded), don't fail hard—just warn.
|
|
|
|
|
bool hasInstalled = false;
|
|
|
|
|
try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
|
|
|
|
|
|
|
|
|
|
if (hasInstalled || TryGetEmbeddedServerSource(out _))
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning($"UnityMCP: Using existing server; skipped install. Details: {ex.Message}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 22:33:14 +08:00
|
|
|
Debug.LogError($"Failed to ensure server installation: {ex.Message}");
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 03:02:59 +08:00
|
|
|
public static string GetServerPath()
|
|
|
|
|
{
|
|
|
|
|
return Path.Combine(GetSaveLocation(), ServerFolder, "src");
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 22:33:14 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the platform-specific save location for the server.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static string GetSaveLocation()
|
2025-04-08 19:52:44 +08:00
|
|
|
{
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
2025-04-08 22:33:14 +08:00
|
|
|
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
|
|
|
);
|
2025-04-08 19:52:44 +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
|
|
|
);
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
|
|
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
|
|
|
{
|
2025-08-09 05:16:25 +08:00
|
|
|
// Use Application Support for a stable, user-writable location
|
|
|
|
|
return Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
|
|
"UnityMCP"
|
|
|
|
|
);
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
2025-04-08 22:33:14 +08:00
|
|
|
throw new Exception("Unsupported operating system.");
|
|
|
|
|
}
|
2025-04-08 19:52:44 +08:00
|
|
|
|
2025-04-08 22:33:14 +08:00
|
|
|
private static bool IsDirectoryWritable(string path)
|
|
|
|
|
{
|
2025-04-08 19:52:44 +08:00
|
|
|
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;
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
2025-04-08 22:33:14 +08:00
|
|
|
return false;
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
2025-04-08 22:33:14 +08:00
|
|
|
}
|
2025-04-08 19:52:44 +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)
|
|
|
|
|
{
|
2025-04-09 03:02:59 +08:00
|
|
|
return Directory.Exists(location)
|
2025-08-08 23:08:30 +08:00
|
|
|
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
|
2025-04-09 19:42:43 +08:00
|
|
|
}
|
|
|
|
|
|
2025-04-08 22:33:14 +08:00
|
|
|
/// <summary>
|
2025-08-08 23:08:30 +08:00
|
|
|
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
|
|
|
|
|
/// or common development locations.
|
2025-04-08 22:33:14 +08:00
|
|
|
/// </summary>
|
2025-08-08 23:08:30 +08:00
|
|
|
private static bool TryGetEmbeddedServerSource(out string srcPath)
|
2025-04-08 19:52:44 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
|
2025-04-08 22:33:14 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 23:08:30 +08:00
|
|
|
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
|
2025-04-08 22:33:14 +08:00
|
|
|
{
|
2025-08-08 23:08:30 +08:00
|
|
|
Directory.CreateDirectory(destinationDir);
|
|
|
|
|
|
|
|
|
|
foreach (string filePath in Directory.GetFiles(sourceDir))
|
2025-04-08 19:52:44 +08:00
|
|
|
{
|
2025-08-08 23:08:30 +08:00
|
|
|
string fileName = Path.GetFileName(filePath);
|
|
|
|
|
string destFile = Path.Combine(destinationDir, fileName);
|
|
|
|
|
File.Copy(filePath, destFile, overwrite: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (string dirPath in Directory.GetDirectories(sourceDir))
|
2025-04-08 19:52:44 +08:00
|
|
|
{
|
2025-08-08 23:08:30 +08:00
|
|
|
string dirName = Path.GetFileName(dirPath);
|
|
|
|
|
string destSubDir = Path.Combine(destinationDir, dirName);
|
|
|
|
|
CopyDirectoryRecursive(dirPath, destSubDir);
|
2025-04-08 19:52:44 +08:00
|
|
|
}
|
2025-04-08 19:22:24 +08:00
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
|
|
|
|
|
public static bool RepairPythonEnvironment()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
string serverSrc = GetServerPath();
|
|
|
|
|
bool hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
|
|
|
|
|
if (!hasServer)
|
|
|
|
|
{
|
|
|
|
|
// In dev mode or if not installed yet, try the embedded/dev source
|
|
|
|
|
if (TryGetEmbeddedServerSource(out string embeddedSrc) && File.Exists(Path.Combine(embeddedSrc, "server.py")))
|
|
|
|
|
{
|
|
|
|
|
serverSrc = embeddedSrc;
|
|
|
|
|
hasServer = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Attempt to install then retry
|
|
|
|
|
EnsureServerInstalled();
|
|
|
|
|
serverSrc = GetServerPath();
|
|
|
|
|
hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasServer)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("RepairPythonEnvironment: server.py not found; ensure server is installed first.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove stale venv and pinned version file if present
|
|
|
|
|
string venvPath = Path.Combine(serverSrc, ".venv");
|
|
|
|
|
if (Directory.Exists(venvPath))
|
|
|
|
|
{
|
|
|
|
|
try { Directory.Delete(venvPath, recursive: true); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .venv: {ex.Message}"); }
|
|
|
|
|
}
|
|
|
|
|
string pyPin = Path.Combine(serverSrc, ".python-version");
|
|
|
|
|
if (File.Exists(pyPin))
|
|
|
|
|
{
|
|
|
|
|
try { File.Delete(pyPin); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .python-version: {ex.Message}"); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string uvPath = FindUvPath();
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)." );
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var psi = new System.Diagnostics.ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = uvPath,
|
|
|
|
|
Arguments = "sync",
|
|
|
|
|
WorkingDirectory = serverSrc,
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-13 02:56:46 +08:00
|
|
|
using var proc = new System.Diagnostics.Process { StartInfo = psi };
|
|
|
|
|
var sbOut = new StringBuilder();
|
|
|
|
|
var sbErr = new StringBuilder();
|
|
|
|
|
proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); };
|
|
|
|
|
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); };
|
2025-08-09 05:16:25 +08:00
|
|
|
|
2025-08-13 02:56:46 +08:00
|
|
|
if (!proc.Start())
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("Failed to start uv process.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proc.BeginOutputReadLine();
|
|
|
|
|
proc.BeginErrorReadLine();
|
|
|
|
|
|
|
|
|
|
if (!proc.WaitForExit(60000))
|
|
|
|
|
{
|
|
|
|
|
try { proc.Kill(); } catch { }
|
|
|
|
|
Debug.LogError("uv sync timed out.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure async buffers flushed
|
|
|
|
|
proc.WaitForExit();
|
|
|
|
|
|
|
|
|
|
string stdout = sbOut.ToString();
|
|
|
|
|
string stderr = sbErr.ToString();
|
|
|
|
|
|
|
|
|
|
if (proc.ExitCode != 0)
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
|
|
|
|
Debug.LogError($"uv sync failed: {stderr}\n{stdout}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 03:05:47 +08:00
|
|
|
Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Python environment repaired successfully.");
|
2025-08-09 05:16:25 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"RepairPythonEnvironment failed: {ex.Message}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
internal static string FindUvPath()
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
|
|
|
|
// Allow user override via EditorPrefs
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
string overridePath = EditorPrefs.GetString("UnityMCP.UvPath", string.Empty);
|
|
|
|
|
if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
|
|
|
|
|
{
|
|
|
|
|
if (ValidateUvBinary(overridePath)) return overridePath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
|
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
2025-08-10 03:05:47 +08:00
|
|
|
|
|
|
|
|
// Platform-specific candidate lists
|
|
|
|
|
string[] candidates;
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
candidates = new[]
|
|
|
|
|
{
|
|
|
|
|
// Common per-user installs
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python313\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python312\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python311\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty, @"Programs\Python\Python310\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python313\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python312\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python311\Scripts\uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty, @"Python\Python310\Scripts\uv.exe"),
|
|
|
|
|
// Program Files style installs (if a native installer was used)
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty, @"uv\uv.exe"),
|
|
|
|
|
// Try simple name resolution later via PATH
|
|
|
|
|
"uv.exe",
|
|
|
|
|
"uv"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
candidates = new[]
|
|
|
|
|
{
|
|
|
|
|
"/opt/homebrew/bin/uv",
|
|
|
|
|
"/usr/local/bin/uv",
|
|
|
|
|
"/usr/bin/uv",
|
|
|
|
|
"/opt/local/bin/uv",
|
|
|
|
|
Path.Combine(home, ".local", "bin", "uv"),
|
|
|
|
|
"/opt/homebrew/opt/uv/bin/uv",
|
|
|
|
|
// Framework Python installs
|
|
|
|
|
"/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
|
|
|
|
|
"/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
|
|
|
|
|
// Fallback to PATH resolution by name
|
|
|
|
|
"uv"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 05:16:25 +08:00
|
|
|
foreach (string c in candidates)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
if (File.Exists(c) && ValidateUvBinary(c)) return c;
|
2025-08-09 05:16:25 +08:00
|
|
|
}
|
|
|
|
|
catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 03:05:47 +08:00
|
|
|
// Use platform-appropriate which/where to resolve from PATH
|
2025-08-09 05:16:25 +08:00
|
|
|
try
|
|
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
var wherePsi = new System.Diagnostics.ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "where",
|
|
|
|
|
Arguments = "uv.exe",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
using var wp = System.Diagnostics.Process.Start(wherePsi);
|
|
|
|
|
string output = wp.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
wp.WaitForExit(3000);
|
|
|
|
|
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
|
|
|
|
{
|
|
|
|
|
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
|
|
|
|
{
|
|
|
|
|
string path = line.Trim();
|
|
|
|
|
if (File.Exists(path) && ValidateUvBinary(path)) return path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
var whichPsi = new System.Diagnostics.ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "/usr/bin/which",
|
|
|
|
|
Arguments = "uv",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
2025-08-13 01:48:46 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
|
|
|
|
|
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
|
|
|
|
string prepend = string.Join(":", new[]
|
|
|
|
|
{
|
|
|
|
|
System.IO.Path.Combine(homeDir, ".local", "bin"),
|
|
|
|
|
"/opt/homebrew/bin",
|
|
|
|
|
"/usr/local/bin",
|
|
|
|
|
"/usr/bin",
|
|
|
|
|
"/bin"
|
|
|
|
|
});
|
|
|
|
|
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
2025-08-13 02:56:46 +08:00
|
|
|
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
|
2025-08-13 01:48:46 +08:00
|
|
|
}
|
|
|
|
|
catch { }
|
2025-08-10 03:05:47 +08:00
|
|
|
using var wp = System.Diagnostics.Process.Start(whichPsi);
|
|
|
|
|
string output = wp.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
wp.WaitForExit(3000);
|
|
|
|
|
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
|
|
|
|
{
|
|
|
|
|
if (ValidateUvBinary(output)) return output;
|
|
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
|
|
// Manual PATH scan
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
|
|
|
|
string[] parts = pathEnv.Split(Path.PathSeparator);
|
|
|
|
|
foreach (string part in parts)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
// Check both uv and uv.exe
|
|
|
|
|
string candidateUv = Path.Combine(part, "uv");
|
|
|
|
|
string candidateUvExe = Path.Combine(part, "uv.exe");
|
|
|
|
|
if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv;
|
|
|
|
|
if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe;
|
2025-08-09 05:16:25 +08:00
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool ValidateUvBinary(string uvPath)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new System.Diagnostics.ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = uvPath,
|
|
|
|
|
Arguments = "--version",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
using var p = System.Diagnostics.Process.Start(psi);
|
|
|
|
|
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
|
|
|
|
|
if (p.ExitCode == 0)
|
|
|
|
|
{
|
|
|
|
|
string output = p.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
return output.StartsWith("uv ");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-04-08 19:22:24 +08:00
|
|
|
}
|
|
|
|
|
}
|