//! Download and install helpers for LSP servers not available via //! system package managers. //! //! Each helper downloads a prebuilt binary (or archive) and places it //! under `~/.local/share/zesdex/lsp//`. //! //! Flow: `run_download_tier` dispatches sentinel command names to the //! appropriate installer (`install_rust_analyzer_binary` or //! `install_jdtls_from_eclipse`). Each installer downloads, extracts, //! and sets executable permissions on the binary. use std::path::{Path, PathBuf}; use tracing::{debug, info}; use super::config::{ProgressFn, DOWNLOAD_JDTLS, DOWNLOAD_RUST_BIN}; use super::discovery::EnvInfo; use super::manager::run_command; /// Resolve the directory where downloaded LSP binaries are stored. /// /// Returns `~/.local/share/zesdex/lsp//` (using `dirs::data_dir`). fn lsp_install_dir(server: &str) -> Result { let base = dirs::data_dir() .ok_or_else(|| "cannot find data directory via dirs crate".to_string())? .join("zesdex") .join("lsp") .join(server); debug!(server = server, path = %base.display(), "LSP install dir"); Ok(base) } /// Check whether `def` was previously installed via the download tier /// (binary/launcher lives under `~/.local/share/zesdex/lsp//`). /// /// Flow: resolve install dir → iterate known binary name patterns under /// that dir → return the first existing file path. /// /// Returns the path to the binary if found. pub(super) fn previous_download_install(def: &super::config::LanguageServerDef) -> Option { let base = lsp_install_dir(&def.name).ok()?; // Candidate relative paths under the install directory for each server. let candidates: &[&str] = match def.name.as_str() { "rust-analyzer" => &["rust-analyzer"], "jdtls" => &["bin/jdtls", "jdtls-launcher.sh", "jdtls"], "typescript-language-server" => &["bin/typescript-language-server"], "gopls" => &["bin/gopls"], _ => return None, }; for sub in candidates { let p = base.join(sub); if p.exists() { // Skip directory entries that exist but are the base dir itself. if p.is_file() { debug!(name = %def.name, path = %p.display(), "found previous install"); return Some(p); } } } debug!(name = %def.name, "no previous install found"); None } /// Download a file from `url` to `dest` using curl. /// /// Flow: build curl args with connect-timeout (15 s) and max-time /// (`max_secs`) → delegate to `run_command` → return error on failure. fn download_url(url: &str, dest: &Path, max_secs: u64) -> Result<(), String> { let path_str = dest.to_str().ok_or("invalid dest path")?.to_string(); info!(url = url, dest = %path_str, max_secs = max_secs, "downloading file"); // curl flags: -f (fail on HTTP error), -sS (silent but show errors), // -L (follow redirects), --connect-timeout, --max-time, -o (output). let args = [ "-fsSL", "--connect-timeout", "15", "--max-time", &max_secs.to_string(), "-o", &path_str, url, ]; let (ok, out) = run_command("curl", &args).map_err(|e| format!("curl spawn: {e}"))?; if !ok { return Err(format!("download failed: {}", out.trim())); } info!(url = url, "download complete"); Ok(()) } /// Download rust-analyzer from GitHub releases and install into /// `~/.local/share/zesdex/lsp/rust-analyzer/rust-analyzer`. /// /// Flow: create install dir → pick platform URL → download gzipped binary → /// decompress with gunzip → set executable permissions → return binary path. fn install_rust_analyzer_binary( env: &EnvInfo, progress: ProgressFn<'_>, ) -> Result { let base = lsp_install_dir("rust-analyzer")?; std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {e}"))?; // GitHub release URLs for the latest rust-analyzer prebuilt binary. let url = if env.is_linux { "https://github.com/rust-lang/rust-analyzer/releases/latest/download/rust-analyzer-x86_64-unknown-linux-gnu.gz" } else if env.is_macos { "https://github.com/rust-lang/rust-analyzer/releases/latest/download/rust-analyzer-aarch64-apple-darwin.gz" } else { return Err("no prebuilt binary for this OS".to_string()); }; let gz = base.join("rust-analyzer.gz"); // downloaded archive let target = base.join("rust-analyzer"); // final binary path info!("rust-analyzer: downloading prebuilt binary"); if let Some(cb) = progress { cb("Rust: downloading prebuilt binary..."); } download_url(url, &gz, 120)?; if let Some(cb) = progress { cb("Rust: decompressing..."); } let (ok, out) = run_command("gunzip", &["-f", &gz.to_string_lossy()]) .map_err(|e| format!("gunzip spawn: {e}"))?; if !ok { return Err(format!("gunzip: {}", out.trim())); } if !target.exists() { return Err("binary missing after decompression".to_string()); } // Set executable bit on Unix (0o755 = rwxr-xr-x). #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755)) .map_err(|e| format!("chmod: {e}"))?; } if let Some(cb) = progress { cb("Rust: installed ✓"); } info!("rust-analyzer: installed at {}", target.display()); Ok(target) } /// Download Eclipse JDT-LS from the official snapshot server, extract it, /// and create a launcher script at `bin/jdtls`. /// /// Flow: create install dir → download ~150 MB tarball → extract with tar → /// verify `plugins/` exists → write a bash launcher script that resolves /// the JDT-LS launcher JAR and config → set launcher executable. fn install_jdtls_from_eclipse(progress: ProgressFn) -> Result { let base = lsp_install_dir("jdtls")?; std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {e}"))?; let url = "https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz"; let tarball = base.join("jdtls.tar.gz"); info!("jdtls: downloading (~150 MB)"); if let Some(cb) = progress { cb("Java: downloading JDT-LS (~150MB)..."); } download_url(url, &tarball, 300)?; if let Some(cb) = progress { cb("Java: extracting..."); } let (ok, out) = run_command( "tar", &[ "-xzf", tarball.to_str().unwrap_or(""), "-C", base.to_str().unwrap_or("."), ], ) .map_err(|e| format!("tar spawn: {e}"))?; if !ok { return Err(format!("tar: {}", out.trim())); } let _ = std::fs::remove_file(&tarball); // Validate that the extracted contents include the plugins directory. if !base.join("plugins").exists() { return Err("extracted archive missing plugins/ directory".to_string()); } let bin_dir = base.join("bin"); std::fs::create_dir_all(&bin_dir).map_err(|e| format!("mkdir bin: {e}"))?; let launcher = bin_dir.join("jdtls"); let script = r#"#!/usr/bin/env bash set -e JDTLS_HOME="$(cd "$(dirname "$0")/.." && pwd)" LAUNCHER=$(ls "${JDTLS_HOME}/plugins/org.eclipse.equinox.launcher_"*.jar 2>/dev/null | head -n1) CONFIG=$(ls -d "${JDTLS_HOME}"/config_* 2>/dev/null | head -n1) WORKSPACE="${JDTLS_HOME}/workspace" mkdir -p "${WORKSPACE}" exec java \ -Declipse.application=org.eclipse.jdt.ls.core.id1 \ -Dosgi.bundles.defaultStartLevel=5 \ -Declipse.product=org.eclipse.jdt.ls.core.product \ -Dlog.level=WARN -noverify -Xmx1G \ -jar "${LAUNCHER}" -configuration "${CONFIG}" -data "${WORKSPACE}" \ --add-modules=ALL-SYSTEM \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.lang=ALL-UNNAMED \ "$@" "#; std::fs::write(&launcher, script).map_err(|e| format!("write launcher: {e}"))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755)) .map_err(|e| format!("chmod launcher: {e}"))?; } if let Some(cb) = progress { cb("Java: JDT-LS installed ✓"); } info!("jdtls: installed at {}", launcher.display()); Ok(launcher) } /// Dispatch a sentinel download tier to the correct helper. /// /// Matches sentinel constants (`DOWNLOAD_RUST_BIN`, `DOWNLOAD_JDTLS`) and /// routes to the appropriate platform-aware installer. pub(super) fn run_download_tier( name: &str, env: &EnvInfo, progress: ProgressFn<'_>, ) -> Result { info!(tier = name, "running download tier"); match name { DOWNLOAD_RUST_BIN => install_rust_analyzer_binary(env, progress), DOWNLOAD_JDTLS => install_jdtls_from_eclipse(progress), other => Err(format!("unknown download tier '{other}'")), } }