python: Use pip provided by our 'base' venv (#36414)

Piotr Osiewicz created

Closes #36218

Release Notes:

- Debugger: Python debugger installation no longer assumes that pip is
available in global Python installation

Change summary

crates/dap_adapters/src/python.rs | 58 +++++++++++++++++++++++++++-----
1 file changed, 48 insertions(+), 10 deletions(-)

Detailed changes

crates/dap_adapters/src/python.rs 🔗

@@ -24,6 +24,7 @@ use util::{ResultExt, maybe};
 
 #[derive(Default)]
 pub(crate) struct PythonDebugAdapter {
+    base_venv_path: OnceCell<Result<Arc<Path>, String>>,
     debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
 }
 
@@ -91,14 +92,12 @@ impl PythonDebugAdapter {
         })
     }
 
-    async fn fetch_wheel(delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
-        let system_python = Self::system_python_name(delegate)
-            .await
-            .ok_or_else(|| String::from("Could not find a Python installation"))?;
-        let command: &OsStr = system_python.as_ref();
+    async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
         let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
         std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
-        let installation_succeeded = util::command::new_smol_command(command)
+        let system_python = self.base_venv_path(delegate).await?;
+
+        let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
             .args([
                 "-m",
                 "pip",
@@ -114,7 +113,7 @@ impl PythonDebugAdapter {
             .status
             .success();
         if !installation_succeeded {
-            return Err("debugpy installation failed".into());
+            return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
         }
 
         let wheel_path = std::fs::read_dir(&download_dir)
@@ -139,7 +138,7 @@ impl PythonDebugAdapter {
         Ok(Arc::from(wheel_path.path()))
     }
 
-    async fn maybe_fetch_new_wheel(delegate: &Arc<dyn DapDelegate>) {
+    async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
         let latest_release = delegate
             .http_client()
             .get(
@@ -191,7 +190,7 @@ impl PythonDebugAdapter {
                     )
                     .await
                     .ok()?;
-                Self::fetch_wheel(delegate).await.ok()?;
+                self.fetch_wheel(delegate).await.ok()?;
             }
             Some(())
         })
@@ -204,7 +203,7 @@ impl PythonDebugAdapter {
     ) -> Result<Arc<Path>, String> {
         self.debugpy_whl_base_path
             .get_or_init(|| async move {
-                Self::maybe_fetch_new_wheel(delegate).await;
+                self.maybe_fetch_new_wheel(delegate).await;
                 Ok(Arc::from(
                     debug_adapters_dir()
                         .join(Self::ADAPTER_NAME)
@@ -217,6 +216,45 @@ impl PythonDebugAdapter {
             .clone()
     }
 
+    async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
+        self.base_venv_path
+            .get_or_init(|| async {
+                let base_python = Self::system_python_name(delegate)
+                    .await
+                    .ok_or_else(|| String::from("Could not find a Python installation"))?;
+
+                let did_succeed = util::command::new_smol_command(base_python)
+                    .args(["-m", "venv", "zed_base_venv"])
+                    .current_dir(
+                        paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()),
+                    )
+                    .spawn()
+                    .map_err(|e| format!("{e:#?}"))?
+                    .status()
+                    .await
+                    .map_err(|e| format!("{e:#?}"))?
+                    .success();
+                if !did_succeed {
+                    return Err("Failed to create base virtual environment".into());
+                }
+
+                const DIR: &'static str = if cfg!(target_os = "windows") {
+                    "Scripts"
+                } else {
+                    "bin"
+                };
+                Ok(Arc::from(
+                    paths::debug_adapters_dir()
+                        .join(Self::DEBUG_ADAPTER_NAME.as_ref())
+                        .join("zed_base_venv")
+                        .join(DIR)
+                        .join("python3")
+                        .as_ref(),
+                ))
+            })
+            .await
+            .clone()
+    }
     async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
         const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
         let mut name = None;