1use 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_tcp, 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 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 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 if let Ok(path) = which::which("dlv") {
61 return Ok(InstallMethod::AlreadyInstalled { path });
62 }
63
64 let managers = PackageManager::detect();
65
66 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 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 verify_dap_adapter_tcp(&path, &["dap".to_string()], crate::common::config::TcpSpawnStyle::TcpListen).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 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 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 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 let platform = platform_str();
216 let arch = arch_str();
217
218 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 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 let dlv_src = temp_dir.path().join(binary_name());
257 let dlv_src = if dlv_src.exists() {
258 dlv_src
259 } else {
260 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 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 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(&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}