chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for platform-specific dependency detection
|
||||
/// </summary>
|
||||
public interface IPlatformDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Platform name this detector handles
|
||||
/// </summary>
|
||||
string PlatformName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this detector can run on the current platform
|
||||
/// </summary>
|
||||
bool CanDetect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Detect Python installation on this platform
|
||||
/// </summary>
|
||||
DependencyStatus DetectPython();
|
||||
|
||||
/// <summary>
|
||||
/// Detect uv package manager on this platform
|
||||
/// </summary>
|
||||
DependencyStatus DetectUv();
|
||||
|
||||
/// <summary>
|
||||
/// Get platform-specific installation recommendations
|
||||
/// </summary>
|
||||
string GetInstallationRecommendations();
|
||||
|
||||
/// <summary>
|
||||
/// Get platform-specific Python installation URL
|
||||
/// </summary>
|
||||
string GetPythonInstallUrl();
|
||||
|
||||
/// <summary>
|
||||
/// Get platform-specific uv installation URL
|
||||
/// </summary>
|
||||
string GetUvInstallUrl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67d73d0e8caef4e60942f4419c6b76bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Linux-specific dependency detection
|
||||
/// </summary>
|
||||
public class LinuxPlatformDetector : PlatformDetectorBase
|
||||
{
|
||||
public override string PlatformName => "Linux";
|
||||
|
||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
|
||||
public override DependencyStatus DetectPython()
|
||||
{
|
||||
var status = new DependencyStatus("Python", isRequired: true)
|
||||
{
|
||||
InstallationHint = GetPythonInstallUrl()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Try running python directly first
|
||||
if (TryValidatePython("python3", out string version, out string fullPath) ||
|
||||
TryValidatePython("python", out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
|
||||
// Fallback: try 'which' command
|
||||
if (TryFindInPath("python3", out string pathResult) ||
|
||||
TryFindInPath("python", out pathResult))
|
||||
{
|
||||
if (TryValidatePython(pathResult, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
status.ErrorMessage = "Python not found in PATH";
|
||||
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public override string GetPythonInstallUrl()
|
||||
{
|
||||
return "https://www.python.org/downloads/source/";
|
||||
}
|
||||
|
||||
public override string GetUvInstallUrl()
|
||||
{
|
||||
return "https://docs.astral.sh/uv/getting-started/installation/#linux";
|
||||
}
|
||||
|
||||
public override string GetInstallationRecommendations()
|
||||
{
|
||||
return @"Linux Installation Recommendations:
|
||||
|
||||
1. Python: Install via package manager or pyenv
|
||||
- Ubuntu/Debian: sudo apt install python3 python3-pip
|
||||
- Fedora/RHEL: sudo dnf install python3 python3-pip
|
||||
- Arch: sudo pacman -S python python-pip
|
||||
- Or use pyenv: https://github.com/pyenv/pyenv
|
||||
|
||||
2. uv Package Manager: Install via curl
|
||||
- Run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
- Or download from: https://github.com/astral-sh/uv/releases
|
||||
|
||||
3. MCP Server: Will be installed automatically by MCP for Unity
|
||||
|
||||
Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
|
||||
}
|
||||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
|
||||
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
|
||||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
status.ErrorMessage = null;
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
{
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
return new[]
|
||||
{
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
"/snap/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b682b492eb80d4ed6834b76f72c9f0f3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
/// <summary>
|
||||
/// macOS-specific dependency detection
|
||||
/// </summary>
|
||||
public class MacOSPlatformDetector : PlatformDetectorBase
|
||||
{
|
||||
public override string PlatformName => "macOS";
|
||||
|
||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
public override DependencyStatus DetectPython()
|
||||
{
|
||||
var status = new DependencyStatus("Python", isRequired: true)
|
||||
{
|
||||
InstallationHint = GetPythonInstallUrl()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Try 'which' command with augmented PATH (prioritizing Homebrew)
|
||||
if (TryFindInPath("python3", out string pathResult) ||
|
||||
TryFindInPath("python", out pathResult))
|
||||
{
|
||||
if (TryValidatePython(pathResult, out string version, out string fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} at {fullPath}";
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: Try running python directly from PATH
|
||||
if (TryValidatePython("python3", out string v, out string p) ||
|
||||
TryValidatePython("python", out v, out p))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = v;
|
||||
status.Path = p;
|
||||
status.Details = $"Found Python {v} in PATH";
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "Python not found in PATH or standard locations";
|
||||
status.Details = "Install Python 3.10+ via Homebrew ('brew install python3') and ensure it's in your PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public override string GetPythonInstallUrl()
|
||||
{
|
||||
return "https://www.python.org/downloads/macos/";
|
||||
}
|
||||
|
||||
public override string GetUvInstallUrl()
|
||||
{
|
||||
return "https://docs.astral.sh/uv/getting-started/installation/#macos";
|
||||
}
|
||||
|
||||
public override string GetInstallationRecommendations()
|
||||
{
|
||||
return @"macOS Installation Recommendations:
|
||||
|
||||
1. Python: Install via Homebrew (recommended) or python.org
|
||||
- Homebrew: brew install python3
|
||||
- Direct download: https://python.org/downloads/macos/
|
||||
|
||||
2. uv Package Manager: Install via curl or Homebrew
|
||||
- Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
- Homebrew: brew install uv
|
||||
|
||||
3. MCP Server: Will be installed automatically by MCP for Unity Bridge
|
||||
|
||||
Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
|
||||
}
|
||||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
|
||||
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
|
||||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
status.ErrorMessage = null;
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
{
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
return new[]
|
||||
{
|
||||
Path.Combine(homeDir, ".pyenv", "shims"), // pyenv: Python/uv when Unity is launched from Dock/Spotlight
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6f602b0a8ca848859197f9a949a7a5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for platform-specific dependency detection
|
||||
/// </summary>
|
||||
public abstract class PlatformDetectorBase : IPlatformDetector
|
||||
{
|
||||
public abstract string PlatformName { get; }
|
||||
public abstract bool CanDetect { get; }
|
||||
|
||||
public abstract DependencyStatus DetectPython();
|
||||
public abstract string GetPythonInstallUrl();
|
||||
public abstract string GetUvInstallUrl();
|
||||
public abstract string GetInstallationRecommendations();
|
||||
|
||||
public virtual DependencyStatus DetectUv()
|
||||
{
|
||||
var status = new DependencyStatus("uv Package Manager", isRequired: true)
|
||||
{
|
||||
InstallationHint = GetUvInstallUrl()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Get uv path from PathResolverService (respects override)
|
||||
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
|
||||
|
||||
// Verify uv executable and get version
|
||||
if (MCPServiceLocator.Paths.TryValidateUvxExecutable(uvxPath, out string version))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = uvxPath;
|
||||
|
||||
// Check if we used fallback from override to system path
|
||||
if (MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
status.Details = $"Found uv {version} (fallback to system path)";
|
||||
status.ErrorMessage = "Override path not found, using system path";
|
||||
}
|
||||
else
|
||||
{
|
||||
status.Details = MCPServiceLocator.Paths.HasUvxPathOverride
|
||||
? $"Found uv {version} (override path)"
|
||||
: $"Found uv {version} in system path";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uvx not found";
|
||||
status.Details = "Install uv package manager or configure path override in Advanced Settings.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uvx: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
protected bool TryParseVersion(string version, out int major, out int minor)
|
||||
{
|
||||
major = 0;
|
||||
minor = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var parts = version.Split('.');
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore parsing errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// In PlatformDetectorBase.cs
|
||||
protected bool TryValidateUvWithPath(string command, string augmentedPath, out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string commandToRun = command;
|
||||
if (TryFindInPath(command, out string resolvedPath))
|
||||
{
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim();
|
||||
|
||||
if (output.StartsWith("uvx ") || output.StartsWith("uv "))
|
||||
{
|
||||
int spaceIndex = output.IndexOf(' ');
|
||||
if (spaceIndex >= 0)
|
||||
{
|
||||
var remainder = output.Substring(spaceIndex + 1).Trim();
|
||||
int nextSpace = remainder.IndexOf(' ');
|
||||
int parenIndex = remainder.IndexOf('(');
|
||||
int endIndex = Math.Min(
|
||||
nextSpace >= 0 ? nextSpace : int.MaxValue,
|
||||
parenIndex >= 0 ? parenIndex : int.MaxValue
|
||||
);
|
||||
version = endIndex < int.MaxValue ? remainder.Substring(0, endIndex).Trim() : remainder;
|
||||
fullPath = commandToRun;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Add abstract method for subclasses to implement
|
||||
protected abstract bool TryFindInPath(string executable, out string fullPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44d715aedea2b8b41bf914433bbb2c49
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,297 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows-specific dependency detection
|
||||
/// </summary>
|
||||
public class WindowsPlatformDetector : PlatformDetectorBase
|
||||
{
|
||||
public override string PlatformName => "Windows";
|
||||
|
||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
public override DependencyStatus DetectPython()
|
||||
{
|
||||
var status = new DependencyStatus("Python", isRequired: true)
|
||||
{
|
||||
InstallationHint = GetPythonInstallUrl()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Try running python directly first (works with Windows App Execution Aliases)
|
||||
if (TryValidatePython("python3.exe", out string version, out string fullPath) ||
|
||||
TryValidatePython("python.exe", out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
|
||||
// Fallback: try 'where' command
|
||||
if (TryFindInPath("python3.exe", out string pathResult) ||
|
||||
TryFindInPath("python.exe", out pathResult))
|
||||
{
|
||||
if (TryValidatePython(pathResult, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to find python via uv
|
||||
if (TryFindPythonViaUv(out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found Python {version} via uv";
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "Python not found in PATH";
|
||||
status.Details = "Install Python 3.10+ and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public override string GetPythonInstallUrl()
|
||||
{
|
||||
return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
|
||||
}
|
||||
|
||||
public override string GetUvInstallUrl()
|
||||
{
|
||||
return "https://docs.astral.sh/uv/getting-started/installation/#windows";
|
||||
}
|
||||
|
||||
public override string GetInstallationRecommendations()
|
||||
{
|
||||
return @"Windows Installation Recommendations:
|
||||
|
||||
1. Python: Install from Microsoft Store or python.org
|
||||
- Microsoft Store: Search for 'Python 3.10' or higher
|
||||
- Direct download: https://python.org/downloads/windows/
|
||||
|
||||
2. uv Package Manager: Install via PowerShell
|
||||
- Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
|
||||
- Or download from: https://github.com/astral-sh/uv/releases
|
||||
|
||||
3. MCP Server: Will be installed automatically by MCP for Unity Bridge";
|
||||
}
|
||||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// try to find uv
|
||||
if (TryValidateUvWithPath("uv.exe", augmentedPath, out string uvVersion, out string uvPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = uvVersion;
|
||||
status.Path = uvPath;
|
||||
status.Details = $"Found uv {uvVersion} at {uvPath}";
|
||||
return status;
|
||||
}
|
||||
|
||||
// try to find uvx
|
||||
if (TryValidateUvWithPath("uvx.exe", augmentedPath, out string uvxVersion, out string uvxPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = uvxVersion;
|
||||
status.Path = uvxPath;
|
||||
status.Details = $"Found uvx {uvxVersion} at {uvxPath} (fallback)";
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
private bool TryFindPythonViaUv(out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
// Try to list installed python versions via uvx
|
||||
if (!ExecPath.TryRun("uv", "python list", null, out string stdout, out string stderr, 5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Contains("<download available>")) continue;
|
||||
|
||||
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
string potentialPath = parts[parts.Length - 1];
|
||||
if (File.Exists(potentialPath) &&
|
||||
(potentialPath.EndsWith("python.exe") || potentialPath.EndsWith("python3.exe")))
|
||||
{
|
||||
if (TryValidatePython(potentialPath, out version, out fullPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors if uv is not installed or fails
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
// Run 'python --version' to get the version
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, 5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
var additions = new List<string>();
|
||||
|
||||
// uv common installation paths
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "Programs", "uv"));
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
additions.Add(Path.Combine(programFiles, "uv"));
|
||||
|
||||
// npm global paths
|
||||
if (!string.IsNullOrEmpty(appData))
|
||||
additions.Add(Path.Combine(appData, "npm"));
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "npm"));
|
||||
|
||||
// Python common paths
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "Programs", "Python"));
|
||||
// Instead of hardcoded versions, enumerate existing directories
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
{
|
||||
try
|
||||
{
|
||||
var pythonDirs = Directory.GetDirectories(programFiles, "Python3*")
|
||||
.OrderByDescending(d => d); // Newest first
|
||||
foreach (var dir in pythonDirs)
|
||||
{
|
||||
additions.Add(dir);
|
||||
}
|
||||
}
|
||||
catch { /* Ignore if directory doesn't exist */ }
|
||||
}
|
||||
|
||||
// User scripts
|
||||
if (!string.IsNullOrEmpty(homeDir))
|
||||
additions.Add(Path.Combine(homeDir, ".local", "bin"));
|
||||
|
||||
return additions.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1aedc29caa5704c07b487d20a27e9334
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user