fix(maven): resolve bare launchers on the IDE search path (#422)

Builds and run configurations started a bare mvn / gradle through the app's own PATH, which can be minimal when the app isn't launched from a shell, so launchers installed by mise, asdf or Homebrew weren't found. Bare names now resolve through the configuration's PATH override or the IDE search path (login shell PATH plus version-manager shims) on every platform, for build processes and run configurations alike.

Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
arookieofcтаa-rookie-of-C-language authored and GitHub committed 2026-10-30 19:23:31 +08:00
1 джерело 86cf348310
коміт f507f55d16
3 files changed
+53 -24

No files matched your search

+20 -2
Переглянути файл
@@ -34,6 +34,22 @@ fn kill_tree(child: &mut Child) {
let _ = child.wait();
}
/// `program` as spawnable: a bare name (`mvn`, `mvn.cmd`) resolves on the IDE's
/// search path — the login shell's `PATH` plus version-manager shims — since the
/// app's own `PATH` can be minimal and `Command` would not see `mvn` there.
fn program_path(program: &str, env: &BTreeMap<String, String>) -> std::path::PathBuf {
let path = std::path::Path::new(program);
if !path.is_relative() || path.components().count() > 1 {
return path.to_path_buf();
}
let search = env
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("PATH"))
.map(|(_, v)| std::ffi::OsString::from(v))
.unwrap_or_else(ide_core::tool_path::search_path);
ide_core::process::find_executable(program, &search).unwrap_or_else(|| path.to_path_buf())
}
/// Runs `program args` in `cwd`, feeding each output line to `on_line`; kills
/// the process group and returns `cancelled` when `cancel` fires.
pub(crate) fn run(
@@ -48,7 +64,8 @@ pub(crate) fn run(
// Output is read as UTF-8: JVMs on Windows are told to write it (see `ide_core::output_encoding`).
let encoding =
ide_core::output_encoding::fix(program, args, env, |k| std::env::var(k).ok(), "UTF-8", cfg!(windows));
let mut command = Command::new(program);
let program = program_path(program, env);
let mut command = Command::new(&program);
command.args(args).current_dir(cwd).envs(env).envs(encoding.env).stdin(Stdio::null());
command.stdout(Stdio::piped()).stderr(Stdio::piped());
#[cfg(unix)]
@@ -57,7 +74,8 @@ pub(crate) fn run(
command.process_group(0);
}
ide_core::process::prepare(&mut command);
let mut child = command.spawn().map_err(|e| IdeError::unavailable(format!("cannot start {program}: {e}")))?;
let mut child =
command.spawn().map_err(|e| IdeError::unavailable(format!("cannot start {}: {e}", program.display())))?;
let (tx, rx) = mpsc::channel::<(Stream, String)>();
let pump = |stream: Box<dyn Read + Send>, kind: Stream, tx: mpsc::Sender<(Stream, String)>| {
std::thread::spawn(move || {
+29 -19
Переглянути файл
@@ -139,7 +139,7 @@ pub(crate) fn invocation(config: &RunConfig, cwd: &Path) -> Invocation {
if config.args.is_empty() && looks_like_shell && !cwd.join(command).is_file() {
return shell_invocation(command, windows, std::env::var("COMSPEC").ok());
}
let program = resolve_program(command, cwd, &config.env, windows);
let program = resolve_program(command, cwd, config, windows);
let mut display = quote(command);
for a in &config.args {
display.push(' ');
@@ -239,22 +239,18 @@ fn shell_invocation(command: &str, windows: bool, comspec: Option<String>) -> In
}
/// The program to spawn: relative paths like `./gradlew` or `.venv/bin/python`
/// against the run directory; on Windows a bare name through `PATH` × `PATHEXT`
/// (`kotlinc` → `kotlinc.bat`), which `Command` would only try as `.exe`.
fn resolve_program(command: &str, cwd: &Path, env: &BTreeMap<String, String>, windows: bool) -> PathBuf {
/// against the run directory; a bare name through the configuration's search path
/// (on Windows also `PATHEXT`: `kotlinc` → `kotlinc.bat`), so launchers installed
/// outside the app's own `PATH` (`mvn` in a version manager's shims) still start.
fn resolve_program(command: &str, cwd: &Path, config: &RunConfig, windows: bool) -> PathBuf {
let path = Path::new(command);
if path.is_relative() && path.components().count() > 1 {
return cwd.join(path);
}
if windows && path.components().count() == 1 && path.is_relative() {
let search = env
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("PATH"))
.map(|(_, v)| std::ffi::OsString::from(v))
.or_else(|| std::env::var_os("PATH"))
.unwrap_or_default();
if path.components().count() == 1 && path.is_relative() {
let search = crate::wrap::search_path(config);
let pathext = std::env::var("PATHEXT").ok();
if let Some(found) = crate::which::find_executable_in(command, &search, true, pathext.as_deref()) {
if let Some(found) = crate::which::find_executable_in(command, &search, windows, pathext.as_deref()) {
return found;
}
}
@@ -1032,14 +1028,28 @@ mod tests {
let cwd = dir.path();
let path = std::env::join_paths([&bin]).unwrap().to_string_lossy().into_owned();
// Windows environment keys are case-insensitive (`Path`).
let env = BTreeMap::from([("Path".to_owned(), path)]);
assert_eq!(resolve_program("kotlinc", cwd, &env, true), bin.join("kotlinc.bat"));
let config = |env: BTreeMap<String, String>| RunConfig {
id: "c".into(),
name: "c".into(),
kind: "maven".into(),
command: String::new(),
args: Vec::new(),
cwd: None,
env,
detected: false,
is_test: false,
debug_adapter: None,
allow_parallel: false,
extras: Default::default(),
};
let config = config(BTreeMap::from([("Path".to_owned(), path)]));
assert_eq!(resolve_program("kotlinc", cwd, &config, true), bin.join("kotlinc.bat"));
// Not found: left to the OS (`command not found` at spawn).
assert_eq!(resolve_program("nope", cwd, &env, true), PathBuf::from("nope"));
// Unix: bare names are left to `execvp`.
assert_eq!(resolve_program("kotlinc", cwd, &env, false), PathBuf::from("kotlinc"));
assert_eq!(resolve_program("nope", cwd, &config, true), PathBuf::from("nope"));
// Unix: extensionless names only (`kotlinc.bat` does not match `kotlinc`).
assert_eq!(resolve_program("kotlinc", cwd, &config, false), PathBuf::from("kotlinc"));
// Relative paths resolve against the run directory on every platform.
assert_eq!(resolve_program("./gradlew", cwd, &env, true), cwd.join("./gradlew"));
assert_eq!(resolve_program(".venv/bin/python", cwd, &env, false), cwd.join(".venv/bin/python"));
assert_eq!(resolve_program("./gradlew", cwd, &config, true), cwd.join("./gradlew"));
assert_eq!(resolve_program(".venv/bin/python", cwd, &config, false), cwd.join(".venv/bin/python"));
}
}
+4 -3
Переглянути файл
@@ -33,15 +33,16 @@ pub fn is_python(command: &str) -> bool {
n == "py" || n.strip_prefix("python").is_some_and(|v| v.chars().all(|c| c.is_ascii_digit() || c == '.'))
}
/// The search path the configuration runs with (its `PATH` override, else the IDE's).
/// The search path the configuration runs with (its `PATH` override, else the IDE's:
/// the login shell's and the version managers', so `mvn` / `gradle` installed by mise,
/// asdf or Homebrew resolve when the app's own `PATH` is minimal).
pub fn search_path(config: &RunConfig) -> OsString {
config
.env
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("PATH"))
.map(|(_, v)| OsString::from(v))
.or_else(|| std::env::var_os("PATH"))
.unwrap_or_default()
.unwrap_or_else(ide_core::tool_path::search_path)
}
/// `node_modules/.bin/<name>` in `dir` or an ancestor up to (and including) `root`.