venus_server/
rust_analyzer.rs

1//! rust-analyzer management module.
2//!
3//! Downloads and caches rust-analyzer for LSP support.
4
5use std::path::PathBuf;
6use std::process::Stdio;
7
8use tokio::io::AsyncWriteExt;
9use tokio::process::Command;
10
11/// rust-analyzer version to download.
12const RUST_ANALYZER_VERSION: &str = "2025-12-29";
13
14/// Get the path to the cached rust-analyzer binary.
15pub fn cache_dir() -> PathBuf {
16    dirs::cache_dir()
17        .unwrap_or_else(|| PathBuf::from("."))
18        .join("venus")
19        .join("bin")
20}
21
22/// Get the expected rust-analyzer binary path.
23pub fn rust_analyzer_path() -> PathBuf {
24    let binary_name = if cfg!(windows) {
25        "rust-analyzer.exe"
26    } else {
27        "rust-analyzer"
28    };
29    cache_dir().join(binary_name)
30}
31
32/// Check if rust-analyzer is available (either cached or in PATH).
33pub async fn is_available() -> bool {
34    // First check cached version
35    let cached = rust_analyzer_path();
36    if cached.exists() {
37        return true;
38    }
39
40    // Fall back to system PATH
41    Command::new("rust-analyzer")
42        .arg("--version")
43        .stdout(Stdio::null())
44        .stderr(Stdio::null())
45        .status()
46        .await
47        .map(|s| s.success())
48        .unwrap_or(false)
49}
50
51/// Get the rust-analyzer command path (cached or system).
52pub async fn get_command_path() -> Option<PathBuf> {
53    let cached = rust_analyzer_path();
54    if cached.exists() {
55        return Some(cached);
56    }
57
58    // Check if available in PATH
59    if Command::new("rust-analyzer")
60        .arg("--version")
61        .stdout(Stdio::null())
62        .stderr(Stdio::null())
63        .status()
64        .await
65        .map(|s| s.success())
66        .unwrap_or(false)
67    {
68        return Some(PathBuf::from("rust-analyzer"));
69    }
70
71    None
72}
73
74/// Download rust-analyzer if not available.
75pub async fn ensure_available() -> Result<PathBuf, String> {
76    // Check if already available
77    if let Some(path) = get_command_path().await {
78        tracing::info!("rust-analyzer available at: {}", path.display());
79        return Ok(path);
80    }
81
82    tracing::info!("rust-analyzer not found, downloading...");
83    download().await
84}
85
86/// Download rust-analyzer from GitHub releases.
87pub async fn download() -> Result<PathBuf, String> {
88    let target = get_target_triple();
89    let url = format!(
90        "https://github.com/rust-lang/rust-analyzer/releases/download/{}/rust-analyzer-{}.gz",
91        RUST_ANALYZER_VERSION, target
92    );
93
94    tracing::info!("Downloading rust-analyzer from: {}", url);
95
96    // Create cache directory
97    let cache = cache_dir();
98    tokio::fs::create_dir_all(&cache)
99        .await
100        .map_err(|e| format!("Failed to create cache directory: {}", e))?;
101
102    // Download with reqwest
103    let response = reqwest::get(&url)
104        .await
105        .map_err(|e| format!("Failed to download rust-analyzer: {}", e))?;
106
107    if !response.status().is_success() {
108        return Err(format!(
109            "Failed to download rust-analyzer: HTTP {}",
110            response.status()
111        ));
112    }
113
114    let bytes = response
115        .bytes()
116        .await
117        .map_err(|e| format!("Failed to read response: {}", e))?;
118
119    // Decompress gzip
120    let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);
121    let mut decompressed = Vec::new();
122    std::io::Read::read_to_end(&mut decoder, &mut decompressed)
123        .map_err(|e| format!("Failed to decompress: {}", e))?;
124
125    // Write binary
126    let binary_path = rust_analyzer_path();
127    let mut file = tokio::fs::File::create(&binary_path)
128        .await
129        .map_err(|e| format!("Failed to create file: {}", e))?;
130    file.write_all(&decompressed)
131        .await
132        .map_err(|e| format!("Failed to write file: {}", e))?;
133
134    // Make executable on Unix
135    #[cfg(unix)]
136    {
137        use std::os::unix::fs::PermissionsExt;
138        let mut perms = tokio::fs::metadata(&binary_path)
139            .await
140            .map_err(|e| format!("Failed to get metadata: {}", e))?
141            .permissions();
142        perms.set_mode(0o755);
143        tokio::fs::set_permissions(&binary_path, perms)
144            .await
145            .map_err(|e| format!("Failed to set permissions: {}", e))?;
146    }
147
148    tracing::info!("rust-analyzer downloaded to: {}", binary_path.display());
149    Ok(binary_path)
150}
151
152/// Get the target triple for the current platform.
153fn get_target_triple() -> &'static str {
154    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
155    {
156        "x86_64-unknown-linux-gnu"
157    }
158    #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
159    {
160        "aarch64-unknown-linux-gnu"
161    }
162    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
163    {
164        "x86_64-apple-darwin"
165    }
166    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
167    {
168        "aarch64-apple-darwin"
169    }
170    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
171    {
172        "x86_64-pc-windows-msvc"
173    }
174    #[cfg(not(any(
175        all(target_os = "linux", target_arch = "x86_64"),
176        all(target_os = "linux", target_arch = "aarch64"),
177        all(target_os = "macos", target_arch = "x86_64"),
178        all(target_os = "macos", target_arch = "aarch64"),
179        all(target_os = "windows", target_arch = "x86_64"),
180    )))]
181    {
182        compile_error!("Unsupported platform for rust-analyzer download")
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_cache_dir() {
192        let dir = cache_dir();
193        assert!(dir.ends_with("venus/bin") || dir.ends_with("venus\\bin"));
194    }
195
196    #[test]
197    fn test_target_triple() {
198        let triple = get_target_triple();
199        assert!(!triple.is_empty());
200    }
201}