mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 12:06:13 +00:00 
			
		
		
		
	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
					
				
					 3 changed files with 138 additions and 92 deletions
				
			
		|  | @ -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}; | ||||
| 
 | ||||
|  | @ -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); | ||||
|  | @ -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)), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -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`
 | ||||
|  | @ -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), | ||||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -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?" | ||||
|  |  | |||
							
								
								
									
										126
									
								
								crates/uv-interpreter/src/py_launcher.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								crates/uv-interpreter/src/py_launcher.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Zanie Blue
						Zanie Blue