mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-02 10:03:27 +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
|
/// 扫描常见路径查找 CLI
|
||||||
fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||||
use std::process::Command;
|
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 home = dirs::home_dir().unwrap_or_default();
|
||||||
|
|
||||||
// 常见的安装路径(原生安装优先)
|
// 常见的安装路径(原生安装优先)
|
||||||
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
let mut search_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||||
home.join(".local/bin"), // Native install (official recommended)
|
if !home.as_os_str().is_empty() {
|
||||||
home.join(".npm-global/bin"),
|
push_unique_path(&mut search_paths, home.join(".local/bin"));
|
||||||
home.join("n/bin"), // n version manager
|
push_unique_path(&mut search_paths, home.join(".npm-global/bin"));
|
||||||
home.join(".volta/bin"), // Volta package manager
|
push_unique_path(&mut search_paths, home.join("n/bin"));
|
||||||
];
|
push_unique_path(&mut search_paths, home.join(".volta/bin"));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin"));
|
push_unique_path(
|
||||||
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
&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")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
push_unique_path(
|
||||||
search_paths.push(std::path::PathBuf::from("/usr/bin"));
|
&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")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
if let Some(appdata) = dirs::data_dir() {
|
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");
|
let fnm_base = home.join(".local/state/fnm_multishells");
|
||||||
if fnm_base.exists() {
|
if fnm_base.exists() {
|
||||||
if let Ok(entries) = std::fs::read_dir(&fnm_base) {
|
if let Ok(entries) = std::fs::read_dir(&fnm_base) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let bin_path = entry.path().join("bin");
|
let bin_path = entry.path().join("bin");
|
||||||
if bin_path.exists() {
|
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");
|
let nvm_base = home.join(".nvm/versions/node");
|
||||||
if nvm_base.exists() {
|
if nvm_base.exists() {
|
||||||
if let Ok(entries) = std::fs::read_dir(&nvm_base) {
|
if let Ok(entries) = std::fs::read_dir(&nvm_base) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let bin_path = entry.path().join("bin");
|
let bin_path = entry.path().join("bin");
|
||||||
if bin_path.exists() {
|
if bin_path.exists() {
|
||||||
search_paths.push(bin_path);
|
push_unique_path(&mut search_paths, bin_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 Go 路径支持 (opencode 使用 go install 安装)
|
|
||||||
if tool == "opencode" {
|
if tool == "opencode" {
|
||||||
search_paths.push(home.join("go/bin")); // go install 默认路径
|
let extra_paths = opencode_extra_search_paths(
|
||||||
if let Ok(gopath) = std::env::var("GOPATH") {
|
&home,
|
||||||
search_paths.push(std::path::PathBuf::from(gopath).join("bin"));
|
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 {
|
for path in &search_paths {
|
||||||
let tool_path = if cfg!(target_os = "windows") {
|
#[cfg(target_os = "windows")]
|
||||||
path.join(format!("{tool}.cmd"))
|
let new_path = format!("{};{}", path.display(), current_path);
|
||||||
} else {
|
|
||||||
path.join(tool)
|
|
||||||
};
|
|
||||||
|
|
||||||
if tool_path.exists() {
|
#[cfg(not(target_os = "windows"))]
|
||||||
// 构建 PATH 环境变量,确保 node 可被找到
|
let new_path = format!("{}:{}", path.display(), current_path);
|
||||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
for tool_path in tool_executable_candidates(tool, path) {
|
||||||
let new_path = format!("{};{}", path.display(), current_path);
|
if !tool_path.exists() {
|
||||||
|
continue;
|
||||||
#[cfg(not(target_os = "windows"))]
|
}
|
||||||
let new_path = format!("{}:{}", path.display(), current_path);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let output = {
|
let output = {
|
||||||
// 使用 cmd /C 包装执行,确保子进程也在隐藏的控制台中运行
|
|
||||||
Command::new("cmd")
|
Command::new("cmd")
|
||||||
.args(["/C", &format!("\"{}\" --version", tool_path.display())])
|
.args(["/C", &format!("\"{}\" --version", tool_path.display())])
|
||||||
.env("PATH", &new_path)
|
.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())
|
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