diff --git a/README.md b/README.md index 50b254d..4eb028a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Install the extension via Zeds extension manager. It should work out of the box - To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`. +- The option to let the extension automatically download a version of OpenJDK can be enabled by setting `jdk_auto_download` to `true`. When enabled, the extension will only download a JDK if no valid java_home is provided or if the specified one does not meet the minimum version requirement. User-provided JDKs **always** take precedence. + Here is a common `settings.json` including the above mentioned configurations: ```jsonc @@ -22,6 +24,7 @@ Here is a common `settings.json` including the above mentioned configurations: "settings": { "java_home": "/path/to/your/JDK21+", "lombok_support": true, + "jdk_auto_download": false } } } diff --git a/src/config.rs b/src/config.rs index 4f677b0..3dc9fe3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,17 @@ pub fn get_java_home(configuration: &Option, worktree: &Worktree) -> Opti } } +pub fn is_java_autodownload(configuration: &Option) -> bool { + configuration + .as_ref() + .and_then(|configuration| { + configuration + .pointer("/jdk_auto_download") + .and_then(|enabled| enabled.as_bool()) + }) + .unwrap_or(false) +} + pub fn is_lombok_enabled(configuration: &Option) -> bool { configuration .as_ref() diff --git a/src/java.rs b/src/java.rs index 7bc8b11..86745aa 100644 --- a/src/java.rs +++ b/src/java.rs @@ -1,5 +1,6 @@ mod config; mod debugger; +mod jdk; mod jdtls; mod lsp; mod util; @@ -289,6 +290,7 @@ impl Extension for Java { &configuration, worktree, lombok_jvm_arg.into_iter().collect(), + language_server_id, )?); } diff --git a/src/jdk.rs b/src/jdk.rs new file mode 100644 index 0000000..0e128c3 --- /dev/null +++ b/src/jdk.rs @@ -0,0 +1,124 @@ +use std::path::{Path, PathBuf}; + +use zed_extension_api::{ + self as zed, Architecture, DownloadedFileType, LanguageServerId, + LanguageServerInstallationStatus, Os, current_platform, download_file, + set_language_server_installation_status, +}; + +use crate::util::{get_curr_dir, path_to_string, remove_all_files_except}; + +// Errors +const JDK_DIR_ERROR: &str = "Failed to read into JDK install directory"; +const NO_JDK_DIR_ERROR: &str = "No match for jdk or corretto in the extracted directory"; + +const CORRETTO_REPO: &str = "corretto/corretto-25"; +const CORRETTO_UNIX_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}.tar.gz"; +const CORRETTO_WINDOWS_URL_TEMPLATE: &str = "https://corretto.aws/downloads/resources/{version}/amazon-corretto-{version}-{platform}-{arch}-jdk.zip"; + +fn build_corretto_url(version: &str, platform: &str, arch: &str) -> String { + let template = match zed::current_platform().0 { + Os::Windows => CORRETTO_WINDOWS_URL_TEMPLATE, + _ => CORRETTO_UNIX_URL_TEMPLATE, + }; + + template + .replace("{version}", version) + .replace("{platform}", platform) + .replace("{arch}", arch) +} + +// For now keep in this file as they are not used anywhere else +// otherwise move to util +fn get_architecture() -> zed::Result { + match zed::current_platform() { + (_, Architecture::Aarch64) => Ok("aarch64".to_string()), + (_, Architecture::X86) => Ok("x86".to_string()), + (_, Architecture::X8664) => Ok("x64".to_string()), + } +} + +fn get_platform() -> zed::Result { + match zed::current_platform() { + (Os::Mac, _) => Ok("macosx".to_string()), + (Os::Linux, _) => Ok("linux".to_string()), + (Os::Windows, _) => Ok("windows".to_string()), + } +} + +pub fn try_to_fetch_and_install_latest_jdk( + language_server_id: &LanguageServerId, +) -> zed::Result { + let version = zed::latest_github_release( + CORRETTO_REPO, + zed_extension_api::GithubReleaseOptions { + require_assets: false, + pre_release: false, + }, + )? + .version; + + let jdk_path = get_curr_dir()?.join("jdk"); + let install_path = jdk_path.join(&version); + + // Check for updates, if same version is already downloaded skip download + + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::CheckingForUpdate, + ); + + if !install_path.exists() { + set_language_server_installation_status( + language_server_id, + &LanguageServerInstallationStatus::Downloading, + ); + + let platform = get_platform()?; + let arch = get_architecture()?; + + download_file( + build_corretto_url(&version, &platform, &arch).as_str(), + path_to_string(install_path.clone())?.as_str(), + match zed::current_platform().0 { + Os::Windows => DownloadedFileType::Zip, + _ => DownloadedFileType::GzipTar, + }, + )?; + + // Remove older versions + let _ = remove_all_files_except(jdk_path, version.as_str()); + } + + // Depending on the platform the name of the extracted dir might differ + // Rather than hard coding, extract it dynamically + let extracted_dir = get_extracted_dir(&install_path)?; + + Ok(install_path + .join(extracted_dir) + .join(match current_platform().0 { + Os::Mac => "Contents/Home/bin", + _ => "bin", + })) +} + +fn get_extracted_dir(path: &Path) -> zed::Result { + let Ok(mut entries) = path.read_dir() else { + return Err(JDK_DIR_ERROR.to_string()); + }; + + match entries.find_map(|entry| { + let entry = entry.ok()?; + let file_name = entry.file_name(); + let name_str = file_name.to_string_lossy().to_string(); + + if name_str.contains("jdk") || name_str.contains("corretto") { + Some(name_str) + } else { + None + } + }) { + Some(dir_path) => Ok(dir_path), + None => Err(NO_JDK_DIR_ERROR.to_string()), + } +} diff --git a/src/jdtls.rs b/src/jdtls.rs index ceccfa5..200446b 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -15,9 +15,13 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::util::{ - get_curr_dir, get_java_executable, get_java_major_version, path_to_string, - remove_all_files_except, +use crate::{ + config::is_java_autodownload, + jdk::try_to_fetch_and_install_latest_jdk, + util::{ + get_curr_dir, get_java_exec_name, get_java_executable, get_java_major_version, + path_to_string, remove_all_files_except, + }, }; const JDTLS_INSTALL_PATH: &str = "jdtls"; @@ -25,22 +29,28 @@ const LOMBOK_INSTALL_PATH: &str = "lombok"; // Errors -const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java 21. If you need to run a JVM < 21, you can specify a different one for JDTLS to use by specifying lsp.jdtls.settings.java.home in the settings"; +const JAVA_VERSION_ERROR: &str = "JDTLS requires at least Java version 21 to run. You can either specify a different JDK to use by configuring lsp.jdtls.settings.java_home to point to a different JDK, or set lsp.jdtls.settings.jdk_auto_download to true to let the extension automatically download one for you."; pub fn build_jdtls_launch_args( jdtls_path: &PathBuf, configuration: &Option, worktree: &Worktree, jvm_args: Vec, + language_server_id: &LanguageServerId, ) -> zed::Result> { if let Some(jdtls_launcher) = get_jdtls_launcher_from_path(worktree) { return Ok(vec![jdtls_launcher]); } - let java_executable = get_java_executable(configuration, worktree)?; + let mut java_executable = get_java_executable(configuration, worktree, language_server_id)?; let java_major_version = get_java_major_version(&java_executable)?; if java_major_version < 21 { - return Err(JAVA_VERSION_ERROR.to_string()); + if is_java_autodownload(configuration) { + java_executable = + try_to_fetch_and_install_latest_jdk(language_server_id)?.join(get_java_exec_name()); + } else { + return Err(JAVA_VERSION_ERROR.to_string()); + } } let extension_workdir = get_curr_dir()?; diff --git a/src/util.rs b/src/util.rs index 2c44c2e..adc8b62 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,9 +4,14 @@ use std::{ fs, path::{Path, PathBuf}, }; -use zed_extension_api::{self as zed, Command, Os, Worktree, current_platform, serde_json::Value}; +use zed_extension_api::{ + self as zed, Command, LanguageServerId, Os, Worktree, current_platform, serde_json::Value, +}; -use crate::config::get_java_home; +use crate::{ + config::{get_java_home, is_java_autodownload}, + jdk::try_to_fetch_and_install_latest_jdk, +}; // Errors const EXPAND_ERROR: &str = "Failed to expand ~"; @@ -59,9 +64,10 @@ pub fn get_curr_dir() -> zed::Result { } /// Retrieve the path to a java exec either: -/// - defined by the user in `settings.json` +/// - defined by the user in `settings.json` under option `java_home` /// - from PATH /// - from JAVA_HOME +/// - from the bundled OpenJDK if option `jdk_auto_download` is true /// /// # Arguments /// @@ -79,11 +85,9 @@ pub fn get_curr_dir() -> zed::Result { pub fn get_java_executable( configuration: &Option, worktree: &Worktree, + language_server_id: &LanguageServerId, ) -> zed::Result { - let java_executable_filename = match current_platform().0 { - Os::Windows => "java.exe", - _ => "java", - }; + let java_executable_filename = get_java_exec_name(); // Get executable from $JAVA_HOME if let Some(java_home) = get_java_home(configuration, worktree) { @@ -93,10 +97,30 @@ pub fn get_java_executable( return Ok(java_executable); } // If we can't, try to get it from $PATH - worktree - .which(java_executable_filename) - .map(PathBuf::from) - .ok_or_else(|| JAVA_EXEC_NOT_FOUND_ERROR.to_string()) + if let Some(java_home) = worktree.which(java_executable_filename.as_str()) { + return Ok(PathBuf::from(java_home)); + } + + // If the user has set the option, retrieve the latest version of Corretto (OpenJDK) + if is_java_autodownload(configuration) { + return Ok( + try_to_fetch_and_install_latest_jdk(language_server_id)?.join(java_executable_filename) + ); + } + + Err(JAVA_EXEC_NOT_FOUND_ERROR.to_string()) +} + +/// Retrieve the executable name for Java on this platform +/// +/// # Returns +/// +/// Returns the executable java name +pub fn get_java_exec_name() -> String { + match current_platform().0 { + Os::Windows => "java.exe".to_string(), + _ => "java".to_string(), + } } /// Retrieve the java major version accessible by the extension