@@ -1,12 +1,12 @@
use crate::*;
-use anyhow::Context as _;
+use anyhow::{Context as _, bail};
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use fs::RemoveOptions;
use futures::{StreamExt, TryStreamExt};
use gpui::http_client::AsyncBody;
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
-use language::LanguageName;
+use language::{LanguageName, Toolchain};
use paths::debug_adapters_dir;
use serde_json::Value;
use smol::fs::File;
@@ -20,7 +20,8 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
-use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
+use util::command::new_smol_command;
+use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
@@ -92,12 +93,16 @@ impl PythonDebugAdapter {
})
}
- async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
+ async fn fetch_wheel(
+ &self,
+ toolchain: Option<Toolchain>,
+ delegate: &Arc<dyn DapDelegate>,
+ ) -> Result<Arc<Path>> {
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 system_python = self.base_venv_path(delegate).await?;
+ std::fs::create_dir_all(&download_dir)?;
+ let venv_python = self.base_venv_path(toolchain, delegate).await?;
- let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
+ let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
.args([
"-m",
"pip",
@@ -109,36 +114,36 @@ impl PythonDebugAdapter {
])
.output()
.await
- .map_err(|e| format!("{e}"))?
+ .context("spawn system python")?
.status
.success();
if !installation_succeeded {
- return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
+ bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
}
- let wheel_path = std::fs::read_dir(&download_dir)
- .map_err(|e| e.to_string())?
+ let wheel_path = std::fs::read_dir(&download_dir)?
.find_map(|entry| {
entry.ok().filter(|e| {
e.file_type().is_ok_and(|typ| typ.is_file())
&& Path::new(&e.file_name()).extension() == Some("whl".as_ref())
})
})
- .ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?;
+ .with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
util::archive::extract_zip(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
- File::open(&wheel_path.path())
- .await
- .map_err(|e| e.to_string())?,
+ File::open(&wheel_path.path()).await?,
)
- .await
- .map_err(|e| e.to_string())?;
+ .await?;
Ok(Arc::from(wheel_path.path()))
}
- async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
+ async fn maybe_fetch_new_wheel(
+ &self,
+ toolchain: Option<Toolchain>,
+ delegate: &Arc<dyn DapDelegate>,
+ ) -> Result<()> {
let latest_release = delegate
.http_client()
.get(
@@ -148,62 +153,61 @@ impl PythonDebugAdapter {
)
.await
.log_err();
- maybe!(async move {
- let response = latest_release.filter(|response| response.status().is_success())?;
-
- let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
- std::fs::create_dir_all(&download_dir).ok()?;
-
- let mut output = String::new();
- response
- .into_body()
- .read_to_string(&mut output)
- .await
- .ok()?;
- let as_json = serde_json::Value::from_str(&output).ok()?;
- let latest_version = as_json.get("info").and_then(|info| {
+ let response = latest_release
+ .filter(|response| response.status().is_success())
+ .context("getting latest release")?;
+
+ let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
+ std::fs::create_dir_all(&download_dir)?;
+
+ let mut output = String::new();
+ response.into_body().read_to_string(&mut output).await?;
+ let as_json = serde_json::Value::from_str(&output)?;
+ let latest_version = as_json
+ .get("info")
+ .and_then(|info| {
info.get("version")
.and_then(|version| version.as_str())
.map(ToOwned::to_owned)
- })?;
- let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
- let is_up_to_date = delegate
- .fs()
- .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
- .await
- .ok()?
- .into_stream()
- .any(async |entry| {
- entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
- })
- .await;
+ })
+ .context("parsing latest release information")?;
+ let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
+ let is_up_to_date = delegate
+ .fs()
+ .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
+ .await?
+ .into_stream()
+ .any(async |entry| {
+ entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
+ })
+ .await;
- if !is_up_to_date {
- delegate
- .fs()
- .remove_dir(
- &debug_adapters_dir().join(Self::ADAPTER_NAME),
- RemoveOptions {
- recursive: true,
- ignore_if_not_exists: true,
- },
- )
- .await
- .ok()?;
- self.fetch_wheel(delegate).await.ok()?;
- }
- Some(())
- })
- .await;
+ if !is_up_to_date {
+ delegate
+ .fs()
+ .remove_dir(
+ &debug_adapters_dir().join(Self::ADAPTER_NAME),
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: true,
+ },
+ )
+ .await?;
+ self.fetch_wheel(toolchain, delegate).await?;
+ }
+ anyhow::Ok(())
}
async fn fetch_debugpy_whl(
&self,
+ toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> 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(toolchain, delegate)
+ .await
+ .map_err(|e| format!("{e}"))?;
Ok(Arc::from(
debug_adapters_dir()
.join(Self::ADAPTER_NAME)
@@ -216,12 +220,24 @@ impl PythonDebugAdapter {
.clone()
}
- async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
- self.base_venv_path
+ async fn base_venv_path(
+ &self,
+ toolchain: Option<Toolchain>,
+ delegate: &Arc<dyn DapDelegate>,
+ ) -> Result<Arc<Path>> {
+ let result = 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 base_python = if let Some(toolchain) = toolchain {
+ toolchain.path.to_string()
+ } else {
+ Self::system_python_name(delegate).await.ok_or_else(|| {
+ let mut message = "Could not find a Python installation".to_owned();
+ if cfg!(windows){
+ message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
+ }
+ message
+ })?
+ };
let did_succeed = util::command::new_smol_command(base_python)
.args(["-m", "venv", "zed_base_venv"])
@@ -239,35 +255,50 @@ impl PythonDebugAdapter {
return Err("Failed to create base virtual environment".into());
}
- const DIR: &str = if cfg!(target_os = "windows") {
- "Scripts"
+ const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
+ "Scripts/python.exe"
} else {
- "bin"
+ "bin/python3"
};
Ok(Arc::from(
paths::debug_adapters_dir()
.join(Self::DEBUG_ADAPTER_NAME.as_ref())
.join("zed_base_venv")
- .join(DIR)
- .join("python3")
+ .join(PYTHON_PATH)
.as_ref(),
))
})
.await
- .clone()
+ .clone();
+ match result {
+ Ok(path) => Ok(path),
+ Err(e) => Err(anyhow::anyhow!("{e}")),
+ }
}
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let mut name = None;
for cmd in BINARY_NAMES {
- name = delegate
- .which(OsStr::new(cmd))
+ let Some(path) = delegate.which(OsStr::new(cmd)).await else {
+ continue;
+ };
+ // Try to detect situations where `python3` exists but is not a real Python interpreter.
+ // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
+ // when run with no arguments, and just fails otherwise.
+ let Some(output) = new_smol_command(&path)
+ .args(["-c", "print(1 + 2)"])
+ .output()
.await
- .map(|path| path.to_string_lossy().into_owned());
- if name.is_some() {
- break;
+ .ok()
+ else {
+ continue;
+ };
+ if output.stdout.trim_ascii() != b"3" {
+ continue;
}
+ name = Some(path.to_string_lossy().into_owned());
+ break;
}
name
}
@@ -746,15 +777,10 @@ impl DebugAdapter for PythonDebugAdapter {
)
.await;
- let debugpy_path = self
- .fetch_debugpy_whl(delegate)
+ self.fetch_debugpy_whl(toolchain.clone(), delegate)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(toolchain) = &toolchain {
- log::debug!(
- "Found debugpy in toolchain environment: {}",
- debugpy_path.display()
- );
return self
.get_installed_binary(
delegate,
@@ -23,6 +23,7 @@ use serde_json::{Value, json};
use smol::lock::OnceCell;
use std::cmp::Ordering;
use std::env::consts;
+use util::command::new_smol_command;
use util::fs::{make_file_executable, remove_matching};
use util::rel_path::RelPath;
@@ -1330,7 +1331,13 @@ impl PyLspAdapter {
async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
let python_path = Self::find_base_python(delegate)
.await
- .context("Could not find Python installation for PyLSP")?;
+ .with_context(|| {
+ let mut message = "Could not find Python installation for PyLSP".to_owned();
+ if cfg!(windows){
+ message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
+ }
+ message
+ })?;
let work_dir = delegate
.language_server_download_dir(&Self::SERVER_NAME)
.await
@@ -1353,9 +1360,24 @@ impl PyLspAdapter {
// Find "baseline", user python version from which we'll create our own venv.
async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
for path in ["python3", "python"] {
- if let Some(path) = delegate.which(path.as_ref()).await {
- return Some(path);
+ let Some(path) = delegate.which(path.as_ref()).await else {
+ continue;
+ };
+ // Try to detect situations where `python3` exists but is not a real Python interpreter.
+ // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
+ // when run with no arguments, and just fails otherwise.
+ let Some(output) = new_smol_command(&path)
+ .args(["-c", "print(1 + 2)"])
+ .output()
+ .await
+ .ok()
+ else {
+ continue;
+ };
+ if output.stdout.trim_ascii() != b"3" {
+ continue;
}
+ return Some(path);
}
None
}