mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(sdk): list every Node on the search path, not only the first one (#502)
Detection stopped at the first `node` on the search path. Over SSH the minimal `PATH` puts `/usr/bin` ahead of `~/.local/bin`, so a Node installed there was never listed and refreshing could not help. Every `node` on the search path is now listed, and installs keep nvm, Volta or fnm as their source. Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
This commit is contained in:
co-authored by
a-rookie-of-C-language
parent
8dc1ee062f
commit
c3d29dc842
@@ -98,6 +98,12 @@ const USER_DIRS: &[&str] = &[
|
||||
|
||||
const SYSTEM_DIRS: &[&str] = &["/opt/homebrew/bin", "/usr/local/go/bin", "/usr/local/bin", "/usr/bin", "/bin"];
|
||||
|
||||
/// Whether `dir` is one of the system's own bin directories, which [`search_path`] adds
|
||||
/// whatever `PATH` holds.
|
||||
pub fn is_system_dir(dir: &Path) -> bool {
|
||||
SYSTEM_DIRS.iter().any(|d| Path::new(d) == dir)
|
||||
}
|
||||
|
||||
/// The `bin` directories of nvm's Node versions in `dir`, newest first.
|
||||
fn nvm_bins(dir: &Path) -> Vec<PathBuf> {
|
||||
let mut versions: Vec<(Vec<u64>, PathBuf)> = std::fs::read_dir(dir)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Finding installed interpreters and toolchains.
|
||||
|
||||
use crate::exec::{query, search_path, which};
|
||||
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;
|
||||
@@ -236,11 +236,32 @@ fn subdirs(dir: &Path) -> Vec<PathBuf> {
|
||||
|
||||
// ------------------------------------------------------------------ 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" }
|
||||
})
|
||||
}
|
||||
|
||||
fn node() -> Vec<Sdk> {
|
||||
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
||||
// A mise / asdf shim picks the version by directory: its installs are listed instead.
|
||||
if let Some(p) = which("node").filter(|p| !version_managers::is_shim(p)) {
|
||||
let source = version_managers::manager_of(&p).unwrap_or("path");
|
||||
// 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() {
|
||||
@@ -257,9 +278,6 @@ fn node() -> Vec<Sdk> {
|
||||
for (v, manager) in version_managers::installs(Tool::Node).into_iter().rev() {
|
||||
found.push((v.join("bin").join(exe("node")), manager));
|
||||
}
|
||||
for p in ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"] {
|
||||
found.push((PathBuf::from(p), "system"));
|
||||
}
|
||||
let found = dedupe_by_real_path(found);
|
||||
par_map(&found, |(path, source)| {
|
||||
let version = query(path, &["--version"], VERSION_TIMEOUT)?;
|
||||
|
||||
@@ -26,6 +26,17 @@ pub(crate) fn which(name: &str) -> Option<PathBuf> {
|
||||
ide_core::process::find_executable(name, &path)
|
||||
}
|
||||
|
||||
/// Every install of `name` on `dirs`, one per directory holding it, in `dirs` order (the
|
||||
/// first is what `which` returns). Tool detection lists them all: a login shell's `PATH`
|
||||
/// and an SSH exec channel's minimal one disagree on which comes first (#440).
|
||||
pub(crate) fn executables_in(name: &str, dirs: impl Iterator<Item = PathBuf>) -> Vec<PathBuf> {
|
||||
let pathext = ide_core::process::host_var_os("PATHEXT");
|
||||
let names = ide_core::process::executable_names(name, cfg!(windows), pathext.as_deref().and_then(|p| p.to_str()));
|
||||
dirs.filter(|d| !d.as_os_str().is_empty())
|
||||
.filter_map(|dir| names.iter().map(|n| dir.join(n)).find(|p| p.is_file()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Runs `program args` and returns trimmed stdout (stderr when stdout is empty)
|
||||
/// on success; `None` on failure or after `timeout`.
|
||||
pub(crate) fn query(program: &Path, args: &[&str], timeout: Duration) -> Option<String> {
|
||||
|
||||
@@ -939,3 +939,31 @@ fn gradle_settings_precedence() {
|
||||
assert_eq!(gradle_command(root, root, &settings), if local { expected } else { wrapper.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
/// The reporter of #440 has Node 26 in `~/.local/bin` and an 18 in `/usr/bin`; an SSH
|
||||
/// exec channel's PATH puts `/usr/bin` first, so only the system's Node was listed.
|
||||
#[test]
|
||||
fn node_lists_every_install_on_the_search_path_not_only_the_first() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (system, user) = (tmp.path().join("usr-bin"), tmp.path().join("local-bin"));
|
||||
for dir in [&system, &user] {
|
||||
std::fs::create_dir(dir).unwrap();
|
||||
std::fs::write(dir.join(exe("node")), "").unwrap();
|
||||
}
|
||||
let found = crate::exec::executables_in("node", [system.clone(), user.clone()].into_iter());
|
||||
assert_eq!(found, [system.join(exe("node")), user.join(exe("node"))]);
|
||||
// A directory without the executable contributes nothing.
|
||||
let found = crate::exec::executables_in("node", [tmp.path().join("empty"), user.clone()].into_iter());
|
||||
assert_eq!(found, [user.join(exe("node"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_on_the_search_path_keeps_its_version_manager_as_source() {
|
||||
let home = Path::new("/home/me");
|
||||
let source = |p: &str| crate::discover::node_source(Path::new(p), Some(home));
|
||||
assert_eq!(source("/home/me/.nvm/versions/node/v24.0.0/bin/node"), "nvm");
|
||||
assert_eq!(source("/home/me/.volta/tools/image/node/24.0.0/bin/node"), "volta");
|
||||
assert_eq!(source("/home/me/.local/share/fnm/node-versions/v24/installation/bin/node"), "fnm");
|
||||
assert_eq!(source("/home/me/.local/bin/node"), "path");
|
||||
assert_eq!(source("/usr/bin/node"), "system");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user