Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ language = "Java"
kind = "process:exec"
command = "*"
args = ["-version"]

[[capabilities]]
kind = "process:exec"
command = "tar"
args = ["**"]
36 changes: 23 additions & 13 deletions src/jdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
5 changes: 3 additions & 2 deletions src/jdtls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
39 changes: 23 additions & 16 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
70 changes: 69 additions & 1 deletion src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -316,6 +317,73 @@ pub fn path_to_string<P: AsRef<Path>>(path: P) -> zed::Result<String> {
.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
Expand Down
Loading