fix: remove hardcoded API key for security reasons
This commit is contained in:
+202
-82
@@ -12,7 +12,7 @@
|
||||
//! user-friendly path first (rustup component, npm global, etc.) and
|
||||
//! only fall back to package managers or manual download if those fail.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -51,6 +51,12 @@ pub enum ProvisionResult {
|
||||
},
|
||||
}
|
||||
|
||||
/// Sentinel command names used by `provision_single` to detect "download"
|
||||
/// tiers (which are dispatched to `download_*` helpers rather than
|
||||
/// `run_command`). Kept as constants so `supported_servers` stays readable.
|
||||
const DOWNLOAD_RUST_BIN: &str = "__download_rust_analyzer__";
|
||||
const DOWNLOAD_JDTLS: &str = "__download_jdtls__";
|
||||
|
||||
/// Static description of a single language server: how to detect it,
|
||||
/// what file extensions it handles, and how to install it.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -93,13 +99,20 @@ pub struct InstallTier {
|
||||
/// computed at startup (compile time would also work, but keeping the
|
||||
/// shape uniform with the rest of the struct makes the call sites tidy).
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct EnvInfo {
|
||||
pub has_rustup: bool,
|
||||
pub has_npm: bool,
|
||||
pub has_go: bool,
|
||||
pub has_java: bool,
|
||||
pub has_cargo: bool,
|
||||
pub has_curl: bool,
|
||||
pub has_wget: bool,
|
||||
pub has_tar: bool,
|
||||
pub has_pacman: bool,
|
||||
pub has_apt: bool,
|
||||
pub has_brew: bool,
|
||||
pub has_dnf: bool,
|
||||
pub is_linux: bool,
|
||||
pub is_macos: bool,
|
||||
}
|
||||
@@ -144,8 +157,14 @@ pub fn detect_env() -> EnvInfo {
|
||||
has_npm: which("npm").is_some(),
|
||||
has_go: which("go").is_some(),
|
||||
has_java: which("java").is_some(),
|
||||
has_cargo: which("cargo").is_some(),
|
||||
has_curl: which("curl").is_some(),
|
||||
has_wget: which("wget").is_some(),
|
||||
has_tar: which("tar").is_some(),
|
||||
has_pacman: which("pacman").is_some(),
|
||||
has_apt: which("apt").is_some() || which("apt-get").is_some(),
|
||||
has_brew: which("brew").is_some(),
|
||||
has_dnf: which("dnf").is_some(),
|
||||
is_linux: cfg!(target_os = "linux"),
|
||||
is_macos: cfg!(target_os = "macos"),
|
||||
}
|
||||
@@ -168,16 +187,38 @@ pub fn supported_servers() -> Vec<LanguageServerDef> {
|
||||
language: "rust".to_string(),
|
||||
extensions: vec![".rs".to_string()],
|
||||
binary_names: vec!["rust-analyzer".to_string()],
|
||||
install_tiers: vec![InstallTier {
|
||||
label: "rustup component".to_string(),
|
||||
requires: vec!["rustup".to_string()],
|
||||
command: "rustup".to_string(),
|
||||
args: vec![
|
||||
"component".to_string(),
|
||||
"add".to_string(),
|
||||
"rust-analyzer".to_string(),
|
||||
],
|
||||
}],
|
||||
install_tiers: vec![
|
||||
InstallTier {
|
||||
label: "rustup component".to_string(),
|
||||
requires: vec!["rustup".to_string()],
|
||||
command: "rustup".to_string(),
|
||||
args: vec!["component".to_string(), "add".to_string(), "rust-analyzer".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "pacman".to_string(),
|
||||
requires: vec!["pacman".to_string()],
|
||||
command: "pacman".to_string(),
|
||||
args: vec!["-S".to_string(), "--noconfirm".to_string(), "--needed".to_string(), "rust-analyzer".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "brew".to_string(),
|
||||
requires: vec!["brew".to_string()],
|
||||
command: "brew".to_string(),
|
||||
args: vec!["install".to_string(), "rust-analyzer".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "cargo install".to_string(),
|
||||
requires: vec!["cargo".to_string()],
|
||||
command: "cargo".to_string(),
|
||||
args: vec!["install".to_string(), "--locked".to_string(), "rust-analyzer".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "download prebuilt".to_string(),
|
||||
requires: vec!["curl".to_string(), "tar".to_string()],
|
||||
command: DOWNLOAD_RUST_BIN.to_string(),
|
||||
args: vec![],
|
||||
},
|
||||
],
|
||||
},
|
||||
LanguageServerDef {
|
||||
name: "typescript-language-server".to_string(),
|
||||
@@ -220,17 +261,19 @@ pub fn supported_servers() -> Vec<LanguageServerDef> {
|
||||
name: "jdtls".to_string(),
|
||||
language: "java".to_string(),
|
||||
extensions: vec![".java".to_string()],
|
||||
binary_names: vec!["jdtls".to_string(), "eclipse-jdt-ls".to_string()],
|
||||
binary_names: vec!["jdtls".to_string(), "eclipse-jdt-ls".to_string(), "jdtls-launcher".to_string()],
|
||||
install_tiers: vec![
|
||||
InstallTier {
|
||||
label: "pacman".to_string(),
|
||||
requires: vec!["java".to_string(), "pacman".to_string()],
|
||||
command: "pacman".to_string(),
|
||||
args: vec!["-S".to_string(), "--noconfirm".to_string(), "--needed".to_string(), "eclipse-jdt-ls".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "apt".to_string(),
|
||||
requires: vec!["java".to_string(), "apt".to_string()],
|
||||
command: "apt".to_string(),
|
||||
args: vec![
|
||||
"install".to_string(),
|
||||
"-y".to_string(),
|
||||
"eclipse-jdt-ls".to_string(),
|
||||
],
|
||||
command: "sudo".to_string(),
|
||||
args: vec!["apt".to_string(), "install".to_string(), "-y".to_string(), "eclipse-jdt-ls".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "brew".to_string(),
|
||||
@@ -239,9 +282,9 @@ pub fn supported_servers() -> Vec<LanguageServerDef> {
|
||||
args: vec!["install".to_string(), "jdtls".to_string()],
|
||||
},
|
||||
InstallTier {
|
||||
label: "manual download".to_string(),
|
||||
requires: vec!["java".to_string()],
|
||||
command: "__jdtls_download__".to_string(),
|
||||
label: "download from eclipse".to_string(),
|
||||
requires: vec!["java".to_string(), "curl".to_string(), "tar".to_string()],
|
||||
command: DOWNLOAD_JDTLS.to_string(),
|
||||
args: vec![],
|
||||
},
|
||||
],
|
||||
@@ -286,7 +329,7 @@ pub fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(bool, String)>
|
||||
})
|
||||
});
|
||||
|
||||
let timeout = Duration::from_secs(120);
|
||||
let timeout = Duration::from_secs(180);
|
||||
let start = Instant::now();
|
||||
let status = loop {
|
||||
match child.try_wait()? {
|
||||
@@ -319,74 +362,157 @@ pub fn run_command(cmd: &str, args: &[&str]) -> std::io::Result<(bool, String)>
|
||||
}
|
||||
}
|
||||
|
||||
/// Manual-install fallback for jdtls when apt and brew both fail (or
|
||||
/// the host has neither): create the install directory and a launcher
|
||||
/// script at `~/.local/share/zesdex/lsp/jdtls/jdtls-launcher.sh`.
|
||||
///
|
||||
/// Flow: create the install dir under `dirs::data_dir()` → write a
|
||||
/// bash launcher that execs the Eclipse Equinox launcher with
|
||||
/// the standard JDT-LS JVM flags → make it executable.
|
||||
///
|
||||
/// Returns the absolute path of the launcher script on success.
|
||||
fn install_jdtls_manual() -> std::io::Result<PathBuf> {
|
||||
/// Resolve the directory where downloaded LSP binaries are stored.
|
||||
fn lsp_install_dir(server: &str) -> Result<PathBuf, String> {
|
||||
let base = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.ok_or_else(|| "cannot find data directory via dirs crate".to_string())?
|
||||
.join("zesdex")
|
||||
.join("lsp")
|
||||
.join("jdtls");
|
||||
std::fs::create_dir_all(&base)?;
|
||||
.join(server);
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
let launcher = base.join("jdtls-launcher.sh");
|
||||
let launcher_script = r#"#!/usr/bin/env bash
|
||||
# Auto-generated launcher for Eclipse JDT-LS, created by zesdex provisioner.
|
||||
# Adjust JAVA_HOME and the plugin path below if your layout differs.
|
||||
/// Download a file from `url` to `dest` using curl.
|
||||
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, "downloading");
|
||||
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()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download rust-analyzer from GitHub releases and install into
|
||||
/// `~/.local/share/zesdex/lsp/rust-analyzer/bin/rust-analyzer`.
|
||||
fn install_rust_analyzer_binary(env: &EnvInfo) -> Result<PathBuf, String> {
|
||||
let base = lsp_install_dir("rust-analyzer")?;
|
||||
std::fs::create_dir_all(&base).map_err(|e| format!("mkdir: {}", e))?;
|
||||
|
||||
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");
|
||||
let target = base.join("rust-analyzer");
|
||||
|
||||
download_url(url, &gz, 120)?;
|
||||
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());
|
||||
}
|
||||
#[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))?;
|
||||
}
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
/// Download Eclipse JDT-LS from the official snapshot server, extract it,
|
||||
/// and create a launcher script at `bin/jdtls`.
|
||||
fn install_jdtls_from_eclipse() -> 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");
|
||||
download_url(url, &tarball, 300)?;
|
||||
|
||||
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);
|
||||
|
||||
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)"
|
||||
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 "${JDTLS_HOME}/plugins/org.eclipse.equinox.launcher_"*.jar \
|
||||
-configuration "${JDTLS_HOME}/config_"* \
|
||||
-data "${JDTLS_HOME}/workspace" \
|
||||
-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, launcher_script)?;
|
||||
|
||||
std::fs::write(&launcher, script).map_err(|e| format!("write launcher: {}", e))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&launcher)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&launcher, perms)?;
|
||||
std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755))
|
||||
.map_err(|e| format!("chmod launcher: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(launcher)
|
||||
}
|
||||
|
||||
/// Dispatch a sentinel download tier to the correct helper.
|
||||
fn run_download_tier(name: &str, env: &EnvInfo) -> Result<PathBuf, String> {
|
||||
match name {
|
||||
DOWNLOAD_RUST_BIN => install_rust_analyzer_binary(env),
|
||||
DOWNLOAD_JDTLS => install_jdtls_from_eclipse(),
|
||||
other => Err(format!("unknown download tier '{}'", other)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the "install by hand" message shown to the user when every
|
||||
/// automated tier fails.
|
||||
fn manual_instructions(def: &LanguageServerDef) -> String {
|
||||
match def.language.as_str() {
|
||||
"rust" => "Install rustup from https://rustup.rs, then run:\n \
|
||||
rustup component add rust-analyzer"
|
||||
"rust" => "Install rust-analyzer:\n \
|
||||
Arch: sudo pacman -S rust-analyzer\n \
|
||||
macOS: brew install rust-analyzer\n \
|
||||
Any: cargo install --locked rust-analyzer\n \
|
||||
Rustup: rustup component add rust-analyzer"
|
||||
.to_string(),
|
||||
"typescript" => "Install Node.js from https://nodejs.org, then run:\n \
|
||||
npm install -g typescript typescript-language-server"
|
||||
"typescript" => "Install typescript-language-server:\n \
|
||||
npm install -g typescript typescript-language-server\n \
|
||||
Arch: sudo pacman -S typescript-language-server"
|
||||
.to_string(),
|
||||
"go" => "Install Go from https://go.dev/dl, then run:\n \
|
||||
go install golang.org/x/tools/gopls@latest"
|
||||
"go" => "Install gopls:\n \
|
||||
go install golang.org/x/tools/gopls@latest\n \
|
||||
Arch: sudo pacman -S gopls"
|
||||
.to_string(),
|
||||
"java" => "Install Eclipse JDT-LS for your platform:\n \
|
||||
Debian/Ubuntu: sudo apt install -y eclipse-jdt-ls\n \
|
||||
macOS: brew install jdtls\n \
|
||||
Other: see https://.eclipse.org/jdtls/#download"
|
||||
"java" => "Install Eclipse JDT-LS:\n \
|
||||
Arch: sudo pacman -S eclipse-jdt-ls\n \
|
||||
Debian: sudo apt install eclipse-jdt-ls\n \
|
||||
macOS: brew install jdtls\n \
|
||||
Other: see https://.eclipse.org/jdtls/#download"
|
||||
.to_string(),
|
||||
_ => format!("No automated install available for '{}'.", def.language),
|
||||
}
|
||||
@@ -446,35 +572,23 @@ pub fn provision_single(def: &LanguageServerDef, env: &EnvInfo) -> ProvisionResu
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special-case jdtls tier 3: no shell command, do it inline.
|
||||
if tier.command == "__jdtls_download__" {
|
||||
match install_jdtls_manual() {
|
||||
// If command is a download sentinel, dispatch to helper.
|
||||
if tier.command.starts_with("__download_") && tier.command.ends_with("__") {
|
||||
match run_download_tier(&tier.command, env) {
|
||||
Ok(path) => {
|
||||
// Prefer the known binary name on PATH; fall back to
|
||||
// the launcher script we just wrote.
|
||||
let found = def
|
||||
.binary_names
|
||||
.iter()
|
||||
.find_map(|b| which(b).map(|p| p.to_string_lossy().to_string()))
|
||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||
|
||||
info!(server = %def.name, tier = %tier.label, binary = %path.display(), "download install succeeded");
|
||||
return ProvisionResult::Installed {
|
||||
server_name: def.name.clone(),
|
||||
language: def.language.clone(),
|
||||
binary_path: found,
|
||||
binary_path: path.to_string_lossy().to_string(),
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
last_reason = format!("jdtls manual install failed: {}", e);
|
||||
warn!(
|
||||
server = %def.name,
|
||||
tier = %tier.label,
|
||||
error = %e,
|
||||
"manual install failed"
|
||||
);
|
||||
last_reason = format!("tier '{}' failed: {}", tier.label, e);
|
||||
warn!(server = %def.name, tier = %tier.label, error = %e, "download install failed");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal tier: shell out.
|
||||
@@ -552,10 +666,16 @@ pub fn provision_all() -> Vec<ProvisionResult> {
|
||||
linux = env.is_linux,
|
||||
macos = env.is_macos,
|
||||
rustup = env.has_rustup,
|
||||
cargo = env.has_cargo,
|
||||
npm = env.has_npm,
|
||||
go = env.has_go,
|
||||
java = env.has_java,
|
||||
curl = env.has_curl,
|
||||
tar = env.has_tar,
|
||||
pacman = env.has_pacman,
|
||||
apt = env.has_apt,
|
||||
brew = env.has_brew,
|
||||
dnf = env.has_dnf,
|
||||
"starting LSP provisioning"
|
||||
);
|
||||
supported_servers()
|
||||
|
||||
Reference in New Issue
Block a user