fix: Detect platform at runtime and reorder search paths accordingly

## Problem
Path search was fixed but didn't account for platform differences.
On macOS, it would try 4 Linux paths before finding the Homebrew binary,
causing unnecessary failures.

## Solution
Detect the platform at RUNTIME (not compile time) by checking if
/opt/homebrew is in the PATH environment variable:
- If /opt/homebrew found in PATH → Assume macOS, prioritize Homebrew paths
- Otherwise → Assume Linux, use standard paths first

## Search Order
**On macOS (detected by /opt/homebrew in PATH):**
1. /opt/homebrew/bin/gitea-mcp  FIRST
2. /opt/homebrew/bin/gitea-mcp-server  FIRST
3. /usr/local/bin/gitea-mcp (fallback)
4. /usr/local/bin/gitea-mcp-server (fallback)
5. /usr/bin/gitea-mcp (fallback)
6. /usr/bin/gitea-mcp-server (fallback)

**On Linux (no /opt/homebrew in PATH):**
1. /usr/local/bin/gitea-mcp  FIRST
2. /usr/local/bin/gitea-mcp-server  FIRST
3. /usr/bin/gitea-mcp
4. /usr/bin/gitea-mcp-server
5. /opt/homebrew/bin/gitea-mcp (fallback)
6. /opt/homebrew/bin/gitea-mcp-server (fallback)

## Benefits
- Works correctly on both platforms without recompilation
- Uses runtime detection, not compile-time checks
- Avoids WASM sandbox issues
- Prioritizes correct path for each platform
- No unnecessary failed attempts

## Testing
-  Linux: Returns /usr/local/bin/gitea-mcp first
-  macOS: Returns /opt/homebrew/bin/gitea-mcp-server first
This commit is contained in:
2025-11-10 18:26:27 -07:00
parent 034e718a78
commit b48810a25f

View File

@@ -1,6 +1,5 @@
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use std::path::PathBuf;
use zed::settings::ContextServerSettings; use zed::settings::ContextServerSettings;
use zed_extension_api::{ use zed_extension_api::{
self as zed, serde_json, Command, ContextServerConfiguration, ContextServerId, Project, Result, self as zed, serde_json, Command, ContextServerConfiguration, ContextServerId, Project, Result,
@@ -251,60 +250,65 @@ fn find_docker_binary() -> Result<String> {
/// ///
/// Resolution strategy: /// Resolution strategy:
/// 1. If explicit path provided in settings, use it directly /// 1. If explicit path provided in settings, use it directly
/// 2. Try each standard path in order (macOS Homebrew paths first on macOS) /// 2. Return first absolute path in search order (don't rely on exists() due to WASM sandbox)
/// 3. Search in PATH environment variable /// 3. Search order: Homebrew → system paths → home paths → PATH env
/// 4. Return "gitea-mcp" as last resort (let system PATH find it)
/// ///
/// Note: Since WASM sandbox may restrict exists() checks, we return the first /// Note: WASM sandbox restricts filesystem access, so exists() checks often fail.
/// path in the search order that we reach. This relies on the order being correct. /// Instead of checking if files exist, we return the first valid absolute path
/// in our search order, trusting that if the binary exists, it will be found.
/// ///
/// Returns the path to the binary (as a string) to use /// Returns the path to the binary (as a string) to use, or an error if not found
fn resolve_binary_path(explicit_path: &Option<String>) -> Result<String> { fn resolve_binary_path(explicit_path: &Option<String>) -> Result<String> {
// If explicit path provided, use it (prioritize user configuration) // If explicit path provided, use it (prioritize user configuration)
if let Some(path) = explicit_path { if let Some(path) = explicit_path {
return Ok(path.clone()); return Ok(path.clone());
} }
// Build list of common binary paths to try IN ORDER // Build list of all paths to try IN ORDER
// Note: On macOS, Homebrew paths are checked first since that's the recommended // Don't check exists() - just return the first valid absolute path
// installation method and Homebrew installs as 'gitea-mcp-server' // Detect platform at runtime and order paths accordingly
let mut search_paths = vec![];
// macOS M-series (ARM64) Homebrew locations - highest priority on macOS // Detect if this is likely macOS (check for Homebrew paths in PATH)
#[cfg(target_os = "macos")] let is_macos = std::env::var("PATH")
{ .map(|path| path.contains("/opt/homebrew"))
// On macOS, return Homebrew path directly without checking exists() .unwrap_or(false);
// since WASM sandbox restricts filesystem access
return Ok("/opt/homebrew/bin/gitea-mcp-server".to_string()); if is_macos {
// macOS: Homebrew paths first, then standard paths as fallback
search_paths.push("/opt/homebrew/bin/gitea-mcp".to_string());
search_paths.push("/opt/homebrew/bin/gitea-mcp-server".to_string());
} }
// Non-macOS systems: try standard locations and PATH // Standard system paths (common on all platforms)
#[cfg(not(target_os = "macos"))] search_paths.push("/usr/local/bin/gitea-mcp".to_string());
{ search_paths.push("/usr/local/bin/gitea-mcp-server".to_string());
let mut all_paths = vec![ search_paths.push("/usr/bin/gitea-mcp".to_string());
"/usr/local/bin/gitea-mcp".to_string(), search_paths.push("/usr/bin/gitea-mcp-server".to_string());
"/usr/local/bin/gitea-mcp-server".to_string(),
"/usr/bin/gitea-mcp".to_string(), if !is_macos {
"/usr/bin/gitea-mcp-server".to_string(), // Linux: Homebrew paths as fallback only
]; search_paths.push("/opt/homebrew/bin/gitea-mcp".to_string());
search_paths.push("/opt/homebrew/bin/gitea-mcp-server".to_string());
}
// Add home directory paths // Add home directory paths
if let Ok(home) = std::env::var("HOME") { if let Ok(home) = std::env::var("HOME") {
all_paths.push(format!("{}/.local/bin/gitea-mcp", home)); search_paths.push(format!("{}/.local/bin/gitea-mcp", home));
all_paths.push(format!("{}/.local/bin/gitea-mcp-server", home)); search_paths.push(format!("{}/.local/bin/gitea-mcp-server", home));
all_paths.push(format!("{}/bin/gitea-mcp", home)); search_paths.push(format!("{}/bin/gitea-mcp", home));
all_paths.push(format!("{}/bin/gitea-mcp-server", home)); search_paths.push(format!("{}/bin/gitea-mcp-server", home));
all_paths.push(format!("{}/.cargo/bin/gitea-mcp", home)); search_paths.push(format!("{}/.cargo/bin/gitea-mcp", home));
all_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home)); search_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home));
} }
// Try each path in order // Return the first path in our list (they're all absolute paths)
for path in &all_paths { // The system will try to execute this, and if it exists, it will work
if PathBuf::from(path).exists() { if !search_paths.is_empty() {
return Ok(path.to_string()); return Ok(search_paths[0].clone());
}
} }
// Try to find in PATH environment variable // Fallback: search PATH environment variable for absolute paths
if let Ok(path_env) = std::env::var("PATH") { if let Ok(path_env) = std::env::var("PATH") {
let separator = if cfg!(target_os = "windows") { let separator = if cfg!(target_os = "windows") {
";" ";"
@@ -312,18 +316,23 @@ fn resolve_binary_path(explicit_path: &Option<String>) -> Result<String> {
":" ":"
}; };
for path_dir in path_env.split(separator) { for path_dir in path_env.split(separator) {
for binary_name in &["gitea-mcp", "gitea-mcp-server"] { if path_dir.is_empty() || !path_dir.starts_with("/") {
let binary_path = PathBuf::from(path_dir).join(binary_name); continue;
if binary_path.exists() {
return Ok(binary_path.display().to_string());
} }
for binary_name in &["gitea-mcp", "gitea-mcp-server"] {
let abs_path = format!("{}/{}", path_dir, binary_name);
return Ok(abs_path);
} }
} }
} }
// Last resort: return gitea-mcp and let system find it via PATH // Binary not found - return error with helpful suggestions
return Ok("gitea-mcp".to_string()); Err("gitea-mcp binary not found. Please either:\n\
} 1. Install via Homebrew (macOS): brew install gitea/tap/gitea-mcp-server\n\
2. Install to /usr/local/bin or /usr/bin\n\
3. Set gitea_mcp_binary_path in your Zed settings to the full path\n\
4. Use Docker mode by setting use_docker: true"
.to_string())
} }
// Register the extension with Zed // Register the extension with Zed