diff --git a/crates/ide-build/src/process.rs b/crates/ide-build/src/process.rs index fcec4b454..d0335def8 100644 --- a/crates/ide-build/src/process.rs +++ b/crates/ide-build/src/process.rs @@ -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) -> 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, kind: Stream, tx: mpsc::Sender<(Stream, String)>| { std::thread::spawn(move || { diff --git a/crates/ide-run/src/process.rs b/crates/ide-run/src/process.rs index 97c97c3b9..6fbf8b58c 100644 --- a/crates/ide-run/src/process.rs +++ b/crates/ide-run/src/process.rs @@ -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) -> 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, 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| 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")); } } diff --git a/crates/ide-run/src/wrap.rs b/crates/ide-run/src/wrap.rs index f3b73ea77..59dab74a5 100644 --- a/crates/ide-run/src/wrap.rs +++ b/crates/ide-run/src/wrap.rs @@ -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/` in `dir` or an ancestor up to (and including) `root`.