Skip to content

Commit

Permalink
Search the PATH if Python version can't be found by using py
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Feb 20, 2024
1 parent 4dfcf32 commit 17d8b2e
Show file tree
Hide file tree
Showing 8 changed files with 722 additions and 259 deletions.
92 changes: 56 additions & 36 deletions crates/uv-interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

Expand All @@ -17,7 +18,7 @@ use uv_fs::write_atomic_sync;

use crate::python_platform::PythonPlatform;
use crate::virtual_env::detect_virtual_env;
use crate::{find_requested_python, Error, PythonVersion};
use crate::{find_default_python, find_requested_python, Error, PythonVersion};

/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -170,38 +171,22 @@ impl Interpreter {

// Look for the requested version with by search for `python{major}.{minor}` in `PATH` on
// Unix and `py --list-paths` on Windows.
if let Some(python_version) = python_version {
if let Some(interpreter) =
find_requested_python(&python_version.string, platform, cache)?
{
if version_matches(&interpreter) {
return Ok(Some(interpreter));
}
let interpreter = if let Some(python_version) = python_version {
find_requested_python(&python_version.string, platform, cache)?
} else {
match find_default_python(platform, cache) {
Ok(interpreter) => Some(interpreter),
Err(Error::NoPythonInstalled) => None,
Err(err) => return Err(err),
}
}
};

// Python discovery failed to find the requested version, maybe the default Python in PATH
// matches?
if cfg!(unix) {
if let Some(executable) = Interpreter::find_executable("python3")? {
debug!("Resolved python3 to {}", executable.display());
let interpreter = Interpreter::query(&executable, &python_platform.0, cache)?;
if version_matches(&interpreter) {
return Ok(Some(interpreter));
}
}
} else if cfg!(windows) {
if let Some(executable) = Interpreter::find_executable("python.exe")? {
let interpreter = Interpreter::query(&executable, &python_platform.0, cache)?;
if version_matches(&interpreter) {
return Ok(Some(interpreter));
}
}
if let Some(interpreter) = interpreter {
debug_assert!(version_matches(&interpreter));
Ok(Some(interpreter))
} else {
unimplemented!("Only Windows and Unix are supported");
Ok(None)
}

Ok(None)
}

/// Find the Python interpreter in `PATH`, respecting `UV_PYTHON_PATH`.
Expand Down Expand Up @@ -324,13 +309,48 @@ pub(crate) struct InterpreterQueryResult {
impl InterpreterQueryResult {
/// Return the resolved [`InterpreterQueryResult`] for the given Python executable.
pub(crate) fn query(interpreter: &Path) -> Result<Self, Error> {
let output = Command::new(interpreter)
.args(["-c", include_str!("get_interpreter_info.py")])
.output()
.map_err(|err| Error::PythonSubcommandLaunch {
interpreter: interpreter.to_path_buf(),
err,
})?;
let script = include_str!("get_interpreter_info.py");
let output = if cfg!(windows)
&& interpreter
.extension()
.is_some_and(|extension| extension == "bat")
{
// pyenv-win shims fail with a syntax error when using `-c`.
// I haven't been able to figure out the reason why.
// So we use `-` to read from stdin. That's more expensive
// so avoid doing it for regular exe files.
let mut child = Command::new(interpreter)
.arg("-")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.map_err(|err| Error::PythonSubcommandLaunch {
interpreter: interpreter.to_path_buf(),
err,
})?;

let mut stdin = child.stdin.take().unwrap();

// From the Rust documentation:
// If the child process fills its stdout buffer, it may end up
// waiting until the parent reads the stdout, and not be able to
// read stdin in the meantime, causing a deadlock.
// Writing from another thread ensures that stdout is being read
// at the same time, avoiding the problem.
std::thread::spawn(move || {
stdin
.write_all(script.as_bytes())
.expect("failed to write to stdin");
});

child.wait_with_output()
} else {
Command::new(interpreter).arg("-c").arg(script).output()
}
.map_err(|err| Error::PythonSubcommandLaunch {
interpreter: interpreter.to_path_buf(),
err,
})?;

// stderr isn't technically a criterion for success, but i don't know of any cases where there
// should be stderr output and if there is, we want to know
Expand Down
8 changes: 1 addition & 7 deletions crates/uv-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,10 @@ pub enum Error {
},
#[error("Failed to run `py --list-paths` to find Python installations. Is Python installed?")]
PyList(#[source] io::Error),
#[cfg(windows)]
#[error("No Python {0} found through `py --list-paths`. Is Python {0} installed?")]
NoSuchPython(String),
#[cfg(unix)]
#[error("No Python {0} In `PATH`. Is Python {0} installed?")]
NoSuchPython(String),
#[error("Neither `python` nor `python3` are in `PATH`. Is Python installed?")]
NoPythonInstalledUnix,
#[error("Could not find `python.exe` in PATH and `py --list-paths` did not list any Python versions. Is Python installed?")]
NoPythonInstalledWindows,
NoPythonInstalled,
#[error("{message}:\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
PythonSubcommandOutput {
message: String,
Expand Down
Loading

0 comments on commit 17d8b2e

Please sign in to comment.