Skip to content

Commit

Permalink
Move py launcher handling into separate module (#3329)
Browse files Browse the repository at this point in the history
Split out of #3266 

Mostly an organizational change, with some error handling
simplification.
  • Loading branch information
zanieb authored May 2, 2024
1 parent 2e27abd commit 528bed5
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 92 deletions.
99 changes: 9 additions & 90 deletions crates/uv-interpreter/src/find_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use uv_warnings::warn_user_once;

use crate::environment::python_environment::{detect_python_executable, detect_virtual_env};
use crate::interpreter::InterpreterInfoError;
use crate::py_launcher::{py_list_paths, Error as PyLauncherError, PyListPath};
use crate::PythonVersion;
use crate::{Error, Interpreter};

Expand Down Expand Up @@ -202,7 +203,7 @@ fn find_python(

if cfg!(windows) && !use_override {
// Use `py` to find the python installation on the system.
match windows::py_list_paths() {
match py_list_paths() {
Ok(paths) => {
for entry in paths {
let installation = PythonInstallation::PyListPath(entry);
Expand All @@ -211,12 +212,9 @@ fn find_python(
}
}
}
Err(Error::PyList(error)) => {
if error.kind() == std::io::ErrorKind::NotFound {
debug!("`py` is not installed");
}
}
Err(error) => return Err(error),
// Do not error when `py` is not available
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
Err(error) => return Err(Error::PyLauncher(error)),
}
}

Expand Down Expand Up @@ -263,7 +261,7 @@ fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(

if cfg!(windows) && !use_override {
// Use `py` to find the python installation on the system.
match windows::py_list_paths() {
match py_list_paths() {
Ok(paths) => {
for entry in paths {
// Ex) `--python python3.12.exe`
Expand All @@ -281,25 +279,15 @@ fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
}
}
}
Err(Error::PyList(error)) => {
if error.kind() == std::io::ErrorKind::NotFound {
debug!("`py` is not installed");
}
}
Err(error) => return Err(error),
// Do not error when `py` is not available
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
Err(error) => return Err(Error::PyLauncher(error)),
}
}

Ok(None)
}

#[derive(Debug, Clone)]
struct PyListPath {
major: u8,
minor: u8,
executable_path: PathBuf,
}

#[derive(Debug, Clone)]
enum PythonInstallation {
PyListPath(PyListPath),
Expand Down Expand Up @@ -545,75 +533,6 @@ fn find_version(
}

mod windows {
use std::path::PathBuf;
use std::process::Command;

use once_cell::sync::Lazy;
use regex::Regex;
use tracing::info_span;

use crate::find_python::PyListPath;
use crate::Error;

/// ```text
/// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe
/// -V:3.8 C:\Users\Ferris\AppData\Local\Programs\Python\Python38\python.exe
/// ```
static PY_LIST_PATHS: Lazy<Regex> = Lazy::new(|| {
// Without the `R` flag, paths have trailing \r
Regex::new(r"(?mR)^ -(?:V:)?(\d).(\d+)-?(?:arm)?\d*\s*\*?\s*(.*)$").unwrap()
});

/// Run `py --list-paths` to find the installed pythons.
///
/// The command takes 8ms on my machine.
/// TODO(konstin): Implement <https://peps.python.org/pep-0514/> to read python installations from the registry instead.
pub(super) fn py_list_paths() -> Result<Vec<PyListPath>, Error> {
let output = info_span!("py_list_paths")
.in_scope(|| Command::new("py").arg("--list-paths").output())
.map_err(Error::PyList)?;

// `py` sometimes prints "Installed Pythons found by py Launcher for Windows" to stderr which we ignore.
if !output.status.success() {
return Err(Error::PythonSubcommandOutput {
message: format!(
"Running `py --list-paths` failed with status {}",
output.status
),
exit_code: output.status,
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}

// Find the first python of the version we want in the list
let stdout =
String::from_utf8(output.stdout).map_err(|err| Error::PythonSubcommandOutput {
message: format!("The stdout of `py --list-paths` isn't UTF-8 encoded: {err}"),
exit_code: output.status,
stdout: String::from_utf8_lossy(err.as_bytes()).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
})?;

Ok(PY_LIST_PATHS
.captures_iter(&stdout)
.filter_map(|captures| {
let (_, [major, minor, path]) = captures.extract();
if let (Some(major), Some(minor)) =
(major.parse::<u8>().ok(), minor.parse::<u8>().ok())
{
Some(PyListPath {
major,
minor,
executable_path: PathBuf::from(path),
})
} else {
None
}
})
.collect())
}

/// On Windows we might encounter the Windows Store proxy shim (enabled in:
/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
Expand Down
5 changes: 3 additions & 2 deletions crates/uv-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod environment;
mod find_python;
mod interpreter;
pub mod managed;
mod py_launcher;
mod python_version;
pub mod selectors;
mod target;
Expand All @@ -49,8 +50,8 @@ pub enum Error {
#[source]
err: io::Error,
},
#[error("Failed to run `py --list-paths` to find Python installations. Is Python installed?")]
PyList(#[source] io::Error),
#[error(transparent)]
PyLauncher(#[from] py_launcher::Error),
#[cfg(windows)]
#[error(
"No Python {0} found through `py --list-paths` or in `PATH`. Is Python {0} installed?"
Expand Down
126 changes: 126 additions & 0 deletions crates/uv-interpreter/src/py_launcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::io;
use std::path::PathBuf;
use std::process::{Command, ExitStatus};

use once_cell::sync::Lazy;
use regex::Regex;
use thiserror::Error;
use tracing::info_span;

#[derive(Debug, Clone)]
pub(crate) struct PyListPath {
pub(crate) major: u8,
pub(crate) minor: u8,
pub(crate) executable_path: PathBuf,
}

#[derive(Error, Debug)]
pub enum Error {
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
StatusCode {
message: String,
exit_code: ExitStatus,
stdout: String,
stderr: String,
},
#[error("Failed to run `py --list-paths` to find Python installations.")]
Io(#[source] io::Error),
#[error("The `py` launcher could not be found.")]
NotFound,
}

/// ```text
/// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe
/// -V:3.8 C:\Users\Ferris\AppData\Local\Programs\Python\Python38\python.exe
/// ```
static PY_LIST_PATHS: Lazy<Regex> = Lazy::new(|| {
// Without the `R` flag, paths have trailing \r
Regex::new(r"(?mR)^ -(?:V:)?(\d).(\d+)-?(?:arm)?\d*\s*\*?\s*(.*)$").unwrap()
});

/// Use the `py` launcher to find installed Python versions.
///
/// Calls `py --list-paths`.
pub(crate) fn py_list_paths() -> Result<Vec<PyListPath>, Error> {
// konstin: The command takes 8ms on my machine.
let output = info_span!("py_list_paths")
.in_scope(|| Command::new("py").arg("--list-paths").output())
.map_err(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
Error::NotFound
} else {
Error::Io(err)
}
})?;

// `py` sometimes prints "Installed Pythons found by py Launcher for Windows" to stderr which we ignore.
if !output.status.success() {
return Err(Error::StatusCode {
message: format!(
"Running `py --list-paths` failed with status {}",
output.status
),
exit_code: output.status,
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}

// Find the first python of the version we want in the list
let stdout = String::from_utf8(output.stdout).map_err(|err| Error::StatusCode {
message: format!("The stdout of `py --list-paths` isn't UTF-8 encoded: {err}"),
exit_code: output.status,
stdout: String::from_utf8_lossy(err.as_bytes()).trim().to_string(),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
})?;

Ok(PY_LIST_PATHS
.captures_iter(&stdout)
.filter_map(|captures| {
let (_, [major, minor, path]) = captures.extract();
if let (Some(major), Some(minor)) = (major.parse::<u8>().ok(), minor.parse::<u8>().ok())
{
Some(PyListPath {
major,
minor,
executable_path: PathBuf::from(path),
})
} else {
None
}
})
.collect())
}

#[cfg(test)]
mod tests {
use std::fmt::Debug;

use insta::assert_snapshot;
use itertools::Itertools;

use uv_cache::Cache;

use crate::{find_requested_python, Error};

fn format_err<T: Debug>(err: Result<T, Error>) -> String {
anyhow::Error::new(err.unwrap_err())
.chain()
.join("\n Caused by: ")
}

#[test]
#[cfg_attr(not(windows), ignore)]
fn no_such_python_path() {
let result =
find_requested_python(r"C:\does\not\exists\python3.12", &Cache::temp().unwrap())
.unwrap()
.ok_or(Error::RequestedPythonNotFound(
r"C:\does\not\exists\python3.12".to_string(),
));
assert_snapshot!(
format_err(result),
@"Failed to locate Python interpreter at `C:\\does\\not\\exists\\python3.12`"
);
}
}

0 comments on commit 528bed5

Please sign in to comment.