diff --git a/crates/ide-host/src/ops/analysis.rs b/crates/ide-host/src/ops/analysis.rs index e40a4edf1..18e2825c3 100644 --- a/crates/ide-host/src/ops/analysis.rs +++ b/crates/ide-host/src/ops/analysis.rs @@ -134,21 +134,22 @@ pub async fn inspect_code( let cancel = host.begin_request(request); let listed = { let (project, cancel) = (project.clone(), cancel.clone()); - blocking(move || files_in_scope(&project, &scope, &cancel)).await + blocking(move || { + let files = files_in_scope(&project, &scope, &cancel)?; + let total = files.len() as u32; + let rel: std::collections::HashMap<_, _> = + files.into_iter().filter(|f| project.lsp.serves_path(&f.path)).map(|f| (f.path, f.rel_path)).collect(); + Ok((total, rel)) + }) + .await }; - let files = match listed { + let (total_in_scope, rel) = match listed { Ok(files) => files, Err(e) => { host.end_request(request); return Err(e); } }; - let total_in_scope = files.len() as u32; - let rel: std::collections::HashMap<_, _> = files - .iter() - .filter(|f| project.lsp.serves_path(&f.path)) - .map(|f| (f.path.clone(), f.rel_path.clone())) - .collect(); let mut summary = InspectSummary { skipped: total_in_scope - rel.len() as u32, ..Default::default() }; let _ = on_event.send(InspectEvent::Started { total: rel.len() as u32 }); diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index ebc12bb16..7ad8e2009 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -170,7 +170,10 @@ pub fn jdtls_jdk(root: &Path, s: &Settings) -> Result, Some(Ok(jdk)) if !ide_core::java_runtime::supports_java_bundles(&jdk.home) => { Err(crate::java::server_jdk_incomplete_message(&jdk, jdk.major.max(crate::java::JDTLS_MIN_JAVA))) } - Some(Ok(jdk)) => Ok(Some(jdk)), + Some(Ok(jdk)) => match crate::java::java_execution_error(&crate::java::java_executable(&jdk.home)) { + Some(error) => Err(error), + None => Ok(Some(jdk)), + }, Some(Err(p)) => { Err(format!("The language server JDK {} (Settings › Languages & Frameworks › Java).", not_a_jdk(&p))) } diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index afecff128..302558652 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -32,8 +32,10 @@ pub fn parse_release(release: &str) -> Option { if first == 1 { parts.next()?.parse().ok() } else { Some(first) } } -/// The JDK at `home`, if it is one (a `release` file and `bin/java`). +/// The JDK at `home` (or a macOS bundle), if it has a `release` file and `bin/java`. pub fn jdk_at(home: &Path) -> Option { + let bundle_home = home.join("Contents/Home"); + let home = if bundle_home.is_dir() { bundle_home.as_path() } else { home }; let release = std::fs::read_to_string(home.join("release")).ok()?; if !home.join("bin").join(crate::exe("java")).is_file() { return None; @@ -41,6 +43,69 @@ pub fn jdk_at(home: &Path) -> Option { Some(Jdk { major: parse_release(&release)?, home: crate::launch_path(home).unwrap_or_else(|_| home.to_path_buf()) }) } +struct JavaProbe { + modified: std::time::SystemTime, + len: u64, + permissions: std::fs::Permissions, + at: Instant, + runnable: bool, +} + +static JAVA_PROBES: Mutex> = Mutex::new(BTreeMap::new()); + +/// Metadata can describe a JDK built for another architecture. Probe the executable +/// before giving it to JDT, with a short cache shared by server and project selection. +pub(crate) fn java_runs(java: &Path) -> bool { + let Ok(metadata) = java.metadata() else { return false }; + let Ok(modified) = metadata.modified() else { return false }; + { + let probes = JAVA_PROBES.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(probe) = probes.get(java) + && probe.modified == modified + && probe.len == metadata.len() + && probe.permissions == metadata.permissions() + && probe.at.elapsed() < RESCAN_AFTER + { + return probe.runnable; + } + } + let runnable = crate::exec::query(java, &["-version"], Duration::from_secs(5)).is_some(); + JAVA_PROBES.lock().unwrap_or_else(|e| e.into_inner()).insert( + java.to_path_buf(), + JavaProbe { modified, len: metadata.len(), permissions: metadata.permissions(), at: Instant::now(), runnable }, + ); + runnable +} + +fn project_jdk_is_usable(jdk: &Jdk) -> bool { + jdk.home.join("bin").join(crate::exe("javac")).is_file() + && (jdk.major < 9 || ide_core::java_runtime::supports_java_bundles(&jdk.home)) + && java_runs(&java_executable(&jdk.home)) +} + +pub(crate) fn server_jdk_error(jdk: &Jdk, min_java: u32) -> Option { + server_jdk_metadata_error(jdk, min_java).or_else(|| java_execution_error(&java_executable(&jdk.home))) +} + +pub(crate) fn server_jdk_metadata_error(jdk: &Jdk, min_java: u32) -> Option { + if jdk.major < min_java { + Some(server_jdk_too_old_message(jdk, min_java)) + } else if !ide_core::java_runtime::supports_java_bundles(&jdk.home) { + Some(server_jdk_incomplete_message(jdk, min_java)) + } else { + None + } +} + +pub(crate) fn java_execution_error(java: &Path) -> Option { + (!java_runs(java)).then(|| { + format!( + "The language server Java executable ({}) cannot run on this machine. Choose a working JDK in {SERVER_JDK_SETTING}, or set it to Auto.", + java.display() + ) + }) +} + fn children(dir: &Path) -> Vec { let mut out: Vec = std::fs::read_dir(dir).map(|rd| rd.flatten().map(|e| e.path()).collect()).unwrap_or_default(); @@ -166,11 +231,14 @@ pub fn installed_jdks() -> Arc<[Jdk]> { /// the list is rescanned (at most every [`RESCAN_AFTER`]), so a JDK installed /// after startup is picked up when the server is restarted. pub fn jdtls_jdks(min_java: u32, data_dir: &Path) -> Arc<[Jdk]> { - let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner()); - let installed = match &*slot { - Some(s) if jdtls_runtime(&s.jdks, min_java).is_some() || s.at.elapsed() < RESCAN_AFTER => s.jdks.clone(), - _ => rescan(&mut slot), + let (installed, expired) = { + let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner()); + match &*slot { + Some(s) => (s.jdks.clone(), s.at.elapsed() >= RESCAN_AFTER), + None => (rescan(&mut slot), false), + } }; + let installed = if expired && jdtls_runtime(&installed, min_java).is_none() { rescan_jdks() } else { installed }; runtime_candidates(&installed, data_dir).into() } @@ -203,7 +271,7 @@ pub const JDTLS_MIN_JAVA: u32 = 21; /// The JDK to run jdtls with: the first found that is new enough (`JAVA_HOME` wins). pub fn jdtls_runtime(jdks: &[Jdk], min_java: u32) -> Option<&Jdk> { - jdks.iter().find(|j| j.major >= min_java && ide_core::java_runtime::supports_java_bundles(&j.home)) + jdks.iter().find(|j| server_jdk_error(j, min_java).is_none()) } /// The Java version the jdtls that `cfg` launches needs: the highest any of its @@ -290,7 +358,7 @@ fn manifest_min_java(manifest: &str) -> Option { /// jdtls itself runs on. pub fn runtimes(jdks: &[Jdk]) -> Vec { let mut by_major: BTreeMap = BTreeMap::new(); - for j in jdks { + for j in jdks.iter().filter(|jdk| project_jdk_is_usable(jdk)) { by_major.entry(j.major).or_insert(j); } by_major @@ -372,7 +440,8 @@ pub fn configure_jdtls( // A Lombok agent from the user's JVM arguments wins. let own_agent = crate::lombok::agent_in_args(cfg.args.iter().map(String::as_str)); let agent = own_agent.as_deref().or(lombok); - if !cfg.args.iter().any(|a| a == "-data") { + let generated_workspace = !cfg.args.iter().any(|a| a == "-data"); + if generated_workspace { // ECJ builds incrementally: classes compiled without (or with another) Lombok // agent would be kept, so each agent gets its own workspace and a full build. let mut key = match agent { @@ -414,12 +483,22 @@ pub fn configure_jdtls( set_jvm_property(&mut cfg.args, ZIP64_LENIENCY); let managed = language_server_runtimes_dir(data_dir); let managed = dunce::canonicalize(&managed).unwrap_or(managed); + let mut excluded_jdks = Vec::new(); let project_jdks: Vec<_> = project_jdk .into_iter() .chain(jdks) .filter(|jdk| !jdk.home.starts_with(&managed) || project_jdk == Some(*jdk)) + .filter(|jdk| { + if project_jdk_is_usable(jdk) { + true + } else { + excluded_jdks.push(jdk.home.clone()); + false + } + }) .cloned() .collect(); + let project_jdk = project_jdk.filter(|jdk| project_jdks.contains(jdk)); let mut all: Vec = Vec::new(); for j in project_jdk.into_iter().chain(jdks).chain(server_jdk) { if !all.contains(j) { @@ -427,13 +506,9 @@ pub fn configure_jdtls( } } let jdks = all.as_slice(); - if !cfg.env.contains_key("JAVA_HOME") { + if cfg.launch_error.is_none() && !cfg.env.contains_key("JAVA_HOME") { let runtime = match server_jdk { - Some(jdk) if jdk.major < min_java => Err(server_jdk_too_old_message(jdk, min_java)), - Some(jdk) if !ide_core::java_runtime::supports_java_bundles(&jdk.home) => { - Err(server_jdk_incomplete_message(jdk, min_java)) - } - Some(jdk) => Ok(jdk), + Some(jdk) => server_jdk_error(jdk, min_java).map_or(Ok(jdk), Err), None => jdtls_runtime(jdks, min_java).ok_or_else(|| missing_runtime_message(jdks, min_java)), }; match runtime { @@ -442,10 +517,10 @@ pub fn configure_jdtls( } Err(e) => cfg.launch_error = Some(e), } - } else if let Some(jdk) = cfg.env.get("JAVA_HOME").and_then(|home| jdk_at(Path::new(home))) - && !ide_core::java_runtime::supports_java_bundles(&jdk.home) + } else if cfg.launch_error.is_none() + && let Some(jdk) = cfg.env.get("JAVA_HOME").and_then(|home| jdk_at(Path::new(home))) { - cfg.launch_error = Some(server_jdk_incomplete_message(&jdk, min_java)); + cfg.launch_error = server_jdk_error(&jdk, min_java); } let mut runtimes = runtimes(&project_jdks); if let Some(project) = project_jdk.or_else(|| pick_jdk(&project_jdks, project_java_version(server_root))) { @@ -457,6 +532,14 @@ pub fn configure_jdtls( } } } + if generated_workspace && !excluded_jdks.is_empty() { + // JDT retains VM installs in its workspace even after they leave the runtime list. + excluded_jdks.sort(); + excluded_jdks.dedup(); + let key = text_key(&json!(excluded_jdks).to_string()); + let data = cfg.args.iter().position(|a| a == "-data").unwrap() + 1; + cfg.args[data].push_str(&format!("-runtimes-{key}")); + } if !runtimes.is_empty() { merge_init_options(cfg, json!({ "settings": { "java": { "configuration": { "runtimes": runtimes } } } })); } @@ -587,6 +670,37 @@ fn parse_java_version(v: &str) -> Option { if first == 1 { parts.next()?.trim().parse().ok() } else { Some(first) } } +#[cfg(test)] +pub(crate) fn write_test_java(home: &Path) { + let java = java_executable(home); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::write(&java, "#!/bin/sh\necho 'openjdk version 25'\n").unwrap(); + std::fs::set_permissions(java, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + #[cfg(windows)] + { + static BINARY: std::sync::OnceLock = std::sync::OnceLock::new(); + let dir = BINARY.get_or_init(|| { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("java.rs"); + std::fs::write(&source, "fn main() {}\n").unwrap(); + assert!( + std::process::Command::new("rustc") + .arg(source) + .arg("-o") + .arg(dir.path().join("java.exe")) + .status() + .unwrap() + .success() + ); + dir + }); + std::fs::copy(dir.path().join("java.exe"), java).unwrap(); + } +} + #[cfg(test)] mod tests { use super::*; @@ -599,7 +713,7 @@ mod tests { format!("JAVA_VERSION=\"{major}\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n"), ) .unwrap(); - std::fs::write(java_executable(&home), "").unwrap(); + write_test_java(&home); std::fs::write(home.join("bin").join(crate::exe("javac")), "").unwrap(); Jdk { major, home } } @@ -635,7 +749,7 @@ mod tests { "JAVA_VERSION=\"21.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n", ) .unwrap(); - std::fs::write(java_executable(&project_home), "").unwrap(); + write_test_java(&project_home); std::fs::write(project_home.join("bin").join(crate::exe("javac")), "").unwrap(); let project = jdk_at(&project_home).unwrap(); let discover = || jdk_homes(&project_root).filter_map(|home| jdk_at(&home)).collect::>(); @@ -649,7 +763,7 @@ mod tests { "JAVA_VERSION=\"25.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n", ) .unwrap(); - std::fs::write(java_executable(&home), "").unwrap(); + write_test_java(&home); std::fs::write(home.join("bin").join(crate::exe("javac")), "").unwrap(); let candidates = runtime_candidates(&installed, data.path()); assert_eq!(jdtls_runtime(&candidates, 25).unwrap().major, 25); @@ -730,6 +844,131 @@ mod tests { assert_eq!(parse_release("OTHER=1"), None); } + #[cfg(unix)] + #[test] + fn issue_1668_unrunnable_jdk_does_not_hide_valid_runtimes() { + use std::os::unix::fs::PermissionsExt; + let tmp = tempfile::tempdir().unwrap(); + let broken = test_jdk(tmp.path(), 8, "jdk1.8.0_202.jdk/Contents/Home"); + std::fs::write(broken.home.join("release"), "JAVA_VERSION=\"1.8.0_202\"\n").unwrap(); + std::fs::write(java_executable(&broken.home), "#!/bin/sh\nexit 126\n").unwrap(); + std::fs::set_permissions(java_executable(&broken.home), std::fs::Permissions::from_mode(0o755)).unwrap(); + let working = test_jdk(tmp.path(), 8, "working8"); + std::fs::write(working.home.join("release"), "JAVA_VERSION=\"1.8.0_372\"\n").unwrap(); + let server = test_jdk(tmp.path(), 25, "jdk25"); + let installed = [broken.clone(), working.clone(), server.clone()]; + for project in [None, Some(&broken)] { + for explicit in [None, Some(&server)] { + let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap(); + configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &installed, project, explicit, 25, &[], None); + assert_eq!(cfg.launch_error, None); + assert_eq!(cfg.env["JAVA_HOME"], server.home.to_string_lossy()); + let options = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap(); + let runtimes = options["settings"]["java"]["configuration"]["runtimes"].as_array().unwrap(); + assert_eq!(runtimes[0]["path"], json!(working.home)); + assert!(!runtimes.iter().any(|r| r["path"] == json!(broken.home))); + } + } + } + + #[cfg(unix)] + #[test] + fn unusable_server_jdks_are_skipped_or_reported_without_losing_the_selected_path() { + use std::os::unix::fs::PermissionsExt; + let tmp = tempfile::tempdir().unwrap(); + let server = test_jdk(tmp.path(), 25, "good25"); + for failure in ["exit126", "not-executable", "missing-javac", "jre", "timeout"] { + let bad = test_jdk(tmp.path(), 25, failure); + let java = java_executable(&bad.home); + match failure { + "exit126" => std::fs::write(&java, "#!/bin/sh\nexit 126\n").unwrap(), + "not-executable" => std::fs::set_permissions(&java, std::fs::Permissions::from_mode(0o644)).unwrap(), + "missing-javac" => std::fs::remove_file(bad.home.join("bin/javac")).unwrap(), + "jre" => std::fs::write(bad.home.join("release"), "JAVA_VERSION=\"25\"\nIMAGE_TYPE=\"JRE\"\n").unwrap(), + "timeout" => std::fs::write(&java, "#!/bin/sh\nexec /bin/sleep 10\n").unwrap(), + _ => unreachable!(), + } + let installed = [bad.clone(), server.clone()]; + assert_eq!(jdtls_runtime(&installed, 25), Some(&server), "{failure}"); + assert_eq!(runtimes(&installed)[0]["path"], json!(server.home), "{failure}"); + for env_override in [false, true] { + let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap(); + if env_override { + cfg.env.insert("JAVA_HOME".into(), bad.home.to_string_lossy().into_owned()); + } + configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &installed, None, Some(&bad), 25, &[], None); + let error = cfg.launch_error.unwrap(); + assert!(error.contains(bad.home.to_string_lossy().as_ref()), "{error}"); + assert!(!error.contains(server.home.to_string_lossy().as_ref()), "{error}"); + } + write_test_java(&bad.home); + if matches!(failure, "exit126" | "not-executable" | "timeout") { + assert!(java_runs(&java), "a replaced or repaired executable is probed again"); + } + } + let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap(); + cfg.launch_error = Some("invalid selected JDK /outside/scan".into()); + configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &[], None, None, 25, &[], None); + assert_eq!(cfg.launch_error.as_deref(), Some("invalid selected JDK /outside/scan")); + } + + #[cfg(unix)] + #[test] + fn slow_java_probe_does_not_hold_the_cache_lock() { + let tmp = tempfile::tempdir().unwrap(); + let jdk = test_jdk(tmp.path(), 25, "slow25"); + let ready = tmp.path().join("ready"); + std::fs::write( + java_executable(&jdk.home), + format!("#!/bin/sh\ntouch '{}'\nexec /bin/sleep 2\n", ready.display()), + ) + .unwrap(); + std::thread::scope(|scope| { + let probe = scope.spawn(|| java_runs(&java_executable(&jdk.home))); + let started = Instant::now(); + while !ready.exists() && started.elapsed() < Duration::from_secs(5) { + std::thread::sleep(Duration::from_millis(10)); + } + assert!(ready.exists(), "the probe must start"); + assert!( + JAVA_PROBES.try_lock().is_ok(), + "a running java -version must not lock out cached or unrelated probes" + ); + assert!(probe.join().unwrap()); + }); + } + + #[cfg(unix)] + #[test] + fn only_excluded_jdks_change_the_generated_workspace() { + let tmp = tempfile::tempdir().unwrap(); + let project = test_jdk(tmp.path(), 8, "project8"); + let server = test_jdk(tmp.path(), 25, "server25"); + let other_server = test_jdk(tmp.path(), 26, "server26"); + let workspace = |jdks: &[Jdk], server: &Jdk, custom: bool| { + let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap(); + if custom { + cfg.args.extend(["-data".into(), "/own/workspace".into()]); + } + configure_jdtls(&mut cfg, tmp.path(), tmp.path(), jdks, Some(&project), Some(server), 25, &[], None); + cfg.args.windows(2).find(|a| a[0] == "-data").unwrap()[1].clone() + }; + let installed = [project.clone(), server.clone()]; + let before = workspace(&installed, &server, false); + let original = tmp.path().join("lsp/jdtls").join(path_key(tmp.path())); + assert_eq!(before, original.to_string_lossy(), "working JDKs retain the pre-existing workspace"); + assert_eq!(before, workspace(&[], &other_server, false), "installing or removing working JDKs keeps the path"); + std::fs::write(java_executable(&project.home), "#!/bin/sh\nexit 126\n").unwrap(); + let after = workspace(&installed, &server, false); + assert_ne!(before, after, "the workspace holding the now-invalid JDK must not be reused"); + assert_eq!(after, workspace(&installed, &server, false)); + assert_eq!(after, workspace(std::slice::from_ref(&other_server), &other_server, false)); + assert_eq!(after, workspace(&[server.clone(), project.clone(), project.clone()], &server, false)); + assert_eq!(workspace(&installed, &server, true), "/own/workspace"); + write_test_java(&project.home); + assert_eq!(before, workspace(&installed, &server, false), "repairing the JDK restores the original path"); + } + #[test] fn jdtls_workspace_and_runtimes() { let fixtures = tempfile::tempdir().unwrap(); diff --git a/crates/ide-sdk/src/jdtls.rs b/crates/ide-sdk/src/jdtls.rs index 6bd8ae15c..a859d3932 100644 --- a/crates/ide-sdk/src/jdtls.rs +++ b/crates/ide-sdk/src/jdtls.rs @@ -292,8 +292,10 @@ pub fn launch_directly(cfg: &mut LanguageServerConfig) { }), }; let Some((java, jdk)) = jdk.filter(|(java, _)| java.is_file()) else { return }; - if !ide_core::java_runtime::supports_java_bundles(&jdk.home) { - cfg.launch_error = Some(crate::java::server_jdk_incomplete_message(&jdk, crate::java::jdtls_min_java(cfg))); + if let Some(error) = crate::java::server_jdk_metadata_error(&jdk, crate::java::jdtls_min_java(cfg)) + .or_else(|| crate::java::java_execution_error(&java)) + { + cfg.launch_error = Some(error); return; } cfg.args = java_args(&install, jdk.major, &cfg.args); @@ -526,7 +528,7 @@ mod tests { ) .unwrap(); std::fs::write(jdk.join("bin/javac"), "").unwrap(); - std::fs::write(jdk.join("bin/java"), "").unwrap(); + crate::java::write_test_java(&jdk); let cfg = |command: &str| -> LanguageServerConfig { let mut cfg: LanguageServerConfig = @@ -555,6 +557,25 @@ mod tests { launch_directly(&mut overridden); assert!(overridden.launch_error.unwrap().contains("jdk.jdi")); + let alternate = jdk.join("bin/alternate-java"); + std::fs::write(&alternate, "#!/bin/sh\nexit 126\n").unwrap(); + std::fs::set_permissions(&alternate, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap(); + let mut overridden = cfg("jdtls"); + overridden.args.push(format!("--java-executable={}", alternate.display())); + launch_directly(&mut overridden); + let error = overridden.launch_error.unwrap(); + assert!(error.contains(alternate.to_string_lossy().as_ref()) && error.contains("cannot run"), "{error}"); + + let old = dir.path().join("jdk-17"); + std::fs::create_dir_all(old.join("bin")).unwrap(); + std::fs::write(old.join("release"), "JAVA_VERSION=\"17\"\n").unwrap(); + crate::java::write_test_java(&old); + let mut overridden = cfg("jdtls"); + overridden.args.push(format!("--java-executable={}", old.join("bin/java").display())); + launch_directly(&mut overridden); + let error = overridden.launch_error.unwrap(); + assert!(error.contains(old.to_string_lossy().as_ref()) && error.contains("too old"), "{error}"); + // Someone's own wrapper, or no usable JDK: the command is kept. std::fs::write(home.join("bin/my-jdtls"), "#!/bin/sh\n").unwrap(); std::fs::set_permissions(home.join("bin/my-jdtls"), std::os::unix::fs::PermissionsExt::from_mode(0o755)) @@ -637,7 +658,7 @@ mod tests { ) .unwrap(); std::fs::write(jdk.join("bin").join(crate::exe("javac")), "").unwrap(); - std::fs::write(jdk.join("bin").join(crate::exe("java")), "").unwrap(); + crate::java::write_test_java(&jdk); let target = format!( "path = \"C:\\Python313\\python.exe\"\nargs = {}\n", home.join("bin").join("jdtls").to_string_lossy() diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index eb988bc37..ec9d12190 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -313,7 +313,7 @@ fn npm_config_runs_on_its_own_node_interpreter() { fn fake_jdk(dir: &Path) { std::fs::create_dir_all(dir.join("bin")).unwrap(); - std::fs::write(dir.join("bin").join(exe("java")), "").unwrap(); + crate::java::write_test_java(dir); std::fs::write( dir.join("release"), "JAVA_VERSION=\"17.0.9\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n", @@ -921,6 +921,17 @@ fn jdtls_jdk_setting() { s.languages.java.jdtls_jdk = "jdk17.0.1".into(); let err = jdtls_jdk(tmp.path(), &s).unwrap_err(); assert!(err.contains("jdk17.0.1 does not exist") && err.contains("Languages & Frameworks › Java"), "{err}"); + + let bundle = tmp.path().join("outside-scan/jdk25.jdk"); + let home = bundle.join("Contents/Home"); + fake_jdk(&home); + for path in [&bundle, &home, &home.join("bin"), &home.join("bin/java")] { + s.languages.java.jdtls_jdk = path.to_string_lossy().into_owned(); + assert_eq!(jdtls_jdk(tmp.path(), &s).unwrap().unwrap().home, dunce::canonicalize(&home).unwrap()); + } + std::fs::write(home.join("bin/java"), "#!/bin/sh\nexit 126\n").unwrap(); + let error = jdtls_jdk(tmp.path(), &s).unwrap_err(); + assert!(error.contains(home.to_string_lossy().as_ref()) && error.contains("cannot run"), "{error}"); } #[test] diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index 71f024e3f..e16d65813 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -1,4 +1,5 @@ { + "The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.": "The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.", "Input queue is full (4 MiB or 1024 writes); input was not sent.": "Input queue is full (4 MiB or 1024 writes); input was not sent.", "Cannot write to terminal": "Cannot write to terminal", "Docker": "Docker", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 6cd919a81..21455ff0a 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -1,4 +1,5 @@ { + "The language server Java executable ({path}) cannot run on this machine. Choose a working JDK in {setting}, or set it to Auto.": "语言服务器的 Java 可执行文件({path})无法在此机器上运行。请在 {setting} 中选择可正常运行的 JDK,或设为自动。", "Input queue is full (4 MiB or 1024 writes); input was not sent.": "输入队列已满(4 MiB 或 1024 次写入);本次输入未发送。", "Cannot write to terminal": "无法写入终端", "Docker": "Docker",