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 serde::Deserialize;
use std::path::PathBuf;
use zed::settings::ContextServerSettings;
use zed_extension_api::{
self as zed, serde_json, Command, ContextServerConfiguration, ContextServerId, Project, Result,
@@ -251,79 +250,89 @@ fn find_docker_binary() -> Result<String> {
///
/// Resolution strategy:
/// 1. If explicit path provided in settings, use it directly
/// 2. Try each standard path in order (macOS Homebrew paths first on macOS)
/// 3. Search in PATH environment variable
/// 4. Return "gitea-mcp" as last resort (let system PATH find it)
/// 2. Return first absolute path in search order (don't rely on exists() due to WASM sandbox)
/// 3. Search order: Homebrew → system paths → home paths → PATH env
///
/// Note: Since WASM sandbox may restrict exists() checks, we return the first
/// path in the search order that we reach. This relies on the order being correct.
/// Note: WASM sandbox restricts filesystem access, so exists() checks often fail.
/// 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> {
// If explicit path provided, use it (prioritize user configuration)
if let Some(path) = explicit_path {
return Ok(path.clone());
}
// Build list of common binary paths to try IN ORDER
// Note: On macOS, Homebrew paths are checked first since that's the recommended
// installation method and Homebrew installs as 'gitea-mcp-server'
// Build list of all paths to try IN ORDER
// Don't check exists() - just return the first valid absolute path
// Detect platform at runtime and order paths accordingly
let mut search_paths = vec![];
// macOS M-series (ARM64) Homebrew locations - highest priority on macOS
#[cfg(target_os = "macos")]
{
// On macOS, return Homebrew path directly without checking exists()
// since WASM sandbox restricts filesystem access
return Ok("/opt/homebrew/bin/gitea-mcp-server".to_string());
// Detect if this is likely macOS (check for Homebrew paths in PATH)
let is_macos = std::env::var("PATH")
.map(|path| path.contains("/opt/homebrew"))
.unwrap_or(false);
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
#[cfg(not(target_os = "macos"))]
{
let mut all_paths = vec![
"/usr/local/bin/gitea-mcp".to_string(),
"/usr/local/bin/gitea-mcp-server".to_string(),
"/usr/bin/gitea-mcp".to_string(),
"/usr/bin/gitea-mcp-server".to_string(),
];
// Standard system paths (common on all platforms)
search_paths.push("/usr/local/bin/gitea-mcp".to_string());
search_paths.push("/usr/local/bin/gitea-mcp-server".to_string());
search_paths.push("/usr/bin/gitea-mcp".to_string());
search_paths.push("/usr/bin/gitea-mcp-server".to_string());
// Add home directory paths
if let Ok(home) = std::env::var("HOME") {
all_paths.push(format!("{}/.local/bin/gitea-mcp", home));
all_paths.push(format!("{}/.local/bin/gitea-mcp-server", home));
all_paths.push(format!("{}/bin/gitea-mcp", home));
all_paths.push(format!("{}/bin/gitea-mcp-server", home));
all_paths.push(format!("{}/.cargo/bin/gitea-mcp", home));
all_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home));
}
// Try each path in order
for path in &all_paths {
if PathBuf::from(path).exists() {
return Ok(path.to_string());
}
}
// Try to find in PATH environment variable
if let Ok(path_env) = std::env::var("PATH") {
let separator = if cfg!(target_os = "windows") {
";"
} else {
":"
};
for path_dir in path_env.split(separator) {
for binary_name in &["gitea-mcp", "gitea-mcp-server"] {
let binary_path = PathBuf::from(path_dir).join(binary_name);
if binary_path.exists() {
return Ok(binary_path.display().to_string());
}
}
}
}
// Last resort: return gitea-mcp and let system find it via PATH
return Ok("gitea-mcp".to_string());
if !is_macos {
// 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
if let Ok(home) = std::env::var("HOME") {
search_paths.push(format!("{}/.local/bin/gitea-mcp", home));
search_paths.push(format!("{}/.local/bin/gitea-mcp-server", home));
search_paths.push(format!("{}/bin/gitea-mcp", home));
search_paths.push(format!("{}/bin/gitea-mcp-server", home));
search_paths.push(format!("{}/.cargo/bin/gitea-mcp", home));
search_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home));
}
// Return the first path in our list (they're all absolute paths)
// The system will try to execute this, and if it exists, it will work
if !search_paths.is_empty() {
return Ok(search_paths[0].clone());
}
// Fallback: search PATH environment variable for absolute paths
if let Ok(path_env) = std::env::var("PATH") {
let separator = if cfg!(target_os = "windows") {
";"
} else {
":"
};
for path_dir in path_env.split(separator) {
if path_dir.is_empty() || !path_dir.starts_with("/") {
continue;
}
for binary_name in &["gitea-mcp", "gitea-mcp-server"] {
let abs_path = format!("{}/{}", path_dir, binary_name);
return Ok(abs_path);
}
}
}
// Binary not found - return error with helpful suggestions
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