diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index ea6775ad477e..cbd4f3f69027 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -852,7 +852,7 @@ async fn run_python_script( ) -> Result { // Prepend the venv bin dir to PATH let new_path = if let Some(old_path) = env::var_os("PATH") { - let new_path = iter::once(venv.bin_dir()).chain(env::split_paths(&old_path)); + let new_path = iter::once(venv.bin_dir().to_path_buf()).chain(env::split_paths(&old_path)); env::join_paths(new_path).map_err(Error::BuildScriptPath)? } else { OsString::from("") diff --git a/crates/uv-interpreter/src/get_interpreter_info.py b/crates/uv-interpreter/src/get_interpreter_info.py index 2e7f267c1c67..a459b0fc0931 100644 --- a/crates/uv-interpreter/src/get_interpreter_info.py +++ b/crates/uv-interpreter/src/get_interpreter_info.py @@ -87,7 +87,7 @@ def format_full_version(info): "markers": markers, "base_prefix": sys.base_prefix, "base_exec_prefix": sys.base_exec_prefix, - "stdlib": sysconfig.get_path("stdlib"), "sys_executable": sys.executable, + "sysconfig": sysconfig.get_paths(), } print(json.dumps(interpreter_info)) diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index cb46f20f0fe0..bbc2cbc2f77c 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -24,11 +24,11 @@ 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) platform: Platform, pub(crate) markers: Box, + pub(crate) sysconfig: Sysconfig, pub(crate) base_exec_prefix: PathBuf, pub(crate) base_prefix: PathBuf, - pub(crate) stdlib: PathBuf, pub(crate) sys_executable: PathBuf, tags: OnceCell, } @@ -49,32 +49,34 @@ impl Interpreter { ); Ok(Self { - platform: PythonPlatform(platform.to_owned()), + platform: platform.to_owned(), markers: Box::new(info.markers), + sysconfig: info.sysconfig, 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), + platform, markers: Box::new(markers), - base_exec_prefix, - base_prefix, - stdlib, - sys_executable, + sysconfig: Sysconfig { + 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"), + }, + base_exec_prefix: PathBuf::from("/dev/null"), + base_prefix: PathBuf::from("/dev/null"), + sys_executable: PathBuf::from("/dev/null"), tags: OnceCell::new(), } } @@ -287,28 +289,82 @@ impl Interpreter { pub fn implementation_name(&self) -> &str { &self.markers.implementation_name } + pub fn base_exec_prefix(&self) -> &Path { &self.base_exec_prefix } + 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 `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn stdlib(&self) -> &Path { + &self.sysconfig.stdlib + } + + /// Return the `platstdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn platstdlib(&self) -> &Path { + &self.sysconfig.platstdlib + } + + /// Return the `purelib` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn purelib(&self) -> &Path { + &self.sysconfig.purelib + } + + /// Return the `platlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn platlib(&self) -> &Path { + &self.sysconfig.platlib + } + + /// Return the `include` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn include(&self) -> &Path { + &self.sysconfig.include + } + + /// Return the `platinclude` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn platinclude(&self) -> &Path { + &self.sysconfig.platinclude + } + + /// Return the `scripts` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn scripts(&self) -> &Path { + &self.sysconfig.scripts + } + + /// Return the `data` path for this Python interpreter, as returned by `sysconfig.get_paths()`. + pub fn data(&self) -> &Path { + &self.sysconfig.data + } +} + +/// The installation paths returned by `sysconfig.get_paths()`. +/// +/// See: +#[derive(Debug, Deserialize, Serialize, Clone)] +pub(crate) struct Sysconfig { + pub(crate) stdlib: PathBuf, + pub(crate) platstdlib: PathBuf, + pub(crate) purelib: PathBuf, + pub(crate) platlib: PathBuf, + pub(crate) include: PathBuf, + pub(crate) platinclude: PathBuf, + pub(crate) scripts: PathBuf, + pub(crate) data: PathBuf, } #[derive(Debug, Deserialize, Serialize, Clone)] pub(crate) struct InterpreterInfo { pub(crate) markers: MarkerEnvironment, + pub(crate) sysconfig: Sysconfig, pub(crate) base_exec_prefix: PathBuf, pub(crate) base_prefix: PathBuf, - pub(crate) stdlib: PathBuf, pub(crate) sys_executable: PathBuf, } diff --git a/crates/uv-interpreter/src/python_platform.rs b/crates/uv-interpreter/src/python_platform.rs index fab530ba13c5..ac45add00e7f 100644 --- a/crates/uv-interpreter/src/python_platform.rs +++ b/crates/uv-interpreter/src/python_platform.rs @@ -36,22 +36,6 @@ impl PythonPlatform { venv.join("bin") } } - - /// Returns the path to the `site-packages` directory inside a virtual environment. - pub(crate) fn venv_site_packages( - &self, - venv_root: impl AsRef, - version: (u8, u8), - ) -> PathBuf { - let venv = venv_root.as_ref(); - if matches!(self.0.os(), Os::Windows) { - venv.join("Lib").join("site-packages") - } else { - venv.join("lib") - .join(format!("python{}.{}", version.0, version.1)) - .join("site-packages") - } - } } impl From for PythonPlatform { diff --git a/crates/uv-interpreter/src/virtual_env.rs b/crates/uv-interpreter/src/virtual_env.rs index 22828cfc2478..bea38014f7af 100644 --- a/crates/uv-interpreter/src/virtual_env.rs +++ b/crates/uv-interpreter/src/virtual_env.rs @@ -20,6 +20,15 @@ pub struct Virtualenv { } impl Virtualenv { + /// Create a new virtual environment for a pre-provided Python interpreter. + pub fn from_python(python: PathBuf, platform: &Platform, cache: &Cache) -> Result { + let interpreter = Interpreter::query(&python, &platform, cache)?; + Ok(Self { + root: interpreter.base_prefix.clone(), + interpreter, + }) + } + /// Venv the current Python executable from the host environment. pub fn from_env(platform: Platform, cache: &Cache) -> Result { let platform = PythonPlatform::from(platform); @@ -51,15 +60,16 @@ impl Virtualenv { } } - /// Returns the location of the python interpreter - pub fn python_executable(&self) -> PathBuf { - self.bin_dir().join(format!("python{EXE_SUFFIX}")) - } - + /// Returns the location of the Python interpreter. pub fn root(&self) -> &Path { &self.root } + /// Returns the location of the Python executable. + pub fn python_executable(&self) -> PathBuf { + self.bin_dir().join(format!("python{EXE_SUFFIX}")) + } + /// Return the [`Interpreter`] for this virtual environment. pub fn interpreter(&self) -> &Interpreter { &self.interpreter @@ -72,20 +82,13 @@ impl Virtualenv { } /// Returns the path to the `site-packages` directory inside a virtual environment. - pub fn site_packages(&self) -> PathBuf { - self.interpreter - .platform - .venv_site_packages(&self.root, self.interpreter().python_tuple()) + pub fn site_packages(&self) -> &Path { + self.interpreter.platlib() } - pub fn bin_dir(&self) -> PathBuf { - if cfg!(unix) { - self.root().join("bin") - } else if cfg!(windows) { - self.root().join("Scripts") - } else { - unimplemented!("Only Windows and Unix are supported") - } + /// Returns the path to the `bin` directory inside a virtual environment. + pub fn bin_dir(&self) -> &Path { + self.interpreter.scripts() } /// Lock the virtual environment to prevent concurrent writes. diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 04f4c7019b5d..81beeb0c118a 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -118,14 +118,7 @@ async fn resolve( let client = RegistryClientBuilder::new(Cache::temp()?).build(); let flat_index = FlatIndex::default(); let index = InMemoryIndex::default(); - let interpreter = Interpreter::artificial( - Platform::current()?, - markers.clone(), - PathBuf::from("/dev/null"), - PathBuf::from("/dev/null"), - PathBuf::from("/dev/null"), - PathBuf::from("/dev/null"), - ); + let interpreter = Interpreter::artificial(Platform::current()?, markers.clone()); let build_context = DummyContext::new(Cache::temp()?, interpreter.clone()); let resolver = Resolver::new( manifest, diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 6fd4e3e96d5c..5dab834bd518 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::fmt::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use anstream::eprint; use anyhow::{anyhow, Context, Result}; @@ -63,6 +63,7 @@ pub(crate) async fn pip_install( no_binary: &NoBinary, strict: bool, exclude_newer: Option>, + python: Option, cache: Cache, mut printer: Printer, ) -> Result { @@ -105,7 +106,11 @@ pub(crate) async fn pip_install( // Detect the current Python interpreter. let platform = Platform::current()?; - let venv = Virtualenv::from_env(platform, &cache)?; + let venv = if let Some(python) = python { + Virtualenv::from_python(python, &platform, &cache)? + } else { + Virtualenv::from_env(platform, &cache)? + }; debug!( "Using Python {} environment at {}", venv.interpreter().python_version(), diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index b2d634bb97e4..87349b44ed1f 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::path::PathBuf; use anyhow::{Context, Result}; use itertools::Itertools; @@ -42,6 +43,7 @@ pub(crate) async fn pip_sync( no_build: &NoBuild, no_binary: &NoBinary, strict: bool, + python: Option, cache: Cache, mut printer: Printer, ) -> Result { @@ -73,7 +75,11 @@ pub(crate) async fn pip_sync( // Detect the current Python interpreter. let platform = Platform::current()?; - let venv = Virtualenv::from_env(platform, &cache)?; + let venv = if let Some(python) = python { + Virtualenv::from_python(python, &platform, &cache)? + } else { + Virtualenv::from_env(platform, &cache)? + }; debug!( "Using Python {} environment at {}", venv.interpreter().python_version(), diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs index 5aa9e8b9d59a..944bc907d017 100644 --- a/crates/uv/src/commands/pip_uninstall.rs +++ b/crates/uv/src/commands/pip_uninstall.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::path::PathBuf; use anyhow::Result; use owo_colors::OwoColorize; @@ -17,6 +18,7 @@ use crate::requirements::{RequirementsSource, RequirementsSpecification}; /// Uninstall packages from the current environment. pub(crate) async fn pip_uninstall( sources: &[RequirementsSource], + python: Option, cache: Cache, mut printer: Printer, ) -> Result { @@ -38,7 +40,11 @@ pub(crate) async fn pip_uninstall( // Detect the current Python interpreter. let platform = Platform::current()?; - let venv = Virtualenv::from_env(platform, &cache)?; + let venv = if let Some(python) = python { + Virtualenv::from_python(python, &platform, &cache)? + } else { + Virtualenv::from_env(platform, &cache)? + }; debug!( "Using Python {} environment at {}", venv.interpreter().python_version(), diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 97baa6026229..4f423b5427c2 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -439,6 +439,10 @@ struct PipSyncArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// The Python interpreter into which to install the packages. + #[clap(long)] + python: Option, + /// Use legacy `setuptools` behavior when building source distributions without a /// `pyproject.toml`. #[clap(long)] @@ -608,6 +612,10 @@ struct PipInstallArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// The Python interpreter into which to install the packages. + #[clap(long)] + python: Option, + /// Use legacy `setuptools` behavior when building source distributions without a /// `pyproject.toml`. #[clap(long)] @@ -676,6 +684,10 @@ struct PipUninstallArgs { /// Uninstall the editable package based on the provided local file path. #[clap(long, short, group = "sources")] editable: Vec, + + /// The Python interpreter into which to install the packages. + #[clap(long)] + python: Option, } #[derive(Args)] @@ -1001,6 +1013,7 @@ async fn run() -> Result { &no_build, &no_binary, args.strict, + args.python, cache, printer, ) @@ -1083,6 +1096,7 @@ async fn run() -> Result { &no_binary, args.strict, args.exclude_newer, + args.python, cache, printer, ) @@ -1102,7 +1116,7 @@ async fn run() -> Result { .map(RequirementsSource::from_path), ) .collect::>(); - commands::pip_uninstall(&sources, cache, printer).await + commands::pip_uninstall(&sources, args.python, cache, printer).await } Commands::Pip(PipNamespace { command: PipCommand::Freeze(args),