diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 668bdc906ebf..46d7019caa37 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3938,6 +3938,22 @@ pub struct PythonInstallArgs { /// See `uv help python` to view supported request formats. pub targets: Vec, + /// Set the URL to use as the source for downloading Python installations. + /// + /// The provided URL will replace `https://github.com/indygreg/python-build-standalone/releases/download` in, e.g., `https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. + /// + /// Distributions can be read from a local directory by using the `file://` URL scheme. + #[arg(long, env = EnvVars::UV_PYTHON_INSTALL_MIRROR)] + pub mirror: Option, + + /// Set the URL to use as the source for downloading PyPy installations. + /// + /// The provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. + /// + /// Distributions can be read from a local directory by using the `file://` URL scheme. + #[arg(long, env = EnvVars::UV_PYPY_INSTALL_MIRROR)] + pub pypy_mirror: Option, + /// Reinstall the requested Python version, if it's already installed. /// /// By default, uv will exit successfully if the version is already diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index b278e5c31f4f..d19018f8f374 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -466,9 +466,11 @@ impl ManagedPythonDownload { installation_dir: &Path, cache_dir: &Path, reinstall: bool, + python_install_mirror: Option, + pypy_install_mirror: Option, reporter: Option<&dyn Reporter>, ) -> Result { - let url = self.download_url()?; + let url = self.download_url(python_install_mirror, pypy_install_mirror)?; let path = installation_dir.join(self.key().to_string()); // If it is not a reinstall and the dir already exists, return it. @@ -585,10 +587,14 @@ impl ManagedPythonDownload { /// Return the [`Url`] to use when downloading the distribution. If a mirror is set via the /// appropriate environment variable, use it instead. - fn download_url(&self) -> Result { + fn download_url( + &self, + python_install_mirror: Option, + pypy_install_mirror: Option, + ) -> Result { match self.key.implementation { LenientImplementationName::Known(ImplementationName::CPython) => { - if let Ok(mirror) = std::env::var(EnvVars::UV_PYTHON_INSTALL_MIRROR) { + if let Some(mirror) = python_install_mirror { let Some(suffix) = self.url.strip_prefix( "https://github.com/indygreg/python-build-standalone/releases/download/", ) else { @@ -601,7 +607,7 @@ impl ManagedPythonDownload { } LenientImplementationName::Known(ImplementationName::PyPy) => { - if let Ok(mirror) = std::env::var(EnvVars::UV_PYPY_INSTALL_MIRROR) { + if let Some(mirror) = pypy_install_mirror { let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/") else { return Err(Error::Mirror(EnvVars::UV_PYPY_INSTALL_MIRROR, self.url)); diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 9faef3438abb..d507992be874 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -86,6 +86,8 @@ impl PythonInstallation { client_builder: &BaseClientBuilder<'a>, cache: &Cache, reporter: Option<&dyn Reporter>, + python_install_mirror: Option, + pypy_install_mirror: Option, ) -> Result { let request = request.unwrap_or_else(|| &PythonRequest::Default); @@ -100,7 +102,16 @@ impl PythonInstallation { { if let Some(request) = PythonDownloadRequest::from_request(request) { debug!("Requested Python not found, checking for available download..."); - match Self::fetch(request.fill()?, client_builder, cache, reporter).await { + match Self::fetch( + request.fill()?, + client_builder, + cache, + reporter, + python_install_mirror, + pypy_install_mirror, + ) + .await + { Ok(installation) => Ok(installation), Err(Error::Download(downloads::Error::NoDownloadFound(_))) => { Err(Error::MissingPython(err)) @@ -121,6 +132,8 @@ impl PythonInstallation { client_builder: &BaseClientBuilder<'a>, cache: &Cache, reporter: Option<&dyn Reporter>, + python_install_mirror: Option, + pypy_install_mirror: Option, ) -> Result { let installations = ManagedPythonInstallations::from_settings()?.init()?; let installations_dir = installations.root(); @@ -132,7 +145,15 @@ impl PythonInstallation { info!("Fetching requested Python..."); let result = download - .fetch(&client, installations_dir, &cache_dir, false, reporter) + .fetch( + &client, + installations_dir, + &cache_dir, + false, + python_install_mirror, + pypy_install_mirror, + reporter, + ) .await?; let path = match result { diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 470cce6bcc73..375a89b7eda2 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -14,6 +14,7 @@ use uv_pep508::Requirement; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode}; +use uv_static::EnvVars; /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[allow(dead_code)] @@ -41,6 +42,9 @@ pub struct Options { #[serde(flatten)] pub top_level: ResolverInstallerOptions, + #[serde(flatten)] + pub install_mirrors: PythonInstallMirrors, + #[serde(flatten)] pub publish: PublishOptions, @@ -676,6 +680,61 @@ pub struct ResolverInstallerOptions { pub no_binary_package: Option>, } +/// Shared settings, relevant to all operations that might create managed python installations. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct PythonInstallMirrors { + /// Mirror URL for downloading managed Python installations. + /// + /// By default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/indygreg/python-build-standalone). + /// This variable can be set to a mirror URL to use a different source for Python installations. + /// The provided URL will replace `https://github.com/indygreg/python-build-standalone/releases/download` in, e.g., `https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. + /// + /// Distributions can be read from a local directory by using the `file://` URL scheme. + #[option( + default = "None", + value_type = "str", + example = r#" + python-install-mirror = "https://github.com/indygreg/python-build-standalone/releases/download" + "# + )] + pub python_install_mirror: Option, + /// Mirror URL to use for downloading managed PyPy installations. + /// + /// By default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/). + /// This variable can be set to a mirror URL to use a different source for PyPy installations. + /// The provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. + /// + /// Distributions can be read from a + /// local directory by using the `file://` URL scheme. + #[option( + default = "None", + value_type = "str", + example = r#" + pypy-install-mirror = "https://downloads.python.org/pypy" + "# + )] + pub pypy_install_mirror: Option, +} + +impl Default for PythonInstallMirrors { + fn default() -> Self { + PythonInstallMirrors::resolve(None, None) + } +} + +impl PythonInstallMirrors { + pub fn resolve(python_mirror: Option, pypy_mirror: Option) -> Self { + let python_mirror_env = std::env::var(EnvVars::UV_PYTHON_INSTALL_MIRROR).ok(); + let pypy_mirror_env = std::env::var(EnvVars::UV_PYPY_INSTALL_MIRROR).ok(); + PythonInstallMirrors { + python_install_mirror: python_mirror_env.or(python_mirror), + pypy_install_mirror: pypy_mirror_env.or(pypy_mirror), + } + } +} + /// Settings that are specific to the `uv pip` command-line interface. /// /// These values will be ignored when running commands outside the `uv pip` namespace (e.g., @@ -1544,6 +1603,11 @@ pub struct OptionsWire { no_binary: Option, no_binary_package: Option>, + // #[serde(flatten)] + // install_mirror: PythonInstallMirrors, + python_install_mirror: Option, + pypy_install_mirror: Option, + // #[serde(flatten)] // publish: PublishOptions publish_url: Option, @@ -1581,6 +1645,8 @@ impl From for Options { preview, python_preference, python_downloads, + python_install_mirror, + pypy_install_mirror, concurrent_downloads, concurrent_builds, concurrent_installs, @@ -1673,6 +1739,10 @@ impl From for Options { override_dependencies, constraint_dependencies, environments, + install_mirrors: PythonInstallMirrors::resolve( + python_install_mirror, + pypy_install_mirror, + ), conflicting_groups, publish: PublishOptions { publish_url, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 76ee538aaadf..3e8c89bfb0d6 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -28,6 +28,7 @@ use uv_python::{ }; use uv_requirements::RequirementsSource; use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython}; +use uv_settings::PythonInstallMirrors; use uv_types::{BuildContext, BuildIsolation, HashStrategy}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError}; @@ -52,6 +53,7 @@ pub(crate) async fn build_frontend( build_constraints: Vec, hash_checking: Option, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverSettings, no_config: bool, python_preference: PythonPreference, @@ -75,6 +77,7 @@ pub(crate) async fn build_frontend( &build_constraints, hash_checking, python.as_deref(), + install_mirrors, settings.as_ref(), no_config, python_preference, @@ -116,6 +119,7 @@ async fn build_impl( build_constraints: &[RequirementsSource], hash_checking: Option, python_request: Option<&str>, + install_mirrors: PythonInstallMirrors, settings: ResolverSettingsRef<'_>, no_config: bool, python_preference: PythonPreference, @@ -251,6 +255,7 @@ async fn build_impl( source.clone(), output_dir, python_request, + install_mirrors.clone(), no_config, workspace.as_ref(), python_preference, @@ -346,6 +351,7 @@ async fn build_package( source: AnnotatedSource<'_>, output_dir: Option<&Path>, python_request: Option<&str>, + install_mirrors: PythonInstallMirrors, no_config: bool, workspace: Result<&Workspace, &WorkspaceError>, python_preference: PythonPreference, @@ -424,6 +430,8 @@ async fn build_package( client_builder, cache, Some(&PythonDownloadReporter::single(printer)), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 00606ba65c33..3c9e87236454 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -33,6 +33,7 @@ use uv_python::{ use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::{FlatIndex, InstallTarget}; use uv_scripts::{Pep723Item, Pep723Script}; +use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user_once; use uv_workspace::pyproject::{DependencyType, Source, SourceError}; @@ -71,6 +72,7 @@ pub(crate) async fn add( extras: Vec, package: Option, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverInstallerSettings, script: Option, python_preference: PythonPreference, @@ -140,6 +142,7 @@ pub(crate) async fn add( } else { let requires_python = init_script_python_requirement( python.as_deref(), + install_mirrors.clone(), project_dir, false, python_preference, @@ -173,6 +176,8 @@ pub(crate) async fn add( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); @@ -227,6 +232,7 @@ pub(crate) async fn add( connectivity, native_tls, allow_insecure_host, + install_mirrors.clone(), no_config, cache, printer, @@ -240,6 +246,7 @@ pub(crate) async fn add( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), + install_mirrors.clone(), python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 09b1f98aa363..206a83b03332 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; use std::path::{Path, PathBuf}; +use uv_settings::PythonInstallMirrors; use uv_cache::Cache; use uv_client::Connectivity; @@ -42,6 +43,7 @@ pub(crate) async fn export( frozen: bool, include_header: bool, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverSettings, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -107,6 +109,7 @@ pub(crate) async fn export( connectivity, native_tls, allow_insecure_host, + install_mirrors, no_config, cache, printer, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 6685ca67032e..a9427e569e74 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -23,6 +23,7 @@ use uv_python::{ }; use uv_resolver::RequiresPython; use uv_scripts::{Pep723Script, ScriptTag}; +use uv_settings::PythonInstallMirrors; use uv_warnings::warn_user_once; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError}; @@ -46,6 +47,7 @@ pub(crate) async fn init( author_from: Option, no_pin_python: bool, python: Option, + install_mirrors: PythonInstallMirrors, no_workspace: bool, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -65,6 +67,7 @@ pub(crate) async fn init( init_script( path, python, + install_mirrors, connectivity, python_preference, python_downloads, @@ -128,6 +131,7 @@ pub(crate) async fn init( author_from, no_pin_python, python, + install_mirrors, no_workspace, python_preference, python_downloads, @@ -175,6 +179,7 @@ pub(crate) async fn init( async fn init_script( script_path: &Path, python: Option, + install_mirrors: PythonInstallMirrors, connectivity: Connectivity, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -233,6 +238,7 @@ async fn init_script( let requires_python = init_script_python_requirement( python.as_deref(), + install_mirrors, &CWD, no_pin_python, python_preference, @@ -266,6 +272,7 @@ async fn init_project( author_from: Option, no_pin_python: bool, python: Option, + install_mirrors: PythonInstallMirrors, no_workspace: bool, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -416,6 +423,8 @@ async fn init_project( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); @@ -438,6 +447,8 @@ async fn init_project( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); @@ -498,6 +509,8 @@ async fn init_project( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); @@ -520,6 +533,8 @@ async fn init_project( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index c1707ed1d1b6..8ed05f2d7853 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -30,6 +30,7 @@ use uv_resolver::{ FlatIndex, InMemoryIndex, Lock, LockVersion, Options, OptionsBuilder, PythonRequirement, RequiresPython, ResolverEnvironment, ResolverManifest, SatisfiesResult, VERSION, }; +use uv_settings::PythonInstallMirrors; use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::{DiscoveryOptions, Workspace}; @@ -76,6 +77,7 @@ pub(crate) async fn lock( frozen: bool, dry_run: bool, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverSettings, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -105,6 +107,7 @@ pub(crate) async fn lock( connectivity, native_tls, allow_insecure_host, + install_mirrors, no_config, cache, printer, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index da26837f8f0c..e2b68b192735 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -35,6 +35,7 @@ use uv_resolver::{ ResolverEnvironment, }; use uv_scripts::Pep723Item; +use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; @@ -544,6 +545,7 @@ impl ProjectInterpreter { connectivity: Connectivity, native_tls: bool, allow_insecure_host: &[TrustedHost], + install_mirrors: PythonInstallMirrors, no_config: bool, cache: &Cache, printer: Printer, @@ -638,6 +640,8 @@ impl ProjectInterpreter { &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await?; @@ -682,6 +686,7 @@ impl ProjectInterpreter { pub(crate) async fn get_or_init_environment( workspace: &Workspace, python: Option, + install_mirrors: PythonInstallMirrors, python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -700,6 +705,7 @@ pub(crate) async fn get_or_init_environment( connectivity, native_tls, allow_insecure_host, + install_mirrors, no_config, cache, printer, @@ -1498,6 +1504,7 @@ pub(crate) async fn update_environment( /// Determine the [`RequiresPython`] requirement for a new PEP 723 script. pub(crate) async fn init_script_python_requirement( python: Option<&str>, + install_mirrors: PythonInstallMirrors, directory: &Path, no_pin_python: bool, python_preference: PythonPreference, @@ -1534,6 +1541,8 @@ pub(crate) async fn init_script_python_requirement( client_builder, cache, Some(reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 2c02c7322cf6..b18556960d65 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use std::fmt::Write; use std::path::Path; +use uv_settings::PythonInstallMirrors; use owo_colors::OwoColorize; use uv_cache::Cache; @@ -39,6 +40,7 @@ pub(crate) async fn remove( dependency_type: DependencyType, package: Option, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverInstallerSettings, script: Option, python_preference: PythonPreference, @@ -189,6 +191,7 @@ pub(crate) async fn remove( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), + install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 4bf0dc019b72..3f59898c2638 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -32,6 +32,7 @@ use uv_python::{ use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_resolver::{InstallTarget, Lock}; use uv_scripts::Pep723Item; +use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError}; @@ -71,6 +72,7 @@ pub(crate) async fn run( dev: DevGroupsSpecification, editable: EditableMode, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverInstallerSettings, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -201,6 +203,8 @@ pub(crate) async fn run( &client_builder, cache, Some(&download_reporter), + install_mirrors.python_install_mirror.clone(), + install_mirrors.pypy_install_mirror.clone(), ) .await? .into_interpreter(); @@ -509,6 +513,8 @@ pub(crate) async fn run( &client_builder, cache, Some(&download_reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); @@ -539,6 +545,7 @@ pub(crate) async fn run( project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), + install_mirrors, python_preference, python_downloads, connectivity, @@ -712,6 +719,8 @@ pub(crate) async fn run( &client_builder, cache, Some(&download_reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await?; diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 84d5cd3e9c39..0156049310b2 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -22,6 +22,7 @@ use uv_pypi_types::{ }; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, InstallTarget}; +use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; @@ -52,6 +53,7 @@ pub(crate) async fn sync( install_options: InstallOptions, modifications: Modifications, python: Option, + install_mirrors: PythonInstallMirrors, python_preference: PythonPreference, python_downloads: PythonDownloads, settings: ResolverInstallerSettings, @@ -114,6 +116,7 @@ pub(crate) async fn sync( let venv = project::get_or_init_environment( project.workspace(), python.as_deref().map(PythonRequest::parse), + install_mirrors, python_preference, python_downloads, connectivity, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 1556157d31cc..48ca9d13c51b 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -15,6 +15,7 @@ use uv_pep440::Version; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_resolver::{PackageMap, TreeDisplay}; +use uv_settings::PythonInstallMirrors; use uv_workspace::{DiscoveryOptions, Workspace}; use crate::commands::pip::latest::LatestClient; @@ -45,6 +46,7 @@ pub(crate) async fn tree( python_version: Option, python_platform: Option, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverSettings, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -82,6 +84,7 @@ pub(crate) async fn tree( connectivity, native_tls, allow_insecure_host, + install_mirrors, no_config, cache, printer, diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 37e7778603b5..2059b28096df 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -113,6 +113,8 @@ pub(crate) async fn install( targets: Vec, reinstall: bool, force: bool, + python_install_mirror: Option, + pypy_install_mirror: Option, python_downloads: PythonDownloads, native_tls: bool, connectivity: Connectivity, @@ -234,6 +236,8 @@ pub(crate) async fn install( installations_dir, &cache_dir, reinstall, + python_install_mirror.clone(), + pypy_install_mirror.clone(), Some(&reporter), ) .await, diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 0ec7a92aee17..63472986e058 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -17,7 +17,7 @@ use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; use uv_requirements::{RequirementsSource, RequirementsSpecification}; -use uv_settings::{ResolverInstallerOptions, ToolOptions}; +use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user; @@ -43,6 +43,7 @@ pub(crate) async fn install( from: Option, with: &[RequirementsSource], python: Option, + install_mirrors: PythonInstallMirrors, force: bool, options: ResolverInstallerOptions, settings: ResolverInstallerSettings, @@ -74,6 +75,8 @@ pub(crate) async fn install( &client_builder, &cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index be811a406444..a41c4713e01f 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -26,6 +26,7 @@ use uv_python::{ PythonPreference, PythonRequest, }; use uv_requirements::{RequirementsSource, RequirementsSpecification}; +use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_tool::{entrypoint_paths, InstalledTools}; use uv_warnings::warn_user; @@ -68,6 +69,7 @@ pub(crate) async fn run( with: &[RequirementsSource], show_resolution: bool, python: Option, + install_mirrors: PythonInstallMirrors, settings: ResolverInstallerSettings, invocation_source: ToolRunCommand, isolated: bool, @@ -111,6 +113,7 @@ pub(crate) async fn run( with, show_resolution, python.as_deref(), + install_mirrors, &settings, isolated, python_preference, @@ -426,6 +429,7 @@ async fn get_or_create_environment( with: &[RequirementsSource], show_resolution: bool, python: Option<&str>, + install_mirrors: PythonInstallMirrors, settings: &ResolverInstallerSettings, isolated: bool, python_preference: PythonPreference, @@ -455,6 +459,8 @@ async fn get_or_create_environment( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 962da4280418..47b997aae731 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -14,7 +14,7 @@ use uv_python::{ PythonRequest, }; use uv_requirements::RequirementsSpecification; -use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions}; +use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use crate::commands::pip::loggers::{ @@ -33,6 +33,7 @@ use crate::settings::ResolverInstallerSettings; pub(crate) async fn upgrade( name: Vec, python: Option, + install_mirrors: PythonInstallMirrors, connectivity: Connectivity, args: ResolverInstallerOptions, filesystem: ResolverInstallerOptions, @@ -84,6 +85,8 @@ pub(crate) async fn upgrade( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await? .into_interpreter(), diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 7f2d8cbf718e..9520ee9c9f85 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -24,6 +24,7 @@ use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; use uv_resolver::{ExcludeNewer, FlatIndex}; +use uv_settings::PythonInstallMirrors; use uv_shell::Shell; use uv_types::{BuildContext, BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; @@ -42,6 +43,7 @@ pub(crate) async fn venv( project_dir: &Path, path: Option, python_request: Option<&str>, + install_mirrors: PythonInstallMirrors, python_preference: PythonPreference, python_downloads: PythonDownloads, link_mode: LinkMode, @@ -68,6 +70,7 @@ pub(crate) async fn venv( project_dir, path, python_request, + install_mirrors, link_mode, index_locations, index_strategy, @@ -125,6 +128,7 @@ async fn venv_impl( project_dir: &Path, path: Option, python_request: Option<&str>, + install_mirrors: PythonInstallMirrors, link_mode: LinkMode, index_locations: &IndexLocations, index_strategy: IndexStrategy, @@ -205,6 +209,8 @@ async fn venv_impl( &client_builder, cache, Some(&reporter), + install_mirrors.python_install_mirror, + install_mirrors.pypy_install_mirror, ) .await .into_diagnostic()?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 52607d585068..d81867e73604 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -734,6 +734,7 @@ async fn run(mut cli: Cli) -> Result { build_constraints, args.hash_checking, args.python, + args.install_mirrors, args.settings, cli.top_level.no_config, globals.python_preference, @@ -778,6 +779,7 @@ async fn run(mut cli: Cli) -> Result { &project_dir, args.path, args.settings.python.as_deref(), + args.settings.install_mirrors, globals.python_preference, globals.python_downloads, args.settings.link_mode, @@ -906,6 +908,7 @@ async fn run(mut cli: Cli) -> Result { &requirements, args.show_resolution || globals.verbose > 0, args.python, + args.install_mirrors, args.settings, invocation_source, args.isolated, @@ -956,6 +959,7 @@ async fn run(mut cli: Cli) -> Result { args.from, &requirements, args.python, + args.install_mirrors, args.force, args.options, args.settings, @@ -1001,6 +1005,7 @@ async fn run(mut cli: Cli) -> Result { Box::pin(commands::tool_upgrade( args.name, args.python, + args.install_mirrors, globals.connectivity, args.args, args.filesystem, @@ -1072,6 +1077,8 @@ async fn run(mut cli: Cli) -> Result { args.targets, args.reinstall, args.force, + args.python_install_mirror, + args.pypy_install_mirror, globals.python_downloads, globals.native_tls, globals.connectivity, @@ -1274,6 +1281,7 @@ async fn run_project( args.author_from, args.no_pin_python, args.python, + args.install_mirrors, args.no_workspace, globals.python_preference, globals.python_downloads, @@ -1332,6 +1340,7 @@ async fn run_project( args.dev, args.editable, args.python, + args.install_mirrors, args.settings, globals.python_preference, globals.python_downloads, @@ -1370,6 +1379,7 @@ async fn run_project( args.install_options, args.modifications, args.python, + args.install_mirrors, globals.python_preference, globals.python_downloads, args.settings, @@ -1400,6 +1410,7 @@ async fn run_project( args.frozen, args.dry_run, args.python, + args.install_mirrors, args.settings, globals.python_preference, globals.python_downloads, @@ -1452,6 +1463,7 @@ async fn run_project( args.extras, args.package, args.python, + args.install_mirrors, args.settings, args.script, globals.python_preference, @@ -1494,6 +1506,7 @@ async fn run_project( args.dependency_type, args.package, args.python, + args.install_mirrors, args.settings, script, globals.python_preference, @@ -1531,6 +1544,7 @@ async fn run_project( args.python_version, args.python_platform, args.python, + args.install_mirrors, args.resolver, globals.python_preference, globals.python_downloads, @@ -1567,6 +1581,7 @@ async fn run_project( args.frozen, args.include_header, args.python, + args.install_mirrors, args.settings, globals.python_preference, globals.python_downloads, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1ea29d8172fd..8b6b44669809 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -33,8 +33,8 @@ use uv_pypi_types::{Requirement, SupportedEnvironments}; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; use uv_settings::{ - Combine, FilesystemOptions, Options, PipOptions, PublishOptions, ResolverInstallerOptions, - ResolverOptions, + Combine, FilesystemOptions, Options, PipOptions, PublishOptions, PythonInstallMirrors, + ResolverInstallerOptions, ResolverOptions, }; use uv_static::EnvVars; use uv_warnings::warn_user_once; @@ -191,12 +191,13 @@ pub(crate) struct InitSettings { pub(crate) no_pin_python: bool, pub(crate) no_workspace: bool, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, } impl InitSettings { /// Resolve the [`InitSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: InitArgs, _filesystem: Option) -> Self { + pub(crate) fn resolve(args: InitArgs, filesystem: Option) -> Self { let InitArgs { path, name, @@ -226,6 +227,10 @@ impl InitSettings { let package = flag(package || build_backend.is_some(), no_package || r#virtual) .unwrap_or(kind.packaged_by_default()); + let install_mirrors = filesystem + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { path, name, @@ -238,6 +243,7 @@ impl InitSettings { no_pin_python, no_workspace, python: python.and_then(Maybe::into_option), + install_mirrors, } } } @@ -261,6 +267,7 @@ pub(crate) struct RunSettings { pub(crate) no_project: bool, pub(crate) no_sync: bool, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, pub(crate) env_file: Vec, @@ -304,6 +311,11 @@ impl RunSettings { no_env_file, } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { locked, frozen, @@ -341,6 +353,7 @@ impl RunSettings { ), env_file, no_env_file, + install_mirrors, } } } @@ -357,6 +370,7 @@ pub(crate) struct ToolRunSettings { pub(crate) isolated: bool, pub(crate) show_resolution: bool, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, } @@ -402,6 +416,11 @@ impl ToolRunSettings { } } + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { command, from, @@ -425,6 +444,7 @@ impl ToolRunSettings { resolver_installer_options(installer, build), filesystem, ), + install_mirrors, } } } @@ -444,6 +464,7 @@ pub(crate) struct ToolInstallSettings { pub(crate) settings: ResolverInstallerSettings, pub(crate) force: bool, pub(crate) editable: bool, + pub(crate) install_mirrors: PythonInstallMirrors, } impl ToolInstallSettings { @@ -466,11 +487,17 @@ impl ToolInstallSettings { let options = resolver_installer_options(installer, build).combine( filesystem + .clone() .map(FilesystemOptions::into_options) .map(|options| options.top_level) .unwrap_or_default(), ); + let install_mirrors = filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.install_mirrors) + .unwrap_or_default(); + let settings = ResolverInstallerSettings::from(options.clone()); Self { @@ -494,6 +521,7 @@ impl ToolInstallSettings { refresh: Refresh::from(refresh), options, settings, + install_mirrors, } } } @@ -504,10 +532,10 @@ impl ToolInstallSettings { pub(crate) struct ToolUpgradeSettings { pub(crate) name: Vec, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) args: ResolverInstallerOptions, pub(crate) filesystem: ResolverInstallerOptions, } - impl ToolUpgradeSettings { /// Resolve the [`ToolUpgradeSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] @@ -529,8 +557,12 @@ impl ToolUpgradeSettings { } let args = resolver_installer_options(installer, build); - let filesystem = filesystem - .map(FilesystemOptions::into_options) + let filesystem = filesystem.map(FilesystemOptions::into_options); + let install_mirrors = filesystem + .clone() + .map(|options| options.install_mirrors) + .unwrap_or_default(); + let top_level = filesystem .map(|options| options.top_level) .unwrap_or_default(); @@ -538,7 +570,8 @@ impl ToolUpgradeSettings { name: if all { vec![] } else { name }, python: python.and_then(Maybe::into_option), args, - filesystem, + filesystem: top_level, + install_mirrors, } } } @@ -669,22 +702,39 @@ pub(crate) struct PythonInstallSettings { pub(crate) targets: Vec, pub(crate) reinstall: bool, pub(crate) force: bool, + pub(crate) python_install_mirror: Option, + pub(crate) pypy_install_mirror: Option, } impl PythonInstallSettings { /// Resolve the [`PythonInstallSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: PythonInstallArgs, _filesystem: Option) -> Self { + pub(crate) fn resolve(args: PythonInstallArgs, filesystem: Option) -> Self { + let options = filesystem.map(uv_settings::FilesystemOptions::into_options); + let (python_mirror, pypy_mirror) = match options { + Some(options) => ( + options.install_mirrors.python_install_mirror, + options.install_mirrors.pypy_install_mirror, + ), + None => (None, None), + }; + let python_mirror = args.mirror.or(python_mirror); + let pypy_mirror = args.pypy_mirror.or(pypy_mirror); + let PythonInstallArgs { targets, reinstall, force, + mirror: _, + pypy_mirror: _, } = args; Self { targets, reinstall, force, + python_install_mirror: python_mirror, + pypy_install_mirror: pypy_mirror, } } } @@ -780,6 +830,7 @@ pub(crate) struct SyncSettings { pub(crate) all_packages: bool, pub(crate) package: Option, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, } @@ -813,6 +864,10 @@ impl SyncSettings { package, python, } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); let settings = ResolverInstallerSettings::combine( resolver_installer_options(installer, build), @@ -845,6 +900,7 @@ impl SyncSettings { python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings, + install_mirrors, } } } @@ -857,6 +913,7 @@ pub(crate) struct LockSettings { pub(crate) frozen: bool, pub(crate) dry_run: bool, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverSettings, } @@ -875,6 +932,11 @@ impl LockSettings { python, } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { locked, frozen, @@ -882,6 +944,7 @@ impl LockSettings { python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + install_mirrors, } } } @@ -905,6 +968,7 @@ pub(crate) struct AddSettings { pub(crate) package: Option, pub(crate) script: Option, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) indexes: Vec, pub(crate) settings: ResolverInstallerSettings, @@ -993,6 +1057,11 @@ impl AddSettings { } } + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { locked, frozen, @@ -1015,6 +1084,7 @@ impl AddSettings { resolver_installer_options(installer, build), filesystem, ), + install_mirrors, } } } @@ -1031,6 +1101,7 @@ pub(crate) struct RemoveSettings { pub(crate) package: Option, pub(crate) script: Option, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, } @@ -1065,6 +1136,11 @@ impl RemoveSettings { DependencyType::Production }; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + Self { locked, frozen, @@ -1079,6 +1155,7 @@ impl RemoveSettings { resolver_installer_options(installer, build), filesystem, ), + install_mirrors, } } } @@ -1100,6 +1177,7 @@ pub(crate) struct TreeSettings { pub(crate) python_version: Option, pub(crate) python_platform: Option, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) resolver: ResolverSettings, } @@ -1123,6 +1201,10 @@ impl TreeSettings { python_platform, python, } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); Self { dev: DevGroupsSpecification::from_args( @@ -1141,6 +1223,7 @@ impl TreeSettings { python_platform, python: python.and_then(Maybe::into_option), resolver: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + install_mirrors, } } } @@ -1162,6 +1245,7 @@ pub(crate) struct ExportSettings { pub(crate) frozen: bool, pub(crate) include_header: bool, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverSettings, } @@ -1199,6 +1283,10 @@ impl ExportSettings { refresh, python, } = args; + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); Self { format, @@ -1225,6 +1313,7 @@ impl ExportSettings { python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + install_mirrors, } } } @@ -1868,6 +1957,7 @@ pub(crate) struct BuildSettings { pub(crate) build_constraint: Vec, pub(crate) hash_checking: Option, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverSettings, } @@ -1895,6 +1985,11 @@ impl BuildSettings { resolver, } = args; + let install_mirrors = match &filesystem { + Some(fs) => fs.install_mirrors.clone(), + None => PythonInstallMirrors::default(), + }; + Self { src, package, @@ -1914,6 +2009,7 @@ impl BuildSettings { python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + install_mirrors, } } } @@ -2269,6 +2365,7 @@ impl From for ResolverInstallerSettings { pub(crate) struct PipSettings { pub(crate) index_locations: IndexLocations, pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) system: bool, pub(crate) extras: ExtrasSpecification, pub(crate) break_system_packages: bool, @@ -2315,7 +2412,12 @@ pub(crate) struct PipSettings { impl PipSettings { /// Resolve the [`PipSettings`] from the CLI and filesystem configuration. pub(crate) fn combine(args: PipOptions, filesystem: Option) -> Self { - let Options { top_level, pip, .. } = filesystem + let Options { + top_level, + pip, + install_mirrors, + .. + } = filesystem .map(FilesystemOptions::into_options) .unwrap_or_default(); @@ -2592,6 +2694,7 @@ impl PipSettings { top_level_no_build_package.unwrap_or_default(), )), ), + install_mirrors, } } } diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 1ec6a7edf922..0eb48500adf6 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -450,7 +450,7 @@ fn help_subcommand() { fn help_subsubcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r###" + uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r##" success: true exit_code: 0 ----- stdout ----- @@ -483,6 +483,27 @@ fn help_subsubcommand() { See `uv help python` to view supported request formats. Options: + --mirror + Set the URL to use as the source for downloading Python installations. + + The provided URL will replace + `https://github.com/indygreg/python-build-standalone/releases/download` in, e.g., + `https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. + + Distributions can be read from a local directory by using the `file://` URL scheme. + + [env: UV_PYTHON_INSTALL_MIRROR=] + + --pypy-mirror + Set the URL to use as the source for downloading PyPy installations. + + The provided URL will replace `https://downloads.python.org/pypy` in, e.g., + `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. + + Distributions can be read from a local directory by using the `file://` URL scheme. + + [env: UV_PYPY_INSTALL_MIRROR=] + -r, --reinstall Reinstall the requested Python version, if it's already installed. @@ -638,7 +659,7 @@ fn help_subsubcommand() { ----- stderr ----- - "###); + "##); } #[test] @@ -724,8 +745,12 @@ fn help_flag_subsubcommand() { [TARGETS]... The Python version(s) to install Options: - -r, --reinstall Reinstall the requested Python version, if it's already installed - -f, --force Replace existing Python executables during installation + --mirror Set the URL to use as the source for downloading Python + installations [env: UV_PYTHON_INSTALL_MIRROR=] + --pypy-mirror Set the URL to use as the source for downloading PyPy + installations [env: UV_PYPY_INSTALL_MIRROR=] + -r, --reinstall Reinstall the requested Python version, if it's already installed + -f, --force Replace existing Python executables during installation Cache options: -n, --no-cache Avoid reading from or writing to the cache, instead using a temporary diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 7dbb846ea583..e5564273d111 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -4286,7 +4286,7 @@ fn lock_requires_python() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 1 ----- stdout ----- @@ -4309,7 +4309,7 @@ fn lock_requires_python() -> Result<()> { hint: Pre-releases are available for pygls in the requested range (e.g., 2.0.0a2), but pre-releases weren't enabled (try: `--prerelease=allow`) hint: The `requires-python` value (>=3.7) includes Python versions that are not supported by your dependencies (e.g., pygls>=1.1.0,<=1.2.1 only supports >=3.7.9, <4). Consider using a more restrictive `requires-python` value (like >=3.7.9, <4). - "###); + "); // Require >=3.7, and allow locking to a version of `pygls` that is compatible (==1.0.1). pyproject_toml.write_str( diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 17f3a7046f93..01fecac90209 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -191,7 +191,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicting-groups`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` + unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicting-groups`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 65593d5b2721..49a889198081 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -123,6 +123,10 @@ fn resolve_uv_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -270,6 +274,10 @@ fn resolve_uv_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -418,6 +426,10 @@ fn resolve_uv_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -598,6 +610,10 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -718,6 +734,10 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -876,6 +896,10 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1076,6 +1100,10 @@ fn resolve_index_url() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1283,6 +1311,10 @@ fn resolve_index_url() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1455,6 +1487,10 @@ fn resolve_find_links() -> anyhow::Result<()> { no_index: true, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1597,6 +1633,10 @@ fn resolve_top_level() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1789,6 +1829,10 @@ fn resolve_top_level() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -1964,6 +2008,10 @@ fn resolve_top_level() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2106,6 +2154,10 @@ fn resolve_user_configuration() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2231,6 +2283,10 @@ fn resolve_user_configuration() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2356,6 +2412,10 @@ fn resolve_user_configuration() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2483,6 +2543,10 @@ fn resolve_user_configuration() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2687,6 +2751,10 @@ fn resolve_tool() -> anyhow::Result<()> { }, force: false, editable: false, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, } ----- stderr ----- @@ -2786,6 +2854,10 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -2968,6 +3040,10 @@ fn resolve_both() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -3238,6 +3314,10 @@ fn resolve_config_file() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -3325,7 +3405,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicting-groups`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` + unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicting-groups`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies` "### ); @@ -3457,6 +3537,10 @@ fn resolve_skip_empty() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -3585,6 +3669,10 @@ fn resolve_skip_empty() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -3732,6 +3820,10 @@ fn allow_insecure_host() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -3930,6 +4022,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -4107,6 +4203,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -4290,6 +4390,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -4468,6 +4572,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -4653,6 +4761,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, @@ -4831,6 +4943,10 @@ fn index_priority() -> anyhow::Result<()> { no_index: false, }, python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + }, system: false, extras: None, break_system_packages: false, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 1e0b9a21a1a4..99f44e8c3f76 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -30,7 +30,7 @@ fn sync() -> Result<()> { )?; // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -41,7 +41,7 @@ fn sync() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4b5c4cb61880..ad493fead8a9 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4457,6 +4457,13 @@ uv python install [OPTIONS] [TARGETS]...
--help, -h

Display the concise help for this command

+
--mirror mirror

Set the URL to use as the source for downloading Python installations.

+ +

The provided URL will replace https://github.com/indygreg/python-build-standalone/releases/download in, e.g., https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz.

+ +

Distributions can be read from a local directory by using the file:// URL scheme.

+ +

May also be set with the UV_PYTHON_INSTALL_MIRROR environment variable.

--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

@@ -4493,6 +4500,13 @@ uv python install [OPTIONS] [TARGETS]...

This setting has no effect when used in the uv pip interface.

+
--pypy-mirror pypy-mirror

Set the URL to use as the source for downloading PyPy installations.

+ +

The provided URL will replace https://downloads.python.org/pypy in, e.g., https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2.

+ +

Distributions can be read from a local directory by using the file:// URL scheme.

+ +

May also be set with the UV_PYPY_INSTALL_MIRROR environment variable.

--python-preference python-preference

Whether to prefer uv-managed or system Python installations.

By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 7e07811452fc..fa947c3f025e 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1226,6 +1226,37 @@ The URL for publishing packages to the Python package index (by default: --- +### [`pypy-install-mirror`](#pypy-install-mirror) {: #pypy-install-mirror } + +Mirror URL to use for downloading managed PyPy installations. + +By default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/). +This variable can be set to a mirror URL to use a different source for PyPy installations. +The provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`. + +Distributions can be read from a +local directory by using the `file://` URL scheme. + +**Default value**: `None` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + pypy-install-mirror = "https://downloads.python.org/pypy" + ``` +=== "uv.toml" + + ```toml + pypy-install-mirror = "https://downloads.python.org/pypy" + ``` + +--- + ### [`python-downloads`](#python-downloads) {: #python-downloads } Whether to allow Python downloads. @@ -1254,6 +1285,36 @@ Whether to allow Python downloads. --- +### [`python-install-mirror`](#python-install-mirror) {: #python-install-mirror } + +Mirror URL for downloading managed Python installations. + +By default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/indygreg/python-build-standalone). +This variable can be set to a mirror URL to use a different source for Python installations. +The provided URL will replace `https://github.com/indygreg/python-build-standalone/releases/download` in, e.g., `https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`. + +Distributions can be read from a local directory by using the `file://` URL scheme. + +**Default value**: `None` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + python-install-mirror = "https://github.com/indygreg/python-build-standalone/releases/download" + ``` +=== "uv.toml" + + ```toml + python-install-mirror = "https://github.com/indygreg/python-build-standalone/releases/download" + ``` + +--- + ### [`python-preference`](#python-preference) {: #python-preference } Whether to prefer using Python installations that are already present on the system, or diff --git a/uv.schema.json b/uv.schema.json index ecf4e8b620ce..aba29e3aabbc 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -357,6 +357,13 @@ ], "format": "uri" }, + "pypy-install-mirror": { + "description": "Mirror URL to use for downloading managed PyPy installations.\n\nBy default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/). This variable can be set to a mirror URL to use a different source for PyPy installations. The provided URL will replace `https://downloads.python.org/pypy` in, e.g., `https://downloads.python.org/pypy/pypy3.8-v7.3.7-osx64.tar.bz2`.\n\nDistributions can be read from a local directory by using the `file://` URL scheme.", + "type": [ + "string", + "null" + ] + }, "python-downloads": { "description": "Whether to allow Python downloads.", "anyOf": [ @@ -368,6 +375,13 @@ } ] }, + "python-install-mirror": { + "description": "Mirror URL for downloading managed Python installations.\n\nBy default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/indygreg/python-build-standalone). This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace `https://github.com/indygreg/python-build-standalone/releases/download` in, e.g., `https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz`.\n\nDistributions can be read from a local directory by using the `file://` URL scheme.", + "type": [ + "string", + "null" + ] + }, "python-preference": { "description": "Whether to prefer using Python installations that are already present on the system, or those that are downloaded and installed by uv.", "anyOf": [