дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(java): provision an isolated runtime for the Java language server (#370)
Recent Eclipse JDT LS releases need Java 25 to run, so projects on JDK 21 had no Java language support until the user downloaded a newer JDK by hand. When JDT LS is about to start, no installed JDK is new enough, and no language-server JDK is set explicitly, the IDE now downloads the Temurin JRE of the required version (about 50 MB, falling back to the JDK image where no JRE build exists) with a cancellable background task, then starts the server on it. The runtime lives in its own folder under the app data directory, outside the roots scanned for project JDKs, so the project JDK, run configurations and JDT LS's project runtimes are unchanged. One download runs at a time across windows and is reused; a failed attempt falls back to the existing manual Download JDK action and is not retried until the next start. Fixes #258 Refs #271 未在 Windows / Linux 实机验证。
Цей коміт міститься в:
1 джерело
0329678124
коміт
9575500dd6
10 files changed
+472
-22
No files matched your search
@@ -0,0 +1,183 @@
|
||||
//! Automatic runtime provisioning belongs to the host, not SDK discovery.
|
||||
use ide_core::{CancellationToken, IdeEvent, SharedSink, TaskId};
|
||||
use ide_language::LanguageServerConfig;
|
||||
use ide_sdk::java::{jdtls_jdks, jdtls_min_java, jdtls_runtime, language_server_runtimes_dir};
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
busy: bool,
|
||||
sinks: Vec<Weak<dyn ide_core::EventSink>>,
|
||||
attempted: HashSet<(PathBuf, u32)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Provisioner {
|
||||
state: Mutex<State>,
|
||||
changed: Condvar,
|
||||
}
|
||||
|
||||
static PROVISIONER: LazyLock<Provisioner> = LazyLock::new(Provisioner::default);
|
||||
|
||||
fn needs_download(explicit: bool, suitable: bool, attempted: bool) -> bool {
|
||||
!explicit && !suitable && !attempted
|
||||
}
|
||||
|
||||
impl Provisioner {
|
||||
fn subscribe(&self, sink: &SharedSink) {
|
||||
let mut state = self.state.lock();
|
||||
state.sinks.retain(|s| s.strong_count() > 0);
|
||||
let weak = Arc::downgrade(sink);
|
||||
if !state.sinks.iter().any(|s| s.ptr_eq(&weak)) {
|
||||
state.sinks.push(weak);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&self, event: IdeEvent) {
|
||||
let sinks: Vec<_> = self.state.lock().sinks.iter().filter_map(Weak::upgrade).collect();
|
||||
for sink in sinks {
|
||||
sink.emit(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self, key: (PathBuf, u32), cancel: &CancellationToken, work: impl FnOnce()) {
|
||||
let mut state = self.state.lock();
|
||||
while state.busy && !cancel.is_cancelled() {
|
||||
self.changed.wait_for(&mut state, Duration::from_millis(100));
|
||||
}
|
||||
if cancel.is_cancelled() || !state.attempted.insert(key) {
|
||||
return;
|
||||
}
|
||||
state.busy = true;
|
||||
drop(state);
|
||||
// Even a panicking installer must release waiters and retain the attempt.
|
||||
struct Flight<'a>(&'a Provisioner);
|
||||
impl Drop for Flight<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.state.lock().busy = false;
|
||||
self.0.changed.notify_all();
|
||||
}
|
||||
}
|
||||
let _flight = Flight(self);
|
||||
work();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(
|
||||
cfg: &LanguageServerConfig,
|
||||
data_dir: &Path,
|
||||
explicit: bool,
|
||||
sink: &SharedSink,
|
||||
launch_cancel: &CancellationToken,
|
||||
) {
|
||||
let min_java = jdtls_min_java(cfg);
|
||||
let jdks = jdtls_jdks(min_java, data_dir);
|
||||
// A launch environment override is explicit too, even if it is invalid.
|
||||
if !needs_download(explicit || cfg.env.contains_key("JAVA_HOME"), jdtls_runtime(&jdks, min_java).is_some(), false) {
|
||||
return;
|
||||
}
|
||||
PROVISIONER.subscribe(sink);
|
||||
PROVISIONER.run((data_dir.to_path_buf(), min_java), launch_cancel, || {
|
||||
if jdtls_runtime(&jdtls_jdks(min_java, data_dir), min_java).is_some() {
|
||||
return;
|
||||
}
|
||||
let id = TaskId::next();
|
||||
let title = format!("Downloading Java {min_java} language server runtime");
|
||||
// Closing one project must not cancel a download other projects await.
|
||||
let cancel = CancellationToken::new();
|
||||
let _handle = ide_core::task::on_cancel(id, {
|
||||
let cancel = cancel.clone();
|
||||
move || cancel.cancel()
|
||||
});
|
||||
let emit = |message, fraction, done| {
|
||||
PROVISIONER.emit(IdeEvent::Task { id, title: title.clone(), message, fraction, done, cancellable: !done });
|
||||
};
|
||||
emit(Some("Eclipse Temurin".into()), None, false);
|
||||
let result = ide_tools::Tools::new(data_dir).download_java_runtime(
|
||||
min_java,
|
||||
&language_server_runtimes_dir(data_dir),
|
||||
&cancel,
|
||||
&|event| match event {
|
||||
ide_tools::InstallEvent::Line { text } => emit(Some(text), None, false),
|
||||
ide_tools::InstallEvent::Progress { fraction, message } => {
|
||||
emit(Some(message), fraction.map(|f| f as f32), false)
|
||||
}
|
||||
},
|
||||
);
|
||||
emit(result.as_ref().err().map(ToString::to_string), Some(1.0), true);
|
||||
if let Err(error) = result {
|
||||
// The configuration hook retains the existing manual download action.
|
||||
tracing::warn!("automatic jdtls runtime download: {error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn download_decision() {
|
||||
assert!(needs_download(false, false, false));
|
||||
for (explicit, suitable, attempted) in [(true, false, false), (false, true, false), (false, false, true)] {
|
||||
assert!(!needs_download(explicit, suitable, attempted));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simultaneous_projects_share_one_attempt_and_failures_do_not_retry() {
|
||||
let provisioner = Provisioner::default();
|
||||
let calls = AtomicUsize::new(0);
|
||||
let barrier = std::sync::Barrier::new(8);
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..8 {
|
||||
scope.spawn(|| {
|
||||
barrier.wait();
|
||||
provisioner.run((PathBuf::from("/data"), 25), &CancellationToken::new(), || {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
provisioner.run((PathBuf::from("/data"), 25), &CancellationToken::new(), || panic!("retried"));
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_versions_are_still_single_flight() {
|
||||
let provisioner = Provisioner::default();
|
||||
let active = AtomicUsize::new(0);
|
||||
let calls = AtomicUsize::new(0);
|
||||
std::thread::scope(|scope| {
|
||||
for feature in [25, 26, 27] {
|
||||
let provisioner = &provisioner;
|
||||
let active = &active;
|
||||
let calls = &calls;
|
||||
scope.spawn(move || {
|
||||
provisioner.run((PathBuf::from("/data"), feature), &CancellationToken::new(), || {
|
||||
assert_eq!(active.fetch_add(1, Ordering::SeqCst), 0);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelled_waiter_does_not_consume_an_attempt() {
|
||||
let provisioner = Provisioner::default();
|
||||
let cancel = CancellationToken::new();
|
||||
cancel.cancel();
|
||||
provisioner.run((PathBuf::from("/data"), 25), &cancel, || panic!("cancelled"));
|
||||
assert!(provisioner.state.lock().attempted.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
//! runs one on a remote machine and serves it over JSON-RPC ([`rpc`]).
|
||||
|
||||
pub mod environment;
|
||||
mod java_runtime;
|
||||
pub mod notebook;
|
||||
pub mod ops;
|
||||
mod project;
|
||||
|
||||
@@ -126,7 +126,7 @@ impl Host {
|
||||
let bundles: Vec<PathBuf> =
|
||||
ide_tools::managed_bundle(&data_dir, "java-debug").into_iter().collect();
|
||||
let min_java = ide_sdk::java::jdtls_min_java(cfg);
|
||||
let jdks = ide_sdk::java::jdtls_jdks(min_java);
|
||||
let jdks = ide_sdk::java::jdtls_jdks(min_java, &data_dir);
|
||||
let lombok = ide_sdk::lombok::lombok_agent(
|
||||
server_root,
|
||||
ide_sdk::lombok::agent_in_args(cfg.args.iter().map(String::as_str)),
|
||||
@@ -158,6 +158,25 @@ impl Host {
|
||||
ide_sdk::apply::configure_maven_import(cfg, &root, &excluded_poms);
|
||||
}));
|
||||
}
|
||||
{
|
||||
let settings = settings.clone();
|
||||
let root = root.clone();
|
||||
let data_dir = self.data_dir.clone();
|
||||
let sink = sink.clone();
|
||||
lsp.set_prepare_hook(Box::new(move |cfg, _, cancel| {
|
||||
let effective = settings.effective();
|
||||
if cfg.name == "jdtls" {
|
||||
crate::java_runtime::prepare(
|
||||
cfg,
|
||||
&data_dir,
|
||||
!effective.languages.java.jdtls_jdk.trim().is_empty()
|
||||
|| ide_sdk::apply::jdtls_jdk(&root, &effective).is_err(),
|
||||
&sink,
|
||||
cancel,
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
{
|
||||
let settings = settings.clone();
|
||||
lsp.set_servers_hook(Box::new(move |language, defined| {
|
||||
|
||||
@@ -75,6 +75,8 @@ pub type ApplyEditHook = Arc<dyn Fn(WorkspaceEdit) -> bool + Send + Sync>;
|
||||
/// settings before each launch; receives the server's workspace root.
|
||||
pub type ConfigFn = dyn Fn(&mut LanguageServerConfig, &Path) + Send + Sync;
|
||||
pub type ConfigHook = Arc<ConfigFn>;
|
||||
/// Prepares dependencies on a blocking worker immediately before launching an installed server.
|
||||
pub type PrepareFn = dyn Fn(&LanguageServerConfig, &Path, &CancellationToken) + Send + Sync;
|
||||
/// The servers a language uses, from its definition's list (user settings
|
||||
/// disable some and add others).
|
||||
pub type ServersFn = dyn Fn(&LanguageId, &[LanguageServerConfig]) -> Vec<LanguageServerConfig> + Send + Sync;
|
||||
@@ -163,6 +165,7 @@ pub struct LspManager {
|
||||
docs: Mutex<HashMap<DocId, DocState>>,
|
||||
apply_edit_hook: RwLock<Option<ApplyEditHook>>,
|
||||
config_hook: RwLock<Option<ConfigHook>>,
|
||||
prepare_hook: RwLock<Option<Arc<PrepareFn>>>,
|
||||
servers_hook: RwLock<Option<Arc<ServersFn>>>,
|
||||
candidates_hook: RwLock<Option<Arc<CandidateFn>>>,
|
||||
shutting_down: AtomicBool,
|
||||
@@ -291,6 +294,7 @@ impl LspManager {
|
||||
docs: Mutex::new(HashMap::new()),
|
||||
apply_edit_hook: RwLock::new(None),
|
||||
config_hook: RwLock::new(None),
|
||||
prepare_hook: RwLock::new(None),
|
||||
servers_hook: RwLock::new(None),
|
||||
candidates_hook: RwLock::new(None),
|
||||
shutting_down: AtomicBool::new(false),
|
||||
@@ -408,7 +412,16 @@ impl LspManager {
|
||||
// The settings hook may touch the filesystem (e.g. scanning for JDKs); TCP
|
||||
// servers may take a while to start listening.
|
||||
let launched = tokio::task::spawn_blocking(move || {
|
||||
let cfg = me.configured(&tail);
|
||||
let mut cfg = me.configured(&tail);
|
||||
tail.set_effective_config(cfg.clone());
|
||||
let prepare = me.prepare_hook.read().clone();
|
||||
if !cancel.is_cancelled()
|
||||
&& me.launcher.available(&cfg, &tail.root)
|
||||
&& let Some(prepare) = prepare
|
||||
{
|
||||
prepare(&cfg, &tail.root, &cancel);
|
||||
cfg = me.configured(&tail);
|
||||
}
|
||||
tail.set_effective_config(cfg.clone());
|
||||
let root = tail.root.clone();
|
||||
if let Some(message) = &cfg.launch_error {
|
||||
@@ -1901,6 +1914,10 @@ impl LspManager {
|
||||
*self.config_hook.write() = Some(Arc::from(hook));
|
||||
}
|
||||
|
||||
pub fn set_prepare_hook(&self, hook: Box<PrepareFn>) {
|
||||
*self.prepare_hook.write() = Some(Arc::from(hook));
|
||||
}
|
||||
|
||||
/// Installed by `src-tauri` to apply the servers enabled / defined in settings.
|
||||
pub fn set_servers_hook(&self, hook: Box<ServersFn>) {
|
||||
*self.servers_hook.write() = Some(Arc::from(hook));
|
||||
|
||||
@@ -905,6 +905,38 @@ async fn launch_error_fails_without_starting_the_server() {
|
||||
assert!(!h.statuses("mock").contains(&ServerStatus::Crashed));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn preparation_refreshes_config_and_survives_unchanged_settings() {
|
||||
use std::sync::atomic::AtomicBool;
|
||||
let h = harness(&[("mock", incremental_caps())]);
|
||||
let prepared = Arc::new(AtomicBool::new(false));
|
||||
let started = Arc::new(AtomicUsize::new(0));
|
||||
let (release, wait) = std::sync::mpsc::channel();
|
||||
let wait = Mutex::new(wait);
|
||||
h.mgr.set_config_hook(Box::new({
|
||||
let prepared = prepared.clone();
|
||||
move |cfg, _| {
|
||||
cfg.launch_error = (!prepared.load(Ordering::SeqCst)).then(|| "needs runtime".into());
|
||||
}
|
||||
}));
|
||||
h.mgr.set_prepare_hook(Box::new({
|
||||
let started = started.clone();
|
||||
move |_, _, cancel| {
|
||||
started.fetch_add(1, Ordering::SeqCst);
|
||||
wait.lock().recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
assert!(!cancel.is_cancelled(), "unchanged settings cancelled preparation");
|
||||
prepared.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}));
|
||||
h.open("a.mk", "x");
|
||||
eventually(|| started.load(Ordering::SeqCst) > 0).await;
|
||||
h.mgr.reconfigure().await;
|
||||
release.send(()).unwrap();
|
||||
h.wait_status("mock", ServerStatus::Ready).await;
|
||||
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(h.launcher.state("mock").launches.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn settings_changes_reach_servers_without_restarts() {
|
||||
let h = harness(&[("mock", incremental_caps()), ("extra", incremental_caps())]);
|
||||
|
||||
@@ -44,6 +44,12 @@ fn children(dir: &Path) -> Vec<PathBuf> {
|
||||
out
|
||||
}
|
||||
|
||||
fn jdk_homes(root: &Path) -> impl Iterator<Item = PathBuf> {
|
||||
children(root)
|
||||
.into_iter()
|
||||
.flat_map(|dir| [dir.join("Contents/Home"), dir.join("libexec/openjdk.jdk/Contents/Home"), dir])
|
||||
}
|
||||
|
||||
/// Vendor directories JDK installers use on Windows, under `Program Files`
|
||||
/// and (per-user installs) `%LOCALAPPDATA%\Programs`.
|
||||
const WINDOWS_VENDORS: &[&str] =
|
||||
@@ -106,9 +112,7 @@ fn scan() -> Vec<Jdk> {
|
||||
if roots[..i].contains(root) {
|
||||
continue;
|
||||
}
|
||||
for dir in children(root) {
|
||||
homes.extend([dir.join("Contents/Home"), dir.join("libexec/openjdk.jdk/Contents/Home"), dir]);
|
||||
}
|
||||
homes.extend(jdk_homes(root));
|
||||
}
|
||||
// mise keeps a macOS JDK as its bundle (`<version>/Contents/Home`), asdf flattens it.
|
||||
for (dir, _) in ide_core::version_managers::installs(ide_core::version_managers::Tool::Java) {
|
||||
@@ -157,12 +161,24 @@ pub fn installed_jdks() -> Arc<[Jdk]> {
|
||||
/// Like [`installed_jdks`], but while none can run jdtls (on Java `min_java`)
|
||||
/// 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) -> Arc<[Jdk]> {
|
||||
pub fn jdtls_jdks(min_java: u32, data_dir: &Path) -> Arc<[Jdk]> {
|
||||
let mut slot = JDKS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match &*slot {
|
||||
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),
|
||||
}
|
||||
};
|
||||
runtime_candidates(&installed, data_dir).into()
|
||||
}
|
||||
|
||||
/// Runtime-only downloads are never registered as project discovery roots.
|
||||
pub fn language_server_runtimes_dir(data_dir: &Path) -> PathBuf {
|
||||
data_dir.join("language-server-runtimes")
|
||||
}
|
||||
|
||||
fn runtime_candidates(installed: &[Jdk], data_dir: &Path) -> Vec<Jdk> {
|
||||
let mut jdks = installed.to_vec();
|
||||
jdks.extend(jdk_homes(&language_server_runtimes_dir(data_dir)).filter_map(|home| jdk_at(&home)));
|
||||
jdks
|
||||
}
|
||||
|
||||
/// For a server with `max-java`: when the `JAVA_HOME` it would get (its env,
|
||||
@@ -341,6 +357,14 @@ pub fn configure_jdtls(
|
||||
cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy()));
|
||||
}
|
||||
crate::jdtls::add_default_vm_args(&mut cfg.args);
|
||||
let managed = language_server_runtimes_dir(data_dir);
|
||||
let managed = dunce::canonicalize(&managed).unwrap_or(managed);
|
||||
let project_jdks: Vec<_> = project_jdk
|
||||
.into_iter()
|
||||
.chain(jdks)
|
||||
.filter(|jdk| !jdk.home.starts_with(&managed) || project_jdk == Some(*jdk))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut all: Vec<Jdk> = Vec::new();
|
||||
for j in project_jdk.into_iter().chain(jdks).chain(server_jdk) {
|
||||
if !all.contains(j) {
|
||||
@@ -361,11 +385,12 @@ pub fn configure_jdtls(
|
||||
Err(e) => cfg.launch_error = Some(e),
|
||||
}
|
||||
}
|
||||
let mut runtimes = runtimes(jdks);
|
||||
if let Some(project) = project_jdk {
|
||||
let home = project.home.to_string_lossy();
|
||||
let mut runtimes = runtimes(&project_jdks);
|
||||
if let Some(project) = project_jdk.or_else(|| pick_jdk(&project_jdks, project_java_version(server_root))) {
|
||||
let name = if project.major == 8 { "JavaSE-1.8".to_owned() } else { format!("JavaSE-{}", project.major) };
|
||||
for r in &mut runtimes {
|
||||
if r["path"] == *home {
|
||||
if r["name"] == name {
|
||||
r["path"] = json!(project.home.to_string_lossy());
|
||||
r["default"] = json!(true);
|
||||
}
|
||||
}
|
||||
@@ -502,6 +527,58 @@ fn parse_java_version(v: &str) -> Option<u32> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provisioning_runtime_preserves_implicit_project_jdk() {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
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(java_executable(&project_home), "").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();
|
||||
assert_eq!(pick_jdk(&installed, None), Some(&project));
|
||||
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(java_executable(&home), "").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));
|
||||
assert_eq!(pick_jdk(&discover(), None), Some(&project));
|
||||
let explicit = jdk_at(&home).unwrap();
|
||||
assert_eq!(pick_jdk(std::slice::from_ref(&explicit), None), Some(&explicit));
|
||||
let mut cfg: LanguageServerConfig = serde_json::from_value(json!({"name":"jdtls", "command":"jdtls"})).unwrap();
|
||||
configure_jdtls(&mut cfg, data.path(), data.path(), &candidates, None, None, 25, &[], None);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], explicit.home.to_string_lossy());
|
||||
let options = serde_json::to_string(&cfg.initialization_options).unwrap();
|
||||
assert!(!options.contains("temurin-25"), "runtime-only JDK must not become a compiler runtime");
|
||||
assert_eq!(
|
||||
cfg.initialization_options.as_ref().unwrap()["settings"]["java"]["configuration"]["runtimes"][0]["default"]
|
||||
.as_bool(),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[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 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();
|
||||
assert_eq!(
|
||||
opts["settings"]["java"]["configuration"]["runtimes"],
|
||||
json!([{
|
||||
"name": "JavaSE-21", "path": pick_jdk(&installed, None).unwrap().home, "default": true
|
||||
}])
|
||||
);
|
||||
assert_eq!(cfg.env["JAVA_HOME"], "/custom-server-runtime");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_versions() {
|
||||
assert_eq!(parse_release("IMPLEMENTOR=\"x\"\nJAVA_VERSION=\"21.0.3\"\n"), Some(21));
|
||||
@@ -544,7 +621,7 @@ mod tests {
|
||||
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" }));
|
||||
assert_eq!(rt[2], json!({ "name": "JavaSE-25", "path": "/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.launch_error, None);
|
||||
|
||||
@@ -108,8 +108,28 @@ 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();
|
||||
format!(
|
||||
"{ADOPTIUM_API}/assets/latest/{feature}/hotspot?architecture={}&image_type=jdk&os={}&vendor=eclipse",
|
||||
"{ADOPTIUM_API}/assets/latest/{feature}/hotspot?architecture={}&image_type={image}&os={}&vendor=eclipse",
|
||||
adoptium_arch(platform.arch),
|
||||
adoptium_os(platform.os),
|
||||
)
|
||||
@@ -170,12 +190,16 @@ 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 == "jdk")
|
||||
a.binary.image_type.as_deref().is_none_or(|t| t == image.name())
|
||||
&& 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)
|
||||
@@ -255,14 +279,50 @@ impl Tools {
|
||||
location: Option<&Path>,
|
||||
cancel: &CancellationToken,
|
||||
emit: Emit,
|
||||
) -> IdeResult<DownloadedJdk> {
|
||||
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.
|
||||
pub fn download_java_runtime(
|
||||
&self,
|
||||
feature: u32,
|
||||
location: &Path,
|
||||
cancel: &CancellationToken,
|
||||
emit: Emit,
|
||||
) -> IdeResult<DownloadedJdk> {
|
||||
self.download_java(TEMURIN, feature, Some(location), true, cancel, emit)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn download_java(
|
||||
&self,
|
||||
vendor: &str,
|
||||
feature: u32,
|
||||
location: Option<&Path>,
|
||||
runtime: bool,
|
||||
cancel: &CancellationToken,
|
||||
emit: Emit,
|
||||
) -> IdeResult<DownloadedJdk> {
|
||||
vendor_or_err(vendor)?;
|
||||
let _guard = InstallGuard::acquire(&format!("jdk-{vendor}-{feature}"))?;
|
||||
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 =
|
||||
adoptium_pick(&http.get_text(&adoptium_assets_url(feature, self.platform), cancel)?, self.platform)?;
|
||||
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)?;
|
||||
}
|
||||
if picked.is_none() && self.platform.os == Os::Mac && self.platform.arch == Arch::Arm64 {
|
||||
// Java 8 has no Apple Silicon build: the Intel one runs under Rosetta.
|
||||
let intel = Platform { os: Os::Mac, arch: Arch::X64 };
|
||||
@@ -376,6 +436,22 @@ mod tests {
|
||||
assert!(adoptium_pick("{\"errorMessage\":\"x\"}", MAC_ARM).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_assets_use_jre_without_changing_jdk_downloads() {
|
||||
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}]"), 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 java_8_versions_and_dir_names() {
|
||||
let v = AssetVersion { major: 8, minor: 0, security: 472, openjdk_version: Some("1.8.0_472-b08".into()) };
|
||||
|
||||
@@ -221,7 +221,25 @@ impl Http {
|
||||
|
||||
/// GETs `url` as text, failing when the response is larger than `max_bytes`.
|
||||
pub fn get_text_limited(&self, url: &str, max_bytes: Option<u64>, cancel: &CancellationToken) -> IdeResult<String> {
|
||||
self.get_text_response(url, max_bytes, false, cancel)
|
||||
}
|
||||
|
||||
/// Adoptium uses 404 as well as an empty array for absent platform builds.
|
||||
pub fn get_text_or_empty_on_404(&self, url: &str, cancel: &CancellationToken) -> IdeResult<String> {
|
||||
self.get_text_response(url, None, true, cancel)
|
||||
}
|
||||
|
||||
fn get_text_response(
|
||||
&self,
|
||||
url: &str,
|
||||
max_bytes: Option<u64>,
|
||||
empty_on_404: bool,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<String> {
|
||||
let mut cmd = self.base(url);
|
||||
if empty_on_404 {
|
||||
cmd.args(["--write-out", "\n%{http_code}"]);
|
||||
}
|
||||
if let Some(max) = max_bytes {
|
||||
cmd.arg("--max-filesize").arg(max.to_string());
|
||||
}
|
||||
@@ -246,7 +264,14 @@ impl Http {
|
||||
None => std::thread::sleep(Duration::from_millis(30)),
|
||||
}
|
||||
};
|
||||
let body = out_t.join().unwrap_or_default();
|
||||
let mut body = out_t.join().unwrap_or_default();
|
||||
if empty_on_404 && let Some(split) = body.iter().rposition(|b| *b == b'\n') {
|
||||
let code = &body[split + 1..];
|
||||
if code == b"404" {
|
||||
return Ok("[]".into());
|
||||
}
|
||||
body.truncate(split);
|
||||
}
|
||||
if let Some(max) = max_bytes
|
||||
&& body.len() as u64 > max
|
||||
{
|
||||
@@ -362,6 +387,25 @@ mod tests {
|
||||
assert!(lines.contains(&"one".to_string()) && lines.contains(&"two".to_string()), "{lines:?}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn absent_build_is_empty_but_http_failures_are_not() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curl = tmp.path().join("curl");
|
||||
let http = Http { curl: curl.clone(), https_only: false };
|
||||
for (code, exit, expected) in [(404, 22, Some("[]")), (200, 0, Some("payload")), (403, 22, None), (0, 7, None)]
|
||||
{
|
||||
std::fs::write(&curl, format!("#!/bin/sh\nprintf 'payload\\n{code:03}'\nexit {exit}\n")).unwrap();
|
||||
std::fs::set_permissions(&curl, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let result = http.get_text_or_empty_on_404("https://example.test/build", &CancellationToken::new());
|
||||
match expected {
|
||||
Some(body) => assert_eq!(result.unwrap(), body),
|
||||
None => assert!(result.is_err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn cancellation_kills_the_child() {
|
||||
|
||||
@@ -240,11 +240,11 @@ export function JavaPage() {
|
||||
label="JDK"
|
||||
kind="jdk"
|
||||
emptyLabel="Auto"
|
||||
emptyDetail="the project JDK if new enough, else the first installed JDK that is"
|
||||
emptyDetail="auto-managed; uses an installed JDK or downloads a runtime when needed"
|
||||
directory
|
||||
browseTitle="Select JDK Home Directory"
|
||||
aria-label="Language server JDK"
|
||||
hint="Runs jdtls only, like IDEA's JDK for importer; the project keeps compiling and running with the Project JDK. jdtls needs JDK 21 or newer (recent releases 25); Download JDK… in the list fetches one. The jdtls executable itself is set in Language Servers."
|
||||
hint="Runs jdtls only; the project keeps its Project JDK. When empty, a suitable installed JDK is used first, otherwise Eclipse Temurin is downloaded automatically into a separate language-server folder. The jdtls executable itself is set in Language Servers."
|
||||
/>
|
||||
<Row
|
||||
label="JVM options"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"Downloading Java {0} language server runtime": "正在下载 Java {0} 语言服务器运行时",
|
||||
"auto-managed; uses an installed JDK or downloads a runtime when needed": "自动管理;使用已安装的 JDK,或在需要时自动下载运行时",
|
||||
"Runs jdtls only; the project keeps its Project JDK. When empty, a suitable installed JDK is used first, otherwise Eclipse Temurin is downloaded automatically into a separate language-server folder. The jdtls executable itself is set in Language Servers.": "仅用于运行 jdtls,项目仍使用项目 JDK。留空时优先使用符合要求的已安装 JDK,否则自动将 Eclipse Temurin 下载到独立的语言服务器目录。jdtls 可执行文件在语言服务器设置中配置。",
|
||||
"Semantic Highlighting": "语义高亮",
|
||||
"Rainbow Brackets": "彩虹括号",
|
||||
"Comment with Line Comment": "使用行注释",
|
||||
@@ -1406,7 +1409,6 @@
|
||||
"Rerun Search": "重新运行搜索",
|
||||
"Right Bottom": "右下",
|
||||
"Right Top": "右上",
|
||||
"Runs jdtls only, like IDEA's JDK for importer; the project keeps compiling and running with the Project JDK. jdtls needs JDK 21 or newer (recent releases 25); Download JDK… in the list fetches one. The jdtls executable itself is set in Language Servers.": "仅用于运行 jdtls(类似 IDEA 的「导入器 JDK」);项目仍使用项目 JDK 编译和运行。jdtls 需要 JDK 21 或更高版本(较新版本需要 25);可通过列表中的「下载 JDK…」获取。jdtls 可执行文件本身在「语言服务器」中设置。",
|
||||
"Language server JDK": "语言服务器 JDK",
|
||||
"Password": "密码",
|
||||
"Password:": "密码:",
|
||||
@@ -2384,7 +2386,6 @@
|
||||
"not installed in this project": "此项目中未安装",
|
||||
"or click a line number to set a breakpoint.": "或点击行号设置断点。",
|
||||
"per module, the JDK of its Java version": "按模块使用与其 Java 版本匹配的 JDK",
|
||||
"the project JDK if new enough, else the first installed JDK that is": "项目 JDK 版本足够时使用它,否则使用第一个版本足够的已安装 JDK",
|
||||
"pip output": "pip 输出",
|
||||
"places the caret. A line with": "放置光标。包含",
|
||||
"println! with Debug": "使用 Debug 的 println!",
|
||||
|
||||
Reference in new issue
Заблокувати користувача