From 0593b967ba848909c46b4c95f4ba8cbdcba147e9 Mon Sep 17 00:00:00 2001 From: Meitar Reihan Date: Wed, 30 Apr 2025 22:52:11 +0300 Subject: [PATCH] Add `python-downloads-json-url` option for `uv.toml` to configure custom Python installations via JSON URL (#12974) ## Summary Part of #12838. Allow users to configure `python-downloads-json-url` in `uv.toml` and not just from env. I followed similar PR #8695, so same as there it's also available in the CLI (I think maybe it's better not to be configurable from the CLI, but since the mirror parameters are, I think it's better to do the same) ## Test Plan --- crates/uv-cli/src/lib.rs | 12 ++++++ crates/uv-python/src/downloads.rs | 52 +++++++++++++----------- crates/uv-python/src/installation.rs | 5 ++- crates/uv-settings/src/settings.rs | 26 +++++++++++- crates/uv/src/commands/build_frontend.rs | 1 + crates/uv/src/commands/project/init.rs | 4 ++ crates/uv/src/commands/project/mod.rs | 3 ++ crates/uv/src/commands/project/run.rs | 2 + crates/uv/src/commands/python/install.rs | 36 +++++++++------- crates/uv/src/commands/python/list.rs | 3 +- crates/uv/src/commands/tool/common.rs | 1 + crates/uv/src/commands/tool/install.rs | 1 + crates/uv/src/commands/tool/run.rs | 1 + crates/uv/src/commands/tool/upgrade.rs | 1 + crates/uv/src/commands/venv.rs | 1 + crates/uv/src/lib.rs | 2 + crates/uv/src/settings.rs | 24 +++++++++-- crates/uv/tests/it/help.rs | 32 +++++++++++---- crates/uv/tests/it/show_settings.rs | 37 ++++++++++++++++- docs/reference/cli.md | 10 +++++ docs/reference/settings.md | 26 ++++++++++++ uv.schema.json | 7 ++++ 22 files changed, 232 insertions(+), 55 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index fc4e84cbf..8cbb05779 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4728,6 +4728,12 @@ pub struct PythonListArgs { /// Select the output format. #[arg(long, value_enum, default_value_t = PythonListFormat::default())] pub output_format: PythonListFormat, + + /// URL pointing to JSON of custom Python installations. + /// + /// Note that currently, only local paths are supported. + #[arg(long, env = EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL)] + pub python_downloads_json_url: Option, } #[derive(Args)] @@ -4791,6 +4797,12 @@ pub struct PythonInstallArgs { #[arg(long, env = EnvVars::UV_PYPY_INSTALL_MIRROR)] pub pypy_mirror: Option, + /// URL pointing to JSON of custom Python installations. + /// + /// Note that currently, only local paths are supported. + #[arg(long, env = EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL)] + pub python_downloads_json_url: 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 8683a0526..8095bbe07 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -268,8 +268,9 @@ impl PythonDownloadRequest { /// Iterate over all [`PythonDownload`]'s that match this request. pub fn iter_downloads( &self, + python_downloads_json_url: Option<&str>, ) -> Result + use<'_>, Error> { - Ok(ManagedPythonDownload::iter_all()? + Ok(ManagedPythonDownload::iter_all(python_downloads_json_url)? .filter(move |download| self.satisfied_by_download(download))) } @@ -496,8 +497,9 @@ impl ManagedPythonDownload { /// be searched for — even if a pre-release was not explicitly requested. pub fn from_request( request: &PythonDownloadRequest, + python_downloads_json_url: Option<&str>, ) -> Result<&'static ManagedPythonDownload, Error> { - if let Some(download) = request.iter_downloads()?.next() { + if let Some(download) = request.iter_downloads(python_downloads_json_url)?.next() { return Ok(download); } @@ -505,7 +507,7 @@ impl ManagedPythonDownload { if let Some(download) = request .clone() .with_prereleases(true) - .iter_downloads()? + .iter_downloads(python_downloads_json_url)? .next() { return Ok(download); @@ -514,32 +516,36 @@ impl ManagedPythonDownload { Err(Error::NoDownloadFound(request.clone())) } - //noinspection RsUnresolvedPath - RustRover can't see through the `include!` + /// Iterate over all [`ManagedPythonDownload`]s. - pub fn iter_all() -> Result, Error> { - let runtime_source = std::env::var(EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL); - + /// + /// Note: The list is generated on the first call to this function. + /// so `python_downloads_json_url` is only used in the first call to this function. + pub fn iter_all( + python_downloads_json_url: Option<&str>, + ) -> Result, Error> { let downloads = PYTHON_DOWNLOADS.get_or_try_init(|| { - let json_downloads: HashMap = - if let Ok(json_source) = &runtime_source { - if Url::parse(json_source).is_ok() { - return Err(Error::RemoteJSONNotSupported()); - } + let json_downloads: HashMap = if let Some(json_source) = + python_downloads_json_url + { + if Url::parse(json_source).is_ok() { + return Err(Error::RemoteJSONNotSupported()); + } - let file = match fs_err::File::open(json_source) { - Ok(file) => file, - Err(e) => { Err(Error::Io(e)) }?, - }; - - serde_json::from_reader(file) - .map_err(|e| Error::InvalidPythonDownloadsJSON(json_source.clone(), e))? - } else { - serde_json::from_str(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| { - Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_string(), e) - })? + let file = match fs_err::File::open(json_source) { + Ok(file) => file, + Err(e) => { Err(Error::Io(e)) }?, }; + serde_json::from_reader(file) + .map_err(|e| Error::InvalidPythonDownloadsJSON(json_source.to_string(), e))? + } else { + serde_json::from_str(BUILTIN_PYTHON_DOWNLOADS_JSON).map_err(|e| { + Error::InvalidPythonDownloadsJSON("EMBEDDED IN THE BINARY".to_string(), e) + })? + }; + let result = parse_json_downloads(json_downloads); Ok(Cow::Owned(result)) })?; diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 8a664a3d6..b72f1d87e 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -88,6 +88,7 @@ impl PythonInstallation { reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, + python_downloads_json_url: Option<&str>, ) -> Result { let request = request.unwrap_or(&PythonRequest::Default); @@ -127,6 +128,7 @@ impl PythonInstallation { reporter, python_install_mirror, pypy_install_mirror, + python_downloads_json_url, ) .await { @@ -146,13 +148,14 @@ impl PythonInstallation { reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, + python_downloads_json_url: Option<&str>, ) -> Result { let installations = ManagedPythonInstallations::from_settings(None)?.init()?; let installations_dir = installations.root(); let scratch_dir = installations.scratch(); let _lock = installations.lock().await?; - let download = ManagedPythonDownload::from_request(&request)?; + let download = ManagedPythonDownload::from_request(&request, python_downloads_json_url)?; let client = client_builder.build(); info!("Fetching requested Python..."); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 769699287..58632511b 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -819,21 +819,40 @@ pub struct PythonInstallMirrors { "# )] pub pypy_install_mirror: Option, + + /// URL pointing to JSON of custom Python installations. + /// + /// Note that currently, only local paths are supported. + #[option( + default = "None", + value_type = "str", + example = r#" + python-downloads-json-url = "/etc/uv/python-downloads.json" + "# + )] + pub python_downloads_json_url: Option, } impl Default for PythonInstallMirrors { fn default() -> Self { - PythonInstallMirrors::resolve(None, None) + PythonInstallMirrors::resolve(None, None, None) } } impl PythonInstallMirrors { - pub fn resolve(python_mirror: Option, pypy_mirror: Option) -> Self { + pub fn resolve( + python_mirror: Option, + pypy_mirror: Option, + python_downloads_json_url: 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(); + let python_downloads_json_url_env = + std::env::var(EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL).ok(); PythonInstallMirrors { python_install_mirror: python_mirror_env.or(python_mirror), pypy_install_mirror: pypy_mirror_env.or(pypy_mirror), + python_downloads_json_url: python_downloads_json_url_env.or(python_downloads_json_url), } } } @@ -1814,6 +1833,7 @@ pub struct OptionsWire { // install_mirror: PythonInstallMirrors, python_install_mirror: Option, pypy_install_mirror: Option, + python_downloads_json_url: Option, // #[serde(flatten)] // publish: PublishOptions @@ -1861,6 +1881,7 @@ impl From for Options { python_downloads, python_install_mirror, pypy_install_mirror, + python_downloads_json_url, concurrent_downloads, concurrent_builds, concurrent_installs, @@ -1967,6 +1988,7 @@ impl From for Options { install_mirrors: PythonInstallMirrors::resolve( python_install_mirror, pypy_install_mirror, + python_downloads_json_url, ), conflicts, publish: PublishOptions { diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 55580ce36..98b42ef4f 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -484,6 +484,7 @@ async fn build_package( Some(&PythonDownloadReporter::single(printer)), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 8ffc95fe5..b75fb2f2a 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -425,6 +425,7 @@ async fn init_project( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); @@ -451,6 +452,7 @@ async fn init_project( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); @@ -516,6 +518,7 @@ async fn init_project( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); @@ -542,6 +545,7 @@ async fn init_project( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 53e51f808..9c25600a6 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -710,6 +710,7 @@ impl ScriptInterpreter { Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); @@ -903,6 +904,7 @@ impl ProjectInterpreter { Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await?; @@ -2280,6 +2282,7 @@ pub(crate) async fn init_script_python_requirement( Some(reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 0d7694e63..b37d0da2f 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -608,6 +608,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl Some(&download_reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); @@ -841,6 +842,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl Some(&download_reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await?; diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index ffd8d7e63..91d265267 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -43,7 +43,7 @@ struct InstallRequest { } impl InstallRequest { - fn new(request: PythonRequest) -> Result { + fn new(request: PythonRequest, python_downloads_json_url: Option<&str>) -> Result { // Make sure the request is a valid download request and fill platform information let download_request = PythonDownloadRequest::from_request(&request) .ok_or_else(|| { @@ -55,18 +55,20 @@ impl InstallRequest { .fill()?; // Find a matching download - let download = match ManagedPythonDownload::from_request(&download_request) { - Ok(download) => download, - Err(downloads::Error::NoDownloadFound(request)) - if request.libc().is_some_and(Libc::is_musl) - && request.arch().is_some_and(Arch::is_arm) => + let download = + match ManagedPythonDownload::from_request(&download_request, python_downloads_json_url) { - return Err(anyhow::anyhow!( - "uv does not yet provide musl Python distributions on aarch64." - )); - } - Err(err) => return Err(err.into()), - }; + Ok(download) => download, + Err(downloads::Error::NoDownloadFound(request)) + if request.libc().is_some_and(Libc::is_musl) + && request.arch().is_some_and(Arch::is_arm) => + { + return Err(anyhow::anyhow!( + "uv does not yet provide musl Python distributions on aarch64." + )); + } + Err(err) => return Err(err.into()), + }; Ok(Self { request, @@ -131,6 +133,7 @@ pub(crate) async fn install( force: bool, python_install_mirror: Option, pypy_install_mirror: Option, + python_downloads_json_url: Option, network_settings: NetworkSettings, default: bool, python_downloads: PythonDownloads, @@ -171,13 +174,13 @@ pub(crate) async fn install( }] }) .into_iter() - .map(InstallRequest::new) + .map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref())) .collect::>>()? } else { targets .iter() .map(|target| PythonRequest::parse(target.as_str())) - .map(InstallRequest::new) + .map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref())) .collect::>>()? }; @@ -219,7 +222,10 @@ pub(crate) async fn install( changelog.existing.insert(installation.key().clone()); if matches!(&request.request, &PythonRequest::Any) { // Construct an install request matching the existing installation - match InstallRequest::new(PythonRequest::Key(installation.into())) { + match InstallRequest::new( + PythonRequest::Key(installation.into()), + python_downloads_json_url.as_deref(), + ) { Ok(request) => { debug!("Will reinstall `{}`", installation.key().green()); unsatisfied.push(Cow::Owned(request)); diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 794ab1bd1..5236b768d 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -59,6 +59,7 @@ pub(crate) async fn list( all_arches: bool, show_urls: bool, output_format: PythonListFormat, + python_downloads_json_url: Option, python_preference: PythonPreference, python_downloads: PythonDownloads, cache: &Cache, @@ -101,7 +102,7 @@ pub(crate) async fn list( let downloads = download_request .as_ref() - .map(PythonDownloadRequest::iter_downloads) + .map(|a| PythonDownloadRequest::iter_downloads(a, python_downloads_json_url.as_deref())) .transpose()? .into_iter() .flatten(); diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index ea504820b..d4ff8b1a8 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -150,6 +150,7 @@ pub(crate) async fn refine_interpreter( Some(reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index c0b42d8f2..5c4f5649a 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -84,6 +84,7 @@ pub(crate) async fn install( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 66bcade91..e9b6801dc 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -724,6 +724,7 @@ async fn get_or_create_environment( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(); diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 215566dd9..05055533f 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -98,6 +98,7 @@ pub(crate) async fn upgrade( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await? .into_interpreter(), diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 8d2087c57..c28d16507 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -222,6 +222,7 @@ async fn venv_impl( Some(&reporter), install_mirrors.python_install_mirror.as_deref(), install_mirrors.pypy_install_mirror.as_deref(), + install_mirrors.python_downloads_json_url.as_deref(), ) .await .into_diagnostic()?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 8f8cd525f..e6dd8d7bb 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1333,6 +1333,7 @@ async fn run(mut cli: Cli) -> Result { args.all_arches, args.show_urls, args.output_format, + args.python_downloads_json_url, globals.python_preference, globals.python_downloads, &cache, @@ -1355,6 +1356,7 @@ async fn run(mut cli: Cli) -> Result { args.force, args.python_install_mirror, args.pypy_install_mirror, + args.python_downloads_json_url, globals.network_settings, args.default, globals.python_downloads, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 698bac2de..162826826 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -849,12 +849,13 @@ pub(crate) struct PythonListSettings { pub(crate) all_versions: bool, pub(crate) show_urls: bool, pub(crate) output_format: PythonListFormat, + pub(crate) python_downloads_json_url: Option, } impl PythonListSettings { /// Resolve the [`PythonListSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: PythonListArgs, _filesystem: Option) -> Self { + pub(crate) fn resolve(args: PythonListArgs, filesystem: Option) -> Self { let PythonListArgs { request, all_versions, @@ -864,8 +865,18 @@ impl PythonListSettings { only_downloads, show_urls, output_format, + python_downloads_json_url: python_downloads_json_url_arg, } = args; + let options = filesystem.map(FilesystemOptions::into_options); + let python_downloads_json_url_option = match options { + Some(options) => options.install_mirrors.python_downloads_json_url, + None => None, + }; + + let python_downloads_json_url = + python_downloads_json_url_arg.or(python_downloads_json_url_option); + let kinds = if only_installed { PythonListKinds::Installed } else if only_downloads { @@ -882,6 +893,7 @@ impl PythonListSettings { all_versions, show_urls, output_format, + python_downloads_json_url, } } } @@ -913,6 +925,7 @@ pub(crate) struct PythonInstallSettings { pub(crate) force: bool, pub(crate) python_install_mirror: Option, pub(crate) pypy_install_mirror: Option, + pub(crate) python_downloads_json_url: Option, pub(crate) default: bool, } @@ -921,15 +934,18 @@ impl PythonInstallSettings { #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: PythonInstallArgs, filesystem: Option) -> Self { let options = filesystem.map(FilesystemOptions::into_options); - let (python_mirror, pypy_mirror) = match options { + let (python_mirror, pypy_mirror, python_downloads_json_url) = match options { Some(options) => ( options.install_mirrors.python_install_mirror, options.install_mirrors.pypy_install_mirror, + options.install_mirrors.python_downloads_json_url, ), - None => (None, None), + None => (None, None, None), }; let python_mirror = args.mirror.or(python_mirror); let pypy_mirror = args.pypy_mirror.or(pypy_mirror); + let python_downloads_json_url = + args.python_downloads_json_url.or(python_downloads_json_url); let PythonInstallArgs { install_dir, @@ -938,6 +954,7 @@ impl PythonInstallSettings { force, mirror: _, pypy_mirror: _, + python_downloads_json_url: _, default, } = args; @@ -948,6 +965,7 @@ impl PythonInstallSettings { force, python_install_mirror: python_mirror, pypy_install_mirror: pypy_mirror, + python_downloads_json_url, default, } } diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index c3d9f853a..99dc0e457 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -519,6 +519,13 @@ fn help_subsubcommand() { [env: UV_PYPY_INSTALL_MIRROR=] + --python-downloads-json-url + URL pointing to JSON of custom Python installations. + + Note that currently, only local paths are supported. + + [env: UV_PYTHON_DOWNLOADS_JSON_URL=] + -r, --reinstall Reinstall the requested Python version, if it's already installed. @@ -772,15 +779,22 @@ fn help_flag_subsubcommand() { [TARGETS]... The Python version(s) to install [env: UV_PYTHON=] Options: - -i, --install-dir The directory to store the Python installation in [env: - UV_PYTHON_INSTALL_DIR=] - --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 - --default Use as the default Python version + -i, --install-dir + The directory to store the Python installation in [env: UV_PYTHON_INSTALL_DIR=] + --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=] + --python-downloads-json-url + URL pointing to JSON of custom Python installations [env: UV_PYTHON_DOWNLOADS_JSON_URL=] + -r, --reinstall + Reinstall the requested Python version, if it's already installed + -f, --force + Replace existing Python executables during installation + --default + Use as the default Python version Cache options: -n, --no-cache Avoid reading from or writing to the cache, instead using a temporary diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 2222c65c6..786cd84dd 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -148,6 +148,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -328,6 +329,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -509,6 +511,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -722,6 +725,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -871,6 +875,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -1063,6 +1068,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -1302,6 +1308,7 @@ fn resolve_index_url() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -1550,6 +1557,7 @@ fn resolve_index_url() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -1755,6 +1763,7 @@ fn resolve_find_links() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -1926,6 +1935,7 @@ fn resolve_top_level() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -2155,6 +2165,7 @@ fn resolve_top_level() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -2367,6 +2378,7 @@ fn resolve_top_level() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -2537,6 +2549,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -2691,6 +2704,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -2845,6 +2859,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -3001,6 +3016,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -3231,6 +3247,7 @@ fn resolve_tool() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, } @@ -3340,6 +3357,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -3555,6 +3573,7 @@ fn resolve_both() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -3860,6 +3879,7 @@ fn resolve_config_file() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -3967,7 +3987,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `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`, `fork-strategy`, `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`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `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`, `fork-strategy`, `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`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` "### ); @@ -4108,6 +4128,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -4265,6 +4286,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -4441,6 +4463,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -4676,6 +4699,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -4890,6 +4914,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -5110,6 +5135,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -5325,6 +5351,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -5547,6 +5574,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -5762,6 +5790,7 @@ fn index_priority() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -5923,6 +5952,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -6070,6 +6100,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -6215,6 +6246,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -6362,6 +6394,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -6507,6 +6540,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( @@ -6653,6 +6687,7 @@ fn verify_hashes() -> anyhow::Result<()> { install_mirrors: PythonInstallMirrors { python_install_mirror: None, pypy_install_mirror: None, + python_downloads_json_url: None, }, system: false, extras: ExtrasSpecification( diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2ccbdc2cb..0d121791e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4770,6 +4770,11 @@ uv python list [OPTIONS] [REQUEST]

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

May also be set with the UV_PROJECT environment variable.

+
--python-downloads-json-url python-downloads-json-url

URL pointing to JSON of custom Python installations.

+ +

Note that currently, only local paths are supported.

+ +

May also be set with the UV_PYTHON_DOWNLOADS_JSON_URL environment variable.

--quiet, -q

Use quiet output.

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

@@ -4941,6 +4946,11 @@ uv python install [OPTIONS] [TARGETS]...

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-downloads-json-url python-downloads-json-url

URL pointing to JSON of custom Python installations.

+ +

Note that currently, only local paths are supported.

+ +

May also be set with the UV_PYTHON_DOWNLOADS_JSON_URL environment variable.

--quiet, -q

Use quiet output.

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 2d83a96d6..424df2273 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1655,6 +1655,32 @@ Whether to allow Python downloads. --- +### [`python-downloads-json-url`](#python-downloads-json-url) {: #python-downloads-json-url } + +URL pointing to JSON of custom Python installations. + +Note that currently, only local paths are supported. + +**Default value**: `None` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + python-downloads-json-url = "/etc/uv/python-downloads.json" + ``` +=== "uv.toml" + + ```toml + python-downloads-json-url = "/etc/uv/python-downloads.json" + ``` + +--- + ### [`python-install-mirror`](#python-install-mirror) {: #python-install-mirror } Mirror URL for downloading managed Python installations. diff --git a/uv.schema.json b/uv.schema.json index ee2b2c1e1..7d9b31cf7 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -431,6 +431,13 @@ } ] }, + "python-downloads-json-url": { + "description": "URL pointing to JSON of custom Python installations.\n\nNote that currently, only local paths are supported.", + "type": [ + "string", + "null" + ] + }, "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/astral-sh/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/astral-sh/python-build-standalone/releases/download` in, e.g., `https://github.com/astral-sh/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": [