Meliputi: - File-level //! doc comment: tujuan file, alur kerja, komponen utama - Function-level /// doc comment: apa, parameter, return, flow, edge cases - Struct/enum/trait /// doc comment: peran, field docs - Tracing logging (tracing::info!/debug!/trace!/warn!/error!) di setiap fungsi - Inline comments untuk variable dan branching logic penting - Seluruh 8 crates di workspace: zesdex-backend, zesdex-cms, zesdex-entities, zesdex-iam, zesdex-infra, zesdex-ipc, zesdex-middleware, zesdex-utils - Build: 0 errors, 242/242 tests passed
240 lines
8.8 KiB
Rust
240 lines
8.8 KiB
Rust
//! 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/<server-name>/`.
|
|
//!
|
|
//! 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/<server>/` (using `dirs::data_dir`).
|
|
fn lsp_install_dir(server: &str) -> Result<PathBuf, String> {
|
|
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/<name>/`).
|
|
///
|
|
/// 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<PathBuf> {
|
|
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<PathBuf, String> {
|
|
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<PathBuf, String> {
|
|
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<PathBuf, String> {
|
|
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}'")),
|
|
}
|
|
}
|