mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 12:06:13 +00:00 
			
		
		
		
	Add a --python flag to allow installation into arbitrary Python interpreters (#2000)
				
					
				
			## Summary This PR adds a `--python` flag that allows users to provide a specific Python interpreter into which `uv` should install packages. This would replace the `VIRTUAL_ENV=` workaround that folks have been using to install into arbitrary, system environments, while _also_ actually being correct for installing into non-virtual environments, where the bin and site-packages paths can differ. The approach taken here is to use `sysconfig.get_paths()` to get the correct paths from the interpreter, and then use those for determining the `bin` and `site-packages` directories, rather than constructing them based on hard-coded expectations for each platform. Closes https://github.com/astral-sh/uv/issues/1396. Closes https://github.com/astral-sh/uv/issues/1779. Closes https://github.com/astral-sh/uv/issues/1988. ## Test Plan - Verified that, on my Windows machine, I was able to install `requests` into a global environment with: `cargo run pip install requests --python 'C:\\Users\\crmarsh\\AppData\\Local\\Programs\\Python\\Python3.12\\python.exe`, then `python` and `import requests`. - Verified that, on macOS, I was able to install `requests` into a global environment installed via Homebrew with: `cargo run pip install requests --python $(which python3.8)`.
This commit is contained in:
		
							parent
							
								
									72a5ebada3
								
							
						
					
					
						commit
						10175143d1
					
				
					 21 changed files with 450 additions and 238 deletions
				
			
		|  | @ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; | |||
| use tracing::{debug, instrument, warn}; | ||||
| 
 | ||||
| use cache_key::digest; | ||||
| use install_wheel_rs::Layout; | ||||
| use pep440_rs::Version; | ||||
| use pep508_rs::MarkerEnvironment; | ||||
| use platform_host::Platform; | ||||
|  | @ -24,18 +25,19 @@ use crate::{find_requested_python, Error, PythonVersion}; | |||
| /// A Python executable and its associated platform markers.
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Interpreter { | ||||
|     pub(crate) platform: PythonPlatform, | ||||
|     pub(crate) markers: Box<MarkerEnvironment>, | ||||
|     pub(crate) base_exec_prefix: PathBuf, | ||||
|     pub(crate) base_prefix: PathBuf, | ||||
|     pub(crate) stdlib: PathBuf, | ||||
|     pub(crate) sys_executable: PathBuf, | ||||
|     platform: PythonPlatform, | ||||
|     markers: Box<MarkerEnvironment>, | ||||
|     sysconfig_paths: SysconfigPaths, | ||||
|     prefix: PathBuf, | ||||
|     base_exec_prefix: PathBuf, | ||||
|     base_prefix: PathBuf, | ||||
|     sys_executable: PathBuf, | ||||
|     tags: OnceCell<Tags>, | ||||
| } | ||||
| 
 | ||||
| impl Interpreter { | ||||
|     /// Detect the interpreter info for the given Python executable.
 | ||||
|     pub fn query(executable: &Path, platform: &Platform, cache: &Cache) -> Result<Self, Error> { | ||||
|     pub fn query(executable: &Path, platform: Platform, cache: &Cache) -> Result<Self, Error> { | ||||
|         let info = InterpreterInfo::query_cached(executable, cache)?; | ||||
| 
 | ||||
|         debug_assert!( | ||||
|  | @ -45,41 +47,65 @@ impl Interpreter { | |||
|         ); | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             platform: PythonPlatform(platform.to_owned()), | ||||
|             platform: PythonPlatform(platform), | ||||
|             markers: Box::new(info.markers), | ||||
|             sysconfig_paths: info.sysconfig_paths, | ||||
|             prefix: info.prefix, | ||||
|             base_exec_prefix: info.base_exec_prefix, | ||||
|             base_prefix: info.base_prefix, | ||||
|             stdlib: info.stdlib, | ||||
|             sys_executable: info.sys_executable, | ||||
|             tags: OnceCell::new(), | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     // TODO(konstin): Find a better way mocking the fields
 | ||||
|     pub fn artificial( | ||||
|         platform: Platform, | ||||
|         markers: MarkerEnvironment, | ||||
|         base_exec_prefix: PathBuf, | ||||
|         base_prefix: PathBuf, | ||||
|         sys_executable: PathBuf, | ||||
|         stdlib: PathBuf, | ||||
|     ) -> Self { | ||||
|     pub fn artificial(platform: Platform, markers: MarkerEnvironment) -> Self { | ||||
|         Self { | ||||
|             platform: PythonPlatform(platform), | ||||
|             markers: Box::new(markers), | ||||
|             base_exec_prefix, | ||||
|             base_prefix, | ||||
|             stdlib, | ||||
|             sys_executable, | ||||
|             sysconfig_paths: SysconfigPaths { | ||||
|                 stdlib: PathBuf::from("/dev/null"), | ||||
|                 platstdlib: PathBuf::from("/dev/null"), | ||||
|                 purelib: PathBuf::from("/dev/null"), | ||||
|                 platlib: PathBuf::from("/dev/null"), | ||||
|                 include: PathBuf::from("/dev/null"), | ||||
|                 platinclude: PathBuf::from("/dev/null"), | ||||
|                 scripts: PathBuf::from("/dev/null"), | ||||
|                 data: PathBuf::from("/dev/null"), | ||||
|             }, | ||||
|             prefix: PathBuf::from("/dev/null"), | ||||
|             base_exec_prefix: PathBuf::from("/dev/null"), | ||||
|             base_prefix: PathBuf::from("/dev/null"), | ||||
|             sys_executable: PathBuf::from("/dev/null"), | ||||
|             tags: OnceCell::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Return a new [`Interpreter`] with the given base prefix.
 | ||||
|     /// Return a new [`Interpreter`] with the given virtual environment root.
 | ||||
|     #[must_use] | ||||
|     pub(crate) fn with_base_prefix(self, base_prefix: PathBuf) -> Self { | ||||
|     pub(crate) fn with_venv_root(self, venv_root: PathBuf) -> Self { | ||||
|         Self { | ||||
|             base_prefix, | ||||
|             // Given that we know `venv_root` is a virtualenv, and not an arbitrary Python
 | ||||
|             // interpreter, we can safely assume that the platform is the same as the host
 | ||||
|             // platform. Further, we can safely assume that the paths follow a predictable
 | ||||
|             // structure, which allows us to avoid querying the interpreter for the `sysconfig`
 | ||||
|             // paths.
 | ||||
|             sysconfig_paths: SysconfigPaths { | ||||
|                 purelib: self | ||||
|                     .platform | ||||
|                     .venv_site_packages(&venv_root, self.python_tuple()), | ||||
|                 platlib: self | ||||
|                     .platform | ||||
|                     .venv_site_packages(&venv_root, self.python_tuple()), | ||||
|                 platstdlib: self | ||||
|                     .platform | ||||
|                     .venv_platstdlib_dir(&venv_root, self.python_tuple()), | ||||
|                 scripts: self.platform.venv_scripts_dir(&venv_root), | ||||
|                 data: self.platform.venv_data_dir(&venv_root), | ||||
|                 ..self.sysconfig_paths | ||||
|             }, | ||||
|             sys_executable: self.platform.venv_python(&venv_root), | ||||
|             prefix: venv_root, | ||||
|             ..self | ||||
|         } | ||||
|     } | ||||
|  | @ -168,7 +194,7 @@ impl Interpreter { | |||
|         let python_platform = PythonPlatform::from(platform.to_owned()); | ||||
|         if let Some(venv) = detect_virtual_env(&python_platform)? { | ||||
|             let executable = python_platform.venv_python(venv); | ||||
|             let interpreter = Self::query(&executable, &python_platform.0, cache)?; | ||||
|             let interpreter = Self::query(&executable, python_platform.0, cache)?; | ||||
| 
 | ||||
|             if version_matches(&interpreter) { | ||||
|                 return Ok(Some(interpreter)); | ||||
|  | @ -234,6 +260,29 @@ impl Interpreter { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns `true` if the environment is a PEP 405-compliant virtual environment.
 | ||||
|     ///
 | ||||
|     /// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
 | ||||
|     pub fn is_virtualenv(&self) -> bool { | ||||
|         self.prefix != self.base_prefix | ||||
|     } | ||||
| 
 | ||||
|     /// Returns `true` if the environment is externally managed.
 | ||||
|     ///
 | ||||
|     /// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
 | ||||
|     pub fn externally_managed(&self) -> bool { | ||||
|         // Per the spec, a virtual environment is never externally managed.
 | ||||
|         if self.is_virtualenv() { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Per the spec, the existence of the file is the only requirement.
 | ||||
|         self.sysconfig_paths | ||||
|             .stdlib | ||||
|             .join("EXTERNALLY-MANAGED") | ||||
|             .is_file() | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the Python version.
 | ||||
|     #[inline] | ||||
|     pub const fn python_version(&self) -> &Version { | ||||
|  | @ -280,32 +329,103 @@ impl Interpreter { | |||
|         (self.implementation_major(), self.implementation_minor()) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the implementation name (e.g., `CPython` or `PyPy`).
 | ||||
|     pub fn implementation_name(&self) -> &str { | ||||
|         &self.markers.implementation_name | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `sys.base_exec_prefix` path for this Python interpreter.
 | ||||
|     pub fn base_exec_prefix(&self) -> &Path { | ||||
|         &self.base_exec_prefix | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `sys.base_prefix` path for this Python interpreter.
 | ||||
|     pub fn base_prefix(&self) -> &Path { | ||||
|         &self.base_prefix | ||||
|     } | ||||
| 
 | ||||
|     /// `sysconfig.get_path("stdlib")`
 | ||||
|     pub fn stdlib(&self) -> &Path { | ||||
|         &self.stdlib | ||||
|     } | ||||
|     /// Return the `sys.executable` path for this Python interpreter.
 | ||||
|     pub fn sys_executable(&self) -> &Path { | ||||
|         &self.sys_executable | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn purelib(&self) -> &Path { | ||||
|         &self.sysconfig_paths.purelib | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `platlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn platlib(&self) -> &Path { | ||||
|         &self.sysconfig_paths.platlib | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `scripts` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn scripts(&self) -> &Path { | ||||
|         &self.sysconfig_paths.scripts | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `data` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn data(&self) -> &Path { | ||||
|         &self.sysconfig_paths.data | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `include` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn include(&self) -> &Path { | ||||
|         &self.sysconfig_paths.include | ||||
|     } | ||||
| 
 | ||||
|     /// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
 | ||||
|     pub fn stdlib(&self) -> &Path { | ||||
|         &self.sysconfig_paths.stdlib | ||||
|     } | ||||
| 
 | ||||
|     /// Return the [`Layout`] environment used to install wheels into this interpreter.
 | ||||
|     pub fn layout(&self) -> Layout { | ||||
|         Layout { | ||||
|             python_version: self.python_tuple(), | ||||
|             sys_executable: self.sys_executable().to_path_buf(), | ||||
|             purelib: self.purelib().to_path_buf(), | ||||
|             platlib: self.platlib().to_path_buf(), | ||||
|             scripts: self.scripts().to_path_buf(), | ||||
|             data: self.data().to_path_buf(), | ||||
|             include: if self.is_virtualenv() { | ||||
|                 // If the interpreter is a venv, then the `include` directory has a different structure.
 | ||||
|                 // See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
 | ||||
|                 self.prefix.join("include").join("site").join(format!( | ||||
|                     "python{}.{}", | ||||
|                     self.python_major(), | ||||
|                     self.python_minor() | ||||
|                 )) | ||||
|             } else { | ||||
|                 self.include().to_path_buf() | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The installation paths returned by `sysconfig.get_paths()`.
 | ||||
| ///
 | ||||
| /// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>
 | ||||
| #[derive(Debug, Deserialize, Serialize, Clone)] | ||||
| struct SysconfigPaths { | ||||
|     stdlib: PathBuf, | ||||
|     platstdlib: PathBuf, | ||||
|     purelib: PathBuf, | ||||
|     platlib: PathBuf, | ||||
|     include: PathBuf, | ||||
|     platinclude: PathBuf, | ||||
|     scripts: PathBuf, | ||||
|     data: PathBuf, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize, Serialize, Clone)] | ||||
| pub(crate) struct InterpreterInfo { | ||||
|     pub(crate) markers: MarkerEnvironment, | ||||
|     pub(crate) base_exec_prefix: PathBuf, | ||||
|     pub(crate) base_prefix: PathBuf, | ||||
|     pub(crate) stdlib: PathBuf, | ||||
|     pub(crate) sys_executable: PathBuf, | ||||
| struct InterpreterInfo { | ||||
|     markers: MarkerEnvironment, | ||||
|     sysconfig_paths: SysconfigPaths, | ||||
|     prefix: PathBuf, | ||||
|     base_exec_prefix: PathBuf, | ||||
|     base_prefix: PathBuf, | ||||
|     sys_executable: PathBuf, | ||||
| } | ||||
| 
 | ||||
| impl InterpreterInfo { | ||||
|  | @ -499,8 +619,18 @@ mod tests { | |||
|                 }, | ||||
|                 "base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0", | ||||
|                 "base_prefix": "/home/ferris/.pyenv/versions/3.12.0", | ||||
|                 "stdlib": "/usr/lib/python3.12", | ||||
|                 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python" | ||||
|                 "prefix": "/home/ferris/projects/uv/.venv", | ||||
|                 "sys_executable": "/home/ferris/projects/uv/.venv/bin/python", | ||||
|                 "sysconfig_paths": { | ||||
|                     "data": "/home/ferris/.pyenv/versions/3.12.0", | ||||
|                     "include": "/home/ferris/.pyenv/versions/3.12.0/include", | ||||
|                     "platinclude": "/home/ferris/.pyenv/versions/3.12.0/include", | ||||
|                     "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", | ||||
|                     "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", | ||||
|                     "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin", | ||||
|                     "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12", | ||||
|                     "platstdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12" | ||||
|                 } | ||||
|             } | ||||
|         "##};
 | ||||
| 
 | ||||
|  | @ -520,7 +650,8 @@ mod tests { | |||
|             std::os::unix::fs::PermissionsExt::from_mode(0o770), | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let interpreter = Interpreter::query(&mocked_interpreter, &platform, &cache).unwrap(); | ||||
|         let interpreter = | ||||
|             Interpreter::query(&mocked_interpreter, platform.clone(), &cache).unwrap(); | ||||
|         assert_eq!( | ||||
|             interpreter.markers.python_version.version, | ||||
|             Version::from_str("3.12").unwrap() | ||||
|  | @ -533,7 +664,8 @@ mod tests { | |||
|             "##, json.replace("3.12", "3.13")},
 | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let interpreter = Interpreter::query(&mocked_interpreter, &platform, &cache).unwrap(); | ||||
|         let interpreter = | ||||
|             Interpreter::query(&mocked_interpreter, platform.clone(), &cache).unwrap(); | ||||
|         assert_eq!( | ||||
|             interpreter.markers.python_version.version, | ||||
|             Version::from_str("3.13").unwrap() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Charlie Marsh
						Charlie Marsh