diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 04bc874b1..b56bd4d55 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -339,54 +339,61 @@ fn python_executables_from_installed<'a>( preference: PythonPreference, preview: Preview, ) -> Box> + 'a> { - let from_managed_installations = iter::once_with(move || { - ManagedPythonInstallations::from_settings(None) - .map_err(Error::from) - .and_then(|installed_installations| { - debug!( + let extra_managed_install_dirs = env::var_os(EnvVars::UV_PYTHON_EXTRA_INSTALL_DIRS) + .into_iter() + .flat_map(|value| env::split_paths(&value).collect::>()) + .filter(|path| !path.as_os_str().is_empty()); + + let from_managed_installations = iter::once(None) + .chain(extra_managed_install_dirs.into_iter().map(Some)) + .map(move |install_dir| { + ManagedPythonInstallations::from_settings(install_dir) + .map_err(Error::from) + .and_then(|installed_installations| { + debug!( "Searching for managed installations at `{}`", installed_installations.root().user_display() ); - let installations = installed_installations.find_matching_current_platform()?; + let installations = installed_installations.find_matching_current_platform()?; - let build_versions = python_build_versions_from_env()?; + let build_versions = python_build_versions_from_env()?; - // Check that the Python version and platform satisfy the request to avoid - // unnecessary interpreter queries later - Ok(installations - .into_iter() - .filter(move |installation| { - if !version.matches_version(&installation.version()) { - debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`"); - return false; - } - if !platform.matches(installation.platform()) { - debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`"); - return false; - } - - if let Some(requested_build) = build_versions.get(&installation.implementation()) { - let Some(installation_build) = installation.build() else { - debug!( - "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation" - ); - return false; - }; - if installation_build != requested_build { - debug!( - "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`" - ); + // Check that the Python version and platform satisfy the request to avoid + // unnecessary interpreter queries later + Ok(installations + .into_iter() + .filter(move |installation| { + if !version.matches_version(&installation.version()) { + debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`"); + return false; + } + if !platform.matches(installation.platform()) { + debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`"); return false; } - } - true - }) - .inspect(|installation| debug!("Found managed installation `{installation}`")) - .map(move |installation| { - // If it's not a patch version request, then attempt to read the stable - // minor version link. - let executable = version + if let Some(requested_build) = build_versions.get(&installation.implementation()) { + let Some(installation_build) = installation.build() else { + debug!( + "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation" + ); + return false; + }; + if installation_build != requested_build { + debug!( + "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`" + ); + return false; + } + } + + true + }) + .inspect(|installation| debug!("Found managed installation `{installation}`")) + .map(move |installation| { + // If it's not a patch version request, then attempt to read the stable + // minor version link. + let executable = version .patch() .is_none() .then(|| { @@ -394,21 +401,21 @@ fn python_executables_from_installed<'a>( &installation, preview, ) - .filter(PythonMinorVersionLink::exists) - .map( - |minor_version_link| { - minor_version_link.symlink_executable.clone() - }, - ) + .filter(PythonMinorVersionLink::exists) + .map( + |minor_version_link| { + minor_version_link.symlink_executable.clone() + }, + ) }) .flatten() .unwrap_or_else(|| installation.executable(false)); - (PythonSource::Managed, executable) - }) - ) - }) - }) - .flatten_ok(); + (PythonSource::Managed, executable) + }) + ) + }) + }) + .flatten_ok(); let from_search_path = iter::once_with(move || { python_executables_from_search_path(version, implementation) diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index b29cdb37b..c767b3371 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -262,7 +262,7 @@ impl ManagedPythonInstallations { ) -> Result + use<>, Error> { let platform = Platform::from_env()?; - let iter = Self::from_settings(None)? + let iter = self .find_all()? .filter(move |installation| { if !platform.supports(installation.platform()) { diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index b6cf12a4d..e1bca252f 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -371,6 +371,13 @@ impl EnvVars { #[attr_added_in("0.2.22")] pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR"; + /// Specifies additional directories to search for managed Python installations. + /// + /// Directories should be separated by the platform-specific path separator, i.e., + /// `:` on Unix and `;` on Windows. + #[attr_added_in("next release")] + pub const UV_PYTHON_EXTRA_INSTALL_DIRS: &'static str = "UV_PYTHON_EXTRA_INSTALL_DIRS"; + /// Whether to install the Python executable into the `UV_PYTHON_BIN_DIR` directory. #[attr_added_in("0.8.0")] pub const UV_PYTHON_INSTALL_BIN: &'static str = "UV_PYTHON_INSTALL_BIN";