mirror of https://github.com/astral-sh/uv
Move `py` launcher handling into separate module (#3329)
Split out of #3266 Mostly an organizational change, with some error handling simplification.
This commit is contained in:
parent
2e27abd34a
commit
528bed5bed
|
|
@ -10,6 +10,7 @@ use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
use crate::environment::python_environment::{detect_python_executable, detect_virtual_env};
|
use crate::environment::python_environment::{detect_python_executable, detect_virtual_env};
|
||||||
use crate::interpreter::InterpreterInfoError;
|
use crate::interpreter::InterpreterInfoError;
|
||||||
|
use crate::py_launcher::{py_list_paths, Error as PyLauncherError, PyListPath};
|
||||||
use crate::PythonVersion;
|
use crate::PythonVersion;
|
||||||
use crate::{Error, Interpreter};
|
use crate::{Error, Interpreter};
|
||||||
|
|
||||||
|
|
@ -202,7 +203,7 @@ fn find_python(
|
||||||
|
|
||||||
if cfg!(windows) && !use_override {
|
if cfg!(windows) && !use_override {
|
||||||
// Use `py` to find the python installation on the system.
|
// Use `py` to find the python installation on the system.
|
||||||
match windows::py_list_paths() {
|
match py_list_paths() {
|
||||||
Ok(paths) => {
|
Ok(paths) => {
|
||||||
for entry in paths {
|
for entry in paths {
|
||||||
let installation = PythonInstallation::PyListPath(entry);
|
let installation = PythonInstallation::PyListPath(entry);
|
||||||
|
|
@ -211,12 +212,9 @@ fn find_python(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::PyList(error)) => {
|
// Do not error when `py` is not available
|
||||||
if error.kind() == std::io::ErrorKind::NotFound {
|
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
|
||||||
debug!("`py` is not installed");
|
Err(error) => return Err(Error::PyLauncher(error)),
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,7 +261,7 @@ fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
|
||||||
|
|
||||||
if cfg!(windows) && !use_override {
|
if cfg!(windows) && !use_override {
|
||||||
// Use `py` to find the python installation on the system.
|
// Use `py` to find the python installation on the system.
|
||||||
match windows::py_list_paths() {
|
match py_list_paths() {
|
||||||
Ok(paths) => {
|
Ok(paths) => {
|
||||||
for entry in paths {
|
for entry in paths {
|
||||||
// Ex) `--python python3.12.exe`
|
// Ex) `--python python3.12.exe`
|
||||||
|
|
@ -281,25 +279,15 @@ fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::PyList(error)) => {
|
// Do not error when `py` is not available
|
||||||
if error.kind() == std::io::ErrorKind::NotFound {
|
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
|
||||||
debug!("`py` is not installed");
|
Err(error) => return Err(Error::PyLauncher(error)),
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct PyListPath {
|
|
||||||
major: u8,
|
|
||||||
minor: u8,
|
|
||||||
executable_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum PythonInstallation {
|
enum PythonInstallation {
|
||||||
PyListPath(PyListPath),
|
PyListPath(PyListPath),
|
||||||
|
|
@ -545,75 +533,6 @@ fn find_version(
|
||||||
}
|
}
|
||||||
|
|
||||||
mod windows {
|
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:
|
/// 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
|
/// 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
|
/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ mod environment;
|
||||||
mod find_python;
|
mod find_python;
|
||||||
mod interpreter;
|
mod interpreter;
|
||||||
pub mod managed;
|
pub mod managed;
|
||||||
|
mod py_launcher;
|
||||||
mod python_version;
|
mod python_version;
|
||||||
pub mod selectors;
|
pub mod selectors;
|
||||||
mod target;
|
mod target;
|
||||||
|
|
@ -49,8 +50,8 @@ pub enum Error {
|
||||||
#[source]
|
#[source]
|
||||||
err: io::Error,
|
err: io::Error,
|
||||||
},
|
},
|
||||||
#[error("Failed to run `py --list-paths` to find Python installations. Is Python installed?")]
|
#[error(transparent)]
|
||||||
PyList(#[source] io::Error),
|
PyLauncher(#[from] py_launcher::Error),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[error(
|
#[error(
|
||||||
"No Python {0} found through `py --list-paths` or in `PATH`. Is Python {0} installed?"
|
"No Python {0} found through `py --list-paths` or in `PATH`. Is Python {0} installed?"
|
||||||
|
|
|
||||||
|
|
@ -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`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue