debugger/setup/adapters/
delve.rs

1//! Delve installer
2//!
3//! Installs the Go debugger with DAP support.
4
5use crate::common::{Error, Result};
6use crate::setup::installer::{
7    adapters_dir, arch_str, download_file, ensure_adapters_dir, extract_tar_gz,
8    get_github_release, make_executable, platform_str, read_version_file, run_command_args,
9    write_version_file, InstallMethod, InstallOptions, InstallResult, InstallStatus, Installer,
10    PackageManager,
11};
12use crate::setup::registry::{DebuggerInfo, Platform};
13use crate::setup::verifier::{verify_dap_adapter, VerifyResult};
14use async_trait::async_trait;
15use std::path::PathBuf;
16
17static INFO: DebuggerInfo = DebuggerInfo {
18    id: "go",
19    name: "Delve",
20    languages: &["go"],
21    platforms: &[Platform::Linux, Platform::MacOS, Platform::Windows],
22    description: "Go debugger with DAP support",
23    primary: true,
24};
25
26const GITHUB_REPO: &str = "go-delve/delve";
27
28pub struct DelveInstaller;
29
30#[async_trait]
31impl Installer for DelveInstaller {
32    fn info(&self) -> &DebuggerInfo {
33        &INFO
34    }
35
36    async fn status(&self) -> Result<InstallStatus> {
37        // Check our managed installation first
38        let adapter_dir = adapters_dir().join("delve");
39        let managed_path = adapter_dir.join("bin").join(binary_name());
40
41        if managed_path.exists() {
42            let version = read_version_file(&adapter_dir);
43            return Ok(InstallStatus::Installed {
44                path: managed_path,
45                version,
46            });
47        }
48
49        // Check if dlv is available in PATH
50        if let Ok(path) = which::which("dlv") {
51            let version = get_version(&path).await;
52            return Ok(InstallStatus::Installed { path, version });
53        }
54
55        Ok(InstallStatus::NotInstalled)
56    }
57
58    async fn best_method(&self) -> Result<InstallMethod> {
59        // Check if already in PATH
60        if let Ok(path) = which::which("dlv") {
61            return Ok(InstallMethod::AlreadyInstalled { path });
62        }
63
64        let managers = PackageManager::detect();
65
66        // Prefer go install if Go is available
67        if managers.contains(&PackageManager::Go) {
68            return Ok(InstallMethod::LanguagePackage {
69                tool: "go".to_string(),
70                package: "github.com/go-delve/delve/cmd/dlv@latest".to_string(),
71            });
72        }
73
74        // Fallback to GitHub releases
75        Ok(InstallMethod::GitHubRelease {
76            repo: GITHUB_REPO.to_string(),
77            asset_pattern: format!("delve_*_{}_*.tar.gz", platform_str()),
78        })
79    }
80
81    async fn install(&self, opts: InstallOptions) -> Result<InstallResult> {
82        let method = self.best_method().await?;
83
84        match method {
85            InstallMethod::AlreadyInstalled { path } => {
86                let version = get_version(&path).await;
87                Ok(InstallResult {
88                    path,
89                    version,
90                    args: vec!["dap".to_string()],
91                })
92            }
93            InstallMethod::LanguagePackage { tool, package } => {
94                install_via_go(&tool, &package, &opts).await
95            }
96            InstallMethod::GitHubRelease { .. } => install_from_github(&opts).await,
97            _ => Err(Error::Internal("Unexpected installation method".to_string())),
98        }
99    }
100
101    async fn uninstall(&self) -> Result<()> {
102        let adapter_dir = adapters_dir().join("delve");
103        if adapter_dir.exists() {
104            std::fs::remove_dir_all(&adapter_dir)?;
105            println!("Removed {}", adapter_dir.display());
106        } else {
107            println!("Delve is not installed in managed location");
108            if let Ok(path) = which::which("dlv") {
109                println!("Found dlv at: {}", path.display());
110                println!("If installed via 'go install', it's in your GOPATH/bin.");
111            }
112        }
113        Ok(())
114    }
115
116    async fn verify(&self) -> Result<VerifyResult> {
117        let status = self.status().await?;
118
119        match status {
120            InstallStatus::Installed { path, .. } => {
121                // Delve uses 'dap' subcommand for DAP mode
122                verify_dap_adapter(&path, &["dap".to_string()]).await
123            }
124            InstallStatus::Broken { reason, .. } => Ok(VerifyResult {
125                success: false,
126                capabilities: None,
127                error: Some(reason),
128            }),
129            InstallStatus::NotInstalled => Ok(VerifyResult {
130                success: false,
131                capabilities: None,
132                error: Some("Not installed".to_string()),
133            }),
134        }
135    }
136}
137
138fn binary_name() -> &'static str {
139    if cfg!(windows) {
140        "dlv.exe"
141    } else {
142        "dlv"
143    }
144}
145
146async fn get_version(path: &PathBuf) -> Option<String> {
147    let output = tokio::process::Command::new(path)
148        .arg("version")
149        .output()
150        .await
151        .ok()?;
152
153    if output.status.success() {
154        let stdout = String::from_utf8_lossy(&output.stdout);
155        // Parse version from output like "Delve Debugger\nVersion: 1.22.0"
156        stdout
157            .lines()
158            .find(|line| line.starts_with("Version:"))
159            .and_then(|line| line.strip_prefix("Version:"))
160            .map(|s| s.trim().to_string())
161    } else {
162        None
163    }
164}
165
166async fn install_via_go(tool: &str, package: &str, opts: &InstallOptions) -> Result<InstallResult> {
167    println!("Checking for existing installation... not found");
168    println!("Installing via go install...");
169
170    let package = if let Some(version) = &opts.version {
171        format!(
172            "github.com/go-delve/delve/cmd/dlv@v{}",
173            version.trim_start_matches('v')
174        )
175    } else {
176        package.to_string()
177    };
178
179    println!("Running: {} install {}", tool, package);
180
181    // Use run_command_args to prevent command injection
182    let go_path = which::which(tool).map_err(|_| {
183        Error::Internal(format!("{} not found in PATH", tool))
184    })?;
185    run_command_args(&go_path, &["install", &package]).await?;
186
187    // Find the installed binary
188    let path = which::which("dlv").map_err(|_| {
189        Error::Internal(
190            "dlv not found after installation. Make sure GOPATH/bin is in your PATH.".to_string(),
191        )
192    })?;
193
194    let version = get_version(&path).await;
195
196    println!("Setting permissions... done");
197    println!("Verifying installation...");
198
199    Ok(InstallResult {
200        path,
201        version,
202        args: vec!["dap".to_string()],
203    })
204}
205
206async fn install_from_github(opts: &InstallOptions) -> Result<InstallResult> {
207    println!("Checking for existing installation... not found");
208    println!("Finding latest Delve release...");
209
210    let release = get_github_release(GITHUB_REPO, opts.version.as_deref()).await?;
211    let version = release.tag_name.trim_start_matches('v').to_string();
212    println!("Found version: {}", version);
213
214    // Find appropriate asset
215    let platform = platform_str();
216    let arch = arch_str();
217
218    // Map arch to delve naming convention
219    let delve_arch = match arch {
220        "x86_64" => "amd64",
221        "aarch64" => "arm64",
222        _ => arch,
223    };
224
225    let patterns = vec![
226        format!("delve_{}_{}.tar.gz", platform, delve_arch),
227        format!("delve_*_{}_{}.tar.gz", platform, delve_arch),
228    ];
229
230    let asset = release
231        .find_asset(&patterns.iter().map(|s| s.as_str()).collect::<Vec<_>>())
232        .ok_or_else(|| {
233            Error::Internal(format!(
234                "No Delve release found for {} {}. Available assets: {:?}",
235                arch,
236                platform,
237                release.assets.iter().map(|a| &a.name).collect::<Vec<_>>()
238            ))
239        })?;
240
241    // Create temp directory for download
242    let temp_dir = tempfile::tempdir()?;
243    let archive_path = temp_dir.path().join(&asset.name);
244
245    println!(
246        "Downloading {}... {:.1} MB",
247        asset.name,
248        asset.size as f64 / 1_000_000.0
249    );
250    download_file(&asset.browser_download_url, &archive_path).await?;
251
252    println!("Extracting...");
253    extract_tar_gz(&archive_path, temp_dir.path())?;
254
255    // Find dlv binary in extracted directory (check root first, then subdirectories)
256    let dlv_src = temp_dir.path().join(binary_name());
257    let dlv_src = if dlv_src.exists() {
258        dlv_src
259    } else {
260        // Try looking in a subdirectory
261        std::fs::read_dir(temp_dir.path())?
262            .filter_map(|e| e.ok())
263            .find(|e| e.path().is_dir())
264            .map(|e| e.path().join(binary_name()))
265            .filter(|p| p.exists())
266            .ok_or_else(|| Error::Internal("dlv binary not found in downloaded archive".to_string()))?
267    };
268
269    // Create installation directory
270    let adapter_dir = ensure_adapters_dir()?.join("delve");
271    let bin_dir = adapter_dir.join("bin");
272    std::fs::create_dir_all(&bin_dir)?;
273
274    // Copy dlv binary
275    let dest_path = bin_dir.join(binary_name());
276    std::fs::copy(&dlv_src, &dest_path)?;
277    make_executable(&dest_path)?;
278
279    // Write version file
280    write_version_file(&adapter_dir, &version)?;
281
282    println!("Setting permissions... done");
283    println!("Verifying installation...");
284
285    Ok(InstallResult {
286        path: dest_path,
287        version: Some(version),
288        args: vec!["dap".to_string()],
289    })
290}