环境管理器(Environment Manager)自动检测系统中安装的开发环境(语言运行时、工具链),显示版本和路径信息,支持添加/移除 PATH 环境变量。
通信链路:
EnvironmentManager.svelte ──→ invoke("list_environments") ──→ environment.rs
──→ invoke("add_to_path") ──→ environment.rs
──→ invoke("remove_from_path") ──→ environment.rs
#[derive(Serialize)]
pub struct Environment {
pub name: String, // "Python 3"
pub version: String, // "3.12.0"
pub path: String, // "/usr/local/bin/python3"
pub status: String, // "Active" / "Not Found"
pub shell_config: Option<String>, // "~/.zshrc" (如果有对应 shell 配置)
pub lang_type: String, // "python" | "node" | "java" | "go" |
// "rust" | "ruby" | "docker" | "git" | "cpp"
}前端对应 (routes/EnvironmentManager.svelte):
let environments = $state([]);
let filteredEnvs = $derived(
environments.filter(env => {
match filterType.value {
"all" => true,
"installed" => env.status === "Active",
"missing" => env.status !== "Active",
}
})
);fn detect_environment(
name: &str, // "Python 3"
lang_type: &str, // "python"
check_cmd: &str, // "python3"
version_args: &[&str], // ["--version"]
config_files: &[&str], // ["~/.zshrc", "~/.bashrc"]
) -> Option<Environment> {
// 1. 用 find_cmd_path 在 PATH 中搜索可执行文件
if let Some(path) = utils::find_cmd_path(check_cmd) {
// 2. 执行版本命令获取版本号
let version = get_version(check_cmd, version_args);
// 3. 在 shell 配置文件中搜索相关配置
let shell_config = config_files.iter()
.find(|&file| {
let resolved = file.strip_prefix("~/")
.map(|rest| format!("{}/{}", home, rest))
.unwrap_or_else(|| file.to_string());
std::path::Path::new(&resolved).exists()
})
.map(|s| s.to_string());
// 4. 返回结果
Some(Environment { name, version, path, status: "Active", shell_config, lang_type })
} else {
None // 未找到该可执行文件
}
}#[tauri::command]
pub fn list_environments() -> Vec<Environment> {
let mut list = Vec::new();
// 每个环境调用 detect_environment
list.push(detect_environment("Python 3", "python", "python3",
&["--version"], &["~/.zshrc", "~/.bashrc"]));
list.push(detect_environment("Node.js", "node", "node",
&["--version"], &["~/.zshrc", "~/.bashrc"]));
list.push(detect_environment("Java (JDK)", "java", "java",
&["-version"], &["~/.zshrc", "~/.bashrc"]));
list.push(detect_environment("Go", "go", "go",
&["version"], &[]));
// ... 更多环境
list.into_iter().flatten().collect()
}flatten() 将 Option<Environment> 转为 Environment,未安装的环境自动跳过。
fn get_version(cmd: &str, args: &[&str]) -> String {
match Command::new(cmd).args(args).output() {
Ok(output) => {
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout);
version.lines().next().unwrap_or("unknown").to_string()
} else {
"unknown".to_string()
}
}
Err(_) => "not found".to_string(),
}
}只取第一行:大多数 --version 输出的第一行包含版本号(如 Python 3.12.0),后续行可能是许可证信息或构建细节。
这是环境管理器的核心功能,在不同平台实现方案完全不同。
#[cfg(not(target_os = "windows"))]
fn add_to_path_impl(env_name: &str, path: &str) -> Result<String, String> {
let shell = std::env::var("SHELL").unwrap_or_default();
let home = utils::user_home();
// 1. 检测用户 shell 类型
let (profile_files, export_line) = match shell.as_str() {
s if s.contains("zsh") => (
vec![format!("{}/.zshrc", home)],
format!("\nexport PATH=\"{}/$PATH\" # {} devnexus\n", path, env_name),
),
s if s.contains("fish") => (
vec![format!("{}/.config/fish/config.fish", home)],
// fish 语法不同: fish_add_path 或 set -gx
format!("\nset -gx PATH \"{}\" $PATH # {} devnexus\n", path, env_name),
),
_ => ( // bash 默认
vec![
format!("{}/.bashrc", home),
format!("{}/.bash_profile", home),
format!("{}/.profile", home),
],
format!("\nexport PATH=\"{}/$PATH\" # {} devnexus\n", path, env_name),
),
};
// 2. 如果已存在同名注解,先移除旧的
remove_path_line(&profile_files, env_name);
// 3. 选择第一个存在的 profile 文件,或创建默认的
let profile = profile_files.iter()
.find(|f| std::path::Path::new(f).exists())
.unwrap_or(&profile_files[0]);
// 4. 追加 export 语句
let existing = std::fs::read_to_string(profile).unwrap_or_default();
std::fs::write(profile, format!("{}{}", existing, export_line))?;
Ok(format!("Added {} to PATH in {}", env_name, profile))
}关键设计:
- 使用 Shell 检测确定写入哪个配置文件
- 在 export 行后添加
# devnexus标记,便于后续识别和移除 - 添加前先清除同名旧条目,避免重复累积
#[cfg(target_os = "windows")]
fn add_to_path_impl(env_name: &str, path: &str) -> Result<String, String> {
// 1. 获取当前 PATH
let output = Command::new("powershell")
.args(["-Command", "[Environment]::GetEnvironmentVariable('PATH', 'User')"])
.output()?;
let current_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
// 2. 检查是否已在 PATH 中
if current_path.split(';').any(|p| p.trim() == path.trim()) {
return Ok(format!("{} already in PATH", path));
}
// 3. 添加新路径
let new_path = format!("{};{}", path.trim(), current_path);
let ps_cmd = format!(
"[Environment]::SetEnvironmentVariable('PATH', '{}', 'User')",
new_path.replace('\'', "''")
);
Command::new("powershell")
.args(["-Command", &ps_cmd])
.output()?;
Ok(format!("Added {} to user PATH", path))
}Windows 与 Unix 的核心差异:
| 维度 | Unix | Windows |
|---|---|---|
| 存储位置 | Shell 配置文件 (.zshrc 等) |
注册表 (用户级) |
| 格式 | export PATH="/p:$PATH" |
C:\p;%PATH% |
| 分隔符 | : |
; |
| 修改方式 | 文件 append | PowerShell .NET API |
| 即刻生效 | 需 source 或新终端 |
需重新打开进程 |
#[cfg(not(target_os = "windows"))]
fn remove_from_path_impl(env_name: &str, _path: &str) -> Result<String, String> {
let shell = std::env::var("SHELL").unwrap_or_default();
let home = utils::user_home();
let profiles = detect_profile_files(&shell, &home);
let count = 0;
for profile in &profiles {
if !std::path::Path::new(profile).exists() { continue; }
// 读取文件, 移除包含 `# env_name devnexus` 的行
let content = std::fs::read_to_string(profile)?;
let new_content = content.lines()
.filter(|line| !line.contains(&format!("# {} devnexus", env_name)))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(profile, new_content)?;
count += 1;
}
// ...
}设计要点: 通过 # devnexus 标记精确识别此前由 DevNexus 添加的行,避免误删用户自己添加的 PATH。
每个环境展示:
┌────────────────────────────┐
│ [icon] Python 3 [Active]│
│ 3.12.0 │
│ /usr/local/bin │
│ Config: ~/.zshrc │
│ [Switch] [Remove] │
└────────────────────────────┘
通过 version_manager 模块支持版本切换(如果该语言有版本管理器):
async function switchVersion(env, version) {
await invoke("switch_version", { langType: env.lang_type, version: version.version });
await loadVersions(env, true); // 刷新缓存
}<dialog>
<h3>Add to PATH</h3>
<input bind:value={newEnvName} placeholder="Environment name" />
<input bind:value={newEnvPath} placeholder="/path/to/bin" />
<button onclick={handleAddToPath}>Add</button>
</dialog>| 环境 | macOS 检测 | Linux 检测 | Windows 检测 |
|---|---|---|---|
| Python | python3 --version |
python3 --version |
python --version |
| Node.js | node --version |
node --version |
node --version |
| Java | java -version |
java -version |
java -version |
| Go | go version |
go version |
go version |
| Rust | rustc --version |
rustc --version |
rustc --version |
| Docker | docker --version |
docker --version |
docker --version |
| Git | git --version |
git --version |
git --version |
| GCC | gcc --version |
gcc --version |
gcc --version |
| .NET | dotnet --version |
dotnet --version |
dotnet --version |
| Flutter | flutter --version |
flutter --version |
flutter --version |
| PHP | php --version |
php --version |
php --version |
注意: 检测命令是跨平台相同的,区别仅在于可执行文件在系统中的路径位置。
utils::find_cmd_path已封装 PATH 查找的跨平台差异。