From 8d25b1bcd01dda3734ff5e2a555bae9cab070485 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Tue, 3 Nov 2026 23:53:56 +0800 Subject: [PATCH] fix(sdk): follow scoop shims to the jdtls release script (#804) * fix(sdk): follow scoop shims to the jdtls release script scoop puts a jdtls.exe shim (driven by a jdtls.shim descriptor) on PATH. launch_directly only recognized the release's own script names, so a scoop install kept the shim's command line and the configured JDK never applied. On Windows, follow the shim's path entry to the release script; a shim without a descriptor is still treated as someone's own wrapper. Fixes #794 * test(sdk): make the scoop-shim fixture a full JDK #815 made launch_directly require javac and the jdk.compiler/jdk.jdi/ jdk.unsupported modules, so the shim fixture's bare release file stopped the launch before the shim path was exercised. * fix(sdk): follow real Scoop Python shims for jdtls --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> --- crates/ide-sdk/src/jdtls.rs | 142 ++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 7 deletions(-) diff --git a/crates/ide-sdk/src/jdtls.rs b/crates/ide-sdk/src/jdtls.rs index 75520eed7..937aeff5f 100644 --- a/crates/ide-sdk/src/jdtls.rs +++ b/crates/ide-sdk/src/jdtls.rs @@ -218,6 +218,33 @@ pub fn java_args(install: &JdtlsInstall, java_major: u32, script_args: &[String] args } +/// Scoop shims either point at the release script or run Python with the +/// script in `args`. Paths may be quoted and contain spaces and backslashes. +fn shim_target(shim_exe: &Path) -> Option { + let text = std::fs::read_to_string(shim_exe.with_extension("shim")).ok()?; + let entry = |name: &str| { + text.trim_start_matches('\u{feff}').lines().find_map(|line| { + let (key, value) = line.split_once('=')?; + if !key.trim().eq_ignore_ascii_case(name) { + return None; + } + let value = value.trim(); + let value = value.strip_prefix('"').and_then(|s| s.strip_suffix('"')).unwrap_or(value); + (!value.is_empty()).then_some(value) + }) + }; + let name = |path: &str| path.rsplit(['/', '\\']).next().unwrap_or(path).to_ascii_lowercase(); + let path = entry("path")?; + if SCRIPTS.contains(&name(path).as_str()) { + return Some(PathBuf::from(path)); + } + if !matches!(name(path).as_str(), "python.exe" | "python3.exe" | "pythonw.exe") { + return None; + } + let script = entry("args")?; + SCRIPTS.contains(&name(script).as_str()).then(|| PathBuf::from(script)) +} + /// Rewrites a jdtls launch (`command` = the release's script, `JAVA_HOME` chosen by /// [`crate::java::configure_jdtls`]) to start `java` directly. Left unchanged when the /// command is someone's own wrapper, the install has another layout, or no JDK is set @@ -228,13 +255,19 @@ pub fn launch_directly(cfg: &mut LanguageServerConfig) { } let search = cfg.env.get("PATH").map(std::ffi::OsString::from).or_else(|| std::env::var_os("PATH")); let Some(script) = search.and_then(|s| ide_run::which::find_executable(&cfg.command, &s)) else { return }; - let is_script = dunce::canonicalize(&script) - .ok() - .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_ascii_lowercase())) - .is_some_and(|n| SCRIPTS.contains(&n.as_str())); - if !is_script { - return; - } + let Ok(resolved) = dunce::canonicalize(&script) else { return }; + let name = resolved.file_name().map(|n| n.to_string_lossy().to_ascii_lowercase()); + let script = match name.as_deref() { + Some(n) if SCRIPTS.contains(&n) => resolved, + // A scoop shim (`shims\jdtls.exe` from PATHEXT) hides the release layout + // behind itself, so the launch ran the shim's own command line and the + // configured JDK never applied (issue #794). + Some("jdtls.exe") if cfg!(windows) => match shim_target(&resolved) { + Some(target) => target, + None => return, + }, + _ => return, + }; let Some(install) = jdtls_home(&script).and_then(|h| find_install(&h, std::env::consts::OS, std::env::consts::ARCH)) else { @@ -520,4 +553,99 @@ mod tests { launch_directly(&mut failing); assert_eq!(failing.command, "jdtls"); } + + #[test] + fn reads_a_scoop_shims_path_entry() { + let dir = tempfile::tempdir().unwrap(); + let shim = dir.path().join("jdtls.shim"); + let shim_exe = dir.path().join("jdtls.exe"); + std::fs::write(&shim, "path = C:\\apps\\jdtls\\current\\bin\\jdtls.cmd\nargs =\n").unwrap(); + assert_eq!(shim_target(&shim_exe), Some(PathBuf::from("C:\\apps\\jdtls\\current\\bin\\jdtls.cmd"))); + std::fs::write(&shim, "args = -x\n").unwrap(); + assert_eq!(shim_target(&shim_exe), None, "no path entry"); + std::fs::write(&shim, "path =\n").unwrap(); + assert_eq!(shim_target(&shim_exe), None, "empty path entry"); + assert_eq!(shim_target(&dir.path().join("other.exe")), None, "no shim file"); + } + + #[test] + fn reads_real_scoop_python_shims_and_quoted_paths() { + let dir = tempfile::tempdir().unwrap(); + let shim_exe = dir.path().join("jdtls.exe"); + let script = r"C:\Users\张 三\scoop\apps\jdtls\current\bin\jdtls"; + for descriptor in [ + format!("path = \"C:\\Python313\\python.exe\"\r\nargs = {script}\r\n"), + format!("\u{feff}path = \"C:\\Python313\\python.exe\"\r\nargs = \"{script}\"\r\n"), + format!("PATH = \"{script}\"\n"), + ] { + std::fs::write(shim_exe.with_extension("shim"), &descriptor).unwrap(); + assert_eq!(shim_target(&shim_exe), Some(PathBuf::from(script)), "{descriptor}"); + } + for descriptor in [ + "path = \"C:\\Python313\\python.exe\"\n", + "path = \"C:\\Python313\\python.exe\"\nargs = C:\\custom.py\n", + "path = C:\\custom.exe\nargs = C:\\bin\\jdtls\n", + "path = C:\\python.exe\nargs = C:\\bin\\jdtls --jvm-arg=-Xmx1g\n", + ] { + std::fs::write(shim_exe.with_extension("shim"), descriptor).unwrap(); + assert_eq!(shim_target(&shim_exe), None, "{descriptor}"); + } + } + + /// Issue #794: scoop puts a `jdtls.exe` shim (plus its `.shim` descriptor) on + /// PATH, hiding the release layout — the launch must follow the shim to the + /// release script so the configured JDK applies. + #[cfg(windows)] + #[test] + fn follows_a_scoop_shim_to_the_release_script() { + let dir = tempfile::tempdir().unwrap(); + let shims = dir.path().join("shims"); + let home = dir.path().join("scoop with spaces").join("apps").join("jdtls").join("current"); + std::fs::create_dir_all(&shims).unwrap(); + std::fs::write(shims.join("jdtls.exe"), "shim binary").unwrap(); + std::fs::create_dir_all(home.join("bin")).unwrap(); + std::fs::write(home.join("bin").join("jdtls"), "# Python launcher\n").unwrap(); + std::fs::create_dir_all(home.join("plugins")).unwrap(); + std::fs::write(home.join("plugins/org.eclipse.equinox.launcher_1.7.0.jar"), "").unwrap(); + for config in config_dir_names(std::env::consts::OS, std::env::consts::ARCH) { + std::fs::create_dir_all(home.join(&config)).unwrap(); + std::fs::write(home.join(&config).join("config.ini"), "").unwrap(); + } + let jdk = dir.path().join("jdk-25"); + std::fs::create_dir_all(jdk.join("bin")).unwrap(); + std::fs::write( + jdk.join("release"), + "JAVA_VERSION=\"25.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n", + ) + .unwrap(); + std::fs::write(jdk.join("bin").join(crate::exe("javac")), "").unwrap(); + std::fs::write(jdk.join("bin").join(crate::exe("java")), "").unwrap(); + let target = format!( + "path = \"C:\\Python313\\python.exe\"\nargs = {}\n", + home.join("bin").join("jdtls").to_string_lossy() + ); + std::fs::write(shims.join("jdtls.shim"), target).unwrap(); + + let cfg = || -> LanguageServerConfig { + let mut cfg: LanguageServerConfig = toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap(); + cfg.env.insert("PATH".into(), shims.to_string_lossy().into_owned()); + cfg.env.insert("JAVA_HOME".into(), jdk.to_string_lossy().into_owned()); + cfg.args = strings(&["-data", r"C:\ws", "--jvm-arg=-Xmx1G"]); + cfg + }; + let mut direct = cfg(); + launch_directly(&mut direct); + let java = dunce::canonicalize(&jdk).unwrap().join("bin").join(crate::exe("java")); + assert_eq!(Path::new(&direct.command), java); + assert!(direct.args.iter().any(|a| a == "-Xmx1G")); + let jar = direct.args.iter().position(|a| a == "-jar").unwrap(); + assert!(direct.args[jar + 1].ends_with("org.eclipse.equinox.launcher_1.7.0.jar")); + assert_eq!(direct.args[jar + 2..], strings(&["-data", r"C:\ws"])); + + // A shim whose descriptor is gone is someone's own wrapper: keep the command. + std::fs::remove_file(shims.join("jdtls.shim")).unwrap(); + let mut own = cfg(); + launch_directly(&mut own); + assert_eq!(own.command, "jdtls"); + } }