mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-22 15:08:22 +08:00
fix(opencode): 补齐 install.sh 安装路径检测 (#988)
补齐 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 <agent@warp.dev>
This commit is contained in:
@@ -313,6 +313,79 @@ fn try_get_version_wsl(_tool: &str, _distro: &str) -> (Option<String>, Option<St
|
||||
)
|
||||
}
|
||||
|
||||
fn push_unique_path(paths: &mut Vec<std::path::PathBuf>, 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<std::path::PathBuf>, value: Option<std::ffi::OsString>) {
|
||||
if let Some(raw) = value {
|
||||
push_unique_path(paths, std::path::PathBuf::from(raw));
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_from_path_list(
|
||||
paths: &mut Vec<std::path::PathBuf>,
|
||||
value: Option<std::ffi::OsString>,
|
||||
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<std::ffi::OsString>,
|
||||
xdg_bin_dir: Option<std::ffi::OsString>,
|
||||
gopath: Option<std::ffi::OsString>,
|
||||
) -> Vec<std::path::PathBuf> {
|
||||
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<std::path::PathBuf> {
|
||||
#[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<String>, Option<String>) {
|
||||
use std::process::Command;
|
||||
@@ -320,88 +393,99 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
// 常见的安装路径(原生安装优先)
|
||||
let mut search_paths: Vec<std::path::PathBuf> = 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<std::path::PathBuf> = 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"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user