mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
fix(java): run the language server on a full JDK
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
//! Capabilities needed by the JVM hosting Java language-server bundles.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// jdtls' javac bundle, java-debug / hot code replace, and Lombok / Gradle tooling.
|
||||
const BUNDLE_MODULES: &[&str] = &["jdk.compiler", "jdk.jdi", "jdk.unsupported"];
|
||||
|
||||
/// Require a full JDK, including the modules used by jdtls and its bundles.
|
||||
/// Temurin JREs can list the build JDK's modules in `release` even when the runtime
|
||||
/// image omits them, so module metadata alone does not establish suitability.
|
||||
pub fn supports_java_bundles(home: &Path) -> bool {
|
||||
let Ok(release) = std::fs::read_to_string(home.join("release")) else { return false };
|
||||
let property =
|
||||
|key: &str| release.lines().find_map(|line| line.trim().strip_prefix(key)).map(|v| v.trim().trim_matches('"'));
|
||||
let javac = if cfg!(windows) { "javac.exe" } else { "javac" };
|
||||
if property("IMAGE_TYPE=").is_some_and(|image| image.eq_ignore_ascii_case("jre"))
|
||||
|| !home.join("bin").join(javac).is_file()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
match property("MODULES=") {
|
||||
Some(modules) => BUNDLE_MODULES.iter().all(|required| modules.split_whitespace().any(|m| m == *required)),
|
||||
None => BUNDLE_MODULES.iter().all(|m| home.join("jmods").join(format!("{m}.jmod")).is_file()),
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod ids;
|
||||
pub mod ignored;
|
||||
pub mod inspection_settings;
|
||||
pub mod ipc_path;
|
||||
pub mod java_runtime;
|
||||
pub mod jsonc;
|
||||
pub mod output_encoding;
|
||||
pub mod process;
|
||||
|
||||
@@ -88,6 +88,9 @@ pub fn project_jdk(root: &Path, s: &Settings) -> Option<crate::java::Jdk> {
|
||||
pub fn jdtls_jdk(root: &Path, s: &Settings) -> Result<Option<crate::java::Jdk>, String> {
|
||||
match jdk_setting(root, &s.languages.java.jdtls_jdk) {
|
||||
None => Ok(None),
|
||||
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(Err(p)) => Err(format!(
|
||||
"The language server JDK {} is not a JDK home (Settings › Languages & Frameworks › Java).",
|
||||
|
||||
+118
-31
@@ -16,6 +16,13 @@ pub struct Jdk {
|
||||
pub home: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn server_jdk_incomplete_message(jdk: &Jdk, min_java: u32) -> String {
|
||||
format!(
|
||||
"The language server JDK ({}) must be a full JDK with jdk.compiler, jdk.jdi and jdk.unsupported for Java language support and debugging. jdtls needs JDK {min_java} or newer. Download JDK {min_java}, choose a full JDK in {SERVER_JDK_SETTING}, or set it to Auto.",
|
||||
jdk.home.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// `JAVA_VERSION="21.0.3"` (or `"1.8.0_372"`) from a JDK's `release` file.
|
||||
pub fn parse_release(release: &str) -> Option<u32> {
|
||||
let line = release.lines().find_map(|l| l.trim().strip_prefix("JAVA_VERSION="))?;
|
||||
@@ -199,7 +206,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)
|
||||
jdks.iter().find(|j| j.major >= min_java && ide_core::java_runtime::supports_java_bundles(&j.home))
|
||||
}
|
||||
|
||||
/// The Java version the jdtls that `cfg` launches needs: the highest any of its
|
||||
@@ -421,6 +428,9 @@ pub fn configure_jdtls(
|
||||
if !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),
|
||||
None => jdtls_runtime(jdks, min_java).ok_or_else(|| missing_runtime_message(jdks, min_java)),
|
||||
};
|
||||
@@ -430,6 +440,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)
|
||||
{
|
||||
cfg.launch_error = Some(server_jdk_incomplete_message(&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))) {
|
||||
@@ -573,6 +587,19 @@ fn parse_java_version(v: &str) -> Option<u32> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_jdk(root: &Path, major: u32, name: &str) -> Jdk {
|
||||
let home = root.join(name.trim_start_matches('/'));
|
||||
std::fs::create_dir_all(home.join("bin")).unwrap();
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
format!("JAVA_VERSION=\"{major}\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n"),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&home), "").unwrap();
|
||||
std::fs::write(home.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
Jdk { major, home }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmanaged_source_models_have_separate_jdtls_workspaces() {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
@@ -599,8 +626,13 @@ mod tests {
|
||||
let project_root = data.path().join("tools/jdks");
|
||||
let project_home = project_root.join("temurin-21");
|
||||
std::fs::create_dir_all(project_home.join("bin")).unwrap();
|
||||
std::fs::write(project_home.join("release"), "JAVA_VERSION=\"21.0.1\"\n").unwrap();
|
||||
std::fs::write(
|
||||
project_home.join("release"),
|
||||
"JAVA_VERSION=\"21.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&project_home), "").unwrap();
|
||||
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::<Vec<_>>();
|
||||
let installed = discover();
|
||||
@@ -608,8 +640,13 @@ mod tests {
|
||||
assert!(jdtls_runtime(&runtime_candidates(&installed, data.path()), 25).is_none());
|
||||
let home = language_server_runtimes_dir(data.path()).join("temurin-25");
|
||||
std::fs::create_dir_all(home.join("bin")).unwrap();
|
||||
std::fs::write(home.join("release"), "JAVA_VERSION=\"25.0.1\"\n").unwrap();
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
"JAVA_VERSION=\"25.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(java_executable(&home), "").unwrap();
|
||||
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);
|
||||
assert_eq!(jdtls_runtime(&candidates, 21), Some(&project));
|
||||
@@ -628,11 +665,47 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jdtls_rejects_jre_for_auto_explicit_and_managed_runtimes() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let jre = test_jdk(tmp.path(), 25, "jre");
|
||||
std::fs::write(jre.home.join("release"), "JAVA_VERSION=\"25\"\nMODULES=\"java.base java.compiler\"\n").unwrap();
|
||||
let jdk = test_jdk(tmp.path(), 25, "jdk");
|
||||
let installed = [jre.clone(), jdk.clone()];
|
||||
assert_eq!(jdtls_runtime(&installed, 25), Some(&jdk));
|
||||
assert!(jdtls_runtime(std::slice::from_ref(&jre), 25).is_none());
|
||||
let managed = language_server_runtimes_dir(tmp.path()).join("temurin-25");
|
||||
std::fs::create_dir_all(managed.parent().unwrap()).unwrap();
|
||||
std::fs::rename(&jre.home, &managed).unwrap();
|
||||
assert!(jdtls_runtime(&runtime_candidates(&[], tmp.path()), 25).is_none());
|
||||
let jre = jdk_at(&managed).unwrap();
|
||||
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(), jre.home.to_string_lossy().into_owned());
|
||||
}
|
||||
configure_jdtls(&mut cfg, tmp.path(), tmp.path(), &installed[1..], None, Some(&jre), 25, &[], None);
|
||||
let err = cfg.launch_error.unwrap();
|
||||
assert!(
|
||||
err.contains("full JDK with jdk.compiler, jdk.jdi and jdk.unsupported")
|
||||
&& err.contains("Download JDK 25")
|
||||
&& err.contains(SERVER_JDK_SETTING),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
std::fs::write(
|
||||
managed.join("release"),
|
||||
"JAVA_VERSION=\"25\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(jdtls_runtime(&runtime_candidates(&[], tmp.path()), 25).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_compiler_default_matches_project_pick_with_duplicate_versions() {
|
||||
let installed =
|
||||
[Jdk { major: 21, home: PathBuf::from("/jdk21a") }, Jdk { major: 21, home: PathBuf::from("/jdk21b") }];
|
||||
let server = Jdk { major: 25, home: PathBuf::from("/custom-server-runtime") };
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
let installed = [test_jdk(fixtures.path(), 21, "/jdk21a"), test_jdk(fixtures.path(), 21, "/jdk21b")];
|
||||
let server = test_jdk(fixtures.path(), 25, "/custom-server-runtime");
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = 'jdtls'\ncommand = 'jdtls'").unwrap();
|
||||
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &installed, None, Some(&server), 25, &[], None);
|
||||
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
||||
@@ -642,7 +715,7 @@ mod tests {
|
||||
"name": "JavaSE-21", "path": pick_jdk(&installed, None).unwrap().home, "default": true
|
||||
}])
|
||||
);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], "/custom-server-runtime");
|
||||
assert_eq!(cfg.env["JAVA_HOME"], fixtures.path().join("custom-server-runtime").to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -655,15 +728,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn jdtls_workspace_and_runtimes() {
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
if cfg!(windows) {
|
||||
return; // Unix paths and shells
|
||||
}
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap();
|
||||
let jdks = [
|
||||
Jdk { major: 25, home: PathBuf::from("/jdk25") },
|
||||
Jdk { major: 21, home: PathBuf::from("/jdk21a") },
|
||||
Jdk { major: 21, home: PathBuf::from("/jdk21b") },
|
||||
Jdk { major: 8, home: PathBuf::from("/jdk8") },
|
||||
test_jdk(fixtures.path(), 25, "/jdk25"),
|
||||
test_jdk(fixtures.path(), 21, "/jdk21a"),
|
||||
test_jdk(fixtures.path(), 21, "/jdk21b"),
|
||||
test_jdk(fixtures.path(), 8, "/jdk8"),
|
||||
];
|
||||
let bundles = [PathBuf::from("/tools/java-debug/bin/com.microsoft.java.debug.plugin.jar")];
|
||||
configure_jdtls(
|
||||
@@ -690,11 +764,15 @@ mod tests {
|
||||
);
|
||||
let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap();
|
||||
let rt = &opts["settings"]["java"]["configuration"]["runtimes"];
|
||||
assert_eq!(rt[0], json!({ "name": "JavaSE-1.8", "path": "/jdk8" }));
|
||||
assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": "/jdk21a" }));
|
||||
assert_eq!(rt[2], json!({ "name": "JavaSE-25", "path": "/jdk25", "default": true }));
|
||||
assert_eq!(rt[0], json!({ "name": "JavaSE-1.8", "path": fixtures.path().join("jdk8") }));
|
||||
assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": fixtures.path().join("jdk21a") }));
|
||||
assert_eq!(rt[2], json!({ "name": "JavaSE-25", "path": fixtures.path().join("jdk25"), "default": true }));
|
||||
assert_eq!(opts["bundles"], json!(["/tools/java-debug/bin/com.microsoft.java.debug.plugin.jar"]));
|
||||
assert_eq!(cfg.env["JAVA_HOME"], "/jdk25", "first JDK new enough to run jdtls");
|
||||
assert_eq!(
|
||||
cfg.env["JAVA_HOME"],
|
||||
fixtures.path().join("jdk25").to_string_lossy(),
|
||||
"first JDK new enough to run jdtls"
|
||||
);
|
||||
assert_eq!(cfg.launch_error, None);
|
||||
|
||||
// Deterministic (reconfigure compares configs) and idempotent.
|
||||
@@ -713,7 +791,7 @@ mod tests {
|
||||
assert_eq!(cfg, before);
|
||||
|
||||
// The project JDK is the default runtime, and runs the server when new enough.
|
||||
let project = Jdk { major: 21, home: PathBuf::from("/jdk21b") };
|
||||
let project = test_jdk(fixtures.path(), 21, "/jdk21b");
|
||||
let mut cfg: LanguageServerConfig = toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap();
|
||||
configure_jdtls(
|
||||
&mut cfg,
|
||||
@@ -728,8 +806,8 @@ mod tests {
|
||||
);
|
||||
let opts = serde_json::to_value(cfg.initialization_options.clone().unwrap()).unwrap();
|
||||
let rt = &opts["settings"]["java"]["configuration"]["runtimes"];
|
||||
assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": "/jdk21b", "default": true }));
|
||||
assert_eq!(cfg.env["JAVA_HOME"], "/jdk21b");
|
||||
assert_eq!(rt[1], json!({ "name": "JavaSE-21", "path": fixtures.path().join("jdk21b"), "default": true }));
|
||||
assert_eq!(cfg.env["JAVA_HOME"], fixtures.path().join("jdk21b").to_string_lossy());
|
||||
|
||||
let mut other: LanguageServerConfig = toml::from_str("name = \"pyright\"\ncommand = \"x\"").unwrap();
|
||||
configure_jdtls(
|
||||
@@ -836,8 +914,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn jdtls_needs_a_new_enough_jdk_to_run() {
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };
|
||||
let old = [Jdk { major: 17, home: PathBuf::from("/jdk17") }, Jdk { major: 8, home: PathBuf::from("/jdk8") }];
|
||||
let old = [test_jdk(fixtures.path(), 17, "/jdk17"), test_jdk(fixtures.path(), 8, "/jdk8")];
|
||||
let mut cfg = jdtls();
|
||||
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, None, JDTLS_MIN_JAVA, &[], None);
|
||||
assert!(!cfg.env.contains_key("JAVA_HOME"));
|
||||
@@ -856,7 +935,7 @@ mod tests {
|
||||
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &old, None, None, JDTLS_MIN_JAVA, &[], None);
|
||||
assert_eq!((cfg.env["JAVA_HOME"].as_str(), cfg.launch_error), ("/custom", None));
|
||||
|
||||
let mixed = [old[0].clone(), Jdk { major: 21, home: PathBuf::from("/jdk21") }];
|
||||
let mixed = [old[0].clone(), test_jdk(fixtures.path(), 21, "/jdk21")];
|
||||
assert_eq!(jdtls_runtime(&mixed, 21).unwrap().major, 21);
|
||||
|
||||
// A jdtls whose bundles need 25 doesn't start on 21, and says what it needs.
|
||||
@@ -869,11 +948,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn issue_49_jdtls_needs_25_with_8_17_21_installed() {
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };
|
||||
let installed = [
|
||||
Jdk { major: 21, home: PathBuf::from("/jdk21") },
|
||||
Jdk { major: 17, home: PathBuf::from("/jdk17") },
|
||||
Jdk { major: 8, home: PathBuf::from("/jdk8") },
|
||||
test_jdk(fixtures.path(), 21, "/jdk21"),
|
||||
test_jdk(fixtures.path(), 17, "/jdk17"),
|
||||
test_jdk(fixtures.path(), 8, "/jdk8"),
|
||||
];
|
||||
let mut cfg = jdtls();
|
||||
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &installed, None, None, 25, &[], None);
|
||||
@@ -882,12 +962,11 @@ mod tests {
|
||||
assert!(err.contains("Download JDK 25"), "{err}");
|
||||
|
||||
// Once a JDK 25 has been downloaded, Auto runs jdtls on it.
|
||||
let downloaded =
|
||||
[installed.as_slice(), &[Jdk { major: 25, home: PathBuf::from("/tools/jdks/temurin-25") }]].concat();
|
||||
let downloaded = [installed.as_slice(), &[test_jdk(fixtures.path(), 25, "/tools/jdks/temurin-25")]].concat();
|
||||
let mut cfg = jdtls();
|
||||
configure_jdtls(&mut cfg, Path::new("/p"), Path::new("/data"), &downloaded, None, None, 25, &[], None);
|
||||
assert_eq!(cfg.launch_error, None);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], Path::new("/tools/jdks/temurin-25").to_string_lossy());
|
||||
assert_eq!(cfg.env["JAVA_HOME"], fixtures.path().join("tools/jdks/temurin-25").to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -924,8 +1003,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn jdtls_runs_on_the_chosen_server_jdk() {
|
||||
let fixtures = tempfile::tempdir().unwrap();
|
||||
let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };
|
||||
let jdk = |major: u32, home: &str| Jdk { major, home: PathBuf::from(home) };
|
||||
let jdk = |major: u32, home: &str| test_jdk(fixtures.path(), major, home);
|
||||
let installed = [jdk(25, "/jdk25"), jdk(21, "/jdk21"), jdk(17, "/jdk17")];
|
||||
let project = jdk(17, "/jdk17");
|
||||
let run = |server: Option<&Jdk>, min_java: u32| {
|
||||
@@ -946,18 +1026,25 @@ mod tests {
|
||||
|
||||
// Project on JDK 17, server on the chosen 21: each keeps its own JDK.
|
||||
let cfg = run(Some(&jdk(21, "/jdk21")), JDTLS_MIN_JAVA);
|
||||
assert_eq!((cfg.env["JAVA_HOME"].as_str(), cfg.launch_error), ("/jdk21", None));
|
||||
assert_eq!(
|
||||
(cfg.env["JAVA_HOME"].clone(), cfg.launch_error),
|
||||
(fixtures.path().join("jdk21").to_string_lossy().into_owned(), None)
|
||||
);
|
||||
let opts = serde_json::to_value(cfg.initialization_options.unwrap()).unwrap();
|
||||
let rt = &opts["settings"]["java"]["configuration"]["runtimes"];
|
||||
assert_eq!(rt[0], json!({ "name": "JavaSE-17", "path": "/jdk17", "default": true }), "still compiles on 17");
|
||||
assert_eq!(
|
||||
rt[0],
|
||||
json!({ "name": "JavaSE-17", "path": fixtures.path().join("jdk17"), "default": true }),
|
||||
"still compiles on 17"
|
||||
);
|
||||
|
||||
// Not set: the first installed JDK new enough, as before.
|
||||
assert_eq!(run(None, JDTLS_MIN_JAVA).env["JAVA_HOME"], "/jdk25");
|
||||
assert_eq!(run(None, JDTLS_MIN_JAVA).env["JAVA_HOME"], fixtures.path().join("jdk25").to_string_lossy());
|
||||
|
||||
// A chosen JDK outside the scanned locations.
|
||||
let own = jdk(25, "/opt/own-jdk");
|
||||
let cfg = run(Some(&own), 25);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], "/opt/own-jdk");
|
||||
assert_eq!(cfg.env["JAVA_HOME"], fixtures.path().join("opt/own-jdk").to_string_lossy());
|
||||
|
||||
// Too old for this jdtls: the user's choice is not overridden, it is explained.
|
||||
let cfg = run(Some(&jdk(21, "/jdk21")), 25);
|
||||
|
||||
@@ -249,6 +249,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)));
|
||||
return;
|
||||
}
|
||||
cfg.args = java_args(&install, jdk.major, &cfg.args);
|
||||
cfg.command = java.to_string_lossy().into_owned();
|
||||
}
|
||||
@@ -465,7 +469,12 @@ mod tests {
|
||||
std::fs::set_permissions(&script, std::os::unix::fs::PermissionsExt::from_mode(0o755)).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\"\n").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/javac"), "").unwrap();
|
||||
std::fs::write(jdk.join("bin/java"), "").unwrap();
|
||||
|
||||
let cfg = |command: &str| -> LanguageServerConfig {
|
||||
@@ -486,6 +495,15 @@ mod tests {
|
||||
assert!(direct.args[jar + 1].ends_with("org.eclipse.equinox.launcher_1.7.0.jar"));
|
||||
assert_eq!(direct.args[jar + 2..], strings(&["-data", "/ws"]));
|
||||
|
||||
let jre = dir.path().join("jre-25");
|
||||
std::fs::create_dir_all(jre.join("bin")).unwrap();
|
||||
std::fs::write(jre.join("bin/java"), "").unwrap();
|
||||
std::fs::write(jre.join("release"), "JAVA_VERSION=\"25\"\nIMAGE_TYPE=\"JRE\"\n").unwrap();
|
||||
let mut overridden = cfg("jdtls");
|
||||
overridden.args.push(format!("--java-executable={}", jre.join("bin/java").display()));
|
||||
launch_directly(&mut overridden);
|
||||
assert!(overridden.launch_error.unwrap().contains("jdk.jdi"));
|
||||
|
||||
// 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))
|
||||
|
||||
@@ -166,7 +166,12 @@ fn run_configs_use_selected_interpreter_and_package_manager() {
|
||||
fn fake_jdk(dir: &Path) {
|
||||
std::fs::create_dir_all(dir.join("bin")).unwrap();
|
||||
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
|
||||
std::fs::write(dir.join("release"), "JAVA_VERSION=\"17.0.9\"\n").unwrap();
|
||||
std::fs::write(
|
||||
dir.join("release"),
|
||||
"JAVA_VERSION=\"17.0.9\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dir.join("bin").join(crate::exe("javac")), "").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -307,6 +312,12 @@ fn jdtls_jdk_setting() {
|
||||
assert_eq!(jdtls_jdk(tmp.path(), &s), Ok(None));
|
||||
s.languages.java.jdtls_jdk = "jdk17/bin".into();
|
||||
assert_eq!(jdtls_jdk(tmp.path(), &s).unwrap().map(|j| j.major), Some(17));
|
||||
std::fs::write(
|
||||
tmp.path().join("jdk17/release"),
|
||||
"JAVA_VERSION=\"25\"\nIMAGE_TYPE=\"JRE\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(jdtls_jdk(tmp.path(), &s).unwrap_err().contains("full JDK"));
|
||||
// A chosen path that is not a JDK is an error, not a silent fallback.
|
||||
s.languages.java.jdtls_jdk = "missing".into();
|
||||
assert!(jdtls_jdk(tmp.path(), &s).unwrap_err().contains("is not a JDK home"));
|
||||
|
||||
+57
-49
@@ -108,26 +108,7 @@ fn adoptium_arch(arch: Arch) -> &'static str {
|
||||
|
||||
/// The latest Temurin JDK build of Java `feature` for `platform`.
|
||||
pub fn adoptium_assets_url(feature: u32, platform: Platform) -> String {
|
||||
assets_url(feature, platform, ImageType::Jdk)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ImageType {
|
||||
Jdk,
|
||||
Jre,
|
||||
}
|
||||
|
||||
impl ImageType {
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Jdk => "jdk",
|
||||
Self::Jre => "jre",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assets_url(feature: u32, platform: Platform, image: ImageType) -> String {
|
||||
let image = image.name();
|
||||
let image = "jdk";
|
||||
format!(
|
||||
"{ADOPTIUM_API}/assets/latest/{feature}/hotspot?architecture={}&image_type={image}&os={}&vendor=eclipse",
|
||||
adoptium_arch(platform.arch),
|
||||
@@ -190,16 +171,12 @@ impl AssetVersion {
|
||||
/// The JDK archive (`.tar.gz` on macOS / Linux, `.zip` on Windows) in an Adoptium
|
||||
/// `assets/latest` response; `None` when there is no build for the platform.
|
||||
pub fn adoptium_pick(json: &str, platform: Platform) -> IdeResult<Option<(Download, u32)>> {
|
||||
pick_image(json, platform, ImageType::Jdk)
|
||||
}
|
||||
|
||||
fn pick_image(json: &str, platform: Platform, image: ImageType) -> IdeResult<Option<(Download, u32)>> {
|
||||
let assets: Vec<Asset> =
|
||||
serde_json::from_str(json).map_err(|e| proto(format!("unexpected Adoptium response: {e}")))?;
|
||||
let ext = if platform.os == Os::Windows { ".zip" } else { ".tar.gz" };
|
||||
let os = adoptium_os(platform.os);
|
||||
let found = assets.into_iter().find(|a| {
|
||||
a.binary.image_type.as_deref().is_none_or(|t| t == image.name())
|
||||
a.binary.image_type.as_deref().is_none_or(|t| t == "jdk")
|
||||
&& a.binary.heap_size.as_deref().is_none_or(|h| h == "normal")
|
||||
&& a.binary.os.as_deref().is_none_or(|o| o == os)
|
||||
&& a.binary.package.name.ends_with(ext)
|
||||
@@ -249,6 +226,10 @@ pub fn find_jdk_home(dir: &Path, platform: Platform) -> Option<PathBuf> {
|
||||
subdirs.iter().flat_map(|d| candidates(d)).find(|h| is_jdk_home(h, platform))
|
||||
}
|
||||
|
||||
fn usable_java_home(dir: &Path, platform: Platform, runtime: bool) -> Option<PathBuf> {
|
||||
find_jdk_home(dir, platform).filter(|home| !runtime || ide_core::java_runtime::supports_java_bundles(home))
|
||||
}
|
||||
|
||||
/// The directory to move into place: the archive's single top-level folder
|
||||
/// holding the JDK, else everything that was unpacked.
|
||||
fn unpacked_root(pkg: &Path, home: &Path) -> PathBuf {
|
||||
@@ -283,8 +264,8 @@ impl Tools {
|
||||
self.download_java(vendor, feature, location, false, cancel, emit)
|
||||
}
|
||||
|
||||
/// Downloads a runtime for a language server, preferring a JRE. Only a
|
||||
/// platform without a JRE build falls back to a full JDK.
|
||||
/// Downloads a full JDK for a language server and its bundles (java-debug
|
||||
/// needs `jdk.jdi`). Kept outside project JDK discovery roots.
|
||||
pub fn download_java_runtime(
|
||||
&self,
|
||||
feature: u32,
|
||||
@@ -310,19 +291,8 @@ impl Tools {
|
||||
let http = self.http()?;
|
||||
let location = location.map(Path::to_path_buf).unwrap_or_else(|| self.root.join("jdks"));
|
||||
emit(InstallEvent::Line { text: format!("Resolving the latest Eclipse Temurin {feature}…") });
|
||||
let mut picked = if runtime {
|
||||
pick_image(
|
||||
&http.get_text_or_empty_on_404(&assets_url(feature, self.platform, ImageType::Jre), cancel)?,
|
||||
self.platform,
|
||||
ImageType::Jre,
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if picked.is_none() {
|
||||
picked =
|
||||
adoptium_pick(&http.get_text(&adoptium_assets_url(feature, self.platform), cancel)?, self.platform)?;
|
||||
}
|
||||
let mut picked =
|
||||
adoptium_pick(&http.get_text(&adoptium_assets_url(feature, self.platform), cancel)?, self.platform)?;
|
||||
if picked.is_none() && matches!(self.platform.os, Os::Mac | Os::Windows) && self.platform.arch == Arch::Arm64 {
|
||||
// Older JDKs may need the OS-provided x64 emulation.
|
||||
let intel = Platform { os: self.platform.os, arch: Arch::X64 };
|
||||
@@ -336,7 +306,7 @@ impl Tools {
|
||||
})?;
|
||||
let target = location.join(jdk_dir_name(vendor, &dl.version));
|
||||
let done = |home: PathBuf| DownloadedJdk { home: home.into(), version: dl.version.clone(), major };
|
||||
if let Some(home) = find_jdk_home(&target, self.platform) {
|
||||
if let Some(home) = usable_java_home(&target, self.platform, runtime) {
|
||||
emit(InstallEvent::Line { text: format!("{} is already installed", target.display()) });
|
||||
return Ok(done(home));
|
||||
}
|
||||
@@ -351,7 +321,7 @@ impl Tools {
|
||||
emit(InstallEvent::Line { text: "Extracting…".into() });
|
||||
let pkg = staging.join("pkg");
|
||||
archive::extract(&archive, archive::ArchiveKind::from_name(&dl.name), &pkg, "java", cancel)?;
|
||||
let home = find_jdk_home(&pkg, self.platform)
|
||||
let home = usable_java_home(&pkg, self.platform, runtime)
|
||||
.ok_or_else(|| IdeError::new(ErrorKind::Io, format!("no JDK found in {}", dl.name)))?;
|
||||
// Last point to stop: from here the JDK replaces what is at `target`.
|
||||
if cancel.is_cancelled() {
|
||||
@@ -362,7 +332,7 @@ impl Tools {
|
||||
}
|
||||
std::fs::rename(unpacked_root(&pkg, &home), &target)?;
|
||||
clear_quarantine(&target);
|
||||
find_jdk_home(&target, self.platform)
|
||||
usable_java_home(&target, self.platform, runtime)
|
||||
.ok_or_else(|| IdeError::new(ErrorKind::Io, format!("no JDK found in {}", target.display())))
|
||||
})();
|
||||
let _ = std::fs::remove_dir_all(&staging);
|
||||
@@ -437,18 +407,56 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_assets_use_jre_without_changing_jdk_downloads() {
|
||||
fn runtime_assets_require_jdk_images() {
|
||||
for platform in [MAC_ARM, LINUX_X64, WIN_X64] {
|
||||
assert!(assets_url(25, platform, ImageType::Jre).contains("image_type=jre"));
|
||||
assert!(adoptium_assets_url(25, platform).contains("image_type=jdk"));
|
||||
let ext = if platform.os == Os::Windows { "zip" } else { "tar.gz" };
|
||||
let jdk = asset(&format!("java.{ext}"), adoptium_os(platform.os), "normal", "");
|
||||
let jre = jdk.replace("\"image_type\":\"jdk\"", "\"image_type\":\"jre\"");
|
||||
assert!(pick_image(&format!("[{jre}]"), platform, ImageType::Jre).unwrap().is_some());
|
||||
assert!(adoptium_pick(&format!("[{jre},{jdk}]"), platform).unwrap().is_some());
|
||||
assert!(adoptium_pick(&format!("[{jre}]"), platform).unwrap().is_none());
|
||||
assert!(pick_image(&format!("[{jdk}]"), platform, ImageType::Jre).unwrap().is_none());
|
||||
assert!(pick_image("[]", platform, ImageType::Jre).unwrap().is_none());
|
||||
assert!(pick_image("offline", platform, ImageType::Jre).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_runtime_is_replaced_even_at_the_same_version() {
|
||||
for platform in [MAC_ARM, LINUX_X64, WIN_X64] {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let target = tmp.path().join(jdk_dir_name(TEMURIN, "25.0.1"));
|
||||
let home = if platform.os == Os::Mac { target.join("Contents/Home") } else { target.clone() };
|
||||
fake_jdk(&home, platform);
|
||||
let javac = home.join("bin").join(if cfg!(windows) { "javac.exe" } else { "javac" });
|
||||
std::fs::write(&javac, "").unwrap();
|
||||
for modules in [None, Some("java.base java.compiler jdk.jdix"), Some("java.base jdk.jdi.agent")] {
|
||||
let line = modules.map(|m| format!("MODULES=\"{m}\"\n")).unwrap_or_default();
|
||||
std::fs::write(home.join("release"), format!("JAVA_VERSION=\"25.0.1\"\n{line}")).unwrap();
|
||||
assert!(usable_java_home(&target, platform, true).is_none(), "{modules:?}");
|
||||
assert_eq!(usable_java_home(&target, platform, false), Some(home.clone()));
|
||||
}
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
"JAVA_VERSION=\"25.0.1\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(usable_java_home(&target, platform, true), Some(home.clone()));
|
||||
std::fs::write(home.join("release"), "JAVA_VERSION=\"25.0.1\"\n").unwrap();
|
||||
std::fs::create_dir_all(home.join("jmods")).unwrap();
|
||||
for module in ["jdk.compiler", "jdk.jdi", "jdk.unsupported"] {
|
||||
std::fs::write(home.join("jmods").join(format!("{module}.jmod")), "").unwrap();
|
||||
}
|
||||
assert_eq!(usable_java_home(&target, platform, true), Some(home.clone()));
|
||||
std::fs::write(
|
||||
home.join("release"),
|
||||
"IMAGE_TYPE=\"JRE\"\nMODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(usable_java_home(&target, platform, true).is_none(), "Temurin JRE release can list absent modules");
|
||||
std::fs::write(home.join("release"), "MODULES=\"java.base jdk.compiler jdk.jdi jdk.unsupported\"\n")
|
||||
.unwrap();
|
||||
std::fs::remove_file(javac).unwrap();
|
||||
assert!(usable_java_home(&target, platform, true).is_none(), "custom runtime without compiler");
|
||||
std::fs::write(home.join("release"), "MODULES=\"java.base\"\n").unwrap();
|
||||
assert!(usable_java_home(&target, platform, true).is_none(), "release describes the runtime image");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"The language server JDK ({path}) must be a full JDK with jdk.compiler, jdk.jdi and jdk.unsupported for Java language support and debugging. jdtls needs JDK {version} or newer. Download JDK {version}, choose a full JDK in {setting}, or set it to Auto.": "语言服务器 JDK({path})必须是包含 jdk.compiler、jdk.jdi 和 jdk.unsupported 的完整 JDK,才能支持 Java 语言功能和调试。jdtls 需要 JDK {version} 或更高版本。请下载 JDK {version},在 {setting} 中选择完整 JDK,或设为自动。",
|
||||
"Apple's clangd does not run clang-tidy, so checks from .clang-tidy are not shown. Install clangd (LLVM) to get them.": "Apple 的 clangd 不运行 clang-tidy,因此不会显示 .clang-tidy 中配置的检查。请安装 clangd(LLVM)以启用这些检查。",
|
||||
"Newer dependency version available": "有更新的依赖版本可用",
|
||||
"Reports newer crate versions that Cargo.toml requirements do not already allow.": "报告 Cargo.toml 版本要求尚未允许的较新 crate 版本。",
|
||||
|
||||
Reference in New Issue
Block a user