Skip to content

Commit 2ea81b3

Browse files
owenbrookszanieb
andauthored
Make Python and PyPy install mirrors configurable in uv.toml (#8695)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) --> ## Summary Adds python-install-mirror and pypy-install-mirror as keys for uv.toml, and cli args for `uv python install`. Could leave the cli args out if we think the env vars and configs are sufficient. Fixes #8186 <!-- What's the purpose of the change? What does it do, and why? --> --------- Co-authored-by: Zanie Blue <[email protected]>
1 parent 2966471 commit 2ea81b3

File tree

29 files changed

+573
-27
lines changed

29 files changed

+573
-27
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3938,6 +3938,22 @@ pub struct PythonInstallArgs {
39383938
/// See `uv help python` to view supported request formats.
39393939
pub targets: Vec<String>,
39403940

3941+
/// Set the URL to use as the source for downloading Python installations.
3942+
///
3943+
/// 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`.
3944+
///
3945+
/// Distributions can be read from a local directory by using the `file://` URL scheme.
3946+
#[arg(long, env = EnvVars::UV_PYTHON_INSTALL_MIRROR)]
3947+
pub mirror: Option<String>,
3948+
3949+
/// Set the URL to use as the source for downloading PyPy installations.
3950+
///
3951+
/// 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`.
3952+
///
3953+
/// Distributions can be read from a local directory by using the `file://` URL scheme.
3954+
#[arg(long, env = EnvVars::UV_PYPY_INSTALL_MIRROR)]
3955+
pub pypy_mirror: Option<String>,
3956+
39413957
/// Reinstall the requested Python version, if it's already installed.
39423958
///
39433959
/// By default, uv will exit successfully if the version is already

crates/uv-python/src/downloads.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,11 @@ impl ManagedPythonDownload {
466466
installation_dir: &Path,
467467
cache_dir: &Path,
468468
reinstall: bool,
469+
python_install_mirror: Option<String>,
470+
pypy_install_mirror: Option<String>,
469471
reporter: Option<&dyn Reporter>,
470472
) -> Result<DownloadResult, Error> {
471-
let url = self.download_url()?;
473+
let url = self.download_url(python_install_mirror, pypy_install_mirror)?;
472474
let path = installation_dir.join(self.key().to_string());
473475

474476
// If it is not a reinstall and the dir already exists, return it.
@@ -585,10 +587,14 @@ impl ManagedPythonDownload {
585587

586588
/// Return the [`Url`] to use when downloading the distribution. If a mirror is set via the
587589
/// appropriate environment variable, use it instead.
588-
fn download_url(&self) -> Result<Url, Error> {
590+
fn download_url(
591+
&self,
592+
python_install_mirror: Option<String>,
593+
pypy_install_mirror: Option<String>,
594+
) -> Result<Url, Error> {
589595
match self.key.implementation {
590596
LenientImplementationName::Known(ImplementationName::CPython) => {
591-
if let Ok(mirror) = std::env::var(EnvVars::UV_PYTHON_INSTALL_MIRROR) {
597+
if let Some(mirror) = python_install_mirror {
592598
let Some(suffix) = self.url.strip_prefix(
593599
"https://github.com/indygreg/python-build-standalone/releases/download/",
594600
) else {
@@ -601,7 +607,7 @@ impl ManagedPythonDownload {
601607
}
602608

603609
LenientImplementationName::Known(ImplementationName::PyPy) => {
604-
if let Ok(mirror) = std::env::var(EnvVars::UV_PYPY_INSTALL_MIRROR) {
610+
if let Some(mirror) = pypy_install_mirror {
605611
let Some(suffix) = self.url.strip_prefix("https://downloads.python.org/pypy/")
606612
else {
607613
return Err(Error::Mirror(EnvVars::UV_PYPY_INSTALL_MIRROR, self.url));

crates/uv-python/src/installation.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ impl PythonInstallation {
8686
client_builder: &BaseClientBuilder<'a>,
8787
cache: &Cache,
8888
reporter: Option<&dyn Reporter>,
89+
python_install_mirror: Option<String>,
90+
pypy_install_mirror: Option<String>,
8991
) -> Result<Self, Error> {
9092
let request = request.unwrap_or_else(|| &PythonRequest::Default);
9193

@@ -100,7 +102,16 @@ impl PythonInstallation {
100102
{
101103
if let Some(request) = PythonDownloadRequest::from_request(request) {
102104
debug!("Requested Python not found, checking for available download...");
103-
match Self::fetch(request.fill()?, client_builder, cache, reporter).await {
105+
match Self::fetch(
106+
request.fill()?,
107+
client_builder,
108+
cache,
109+
reporter,
110+
python_install_mirror,
111+
pypy_install_mirror,
112+
)
113+
.await
114+
{
104115
Ok(installation) => Ok(installation),
105116
Err(Error::Download(downloads::Error::NoDownloadFound(_))) => {
106117
Err(Error::MissingPython(err))
@@ -121,6 +132,8 @@ impl PythonInstallation {
121132
client_builder: &BaseClientBuilder<'a>,
122133
cache: &Cache,
123134
reporter: Option<&dyn Reporter>,
135+
python_install_mirror: Option<String>,
136+
pypy_install_mirror: Option<String>,
124137
) -> Result<Self, Error> {
125138
let installations = ManagedPythonInstallations::from_settings()?.init()?;
126139
let installations_dir = installations.root();
@@ -132,7 +145,15 @@ impl PythonInstallation {
132145

133146
info!("Fetching requested Python...");
134147
let result = download
135-
.fetch(&client, installations_dir, &cache_dir, false, reporter)
148+
.fetch(
149+
&client,
150+
installations_dir,
151+
&cache_dir,
152+
false,
153+
python_install_mirror,
154+
pypy_install_mirror,
155+
reporter,
156+
)
136157
.await?;
137158

138159
let path = match result {

crates/uv-settings/src/settings.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use uv_pep508::Requirement;
1414
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
1515
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
1616
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
17+
use uv_static::EnvVars;
1718

1819
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
1920
#[allow(dead_code)]
@@ -41,6 +42,9 @@ pub struct Options {
4142
#[serde(flatten)]
4243
pub top_level: ResolverInstallerOptions,
4344

45+
#[serde(flatten)]
46+
pub install_mirrors: PythonInstallMirrors,
47+
4448
#[serde(flatten)]
4549
pub publish: PublishOptions,
4650

@@ -676,6 +680,61 @@ pub struct ResolverInstallerOptions {
676680
pub no_binary_package: Option<Vec<PackageName>>,
677681
}
678682

683+
/// Shared settings, relevant to all operations that might create managed python installations.
684+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata)]
685+
#[serde(rename_all = "kebab-case")]
686+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
687+
pub struct PythonInstallMirrors {
688+
/// Mirror URL for downloading managed Python installations.
689+
///
690+
/// By default, managed Python installations are downloaded from [`python-build-standalone`](https://github.com/indygreg/python-build-standalone).
691+
/// This variable can be set to a mirror URL to use a different source for Python installations.
692+
/// 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`.
693+
///
694+
/// Distributions can be read from a local directory by using the `file://` URL scheme.
695+
#[option(
696+
default = "None",
697+
value_type = "str",
698+
example = r#"
699+
python-install-mirror = "https://github.com/indygreg/python-build-standalone/releases/download"
700+
"#
701+
)]
702+
pub python_install_mirror: Option<String>,
703+
/// Mirror URL to use for downloading managed PyPy installations.
704+
///
705+
/// By default, managed PyPy installations are downloaded from [downloads.python.org](https://downloads.python.org/).
706+
/// This variable can be set to a mirror URL to use a different source for PyPy installations.
707+
/// 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`.
708+
///
709+
/// Distributions can be read from a
710+
/// local directory by using the `file://` URL scheme.
711+
#[option(
712+
default = "None",
713+
value_type = "str",
714+
example = r#"
715+
pypy-install-mirror = "https://downloads.python.org/pypy"
716+
"#
717+
)]
718+
pub pypy_install_mirror: Option<String>,
719+
}
720+
721+
impl Default for PythonInstallMirrors {
722+
fn default() -> Self {
723+
PythonInstallMirrors::resolve(None, None)
724+
}
725+
}
726+
727+
impl PythonInstallMirrors {
728+
pub fn resolve(python_mirror: Option<String>, pypy_mirror: Option<String>) -> Self {
729+
let python_mirror_env = std::env::var(EnvVars::UV_PYTHON_INSTALL_MIRROR).ok();
730+
let pypy_mirror_env = std::env::var(EnvVars::UV_PYPY_INSTALL_MIRROR).ok();
731+
PythonInstallMirrors {
732+
python_install_mirror: python_mirror_env.or(python_mirror),
733+
pypy_install_mirror: pypy_mirror_env.or(pypy_mirror),
734+
}
735+
}
736+
}
737+
679738
/// Settings that are specific to the `uv pip` command-line interface.
680739
///
681740
/// These values will be ignored when running commands outside the `uv pip` namespace (e.g.,
@@ -1544,6 +1603,11 @@ pub struct OptionsWire {
15441603
no_binary: Option<bool>,
15451604
no_binary_package: Option<Vec<PackageName>>,
15461605

1606+
// #[serde(flatten)]
1607+
// install_mirror: PythonInstallMirrors,
1608+
python_install_mirror: Option<String>,
1609+
pypy_install_mirror: Option<String>,
1610+
15471611
// #[serde(flatten)]
15481612
// publish: PublishOptions
15491613
publish_url: Option<Url>,
@@ -1581,6 +1645,8 @@ impl From<OptionsWire> for Options {
15811645
preview,
15821646
python_preference,
15831647
python_downloads,
1648+
python_install_mirror,
1649+
pypy_install_mirror,
15841650
concurrent_downloads,
15851651
concurrent_builds,
15861652
concurrent_installs,
@@ -1673,6 +1739,10 @@ impl From<OptionsWire> for Options {
16731739
override_dependencies,
16741740
constraint_dependencies,
16751741
environments,
1742+
install_mirrors: PythonInstallMirrors::resolve(
1743+
python_install_mirror,
1744+
pypy_install_mirror,
1745+
),
16761746
conflicting_groups,
16771747
publish: PublishOptions {
16781748
publish_url,

crates/uv/src/commands/build_frontend.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use uv_python::{
2828
};
2929
use uv_requirements::RequirementsSource;
3030
use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython};
31+
use uv_settings::PythonInstallMirrors;
3132
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
3233
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError};
3334

@@ -52,6 +53,7 @@ pub(crate) async fn build_frontend(
5253
build_constraints: Vec<RequirementsSource>,
5354
hash_checking: Option<HashCheckingMode>,
5455
python: Option<String>,
56+
install_mirrors: PythonInstallMirrors,
5557
settings: ResolverSettings,
5658
no_config: bool,
5759
python_preference: PythonPreference,
@@ -75,6 +77,7 @@ pub(crate) async fn build_frontend(
7577
&build_constraints,
7678
hash_checking,
7779
python.as_deref(),
80+
install_mirrors,
7881
settings.as_ref(),
7982
no_config,
8083
python_preference,
@@ -116,6 +119,7 @@ async fn build_impl(
116119
build_constraints: &[RequirementsSource],
117120
hash_checking: Option<HashCheckingMode>,
118121
python_request: Option<&str>,
122+
install_mirrors: PythonInstallMirrors,
119123
settings: ResolverSettingsRef<'_>,
120124
no_config: bool,
121125
python_preference: PythonPreference,
@@ -251,6 +255,7 @@ async fn build_impl(
251255
source.clone(),
252256
output_dir,
253257
python_request,
258+
install_mirrors.clone(),
254259
no_config,
255260
workspace.as_ref(),
256261
python_preference,
@@ -346,6 +351,7 @@ async fn build_package(
346351
source: AnnotatedSource<'_>,
347352
output_dir: Option<&Path>,
348353
python_request: Option<&str>,
354+
install_mirrors: PythonInstallMirrors,
349355
no_config: bool,
350356
workspace: Result<&Workspace, &WorkspaceError>,
351357
python_preference: PythonPreference,
@@ -424,6 +430,8 @@ async fn build_package(
424430
client_builder,
425431
cache,
426432
Some(&PythonDownloadReporter::single(printer)),
433+
install_mirrors.python_install_mirror,
434+
install_mirrors.pypy_install_mirror,
427435
)
428436
.await?
429437
.into_interpreter();

crates/uv/src/commands/project/add.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use uv_python::{
3333
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
3434
use uv_resolver::{FlatIndex, InstallTarget};
3535
use uv_scripts::{Pep723Item, Pep723Script};
36+
use uv_settings::PythonInstallMirrors;
3637
use uv_types::{BuildIsolation, HashStrategy};
3738
use uv_warnings::warn_user_once;
3839
use uv_workspace::pyproject::{DependencyType, Source, SourceError};
@@ -71,6 +72,7 @@ pub(crate) async fn add(
7172
extras: Vec<ExtraName>,
7273
package: Option<PackageName>,
7374
python: Option<String>,
75+
install_mirrors: PythonInstallMirrors,
7476
settings: ResolverInstallerSettings,
7577
script: Option<PathBuf>,
7678
python_preference: PythonPreference,
@@ -140,6 +142,7 @@ pub(crate) async fn add(
140142
} else {
141143
let requires_python = init_script_python_requirement(
142144
python.as_deref(),
145+
install_mirrors.clone(),
143146
project_dir,
144147
false,
145148
python_preference,
@@ -173,6 +176,8 @@ pub(crate) async fn add(
173176
&client_builder,
174177
cache,
175178
Some(&reporter),
179+
install_mirrors.python_install_mirror,
180+
install_mirrors.pypy_install_mirror,
176181
)
177182
.await?
178183
.into_interpreter();
@@ -227,6 +232,7 @@ pub(crate) async fn add(
227232
connectivity,
228233
native_tls,
229234
allow_insecure_host,
235+
install_mirrors.clone(),
230236
no_config,
231237
cache,
232238
printer,
@@ -240,6 +246,7 @@ pub(crate) async fn add(
240246
let venv = project::get_or_init_environment(
241247
project.workspace(),
242248
python.as_deref().map(PythonRequest::parse),
249+
install_mirrors.clone(),
243250
python_preference,
244251
python_downloads,
245252
connectivity,

crates/uv/src/commands/project/export.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::{Context, Result};
44
use itertools::Itertools;
55
use owo_colors::OwoColorize;
66
use std::path::{Path, PathBuf};
7+
use uv_settings::PythonInstallMirrors;
78

89
use uv_cache::Cache;
910
use uv_client::Connectivity;
@@ -42,6 +43,7 @@ pub(crate) async fn export(
4243
frozen: bool,
4344
include_header: bool,
4445
python: Option<String>,
46+
install_mirrors: PythonInstallMirrors,
4547
settings: ResolverSettings,
4648
python_preference: PythonPreference,
4749
python_downloads: PythonDownloads,
@@ -107,6 +109,7 @@ pub(crate) async fn export(
107109
connectivity,
108110
native_tls,
109111
allow_insecure_host,
112+
install_mirrors,
110113
no_config,
111114
cache,
112115
printer,

0 commit comments

Comments
 (0)