diff --git a/Cargo.lock b/Cargo.lock index ef34d6501..5605331d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2616,6 +2616,7 @@ dependencies = [ "platform-tags", "puffin-cache", "puffin-fs", + "puffin-warnings", "regex", "rmp-serde", "serde", diff --git a/crates/puffin-interpreter/Cargo.toml b/crates/puffin-interpreter/Cargo.toml index a03ebe299..6d0eae9a9 100644 --- a/crates/puffin-interpreter/Cargo.toml +++ b/crates/puffin-interpreter/Cargo.toml @@ -20,6 +20,7 @@ platform-host = { path = "../platform-host" } platform-tags = { path = "../platform-tags" } puffin-cache = { path = "../puffin-cache" } puffin-fs = { path = "../puffin-fs" } +puffin-warnings = { path = "../puffin-warnings" } fs-err = { workspace = true, features = ["tokio"] } once_cell = { workspace = true } diff --git a/crates/puffin-interpreter/src/interpreter.rs b/crates/puffin-interpreter/src/interpreter.rs index 9b5cf3ea4..fe6c506be 100644 --- a/crates/puffin-interpreter/src/interpreter.rs +++ b/crates/puffin-interpreter/src/interpreter.rs @@ -1,3 +1,4 @@ +use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -18,6 +19,7 @@ use crate::python_platform::PythonPlatform; use crate::python_query::find_python_windows; use crate::virtual_env::detect_virtual_env; use crate::{Error, PythonVersion}; +use puffin_warnings::warn_user_once; /// A Python executable and its associated platform markers. #[derive(Debug, Clone)] @@ -132,6 +134,10 @@ impl Interpreter { /// - If a python version is given: `pythonx.y` /// - `python3` (unix) or `python.exe` (windows) /// + /// If `PUFFIN_PYTHON_PATH` is set, we will not check for Python versions in the + /// global PATH, instead we will search using the provided path. Virtual environments + /// will still be respected. + /// /// If a version is provided and an interpreter cannot be found with the given version, /// we will return [`None`]. pub fn find_version( @@ -166,7 +172,8 @@ impl Interpreter { python_version.major(), python_version.minor() ); - if let Ok(executable) = which::which(&requested) { + + if let Ok(executable) = Interpreter::find_executable(&requested) { debug!("Resolved {requested} to {}", executable.display()); let interpreter = Interpreter::query(&executable, &platform.0, cache)?; if version_matches(&interpreter) { @@ -175,7 +182,7 @@ impl Interpreter { } } - if let Ok(executable) = which::which("python3") { + if let Ok(executable) = Interpreter::find_executable("python3") { debug!("Resolved python3 to {}", executable.display()); let interpreter = Interpreter::query(&executable, &platform.0, cache)?; if version_matches(&interpreter) { @@ -194,7 +201,7 @@ impl Interpreter { } } - if let Ok(executable) = which::which("python.exe") { + if let Ok(executable) = Interpreter::find_executable("python.exe") { let interpreter = Interpreter::query(&executable, &platform.0, cache)?; if version_matches(&interpreter) { return Ok(Some(interpreter)); @@ -207,6 +214,26 @@ impl Interpreter { Ok(None) } + pub fn find_executable + Into + Copy>( + requested: R, + ) -> Result { + if let Some(isolated) = std::env::var_os("PUFFIN_PYTHON_PATH") { + warn_user_once!( + "PUFFIN_PYTHON_PATH is set; ignoring system PATH when looking for binaries." + ); + if let Ok(cwd) = std::env::current_dir() { + which::which_in(&requested, Some(isolated), cwd) + .map_err(|err| Error::Which(requested.into(), err)) + } else { + which::which_in_global(requested, Some(isolated)) + .map_err(|err| Error::Which(requested.into(), err)) + .and_then(|mut paths| paths.next().ok_or(Error::PythonNotFound)) + } + } else { + which::which(&requested).map_err(|err| Error::Which(requested.into(), err)) + } + } + /// Returns the path to the Python virtual environment. #[inline] pub fn platform(&self) -> &Platform { diff --git a/crates/puffin-interpreter/src/lib.rs b/crates/puffin-interpreter/src/lib.rs index 193110e40..387af3efa 100644 --- a/crates/puffin-interpreter/src/lib.rs +++ b/crates/puffin-interpreter/src/lib.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use std::io; use std::path::PathBuf; use std::time::SystemTimeError; @@ -59,6 +60,6 @@ pub enum Error { Encode(#[from] rmp_serde::encode::Error), #[error("Failed to parse pyvenv.cfg")] Cfg(#[from] cfg::Error), - #[error("Couldn't find `{0}` in PATH")] - Which(PathBuf, #[source] which::Error), + #[error("Couldn't find {0:?} in PATH")] + Which(OsString, #[source] which::Error), } diff --git a/crates/puffin-interpreter/src/python_query.rs b/crates/puffin-interpreter/src/python_query.rs index 6251b5d4e..605f49d7e 100644 --- a/crates/puffin-interpreter/src/python_query.rs +++ b/crates/puffin-interpreter/src/python_query.rs @@ -7,7 +7,7 @@ use once_cell::sync::Lazy; use regex::Regex; use tracing::info_span; -use crate::Error; +use crate::{Error, Interpreter}; /// ```text /// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe @@ -34,7 +34,7 @@ pub fn find_requested_python(request: &str) -> Result { // `-p 3.10` or `-p 3.10.1` if cfg!(unix) { let formatted = PathBuf::from(format!("python{request}")); - which::which_global(&formatted).map_err(|err| Error::Which(formatted, err)) + Interpreter::find_executable(&formatted) } else if cfg!(windows) { if let [major, minor] = versions.as_slice() { find_python_windows(*major, *minor)?.ok_or(Error::NoSuchPython { @@ -49,8 +49,7 @@ pub fn find_requested_python(request: &str) -> Result { } } else if !request.contains(std::path::MAIN_SEPARATOR) { // `-p python3.10`; Generally not used on windows because all Python are `python.exe`. - let request = PathBuf::from(request); - which::which_global(&request).map_err(|err| Error::Which(request, err)) + Interpreter::find_executable(&request) } else { // `-p /home/ferris/.local/bin/python3.10` Ok(fs_err::canonicalize(request)?) diff --git a/crates/puffin/src/commands/venv.rs b/crates/puffin/src/commands/venv.rs index 67c8ae01a..afce587db 100644 --- a/crates/puffin/src/commands/venv.rs +++ b/crates/puffin/src/commands/venv.rs @@ -85,8 +85,8 @@ async fn venv_impl( find_requested_python(python_request).into_diagnostic()? } else { fs::canonicalize( - which::which_global("python3") - .or_else(|_| which::which_global("python")) + Interpreter::find_executable("python3") + .or_else(|_| Interpreter::find_executable("python")) .map_err(|_| VenvError::PythonNotFound)?, ) .into_diagnostic()?