From 7b20c17ea42753de481f35b2bf1fa74d12dcdb9d Mon Sep 17 00:00:00 2001 From: makoMako <48956204+zhu-jl18@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:35:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(opencode):=20=E8=A1=A5=E9=BD=90=20install.s?= =?UTF-8?q?h=20=E5=AE=89=E8=A3=85=E8=B7=AF=E5=BE=84=E6=A3=80=E6=B5=8B=20(#?= =?UTF-8?q?988)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 OpenCode 路径扫描来源(按官方 install.sh 优先级): OPENCODE_INSTALL_DIR > XDG_BIN_DIR > ~/bin > ~/.opencode/bin 保留并增强 Go 安装路径扫描(~/go/bin、GOPATH/*/bin)。 区分单值环境变量(push_env_single_dir)与 path-list 环境变量 (extend_from_path_list),避免对 OPENCODE_INSTALL_DIR 等单值 变量误用 split_paths。 增加路径去重逻辑(push_unique_path),避免重复扫描。 增加跨平台可执行候选逻辑(tool_executable_candidates): Windows: .cmd / .exe / 裸命令 Unix: 裸命令 将 PATH 拼接提至外层循环,减少重复 syscall。 增加单元测试覆盖路径拼装、去重及 Windows 候选顺序。 Closes #958 Co-authored-by: Warp --- src-tauri/src/commands/misc.rs | 218 +++++++++++++++++++++++++++------ 1 file changed, 183 insertions(+), 35 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index e7c21471..f5bf331f 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -313,6 +313,79 @@ fn try_get_version_wsl(_tool: &str, _distro: &str) -> (Option, Option, path: std::path::PathBuf) { + if path.as_os_str().is_empty() { + return; + } + + if !paths.iter().any(|existing| existing == &path) { + paths.push(path); + } +} + +fn push_env_single_dir(paths: &mut Vec, value: Option) { + if let Some(raw) = value { + push_unique_path(paths, std::path::PathBuf::from(raw)); + } +} + +fn extend_from_path_list( + paths: &mut Vec, + value: Option, + suffix: Option<&str>, +) { + if let Some(raw) = value { + for p in std::env::split_paths(&raw) { + let dir = match suffix { + Some(s) => p.join(s), + None => p, + }; + push_unique_path(paths, dir); + } + } +} + +/// OpenCode install.sh 路径优先级(见 https://github.com/anomalyco/opencode README): +/// $OPENCODE_INSTALL_DIR > $XDG_BIN_DIR > $HOME/bin > $HOME/.opencode/bin +/// 额外扫描 Go 安装路径(~/go/bin、$GOPATH/*/bin)。 +fn opencode_extra_search_paths( + home: &Path, + opencode_install_dir: Option, + xdg_bin_dir: Option, + gopath: Option, +) -> Vec { + let mut paths = Vec::new(); + + push_env_single_dir(&mut paths, opencode_install_dir); + push_env_single_dir(&mut paths, xdg_bin_dir); + + if !home.as_os_str().is_empty() { + push_unique_path(&mut paths, home.join("bin")); + push_unique_path(&mut paths, home.join(".opencode").join("bin")); + push_unique_path(&mut paths, home.join("go").join("bin")); + } + + extend_from_path_list(&mut paths, gopath, Some("bin")); + + paths +} + +fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec { + #[cfg(target_os = "windows")] + { + vec![ + dir.join(format!("{tool}.cmd")), + dir.join(format!("{tool}.exe")), + dir.join(tool), + ] + } + + #[cfg(not(target_os = "windows"))] + { + vec![dir.join(tool)] + } +} + /// 扫描常见路径查找 CLI fn scan_cli_version(tool: &str) -> (Option, Option) { use std::process::Command; @@ -320,88 +393,99 @@ fn scan_cli_version(tool: &str) -> (Option, Option) { let home = dirs::home_dir().unwrap_or_default(); // 常见的安装路径(原生安装优先) - let mut search_paths: Vec = vec![ - home.join(".local/bin"), // Native install (official recommended) - home.join(".npm-global/bin"), - home.join("n/bin"), // n version manager - home.join(".volta/bin"), // Volta package manager - ]; + let mut search_paths: Vec = Vec::new(); + if !home.as_os_str().is_empty() { + push_unique_path(&mut search_paths, home.join(".local/bin")); + push_unique_path(&mut search_paths, home.join(".npm-global/bin")); + push_unique_path(&mut search_paths, home.join("n/bin")); + push_unique_path(&mut search_paths, home.join(".volta/bin")); + } #[cfg(target_os = "macos")] { - search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin")); - search_paths.push(std::path::PathBuf::from("/usr/local/bin")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/opt/homebrew/bin"), + ); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/usr/local/bin"), + ); } #[cfg(target_os = "linux")] { - search_paths.push(std::path::PathBuf::from("/usr/local/bin")); - search_paths.push(std::path::PathBuf::from("/usr/bin")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("/usr/local/bin"), + ); + push_unique_path(&mut search_paths, std::path::PathBuf::from("/usr/bin")); } #[cfg(target_os = "windows")] { if let Some(appdata) = dirs::data_dir() { - search_paths.push(appdata.join("npm")); + push_unique_path(&mut search_paths, appdata.join("npm")); } - search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs")); + push_unique_path( + &mut search_paths, + std::path::PathBuf::from("C:\\Program Files\\nodejs"), + ); } - // 添加 fnm 路径支持 let fnm_base = home.join(".local/state/fnm_multishells"); if fnm_base.exists() { if let Ok(entries) = std::fs::read_dir(&fnm_base) { for entry in entries.flatten() { let bin_path = entry.path().join("bin"); if bin_path.exists() { - search_paths.push(bin_path); + push_unique_path(&mut search_paths, bin_path); } } } } - // 扫描 nvm 目录下的所有 node 版本 let nvm_base = home.join(".nvm/versions/node"); if nvm_base.exists() { if let Ok(entries) = std::fs::read_dir(&nvm_base) { for entry in entries.flatten() { let bin_path = entry.path().join("bin"); if bin_path.exists() { - search_paths.push(bin_path); + push_unique_path(&mut search_paths, bin_path); } } } } - // 添加 Go 路径支持 (opencode 使用 go install 安装) if tool == "opencode" { - search_paths.push(home.join("go/bin")); // go install 默认路径 - if let Ok(gopath) = std::env::var("GOPATH") { - search_paths.push(std::path::PathBuf::from(gopath).join("bin")); + let extra_paths = opencode_extra_search_paths( + &home, + std::env::var_os("OPENCODE_INSTALL_DIR"), + std::env::var_os("XDG_BIN_DIR"), + std::env::var_os("GOPATH"), + ); + + for path in extra_paths { + push_unique_path(&mut search_paths, path); } } - // 在每个路径中查找工具 + let current_path = std::env::var("PATH").unwrap_or_default(); + for path in &search_paths { - let tool_path = if cfg!(target_os = "windows") { - path.join(format!("{tool}.cmd")) - } else { - path.join(tool) - }; + #[cfg(target_os = "windows")] + let new_path = format!("{};{}", path.display(), current_path); - if tool_path.exists() { - // 构建 PATH 环境变量,确保 node 可被找到 - let current_path = std::env::var("PATH").unwrap_or_default(); + #[cfg(not(target_os = "windows"))] + let new_path = format!("{}:{}", path.display(), current_path); - #[cfg(target_os = "windows")] - let new_path = format!("{};{}", path.display(), current_path); - - #[cfg(not(target_os = "windows"))] - let new_path = format!("{}:{}", path.display(), current_path); + for tool_path in tool_executable_candidates(tool, path) { + if !tool_path.exists() { + continue; + } #[cfg(target_os = "windows")] let output = { - // 使用 cmd /C 包装执行,确保子进程也在隐藏的控制台中运行 Command::new("cmd") .args(["/C", &format!("\"{}\" --version", tool_path.display())]) .env("PATH", &new_path) @@ -971,3 +1055,67 @@ pub async fn set_window_theme(window: tauri::Window, theme: String) -> Result<() window.set_theme(tauri_theme).map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn opencode_extra_search_paths_includes_install_and_fallback_dirs() { + let home = PathBuf::from("/home/tester"); + let install_dir = Some(std::ffi::OsString::from("/custom/opencode/bin")); + let xdg_bin_dir = Some(std::ffi::OsString::from("/xdg/bin")); + let gopath = + std::env::join_paths([PathBuf::from("/go/path1"), PathBuf::from("/go/path2")]).ok(); + + let paths = opencode_extra_search_paths(&home, install_dir, xdg_bin_dir, gopath); + + assert_eq!(paths[0], PathBuf::from("/custom/opencode/bin")); + assert_eq!(paths[1], PathBuf::from("/xdg/bin")); + assert!(paths.contains(&PathBuf::from("/home/tester/bin"))); + assert!(paths.contains(&PathBuf::from("/home/tester/.opencode/bin"))); + assert!(paths.contains(&PathBuf::from("/home/tester/go/bin"))); + assert!(paths.contains(&PathBuf::from("/go/path1/bin"))); + assert!(paths.contains(&PathBuf::from("/go/path2/bin"))); + } + + #[test] + fn opencode_extra_search_paths_deduplicates_repeated_entries() { + let home = PathBuf::from("/home/tester"); + let same_dir = Some(std::ffi::OsString::from("/same/path")); + + let paths = opencode_extra_search_paths(&home, same_dir.clone(), same_dir.clone(), None); + + let count = paths + .iter() + .filter(|path| **path == PathBuf::from("/same/path")) + .count(); + assert_eq!(count, 1); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn tool_executable_candidates_non_windows_uses_plain_binary_name() { + let dir = PathBuf::from("/usr/local/bin"); + let candidates = tool_executable_candidates("opencode", &dir); + + assert_eq!(candidates, vec![PathBuf::from("/usr/local/bin/opencode")]); + } + + #[cfg(target_os = "windows")] + #[test] + fn tool_executable_candidates_windows_includes_cmd_exe_and_plain_name() { + let dir = PathBuf::from("C:\\tools"); + let candidates = tool_executable_candidates("opencode", &dir); + + assert_eq!( + candidates, + vec![ + PathBuf::from("C:\\tools\\opencode.cmd"), + PathBuf::from("C:\\tools\\opencode.exe"), + PathBuf::from("C:\\tools\\opencode"), + ] + ); + } +}