diff --git a/extension.toml b/extension.toml index 6eb9a14..8f69e0e 100644 --- a/extension.toml +++ b/extension.toml @@ -26,3 +26,8 @@ language = "Java" kind = "process:exec" command = "*" args = ["-version"] + +[[capabilities]] +kind = "process:exec" +command = "tar" +args = ["**"] diff --git a/src/jdk.rs b/src/jdk.rs index a844c9a..401c55a 100644 --- a/src/jdk.rs +++ b/src/jdk.rs @@ -7,8 +7,8 @@ use zed_extension_api::{ }; use crate::util::{ - get_curr_dir, mark_checked_once, path_to_string, remove_all_files_except, - should_use_local_or_download, + download_and_extract_tar_gz, get_curr_dir, mark_checked_once, path_to_string, + remove_all_files_except, should_use_local_or_download, }; // Errors @@ -113,17 +113,27 @@ pub fn try_to_fetch_and_install_latest_jdk( .map_err(|err| format!("Failed to detect architecture for JDK download: {err}"))?; let download_url = build_corretto_url(&version, &platform, &arch); - download_file( - download_url.as_str(), - path_to_string(install_path.clone()) - .map_err(|err| format!("Invalid JDK install path {install_path:?}: {err}"))? - .as_str(), - match zed::current_platform().0 { - Os::Windows => DownloadedFileType::Zip, - _ => DownloadedFileType::GzipTar, - }, - ) - .map_err(|err| format!("Failed to download Corretto JDK from {download_url}: {err}"))?; + let install_path_str = path_to_string(install_path.clone()) + .map_err(|err| format!("Invalid JDK install path {install_path:?}: {err}"))?; + + match zed::current_platform().0 { + Os::Windows => { + download_file( + download_url.as_str(), + install_path_str.as_str(), + DownloadedFileType::Zip, + ) + .map_err(|err| { + format!("Failed to download Corretto JDK from {download_url}: {err}") + })?; + } + _ => { + download_and_extract_tar_gz(download_url.as_str(), install_path_str.as_str()) + .map_err(|err| { + format!("Failed to download Corretto JDK from {download_url}: {err}") + })?; + } + } // Remove older versions let _ = remove_all_files_except(&jdk_path, version.as_str()); diff --git a/src/jdtls.rs b/src/jdtls.rs index 18972a1..01f7dc6 100644 --- a/src/jdtls.rs +++ b/src/jdtls.rs @@ -14,6 +14,8 @@ use zed_extension_api::{ set_language_server_installation_status, }; +use crate::util::download_and_extract_tar_gz; + use crate::{ config::is_java_autodownload, jdk::try_to_fetch_and_install_latest_jdk, @@ -210,12 +212,11 @@ pub fn try_to_fetch_and_install_latest_jdtls( let download_url = format!( "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/{latest_version}/{latest_version_build}" ); - download_file( + download_and_extract_tar_gz( &download_url, path_to_string(build_path.clone()) .map_err(|err| format!("Invalid JDTLS build path {build_path:?}: {err}"))? .as_str(), - DownloadedFileType::GzipTar, ) .map_err(|err| format!("Failed to download JDTLS from {download_url}: {err}"))?; make_file_executable( diff --git a/src/proxy.rs b/src/proxy.rs index 62adf73..f832f7a 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -7,33 +7,32 @@ use zed_extension_api::{ set_language_server_installation_status, }; -use crate::util::{mark_checked_once, remove_all_files_except, should_use_local_or_download}; +use crate::util::{ + download_and_extract_tar_gz, mark_checked_once, remove_all_files_except, + should_use_local_or_download, +}; const PROXY_BINARY: &str = "java-lsp-proxy"; const PROXY_INSTALL_PATH: &str = "proxy-bin"; const GITHUB_REPO: &str = "zed-extensions/java"; -fn asset_name() -> zed::Result<(String, DownloadedFileType)> { +fn asset_name() -> zed::Result { let (os, arch) = zed::current_platform(); - let (os_str, file_type) = match os { - zed::Os::Mac => ("darwin", DownloadedFileType::GzipTar), - zed::Os::Linux => ("linux", DownloadedFileType::GzipTar), - zed::Os::Windows => ("windows", DownloadedFileType::Zip), + let os_str = match os { + zed::Os::Mac => "darwin", + zed::Os::Linux => "linux", + zed::Os::Windows => "windows", }; let arch_str = match arch { zed::Architecture::Aarch64 => "aarch64", zed::Architecture::X8664 => "x86_64", _ => return Err("Unsupported architecture".into()), }; - let ext = if matches!(file_type, DownloadedFileType::Zip) { - "zip" - } else { - "tar.gz" + let ext = match os { + zed::Os::Windows => "zip", + _ => "tar.gz", }; - Ok(( - format!("java-lsp-proxy-{os_str}-{arch_str}.{ext}"), - file_type, - )) + Ok(format!("java-lsp-proxy-{os_str}-{arch_str}.{ext}")) } fn proxy_exec() -> String { @@ -83,7 +82,7 @@ pub fn binary_path( } // 2. Auto-download from GitHub releases - if let Ok((name, file_type)) = asset_name() + if let Ok(name) = asset_name() && let Ok(release) = zed::latest_github_release( GITHUB_REPO, GithubReleaseOptions { @@ -107,7 +106,15 @@ pub fn binary_path( &LanguageServerInstallationStatus::Downloading, ); - if zed::download_file(&asset.download_url, &version_dir, file_type).is_ok() { + let download_ok = match zed::current_platform().0 { + zed::Os::Windows => { + zed::download_file(&asset.download_url, &version_dir, DownloadedFileType::Zip) + .is_ok() + } + _ => download_and_extract_tar_gz(&asset.download_url, &version_dir).is_ok(), + }; + + if download_ok { let _ = zed::make_file_executable(&bin_path); set_language_server_installation_status( language_server_id, diff --git a/src/util.rs b/src/util.rs index f78ebb1..ae9c003 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,7 +6,8 @@ use std::{ path::{Path, PathBuf}, }; use zed_extension_api::{ - self as zed, Command, LanguageServerId, Os, Worktree, current_platform, + self as zed, Command, DownloadedFileType, LanguageServerId, Os, Worktree, current_platform, + download_file, http_client::{HttpMethod, HttpRequest, fetch}, serde_json::Value, }; @@ -316,6 +317,73 @@ pub fn path_to_string>(path: P) -> zed::Result { .map_err(|_| PATH_TO_STR_ERROR.to_string()) } +/// Downloads a `.tar.gz` archive to disk and then extracts it via the system `tar` command. +/// +/// This is a two-step replacement for `download_file(..., DownloadedFileType::GzipTar)`: +/// the full archive is written to disk first, avoiding premature-EOF issues with streaming +/// extraction, and then `tar` handles the decompression and unpacking. +/// +/// # Arguments +/// +/// * `url` – URL of the `.tar.gz` file to download. +/// * `destination_dir` – Directory into which the archive contents will be extracted. +/// Created automatically if it does not exist. +/// +/// # Errors +/// +/// Returns an error if the download, directory creation, `tar` invocation, or cleanup fails. +pub fn download_and_extract_tar_gz(url: &str, destination_dir: &str) -> zed::Result<()> { + let archive_path = format!("{destination_dir}.tar.gz"); + + // 1. Ensure the parent directory of the archive file exists + if let Some(parent) = Path::new(&archive_path).parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("failed to create parent directory for archive: {e}"))?; + } + + // 2. Download the raw .tar.gz bytes as an uncompressed file + download_file(url, &archive_path, DownloadedFileType::Uncompressed) + .map_err(|e| format!("failed to download archive from {url}: {e}"))?; + + // 3. Create the destination directory if it doesn't already exist + std::fs::create_dir_all(destination_dir) + .map_err(|e| format!("failed to create destination directory '{destination_dir}': {e}"))?; + + // 4. Resolve to absolute paths for the host `tar` command. + // `download_file` works with paths relative to the WASI sandbox (extension work dir), + // but `Command::new("tar")` runs on the host and needs absolute paths. + let workdir = + current_dir().map_err(|e| format!("failed to get extension working directory: {e}"))?; + let abs_archive = workdir.join(&archive_path); + let abs_destination = workdir.join(destination_dir); + + let abs_archive_str = abs_archive + .to_str() + .ok_or_else(|| format!("archive path is not valid UTF-8: {abs_archive:?}"))?; + let abs_destination_str = abs_destination + .to_str() + .ok_or_else(|| format!("destination path is not valid UTF-8: {abs_destination:?}"))?; + + // 5. Extract via the system `tar` command + let output = Command::new("tar") + .args(["-xzf", abs_archive_str, "-C", abs_destination_str]) + .output() + .map_err(|e| format!("failed to run tar: {e}"))?; + + if output.status != Some(0) { + let stderr = String::from_utf8_lossy(&output.stderr); + // Clean up the archive even on failure (best-effort) + let _ = std::fs::remove_file(&archive_path); + return Err(format!("tar extraction failed: {stderr}")); + } + + // 6. Clean up the archive + std::fs::remove_file(&archive_path) + .map_err(|e| format!("failed to remove archive '{archive_path}': {e}"))?; + + Ok(()) +} + /// Remove all files or directories that aren't equal to [`filename`]. /// /// This function scans the directory given by [`prefix`] and removes any