diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9a1984070..b4cb20290 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -6032,6 +6032,12 @@ pub struct PythonFindArgs { /// Show the Python version that would be used instead of the path to the interpreter. #[arg(long)] pub show_version: bool, + + /// URL pointing to JSON of custom Python installations. + /// + /// Note that currently, only local paths are supported. + #[arg(long)] + pub python_downloads_json_url: Option, } #[derive(Args)] diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 22bcc854a..065336441 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -59,12 +59,13 @@ impl PythonInstallation { request: &PythonRequest, environments: EnvironmentPreference, preference: PythonPreference, + python_downloads_json_url: Option<&str>, cache: &Cache, preview: Preview, ) -> Result { let installation = find_python_installation(request, environments, preference, cache, preview)??; - installation.warn_if_outdated_prerelease(request, None); + installation.warn_if_outdated_prerelease(request, python_downloads_json_url); Ok(installation) } @@ -74,12 +75,13 @@ impl PythonInstallation { request: &PythonRequest, environments: EnvironmentPreference, preference: PythonPreference, + python_downloads_json_url: Option<&str>, cache: &Cache, preview: Preview, ) -> Result { let installation = find_best_python_installation(request, environments, preference, cache, preview)??; - installation.warn_if_outdated_prerelease(request, None); + installation.warn_if_outdated_prerelease(request, python_downloads_json_url); Ok(installation) } @@ -102,7 +104,14 @@ impl PythonInstallation { let request = request.unwrap_or(&PythonRequest::Default); // Search for the installation - let err = match Self::find(request, environments, preference, cache, preview) { + let err = match Self::find( + request, + environments, + preference, + python_downloads_json_url, + cache, + preview, + ) { Ok(installation) => return Ok(installation), Err(err) => err, }; diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 351f9228a..e5779bcdd 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -44,6 +44,7 @@ use uv_resolver::{ InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, ResolutionMode, ResolverEnvironment, }; +use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_torch::{TorchMode, TorchSource, TorchStrategy}; use uv_types::{EmptyInstalledPackages, HashStrategy}; @@ -102,6 +103,7 @@ pub(crate) async fn pip_compile( extra_build_dependencies: &ExtraBuildDependencies, extra_build_variables: &ExtraBuildVariables, build_options: BuildOptions, + install_mirrors: PythonInstallMirrors, mut python_version: Option, python_platform: Option, universal: bool, @@ -296,6 +298,7 @@ pub(crate) async fn pip_compile( &request, environment_preference, python_preference, + install_mirrors.python_downloads_json_url.as_deref(), &cache, preview, ) @@ -312,6 +315,7 @@ pub(crate) async fn pip_compile( &request, environment_preference, python_preference, + install_mirrors.python_downloads_json_url.as_deref(), &cache, preview, ) diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index cb832d2ce..4df03d85d 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -31,6 +31,7 @@ pub(crate) async fn find( no_config: bool, system: bool, python_preference: PythonPreference, + python_downloads_json_url: Option<&str>, cache: &Cache, printer: Printer, preview: Preview, @@ -78,6 +79,7 @@ pub(crate) async fn find( &python_request.unwrap_or_default(), environment_preference, python_preference, + python_downloads_json_url, cache, preview, )?; diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 95bf78169..ef5585036 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -100,6 +100,7 @@ pub(crate) async fn pin( pin, virtual_project, python_preference, + install_mirrors.python_downloads_json_url.as_deref(), cache, preview, ); @@ -264,6 +265,7 @@ fn warn_if_existing_pin_incompatible_with_project( pin: &PythonRequest, virtual_project: &VirtualProject, python_preference: PythonPreference, + python_downloads_json_url: Option<&str>, cache: &Cache, preview: Preview, ) { @@ -289,6 +291,7 @@ fn warn_if_existing_pin_incompatible_with_project( pin, EnvironmentPreference::OnlySystem, python_preference, + python_downloads_json_url, cache, preview, ) { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 1df8c1e67..00a572a9b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -631,6 +631,7 @@ async fn run(mut cli: Cli) -> Result { &args.settings.extra_build_dependencies, &args.settings.extra_build_variables, args.settings.build_options, + args.settings.install_mirrors, args.settings.python_version, args.settings.python_platform, args.settings.universal, @@ -1614,7 +1615,7 @@ async fn run(mut cli: Cli) -> Result { command: PythonCommand::Find(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::PythonFindSettings::resolve(args, filesystem); + let args = settings::PythonFindSettings::resolve(args, filesystem, environment); // Initialize the cache. let cache = cache.init()?; @@ -1641,6 +1642,7 @@ async fn run(mut cli: Cli) -> Result { cli.top_level.no_config, args.system, globals.python_preference, + args.python_downloads_json_url.as_deref(), &cache, printer, globals.preview, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 855d046c2..cde4671f4 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1235,12 +1235,17 @@ pub(crate) struct PythonFindSettings { pub(crate) show_version: bool, pub(crate) no_project: bool, pub(crate) system: bool, + pub(crate) python_downloads_json_url: Option, } impl PythonFindSettings { /// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: PythonFindArgs, _filesystem: Option) -> Self { + pub(crate) fn resolve( + args: PythonFindArgs, + filesystem: Option, + environment: EnvironmentOptions, + ) -> Self { let PythonFindArgs { request, show_version, @@ -1248,13 +1253,32 @@ impl PythonFindSettings { system, no_system, script: _, + python_downloads_json_url, } = args; + let filesystem_install_mirrors = filesystem + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + + let install_mirrors = PythonInstallMirrors { + python_downloads_json_url, + ..Default::default() + } + .combine(environment.install_mirrors) + .combine(filesystem_install_mirrors); + + let PythonInstallMirrors { + python_install_mirror: _, + pypy_install_mirror: _, + python_downloads_json_url, + } = install_mirrors; + Self { request, show_version, no_project, system: flag(system, no_system, "system").unwrap_or_default(), + python_downloads_json_url, } } } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index e15e9c782..07cd37085 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -631,7 +631,7 @@ impl TestContext { .iter() .map(|version| PythonVersion::from_str(version).unwrap()) .zip( - python_installations_for_versions(&temp_dir, python_versions) + python_installations_for_versions(&temp_dir, python_versions, None) .expect("Failed to find test Python versions"), ) .collect(); @@ -1689,7 +1689,7 @@ pub fn python_path_with_versions( python_versions: &[&str], ) -> anyhow::Result { Ok(env::join_paths( - python_installations_for_versions(temp_dir, python_versions)? + python_installations_for_versions(temp_dir, python_versions, None)? .into_iter() .map(|path| path.parent().unwrap().to_path_buf()), )?) @@ -1701,6 +1701,7 @@ pub fn python_path_with_versions( pub fn python_installations_for_versions( temp_dir: &ChildPath, python_versions: &[&str], + python_downloads_json_url: Option<&str>, ) -> anyhow::Result> { let cache = Cache::from_path(temp_dir.child("cache").to_path_buf()).init()?; let selected_pythons = python_versions @@ -1710,6 +1711,7 @@ pub fn python_installations_for_versions( &PythonRequest::parse(python_version), EnvironmentPreference::OnlySystem, PythonPreference::Managed, + python_downloads_json_url, &cache, Preview::default(), ) { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 59272a6fc..25b0e10ba 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3686,7 +3686,9 @@ uv python find [OPTIONS] [REQUEST]

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

See --directory to change the working directory entirely.

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

-

May also be set with the UV_PROJECT environment variable.

--quiet, -q

Use quiet output.

+

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.

+
--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.

--script script

Find the environment for a Python script, rather than the current project

--show-version

Show the Python version that would be used instead of the path to the interpreter