mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
The two mapped-drive tests run in parallel processes and could race for the same free drive letter, then see the other test's directory. Mark the target and accept a drive only when the marker shows through it.
733 lines
29 KiB
Rust
733 lines
29 KiB
Rust
//! Finding installed interpreters and toolchains.
|
|
|
|
use crate::exec::{executables_in, query, search_path, which};
|
|
use crate::{Sdk, SdkKind, bin_dir, exe, home_dir, pinned, venv_of};
|
|
use ide_core::version_managers::{self, Tool};
|
|
use std::collections::HashSet;
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
|
|
const VERSION_TIMEOUT: Duration = Duration::from_secs(4);
|
|
|
|
/// Installed SDKs of `kind`, project-local ones (`root`) first. Blocking: runs
|
|
/// `--version` queries in parallel.
|
|
pub fn detect(kind: SdkKind, root: Option<&Path>) -> Vec<Sdk> {
|
|
let mut out = match kind {
|
|
SdkKind::Python => python(root),
|
|
SdkKind::Node => node(),
|
|
SdkKind::Php => php(),
|
|
SdkKind::PackageManager => package_managers(),
|
|
SdkKind::Rust => rust(),
|
|
SdkKind::Go => go(),
|
|
SdkKind::TypeScript => typescript(root),
|
|
SdkKind::Jdk => jdk(),
|
|
SdkKind::Maven => maven(),
|
|
SdkKind::Ruby => crate::ruby::detect(root),
|
|
};
|
|
if let Some(root) = root {
|
|
pinned_first(&mut out, kind, root);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn tool_of(kind: SdkKind) -> Option<Tool> {
|
|
match kind {
|
|
SdkKind::Python => Some(Tool::Python),
|
|
SdkKind::Node => Some(Tool::Node),
|
|
SdkKind::Go => Some(Tool::Go),
|
|
SdkKind::Jdk => Some(Tool::Java),
|
|
SdkKind::Maven => Some(Tool::Maven),
|
|
SdkKind::Ruby => Some(Tool::Ruby),
|
|
SdkKind::Php | SdkKind::PackageManager | SdkKind::Rust | SdkKind::TypeScript => None,
|
|
}
|
|
}
|
|
|
|
/// Moves the install the project pins for mise / asdf after the project's own SDKs
|
|
/// (virtualenvs), ahead of the rest.
|
|
fn pinned_first(sdks: &mut Vec<Sdk>, kind: SdkKind, root: &Path) {
|
|
let Some((dir, _)) = tool_of(kind).and_then(|t| pinned::pinned_install(root, t)) else { return };
|
|
let Some(i) = sdks.iter().position(|s| s.path.starts_with(&dir)) else { return };
|
|
let at = sdks.iter().take_while(|s| s.source == "venv").count().min(i);
|
|
let sdk = sdks.remove(i);
|
|
sdks.insert(at, sdk);
|
|
}
|
|
|
|
/// Runs `f` over `items` on scoped threads (version queries spawn processes).
|
|
pub(crate) fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> Option<R> + Sync) -> Vec<R> {
|
|
std::thread::scope(|s| {
|
|
let handles: Vec<_> = items.iter().map(|item| s.spawn(|| f(item))).collect();
|
|
handles.into_iter().filter_map(|h| h.join().ok().flatten()).collect()
|
|
})
|
|
}
|
|
|
|
fn dedupe_by_real_path(paths: Vec<(PathBuf, &'static str)>) -> Vec<(PathBuf, &'static str)> {
|
|
let mut seen = HashSet::new();
|
|
paths
|
|
.into_iter()
|
|
.filter(|(p, _)| p.is_file() && seen.insert(p.canonicalize().unwrap_or_else(|_| p.clone())))
|
|
.collect()
|
|
}
|
|
|
|
// ------------------------------------------------------------------ python
|
|
|
|
/// Project-local virtualenv directories checked by auto-detection, in order.
|
|
pub(crate) const PROJECT_VENVS: [&str; 4] = [".venv", "venv", "env", ".env"];
|
|
|
|
/// `<root>/<venv>/bin/python` of the first project virtualenv.
|
|
pub(crate) fn project_venv_python(root: &Path) -> Option<PathBuf> {
|
|
PROJECT_VENVS
|
|
.iter()
|
|
.map(|v| root.join(v))
|
|
.filter(|v| v.join("pyvenv.cfg").is_file())
|
|
.map(|v| v.join(bin_dir()).join(exe("python")))
|
|
.find(|p| p.is_file())
|
|
}
|
|
|
|
/// `/usr/bin/python3` on macOS is a stub that opens an installer dialog when
|
|
/// the Command Line Tools are missing; never run it then.
|
|
fn usable(path: &Path) -> bool {
|
|
if cfg!(target_os = "macos") && path.starts_with("/usr/bin") {
|
|
return Path::new("/Library/Developer/CommandLineTools/usr/bin").is_dir()
|
|
|| Path::new("/Applications/Xcode.app").is_dir();
|
|
}
|
|
true
|
|
}
|
|
|
|
fn is_python_name(name: &str) -> bool {
|
|
let name = name.strip_suffix(".exe").unwrap_or(name);
|
|
let Some(rest) = name.strip_prefix("python") else { return false };
|
|
rest.is_empty()
|
|
|| rest == "3"
|
|
|| rest.strip_prefix("3.").is_some_and(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
|
|
}
|
|
|
|
fn python(root: Option<&Path>) -> Vec<Sdk> {
|
|
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
|
if let Some(root) = root {
|
|
for v in PROJECT_VENVS {
|
|
let dir = root.join(v);
|
|
if dir.join("pyvenv.cfg").is_file() {
|
|
found.push((dir.join(bin_dir()).join(exe("python")), "venv"));
|
|
}
|
|
}
|
|
}
|
|
if let Ok(venv) = std::env::var("VIRTUAL_ENV") {
|
|
found.push((Path::new(&venv).join(bin_dir()).join(exe("python")), "venv"));
|
|
}
|
|
if let Some(home) = home_dir() {
|
|
// Virtualenvs managed by virtualenvwrapper / pipenv / poetry.
|
|
for dir in [".virtualenvs", ".local/share/virtualenvs", "Library/Caches/pypoetry/virtualenvs"] {
|
|
for env in subdirs(&home.join(dir)) {
|
|
found.push((env.join(bin_dir()).join(exe("python")), "venv"));
|
|
}
|
|
}
|
|
let pyenv = std::env::var_os("PYENV_ROOT").map(PathBuf::from).unwrap_or_else(|| home.join(".pyenv"));
|
|
for v in subdirs(&pyenv.join("versions")) {
|
|
found.push((v.join(bin_dir()).join(exe("python")), "pyenv"));
|
|
}
|
|
for v in subdirs(&home.join(".local/share/uv/python")) {
|
|
found.push((v.join(bin_dir()).join(exe("python3")), "uv"));
|
|
}
|
|
for (v, manager) in version_managers::installs(Tool::Python) {
|
|
found.extend(pinned::python_in_install(&v).map(|p| (p, manager)));
|
|
}
|
|
let mut conda_roots: Vec<PathBuf> =
|
|
["miniconda3", "anaconda3", "miniforge3", "mambaforge", "micromamba", "opt/miniconda3"]
|
|
.iter()
|
|
.map(|d| home.join(d))
|
|
.collect();
|
|
conda_roots.push(PathBuf::from("/opt/homebrew/Caskroom/miniconda/base"));
|
|
conda_roots.push(PathBuf::from("/opt/miniconda3"));
|
|
conda_roots.push(PathBuf::from("/opt/anaconda3"));
|
|
if let Some(prefix) = std::env::var_os("CONDA_PREFIX") {
|
|
conda_roots.push(PathBuf::from(prefix));
|
|
}
|
|
for base in conda_roots {
|
|
found.push((base.join(bin_dir()).join(exe("python")), "conda"));
|
|
for env in subdirs(&base.join("envs")) {
|
|
found.push((env.join(bin_dir()).join(exe("python")), "conda"));
|
|
}
|
|
}
|
|
}
|
|
for dir in std::env::split_paths(&search_path()) {
|
|
if dir.ends_with(".pyenv/shims") || version_managers::is_shims_dir(&dir) {
|
|
continue;
|
|
}
|
|
let mut names: Vec<String> = std::fs::read_dir(&dir)
|
|
.into_iter()
|
|
.flatten()
|
|
.flatten()
|
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
|
.filter(|n| is_python_name(n))
|
|
.collect();
|
|
names.sort();
|
|
for n in names {
|
|
found.push((dir.join(n), "system"));
|
|
}
|
|
}
|
|
// A virtualenv's python is a symlink to its base interpreter: dedupe venvs by
|
|
// their own path, the rest by the resolved binary.
|
|
let mut seen_venv = HashSet::new();
|
|
let mut seen_real = HashSet::new();
|
|
let candidates: Vec<(PathBuf, &'static str)> = found
|
|
.into_iter()
|
|
.filter(|(p, _)| p.is_file() && usable(p))
|
|
.filter(|(p, source)| {
|
|
if *source == "venv" || venv_of(p).is_some() {
|
|
seen_venv.insert(venv_of(p).unwrap_or_else(|| p.clone()))
|
|
} else {
|
|
seen_real.insert(p.canonicalize().unwrap_or_else(|_| p.clone()))
|
|
}
|
|
})
|
|
.map(|(p, source)| if venv_of(&p).is_some() && source != "conda" { (p, "venv") } else { (p, source) })
|
|
.collect();
|
|
par_map(&candidates, |(path, source)| python_sdk(path, source, root))
|
|
}
|
|
|
|
pub(crate) fn python_sdk(path: &Path, source: &str, root: Option<&Path>) -> Option<Sdk> {
|
|
let version = query(path, &["--version"], VERSION_TIMEOUT)
|
|
.and_then(|v| v.strip_prefix("Python ").map(str::to_owned))
|
|
.or_else(|| pyvenv_version(path))?;
|
|
let short = version.split('.').take(2).collect::<Vec<_>>().join(".");
|
|
let venv = venv_of(path);
|
|
let label = match (&venv, source) {
|
|
(Some(home), _) => {
|
|
let dir = home.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
|
// `.venv` inside a project reads better as the project name, like PyCharm.
|
|
let owner = home.parent().and_then(|p| p.file_name()).map(|n| n.to_string_lossy().into_owned());
|
|
match (dir.starts_with('.') || dir == "venv" || dir == "env", owner, root) {
|
|
(true, Some(owner), Some(root)) if home.parent() == Some(root) => format!("Python {short} ({owner})"),
|
|
_ => format!("Python {short} ({dir})"),
|
|
}
|
|
}
|
|
(None, "conda") => {
|
|
let env = path
|
|
.parent()
|
|
.and_then(Path::parent)
|
|
.and_then(|p| p.file_name())
|
|
.map(|n| n.to_string_lossy().into_owned());
|
|
format!("Python {short} (conda: {})", env.unwrap_or_default())
|
|
}
|
|
(None, "pyenv") => format!("Python {short} (pyenv)"),
|
|
(None, tool @ ("uv" | "mise" | "asdf")) => format!("Python {short} ({tool})"),
|
|
_ => format!("Python {short}"),
|
|
};
|
|
Some(Sdk {
|
|
path: path.into(),
|
|
name: label,
|
|
version: Some(version),
|
|
source: source.to_owned(),
|
|
home: venv.or_else(|| path.parent().and_then(Path::parent).map(Path::to_path_buf)).map(Into::into),
|
|
extra: None,
|
|
})
|
|
}
|
|
|
|
fn pyvenv_version(python: &Path) -> Option<String> {
|
|
let cfg = std::fs::read_to_string(venv_of(python)?.join("pyvenv.cfg")).ok()?;
|
|
cfg.lines().find_map(|l| {
|
|
let (k, v) = l.split_once('=')?;
|
|
matches!(k.trim(), "version" | "version_info").then(|| v.trim().to_owned())
|
|
})
|
|
}
|
|
|
|
pub(crate) fn subdirs(dir: &Path) -> Vec<PathBuf> {
|
|
let mut out: Vec<PathBuf> =
|
|
std::fs::read_dir(dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.is_dir()).collect();
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
// ------------------------------------------------------------------ node
|
|
|
|
/// Node's own version managers, by where they keep installs below the home directory.
|
|
const NODE_MANAGERS: &[(&str, &str)] =
|
|
&[(".nvm/versions/node", "nvm"), (".volta/tools/image/node", "volta"), (".local/share/fnm/node-versions", "fnm")];
|
|
|
|
/// Where a Node on the search path comes from: its version manager, "system" for one
|
|
/// of the system's own bin directories, "path" for anything else the user put on `PATH`.
|
|
pub(crate) fn node_source(path: &Path, home: Option<&Path>) -> &'static str {
|
|
let managed = home.and_then(|home| {
|
|
NODE_MANAGERS.iter().find(|(dir, _)| path.starts_with(home.join(dir))).map(|(_, manager)| *manager)
|
|
});
|
|
managed.or_else(|| version_managers::manager_of(path)).unwrap_or_else(|| {
|
|
if path.parent().is_some_and(ide_core::tool_path::is_system_dir) { "system" } else { "path" }
|
|
})
|
|
}
|
|
|
|
/// Where a PHP on the search path comes from: its version manager or distribution,
|
|
/// "system" for one of the system's own bin directories, "path" for anything else.
|
|
pub(crate) fn php_source(path: &Path, home: Option<&Path>) -> &'static str {
|
|
let under = |dir: &str| home.is_some_and(|h| path.starts_with(h.join(dir)));
|
|
if under(".phpbrew") {
|
|
"phpbrew"
|
|
} else if under(".phpenv") {
|
|
"phpenv"
|
|
} else if under(".config/herd-lite") || under("Library/Application Support/Herd") {
|
|
"herd"
|
|
} else if path.starts_with("/opt/homebrew") || path.starts_with("/usr/local/opt") {
|
|
"homebrew"
|
|
} else if let Some(manager) = version_managers::manager_of(path) {
|
|
manager
|
|
} else if path.parent().is_some_and(ide_core::tool_path::is_system_dir) {
|
|
"system"
|
|
} else {
|
|
"path"
|
|
}
|
|
}
|
|
|
|
/// `php` CLIs: every one on `PATH`, Homebrew's versioned formulae, PHP version managers,
|
|
/// Laravel Herd, and the usual Windows distributions (XAMPP, Laragon).
|
|
fn php() -> Vec<Sdk> {
|
|
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
|
let home = home_dir();
|
|
for p in executables_in("php", std::env::split_paths(&search_path())) {
|
|
if version_managers::is_shim(&p) {
|
|
continue;
|
|
}
|
|
let source = php_source(&p, home.as_deref());
|
|
found.push((p, source));
|
|
}
|
|
let named = |dir: &Path, prefix: &str| -> Vec<PathBuf> {
|
|
subdirs(dir)
|
|
.into_iter()
|
|
.filter(|d| d.file_name().is_some_and(|n| n.to_string_lossy().starts_with(prefix)))
|
|
.collect()
|
|
};
|
|
for opt in ["/opt/homebrew/opt", "/usr/local/opt"] {
|
|
for d in named(Path::new(opt), "php") {
|
|
found.push((d.join("bin").join(exe("php")), "homebrew"));
|
|
}
|
|
}
|
|
for d in named(Path::new("/usr/local"), "php") {
|
|
found.push((d.join("bin").join(exe("php")), "system"));
|
|
}
|
|
if let Some(home) = &home {
|
|
for v in subdirs(&home.join(".phpbrew/php")).into_iter().rev() {
|
|
found.push((v.join("bin").join(exe("php")), "phpbrew"));
|
|
}
|
|
for v in subdirs(&home.join(".phpenv/versions")).into_iter().rev() {
|
|
found.push((v.join("bin").join(exe("php")), "phpenv"));
|
|
}
|
|
found.push((home.join(".config/herd-lite/bin").join(exe("php")), "herd"));
|
|
let herd = home.join("Library/Application Support/Herd/bin");
|
|
for entry in std::fs::read_dir(&herd).into_iter().flatten().flatten() {
|
|
let name = entry.file_name().to_string_lossy().into_owned();
|
|
if name.starts_with("php") && !name.contains('-') && entry.path().is_file() {
|
|
found.push((entry.path(), "herd"));
|
|
}
|
|
}
|
|
}
|
|
if cfg!(windows) {
|
|
found.push((PathBuf::from(r"C:\xampp\php\php.exe"), "xampp"));
|
|
found.push((PathBuf::from(r"C:\php\php.exe"), "system"));
|
|
for d in subdirs(Path::new(r"C:\laragon\bin\php")) {
|
|
found.push((d.join("php.exe"), "laragon"));
|
|
}
|
|
}
|
|
let found = dedupe_by_real_path(found);
|
|
par_map(&found, |(path, source)| {
|
|
let version = query(path, &["-r", "echo PHP_VERSION;"], VERSION_TIMEOUT)?;
|
|
let version = version.trim().to_owned();
|
|
if version.is_empty() {
|
|
return None;
|
|
}
|
|
Some(Sdk {
|
|
path: path.clone().into(),
|
|
name: format!("PHP {version}"),
|
|
version: Some(version),
|
|
source: (*source).to_owned(),
|
|
home: path.parent().and_then(Path::parent).map(Into::into),
|
|
extra: None,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn node() -> Vec<Sdk> {
|
|
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
|
// Every Node on the search path, not only the first one `which` finds: an SSH
|
|
// session's minimal PATH puts /usr/bin ahead of the user's own ~/.local/bin, so
|
|
// /usr/bin/node would be the only install listed (#440).
|
|
for p in executables_in("node", std::env::split_paths(&search_path())) {
|
|
// A mise / asdf shim picks the version by directory: its installs are listed instead.
|
|
if version_managers::is_shim(&p) {
|
|
continue;
|
|
}
|
|
let source = node_source(&p, home_dir().as_deref());
|
|
found.push((p, source));
|
|
}
|
|
if let Some(home) = home_dir() {
|
|
for v in subdirs(&home.join(".nvm/versions/node")).into_iter().rev() {
|
|
found.push((v.join("bin").join(exe("node")), "nvm"));
|
|
}
|
|
for v in subdirs(&home.join(".volta/tools/image/node")).into_iter().rev() {
|
|
found.push((v.join("bin").join(exe("node")), "volta"));
|
|
}
|
|
for v in subdirs(&home.join(".local/share/fnm/node-versions")).into_iter().rev() {
|
|
found.push((v.join("installation/bin").join(exe("node")), "fnm"));
|
|
}
|
|
}
|
|
for (v, manager) in version_managers::installs(Tool::Node).into_iter().rev() {
|
|
found.push((v.join("bin").join(exe("node")), manager));
|
|
}
|
|
let found = dedupe_by_real_path(found);
|
|
par_map(&found, |(path, source)| {
|
|
let version = query(path, &["--version"], VERSION_TIMEOUT)?;
|
|
let version = version.trim_start_matches('v').to_owned();
|
|
Some(Sdk {
|
|
path: path.clone().into(),
|
|
name: format!("Node.js {version}"),
|
|
version: Some(version),
|
|
source: (*source).to_owned(),
|
|
home: path.parent().and_then(Path::parent).map(Into::into),
|
|
extra: None,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn package_managers() -> Vec<Sdk> {
|
|
let found: Vec<(&str, PathBuf)> =
|
|
["npm", "pnpm", "yarn", "bun"].iter().filter_map(|n| Some((*n, which(n)?))).collect();
|
|
par_map(&found, |(name, path)| {
|
|
let version = query(path, &["--version"], VERSION_TIMEOUT);
|
|
Some(Sdk {
|
|
path: path.clone().into(),
|
|
name: (*name).to_owned(),
|
|
version,
|
|
source: "path".into(),
|
|
home: None,
|
|
extra: None,
|
|
})
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------------ rust
|
|
|
|
fn rust_sdk(bin: &Path, name: String, source: &str, home: Option<PathBuf>) -> Option<Sdk> {
|
|
let rustc = bin.join(exe("rustc"));
|
|
if !rustc.is_file() {
|
|
return None;
|
|
}
|
|
let version = query(&rustc, &["--version"], VERSION_TIMEOUT)
|
|
.map(|v| v.trim_start_matches("rustc ").split_whitespace().next().unwrap_or_default().to_owned());
|
|
let stdlib = home
|
|
.as_ref()
|
|
.map(|h| h.join("lib/rustlib/src/rust/library"))
|
|
.or_else(|| {
|
|
let sysroot = query(&rustc, &["--print", "sysroot"], VERSION_TIMEOUT)?;
|
|
Some(PathBuf::from(sysroot).join("lib/rustlib/src/rust/library"))
|
|
})
|
|
.filter(|p| p.is_dir());
|
|
Some(Sdk {
|
|
path: bin.into(),
|
|
name,
|
|
version,
|
|
source: source.to_owned(),
|
|
home: home.map(Into::into),
|
|
extra: stdlib.map(Into::into),
|
|
})
|
|
}
|
|
|
|
fn rust() -> Vec<Sdk> {
|
|
let rustup_home =
|
|
std::env::var_os("RUSTUP_HOME").map(PathBuf::from).or_else(|| home_dir().map(|h| h.join(".rustup")));
|
|
let toolchains = rustup_home.map(|h| subdirs(&h.join("toolchains"))).unwrap_or_default();
|
|
let mut out = par_map(&toolchains, |dir| {
|
|
let name = dir.file_name()?.to_string_lossy().into_owned();
|
|
rust_sdk(&dir.join("bin"), name, "rustup", Some(dir.clone()))
|
|
});
|
|
// A rustc not managed by rustup (Homebrew, distro packages).
|
|
if let Some(cargo) = which("cargo") {
|
|
let bin = cargo.parent().map(Path::to_path_buf).unwrap_or_default();
|
|
let is_proxy = bin.ends_with(".cargo/bin");
|
|
if !is_proxy && !out.iter().any(|s| s.path == bin) {
|
|
out.extend(rust_sdk(&bin, format!("{}", bin.display()), "path", None));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
// ------------------------------------------------------------------ go
|
|
|
|
fn go_version(goroot: &Path) -> Option<String> {
|
|
let text = std::fs::read_to_string(goroot.join("VERSION")).ok();
|
|
let first = text.as_deref().and_then(|t| t.lines().next()).map(str::to_owned);
|
|
first
|
|
.or_else(|| {
|
|
query(&goroot.join("bin").join(exe("go")), &["version"], VERSION_TIMEOUT)?
|
|
.split_whitespace()
|
|
.nth(2)
|
|
.map(str::to_owned)
|
|
})
|
|
.map(|v| v.trim_start_matches("go").to_owned())
|
|
}
|
|
|
|
fn go() -> Vec<Sdk> {
|
|
let mut roots: Vec<(PathBuf, &'static str)> = Vec::new();
|
|
let path_go = which("go");
|
|
let default_gopath =
|
|
path_go.as_ref().and_then(|go| query(go, &["env", "GOPATH"], VERSION_TIMEOUT)).map(PathBuf::from);
|
|
if let Some(go) = path_go.as_ref().filter(|p| !version_managers::is_shim(p))
|
|
&& let Some(root) = query(go, &["env", "GOROOT"], VERSION_TIMEOUT)
|
|
{
|
|
roots.push((PathBuf::from(root), "path"));
|
|
}
|
|
for p in ["/usr/local/go", "/opt/homebrew/opt/go/libexec", "/usr/local/opt/go/libexec", "/usr/lib/go"] {
|
|
roots.push((PathBuf::from(p), "system"));
|
|
}
|
|
if let Some(home) = home_dir() {
|
|
for d in subdirs(&home.join("sdk")) {
|
|
roots.push((d, "sdk"));
|
|
}
|
|
for d in subdirs(&home.join("go/pkg/mod/golang.org")) {
|
|
if d.file_name().is_some_and(|n| n.to_string_lossy().starts_with("toolchain@")) {
|
|
roots.push((d, "toolchain"));
|
|
}
|
|
}
|
|
}
|
|
for (d, manager) in version_managers::installs(Tool::Go) {
|
|
roots.extend(pinned::goroot_in_install(&d).map(|r| (r, manager)));
|
|
}
|
|
let mut seen = HashSet::new();
|
|
roots.retain(|(r, _)| {
|
|
r.join("bin").join(exe("go")).is_file() && seen.insert(r.canonicalize().unwrap_or_else(|_| r.clone()))
|
|
});
|
|
par_map(&roots, |(root, source)| {
|
|
let version = go_version(root);
|
|
Some(Sdk {
|
|
path: root.clone().into(),
|
|
name: format!("Go {}", version.clone().unwrap_or_default()).trim().to_owned(),
|
|
version,
|
|
source: (*source).to_owned(),
|
|
home: Some(root.clone().into()),
|
|
extra: default_gopath.clone().map(Into::into),
|
|
})
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------------ typescript
|
|
|
|
/// `node_modules/typescript/lib` of the project (root or a direct workspace member).
|
|
pub(crate) fn workspace_tsdk(root: &Path) -> Option<PathBuf> {
|
|
let lib = root.join("node_modules/typescript/lib");
|
|
lib.join("tsserver.js").is_file().then_some(lib)
|
|
}
|
|
|
|
fn typescript(root: Option<&Path>) -> Vec<Sdk> {
|
|
let mut out = Vec::new();
|
|
if let Some(lib) = root.and_then(workspace_tsdk) {
|
|
let version = std::fs::read_to_string(lib.parent().unwrap_or(&lib).join("package.json"))
|
|
.ok()
|
|
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
|
|
.and_then(|v| v.get("version").and_then(|v| v.as_str()).map(str::to_owned));
|
|
out.push(Sdk {
|
|
path: lib.clone().into(),
|
|
name: format!("TypeScript {}", version.clone().unwrap_or_default()).trim().to_owned(),
|
|
version,
|
|
source: "workspace".into(),
|
|
home: lib.parent().map(Into::into),
|
|
extra: None,
|
|
});
|
|
}
|
|
out
|
|
}
|
|
|
|
// ------------------------------------------------------------------ jdk
|
|
|
|
/// `KEY="value"` of a JDK `release` file.
|
|
fn release_value(release: &str, key: &str) -> Option<String> {
|
|
let line = release.lines().find_map(|l| l.trim().strip_prefix(key)?.strip_prefix('='))?;
|
|
Some(line.trim().trim_matches('"').to_owned()).filter(|v| !v.is_empty())
|
|
}
|
|
|
|
fn jdk() -> Vec<Sdk> {
|
|
crate::java::rescan_jdks().iter().map(jdk_sdk).collect()
|
|
}
|
|
|
|
/// A JDK as the SDK pickers list it (`Eclipse Adoptium 25.0.1`).
|
|
pub fn jdk_sdk(j: &crate::java::Jdk) -> Sdk {
|
|
let java_home = std::env::var_os("JAVA_HOME").and_then(|h| crate::launch_path(Path::new(&h)).ok());
|
|
let release = std::fs::read_to_string(j.home.join("release")).unwrap_or_default();
|
|
let version = release_value(&release, "JAVA_VERSION").unwrap_or_else(|| j.major.to_string());
|
|
let name = match release_value(&release, "IMPLEMENTOR") {
|
|
Some(vendor) => format!("{vendor} {version}"),
|
|
None => format!("JDK {version}"),
|
|
};
|
|
let source = if java_home.as_ref() == Some(&j.home) {
|
|
"JAVA_HOME"
|
|
} else if crate::java::is_downloaded(&j.home) {
|
|
"downloaded"
|
|
} else {
|
|
version_managers::manager_of(&j.home).unwrap_or("system")
|
|
};
|
|
Sdk {
|
|
path: j.home.clone().into(),
|
|
name,
|
|
version: Some(version),
|
|
source: source.to_owned(),
|
|
home: Some(j.home.clone().into()),
|
|
extra: None,
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ maven
|
|
|
|
/// Maven version from `lib/maven-core-<version>.jar`.
|
|
fn maven_version(home: &Path) -> Option<String> {
|
|
std::fs::read_dir(home.join("lib")).ok()?.flatten().find_map(|e| {
|
|
let name = e.file_name().to_string_lossy().into_owned();
|
|
Some(name.strip_prefix("maven-core-")?.strip_suffix(".jar")?.to_owned())
|
|
})
|
|
}
|
|
|
|
/// A Maven home (`bin/mvn` and `lib/`); Homebrew's formula keeps it in `libexec`.
|
|
fn maven_home(dir: &Path) -> Option<PathBuf> {
|
|
let mvn = if cfg!(windows) { "mvn.cmd" } else { "mvn" };
|
|
[dir.join("libexec"), dir.to_path_buf()]
|
|
.into_iter()
|
|
.find(|h| h.join("bin").join(mvn).is_file() && h.join("lib").is_dir())
|
|
}
|
|
|
|
fn maven() -> Vec<Sdk> {
|
|
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
|
// Windows' Maven ships `mvn.cmd` only (`which` looks for `mvn.exe`).
|
|
let on_path = if cfg!(windows) {
|
|
std::env::split_paths(&search_path()).map(|d| d.join("mvn.cmd")).find(|p| p.is_file())
|
|
} else {
|
|
which("mvn")
|
|
};
|
|
if let Some(mvn) = on_path.and_then(|p| crate::launch_path(&p).ok())
|
|
&& let Some(home) = mvn.parent().and_then(Path::parent)
|
|
{
|
|
found.push((home.to_path_buf(), "path"));
|
|
}
|
|
for var in ["MAVEN_HOME", "M2_HOME"] {
|
|
if let Some(h) = std::env::var_os(var) {
|
|
found.push((PathBuf::from(h), "env"));
|
|
}
|
|
}
|
|
for p in ["/opt/homebrew/opt/maven", "/usr/local/opt/maven", "/usr/share/maven", "/opt/maven"] {
|
|
found.push((PathBuf::from(p), "system"));
|
|
}
|
|
if let Some(home) = home_dir() {
|
|
for d in subdirs(&home.join(".sdkman/candidates/maven")) {
|
|
found.push((d, "sdkman"));
|
|
}
|
|
for (d, manager) in version_managers::installs(Tool::Maven) {
|
|
found.extend(subdirs(&d).into_iter().map(|s| (s, manager)));
|
|
found.push((d, manager));
|
|
}
|
|
// Distributions the Maven wrapper downloaded: `dists/apache-maven-3.9.6-bin/<hash>/apache-maven-3.9.6`,
|
|
// or since wrapper 3.3 the home itself, `dists/apache-maven-3.9.6/<hash>`.
|
|
for dist in subdirs(&home.join(".m2/wrapper/dists")) {
|
|
for hash in subdirs(&dist) {
|
|
found.extend(subdirs(&hash).into_iter().map(|d| (d, "wrapper")));
|
|
found.push((hash, "wrapper"));
|
|
}
|
|
}
|
|
}
|
|
let mut seen = HashSet::new();
|
|
let mut out = Vec::new();
|
|
for (dir, source) in found {
|
|
let Some(home) = maven_home(&dir) else { continue };
|
|
let home = crate::launch_path(&home).unwrap_or(home);
|
|
if !seen.insert(home.clone()) {
|
|
continue;
|
|
}
|
|
let version = maven_version(&home);
|
|
out.push(Sdk {
|
|
path: home.clone().into(),
|
|
name: format!("Maven {}", version.clone().unwrap_or_default()).trim().to_owned(),
|
|
version,
|
|
source: source.to_owned(),
|
|
home: Some(home.into()),
|
|
extra: None,
|
|
});
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(all(test, windows))]
|
|
mod maven_path_tests {
|
|
use super::*;
|
|
use crate::launch_path;
|
|
use std::process::Command;
|
|
|
|
struct Subst {
|
|
name: String,
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl Subst {
|
|
fn new(target: &Path) -> Self {
|
|
// Tests run in parallel and may race for the same free letter; only a drive that
|
|
// shows this target's marker is ours.
|
|
let marker = format!("{}.subst", target.file_name().unwrap().to_string_lossy());
|
|
std::fs::write(target.join(&marker), "").unwrap();
|
|
for letter in (b'D'..=b'Z').rev() {
|
|
let name = format!("{}:", char::from(letter));
|
|
let root = PathBuf::from(format!("{name}\\"));
|
|
if root.exists() {
|
|
continue;
|
|
}
|
|
let output = Command::new("subst").arg(&name).arg(target).output().unwrap();
|
|
if output.status.success() && root.join(&marker).exists() {
|
|
return Self { name, root };
|
|
}
|
|
}
|
|
panic!("no free drive for Maven path verification");
|
|
}
|
|
}
|
|
|
|
impl Drop for Subst {
|
|
fn drop(&mut self) {
|
|
let _ = Command::new("subst").args([&self.name, "/D"]).output();
|
|
}
|
|
}
|
|
|
|
fn install(home: &Path) {
|
|
std::fs::create_dir_all(home.join("bin")).unwrap();
|
|
std::fs::create_dir_all(home.join("lib")).unwrap();
|
|
std::fs::write(home.join("bin/mvn.cmd"), "@echo off\n").unwrap();
|
|
std::fs::write(home.join("lib/maven-core-3.9.16.jar"), "").unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn mapped_maven_home_and_launcher_keep_their_drive() {
|
|
let mapped = tempfile::tempdir().unwrap();
|
|
install(&mapped.path().join("apache-maven"));
|
|
let drive = Subst::new(mapped.path());
|
|
let home = drive.root.join("apache-maven");
|
|
let launcher = home.join("bin/mvn.cmd");
|
|
assert_ne!(dunce::canonicalize(&home).unwrap(), home);
|
|
assert_eq!(launch_path(&home).unwrap(), home);
|
|
assert_eq!(launch_path(&launcher).unwrap(), launcher);
|
|
assert_eq!(maven_home(&home), Some(home.clone()));
|
|
assert_eq!(maven_version(&home).as_deref(), Some("3.9.16"));
|
|
let jdk = drive.root.join("jdk");
|
|
std::fs::create_dir_all(jdk.join("bin")).unwrap();
|
|
std::fs::write(jdk.join("bin/java.exe"), "").unwrap();
|
|
std::fs::write(jdk.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap();
|
|
assert_eq!(crate::java::jdk_at(&jdk).unwrap().home, jdk);
|
|
}
|
|
|
|
#[test]
|
|
fn a_launcher_link_outside_the_mapped_root_still_finds_its_install() {
|
|
let mapped = tempfile::tempdir().unwrap();
|
|
let real = tempfile::tempdir().unwrap();
|
|
install(real.path());
|
|
let drive = Subst::new(mapped.path());
|
|
let link = mapped.path().join("shims");
|
|
let output = Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()))
|
|
.args(["/D", "/C", "mklink", "/J"])
|
|
.arg(&link)
|
|
.arg(real.path().join("bin"))
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
|
let launcher = launch_path(&drive.root.join("shims/mvn.cmd")).unwrap();
|
|
assert_eq!(launcher, dunce::canonicalize(real.path().join("bin/mvn.cmd")).unwrap());
|
|
let home = launcher.parent().and_then(Path::parent).and_then(maven_home).unwrap();
|
|
assert_eq!(home, dunce::canonicalize(real.path()).unwrap());
|
|
}
|
|
}
|