debugger/setup/adapters/
debugpy.rs

1//! debugpy installer
2//!
3//! Installs Microsoft's Python debugger via pip in an isolated virtual environment.
4
5use crate::common::{Error, Result};
6use crate::setup::installer::{
7    adapters_dir, ensure_adapters_dir, run_command_args, write_version_file,
8    InstallMethod, InstallOptions, InstallResult, InstallStatus, Installer,
9};
10use crate::setup::registry::{DebuggerInfo, Platform};
11use crate::setup::verifier::{verify_dap_adapter, VerifyResult};
12use async_trait::async_trait;
13use std::path::PathBuf;
14
15static INFO: DebuggerInfo = DebuggerInfo {
16    id: "python",
17    name: "debugpy",
18    languages: &["python"],
19    platforms: &[Platform::Linux, Platform::MacOS, Platform::Windows],
20    description: "Microsoft's Python debugger",
21    primary: true,
22};
23
24pub struct DebugpyInstaller;
25
26#[async_trait]
27impl Installer for DebugpyInstaller {
28    fn info(&self) -> &DebuggerInfo {
29        &INFO
30    }
31
32    async fn status(&self) -> Result<InstallStatus> {
33        let adapter_dir = adapters_dir().join("debugpy");
34        let venv_dir = adapter_dir.join("venv");
35        let python_path = get_venv_python(&venv_dir);
36
37        if python_path.exists() {
38            // Verify debugpy is installed in the venv
39            let check = run_command_args(
40                &python_path,
41                &["-c", "import debugpy; print(debugpy.__version__)"],
42            )
43            .await;
44
45            match check {
46                Ok(version) => {
47                    return Ok(InstallStatus::Installed {
48                        path: python_path,
49                        version: Some(version.trim().to_string()),
50                    });
51                }
52                Err(_) => {
53                    return Ok(InstallStatus::Broken {
54                        path: python_path,
55                        reason: "debugpy module not found in venv".to_string(),
56                    });
57                }
58            }
59        }
60
61        // Check if debugpy is installed globally
62        if let Ok(python_path) = which::which("python3") {
63            if let Ok(version) = run_command_args(
64                &python_path,
65                &["-c", "import debugpy; print(debugpy.__version__)"],
66            )
67            .await
68            {
69                return Ok(InstallStatus::Installed {
70                    path: python_path,
71                    version: Some(version.trim().to_string()),
72                });
73            }
74        }
75
76        Ok(InstallStatus::NotInstalled)
77    }
78
79    async fn best_method(&self) -> Result<InstallMethod> {
80        // Check if Python is available
81        let python = find_python().await?;
82
83        Ok(InstallMethod::LanguagePackage {
84            tool: python.to_string_lossy().to_string(),
85            package: "debugpy".to_string(),
86        })
87    }
88
89    async fn install(&self, opts: InstallOptions) -> Result<InstallResult> {
90        install_debugpy(&opts).await
91    }
92
93    async fn uninstall(&self) -> Result<()> {
94        let adapter_dir = adapters_dir().join("debugpy");
95        if adapter_dir.exists() {
96            std::fs::remove_dir_all(&adapter_dir)?;
97            println!("Removed {}", adapter_dir.display());
98        } else {
99            println!("debugpy managed installation not found");
100            println!("If installed globally, use: pip uninstall debugpy");
101        }
102        Ok(())
103    }
104
105    async fn verify(&self) -> Result<VerifyResult> {
106        let status = self.status().await?;
107
108        match status {
109            InstallStatus::Installed { path, .. } => {
110                // debugpy requires special arguments to start as DAP adapter
111                verify_dap_adapter(&path, &["-m".to_string(), "debugpy.adapter".to_string()]).await
112            }
113            InstallStatus::Broken { reason, .. } => Ok(VerifyResult {
114                success: false,
115                capabilities: None,
116                error: Some(reason),
117            }),
118            InstallStatus::NotInstalled => Ok(VerifyResult {
119                success: false,
120                capabilities: None,
121                error: Some("Not installed".to_string()),
122            }),
123        }
124    }
125}
126
127/// Find a suitable Python interpreter
128async fn find_python() -> Result<PathBuf> {
129    // Try python3 first, then python
130    for cmd in &["python3", "python"] {
131        if let Ok(path) = which::which(cmd) {
132            // Verify it's Python 3.7+ using explicit exit code (not assertion)
133            let version_check = run_command_args(
134                &path,
135                &["-c", "import sys; sys.exit(0 if sys.version_info >= (3, 7) else 1)"],
136            )
137            .await;
138
139            if version_check.is_ok() {
140                return Ok(path);
141            }
142        }
143    }
144
145    Err(Error::Internal(
146        "Python 3.7+ not found. Please install Python first.".to_string(),
147    ))
148}
149
150/// Get the path to Python in a venv
151fn get_venv_python(venv_dir: &PathBuf) -> PathBuf {
152    if cfg!(windows) {
153        venv_dir.join("Scripts").join("python.exe")
154    } else {
155        venv_dir.join("bin").join("python")
156    }
157}
158
159/// Get the path to pip in a venv
160fn get_venv_pip(venv_dir: &PathBuf) -> PathBuf {
161    if cfg!(windows) {
162        venv_dir.join("Scripts").join("pip.exe")
163    } else {
164        venv_dir.join("bin").join("pip")
165    }
166}
167
168async fn install_debugpy(opts: &InstallOptions) -> Result<InstallResult> {
169    println!("Checking for existing installation... not found");
170
171    // Find Python
172    let python = find_python().await?;
173    println!("Using Python: {}", python.display());
174
175    // Create installation directory
176    let adapter_dir = ensure_adapters_dir()?.join("debugpy");
177    let venv_dir = adapter_dir.join("venv");
178
179    // Remove existing venv if force
180    if opts.force && venv_dir.exists() {
181        std::fs::remove_dir_all(&venv_dir)?;
182    }
183
184    // Create virtual environment
185    if !venv_dir.exists() {
186        println!("Creating virtual environment...");
187        run_command_args(&python, &["-m", "venv", venv_dir.to_str().unwrap_or("venv")]).await?;
188    }
189
190    // Get venv pip
191    let pip = get_venv_pip(&venv_dir);
192    let venv_python = get_venv_python(&venv_dir);
193
194    // Upgrade pip first
195    println!("Upgrading pip...");
196    let _ = run_command_args(&venv_python, &["-m", "pip", "install", "--upgrade", "pip"]).await;
197
198    // Install debugpy - use separate args to prevent command injection
199    let package = if let Some(version) = &opts.version {
200        format!("debugpy=={}", version)
201    } else {
202        "debugpy".to_string()
203    };
204
205    println!("Installing {}...", package);
206    run_command_args(&pip, &["install", &package]).await?;
207
208    // Get installed version
209    let version = run_command_args(
210        &venv_python,
211        &["-c", "import debugpy; print(debugpy.__version__)"],
212    )
213    .await
214    .ok()
215    .map(|s| s.trim().to_string());
216
217    // Write version file
218    if let Some(v) = &version {
219        write_version_file(&adapter_dir, v)?;
220    }
221
222    println!("Setting permissions... done");
223    println!("Verifying installation...");
224
225    Ok(InstallResult {
226        path: venv_python,
227        version,
228        args: vec!["-m".to_string(), "debugpy.adapter".to_string()],
229    })
230}