From 6799cc883ac960abad4ee6270538de7ad843455f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 1 Jul 2024 21:54:24 -0400 Subject: [PATCH] Allow configuring the toolchain fetch strategy (#4601) Adds a `toolchain-fetch` option alongside `toolchain-preference` with `automatic` (default) and `manual` values allowing automatic toolchain fetches to be disabled (replaces https://github.com/astral-sh/uv/pull/4425). When `manual`, toolchains must be installed with `uv toolchain install`. Note this was previously implemented with `if-necessary`, `always`, `never` variants but the interaction between this and `toolchain-preference` was too confusing. By reducing to a binary option, things should be clearer. The `if-necessary` behavior moved to `toolchain-preference=installed`. See https://github.com/astral-sh/uv/pull/4601#discussion_r1657839633 and https://github.com/astral-sh/uv/pull/4601#discussion_r1658658755 --- crates/uv-cli/src/lib.rs | 8 +++- crates/uv-settings/src/combine.rs | 3 +- crates/uv-settings/src/settings.rs | 3 +- crates/uv-toolchain/src/discovery.rs | 46 +++++++++++++-------- crates/uv-toolchain/src/downloads.rs | 38 ++++++++++------- crates/uv-toolchain/src/lib.rs | 2 +- crates/uv-toolchain/src/toolchain.rs | 39 ++++++++++++----- crates/uv/src/commands/project/add.rs | 4 +- crates/uv/src/commands/project/lock.rs | 4 +- crates/uv/src/commands/project/mod.rs | 8 +++- crates/uv/src/commands/project/remove.rs | 4 +- crates/uv/src/commands/project/run.rs | 13 ++++-- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/src/commands/tool/install.rs | 5 ++- crates/uv/src/commands/tool/run.rs | 5 ++- crates/uv/src/commands/toolchain/install.rs | 6 +-- crates/uv/src/commands/toolchain/list.rs | 23 +++++++---- crates/uv/src/commands/venv.rs | 10 +++-- crates/uv/src/main.rs | 9 ++++ crates/uv/src/settings.rs | 7 +++- crates/uv/tests/common/mod.rs | 2 +- crates/uv/tests/lock.rs | 4 +- crates/uv/tests/pip_tree.rs | 4 -- crates/uv/tests/show_settings.rs | 17 ++++++++ uv.schema.json | 40 +++++++++++++++--- 25 files changed, 219 insertions(+), 89 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 31b4fcf52..109598f0d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -13,7 +13,7 @@ use uv_configuration::{ }; use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; -use uv_toolchain::{PythonVersion, ToolchainPreference}; +use uv_toolchain::{PythonVersion, ToolchainFetch, ToolchainPreference}; pub mod compat; pub mod options; @@ -118,10 +118,14 @@ pub struct GlobalArgs { #[arg(global = true, long, overrides_with("offline"), hide = true)] pub no_offline: bool, - /// Whether to use system or uv-managed Python toolchains. + /// Whether to prefer Python toolchains from uv or on the system. #[arg(global = true, long)] pub toolchain_preference: Option, + /// Whether to automatically download Python toolchains when required. + #[arg(global = true, long)] + pub toolchain_fetch: Option, + /// Whether to enable experimental, preview features. #[arg(global = true, long, hide = true, env = "UV_PREVIEW", value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))] pub preview: bool, diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 1c02fdf8f..0a74853eb 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -5,7 +5,7 @@ use distribution_types::IndexUrl; use install_wheel_rs::linker::LinkMode; use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, TargetTriple}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; -use uv_toolchain::{PythonVersion, ToolchainPreference}; +use uv_toolchain::{PythonVersion, ToolchainFetch, ToolchainPreference}; use crate::{FilesystemOptions, PipOptions}; @@ -70,6 +70,7 @@ impl_combine_or!(ResolutionMode); impl_combine_or!(String); impl_combine_or!(TargetTriple); impl_combine_or!(ToolchainPreference); +impl_combine_or!(ToolchainFetch); impl_combine_or!(bool); impl Combine for Option> { diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 7ca4c2b34..8ca0014f7 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -11,7 +11,7 @@ use uv_configuration::{ use uv_macros::CombineOptions; use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; -use uv_toolchain::{PythonVersion, ToolchainPreference}; +use uv_toolchain::{PythonVersion, ToolchainFetch, ToolchainPreference}; /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[allow(dead_code)] @@ -60,6 +60,7 @@ pub struct GlobalOptions { pub cache_dir: Option, pub preview: Option, pub toolchain_preference: Option, + pub toolchain_fetch: Option, } /// Settings relevant to all installer operations. diff --git a/crates/uv-toolchain/src/discovery.rs b/crates/uv-toolchain/src/discovery.rs index a86122072..8e81dea70 100644 --- a/crates/uv-toolchain/src/discovery.rs +++ b/crates/uv-toolchain/src/discovery.rs @@ -57,20 +57,31 @@ pub enum ToolchainRequest { #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum ToolchainPreference { - /// Only use managed interpreters, never use system interpreters. + /// Only use managed toolchains, never use system toolchains. OnlyManaged, - /// Prefer installed managed interpreters, but use system interpreters if not found. - /// If neither can be found, download a managed interpreter. + /// Prefer installed toolchains, only download managed toolchains if no system toolchain is found. #[default] - PreferInstalledManaged, - /// Prefer managed interpreters, even if one needs to be downloaded, but use system interpreters if found. + Installed, + /// Prefer managed toolchains over system toolchains, even if one needs to be downloaded. PreferManaged, - /// Prefer system interpreters, only use managed interpreters if no system interpreter is found. + /// Prefer system toolchains, only use managed toolchains if no system toolchain is found. PreferSystem, - /// Only use system interpreters, never use managed interpreters. + /// Only use system toolchains, never use managed toolchains. OnlySystem, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum ToolchainFetch { + /// Automatically fetch managed toolchains when needed. + #[default] + Automatic, + /// Do not automatically fetch managed toolchains; require explicit installation. + Manual, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum EnvironmentPreference { /// Only use virtual environments, never allow a system environment. @@ -302,12 +313,7 @@ fn python_executables_from_installed<'a>( match preference { ToolchainPreference::OnlyManaged => Box::new(from_managed_toolchains), - ToolchainPreference::PreferInstalledManaged => Box::new( - from_managed_toolchains - .chain(from_search_path) - .chain(from_py_launcher), - ), - ToolchainPreference::PreferManaged => Box::new( + ToolchainPreference::PreferManaged | ToolchainPreference::Installed => Box::new( from_managed_toolchains .chain(from_search_path) .chain(from_py_launcher), @@ -1147,9 +1153,7 @@ impl ToolchainPreference { match self { ToolchainPreference::OnlyManaged => matches!(source, ToolchainSource::Managed), - ToolchainPreference::PreferInstalledManaged - | Self::PreferManaged - | Self::PreferSystem => matches!( + Self::PreferManaged | Self::PreferSystem | Self::Installed => matches!( source, ToolchainSource::Managed | ToolchainSource::SearchPath @@ -1179,11 +1183,17 @@ impl ToolchainPreference { pub(crate) fn allows_managed(self) -> bool { matches!( self, - Self::PreferManaged | Self::PreferInstalledManaged | Self::OnlyManaged + Self::PreferManaged | Self::OnlyManaged | Self::Installed ) } } +impl ToolchainFetch { + pub fn is_automatic(self) -> bool { + matches!(self, Self::Automatic) + } +} + impl EnvironmentPreference { pub fn from_system_flag(system: bool, mutable: bool) -> Self { match (system, mutable) { @@ -1481,7 +1491,7 @@ impl fmt::Display for ToolchainPreference { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let s = match self { Self::OnlyManaged => "managed toolchains", - Self::PreferManaged | Self::PreferInstalledManaged | Self::PreferSystem => { + Self::PreferManaged | Self::Installed | Self::PreferSystem => { if cfg!(windows) { "managed toolchains, system path, or `py` launcher" } else { diff --git a/crates/uv-toolchain/src/downloads.rs b/crates/uv-toolchain/src/downloads.rs index c70c0cdc2..cc25ffce5 100644 --- a/crates/uv-toolchain/src/downloads.rs +++ b/crates/uv-toolchain/src/downloads.rs @@ -125,32 +125,38 @@ impl PythonDownloadRequest { self } + /// Construct a new [`PythonDownloadRequest`] from a [`ToolchainRequest`] if possible. + /// + /// Returns [`None`] if the request kind is not compatible with a download, e.g., it is + /// a request for a specific directory or executable name. + pub fn try_from_request(request: &ToolchainRequest) -> Option { + Self::from_request(request).ok() + } + /// Construct a new [`PythonDownloadRequest`] from a [`ToolchainRequest`]. - pub fn from_request(request: ToolchainRequest) -> Result { - let result = match request { - ToolchainRequest::Version(version) => Self::default().with_version(version), + pub fn from_request(request: &ToolchainRequest) -> Result { + match request { + ToolchainRequest::Version(version) => Ok(Self::default().with_version(version.clone())), ToolchainRequest::Implementation(implementation) => { - Self::default().with_implementation(implementation) + Ok(Self::default().with_implementation(*implementation)) } - ToolchainRequest::ImplementationVersion(implementation, version) => Self::default() - .with_implementation(implementation) - .with_version(version), - ToolchainRequest::Key(request) => request, - ToolchainRequest::Any => Self::default(), + ToolchainRequest::ImplementationVersion(implementation, version) => Ok(Self::default() + .with_implementation(*implementation) + .with_version(version.clone())), + ToolchainRequest::Key(request) => Ok(request.clone()), + ToolchainRequest::Any => Ok(Self::default()), // We can't download a toolchain for these request kinds ToolchainRequest::Directory(_) | ToolchainRequest::ExecutableName(_) - | ToolchainRequest::File(_) => { - return Err(Error::InvalidRequestKind(request)); - } - }; - Ok(result) + | ToolchainRequest::File(_) => Err(Error::InvalidRequestKind(request.clone())), + } } /// Fill empty entries with default values. /// /// Platform information is pulled from the environment. - pub fn fill(mut self) -> Result { + #[must_use] + pub fn fill(mut self) -> Self { if self.implementation.is_none() { self.implementation = Some(ImplementationName::CPython); } @@ -163,7 +169,7 @@ impl PythonDownloadRequest { if self.libc.is_none() { self.libc = Some(Libc::from_env()); } - Ok(self) + self } /// Construct a new [`PythonDownloadRequest`] with platform information from the environment. diff --git a/crates/uv-toolchain/src/lib.rs b/crates/uv-toolchain/src/lib.rs index e36b47cda..6b392056c 100644 --- a/crates/uv-toolchain/src/lib.rs +++ b/crates/uv-toolchain/src/lib.rs @@ -2,7 +2,7 @@ use thiserror::Error; pub use crate::discovery::{ - find_toolchains, EnvironmentPreference, Error as DiscoveryError, SystemPython, + find_toolchains, EnvironmentPreference, Error as DiscoveryError, SystemPython, ToolchainFetch, ToolchainNotFound, ToolchainPreference, ToolchainRequest, ToolchainSource, VersionRequest, }; pub use crate::environment::PythonEnvironment; diff --git a/crates/uv-toolchain/src/toolchain.rs b/crates/uv-toolchain/src/toolchain.rs index dda22dd25..8ee277793 100644 --- a/crates/uv-toolchain/src/toolchain.rs +++ b/crates/uv-toolchain/src/toolchain.rs @@ -14,7 +14,9 @@ use crate::downloads::{DownloadResult, PythonDownload, PythonDownloadRequest}; use crate::implementation::LenientImplementationName; use crate::managed::{InstalledToolchain, InstalledToolchains}; use crate::platform::{Arch, Libc, Os}; -use crate::{Error, Interpreter, PythonVersion, ToolchainPreference, ToolchainSource}; +use crate::{ + Error, Interpreter, PythonVersion, ToolchainFetch, ToolchainPreference, ToolchainSource, +}; /// A Python interpreter and accompanying tools. #[derive(Clone, Debug)] @@ -79,18 +81,36 @@ impl Toolchain { request: Option, environments: EnvironmentPreference, preference: ToolchainPreference, - client_builder: BaseClientBuilder<'a>, + toolchain_fetch: ToolchainFetch, + client_builder: &BaseClientBuilder<'a>, cache: &Cache, ) -> Result { let request = request.unwrap_or_default(); - // Perform a find first + + // Perform a fetch aggressively if managed toolchains are preferred + if matches!(preference, ToolchainPreference::PreferManaged) + && toolchain_fetch.is_automatic() + { + if let Some(request) = PythonDownloadRequest::try_from_request(&request) { + return Self::fetch(request, client_builder, cache).await; + } + } + + // Search for the toolchain match Self::find(&request, environments, preference, cache) { Ok(venv) => Ok(venv), - Err(Error::MissingToolchain(_)) - if preference.allows_managed() && client_builder.connectivity.is_online() => + // If missing and allowed, perform a fetch + err @ Err(Error::MissingToolchain(_)) + if preference.allows_managed() + && toolchain_fetch.is_automatic() + && client_builder.connectivity.is_online() => { - debug!("Requested Python not found, checking for available download..."); - Self::fetch(request, client_builder, cache).await + if let Some(request) = PythonDownloadRequest::try_from_request(&request) { + debug!("Requested Python not found, checking for available download..."); + Self::fetch(request, client_builder, cache).await + } else { + err + } } Err(err) => Err(err), } @@ -98,14 +118,13 @@ impl Toolchain { /// Download and install the requested toolchain. pub async fn fetch<'a>( - request: ToolchainRequest, - client_builder: BaseClientBuilder<'a>, + request: PythonDownloadRequest, + client_builder: &BaseClientBuilder<'a>, cache: &Cache, ) -> Result { let toolchains = InstalledToolchains::from_settings()?.init()?; let toolchain_dir = toolchains.root(); - let request = PythonDownloadRequest::from_request(request)?.fill()?; let download = PythonDownload::from_request(&request)?; let client = client_builder.build(); diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index be2cb3ed8..4b4f59fc8 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -12,7 +12,7 @@ use uv_git::GitResolver; use uv_normalize::PackageName; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::{FlatIndex, InMemoryIndex}; -use uv_toolchain::{ToolchainPreference, ToolchainRequest}; +use uv_toolchain::{ToolchainFetch, ToolchainPreference, ToolchainRequest}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user_once; @@ -39,6 +39,7 @@ pub(crate) async fn add( python: Option, settings: ResolverInstallerSettings, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, preview: PreviewMode, connectivity: Connectivity, concurrency: Concurrency, @@ -65,6 +66,7 @@ pub(crate) async fn add( project.workspace(), python.as_deref().map(ToolchainRequest::parse), toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 1ebbd20a9..5576b879a 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -11,7 +11,7 @@ use uv_distribution::{Workspace, DEV_DEPENDENCIES}; use uv_git::ResolvedRepositoryReference; use uv_requirements::upgrade::{read_lockfile, LockedRequirements}; use uv_resolver::{FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython}; -use uv_toolchain::{Interpreter, ToolchainPreference, ToolchainRequest}; +use uv_toolchain::{Interpreter, ToolchainFetch, ToolchainPreference, ToolchainRequest}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::{warn_user, warn_user_once}; @@ -26,6 +26,7 @@ pub(crate) async fn lock( settings: ResolverSettings, preview: PreviewMode, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, connectivity: Connectivity, concurrency: Concurrency, native_tls: bool, @@ -44,6 +45,7 @@ pub(crate) async fn lock( &workspace, python.as_deref().map(ToolchainRequest::parse), toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index a2cca0b8d..2caf6ddee 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -19,7 +19,7 @@ use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, PythonRequirement, RequiresPython}; use uv_toolchain::{ request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain, - ToolchainPreference, ToolchainRequest, VersionRequest, + ToolchainFetch, ToolchainPreference, ToolchainRequest, VersionRequest, }; use uv_types::{BuildIsolation, HashStrategy, InFlight}; @@ -127,6 +127,7 @@ impl FoundInterpreter { workspace: &Workspace, python_request: Option, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, connectivity: Connectivity, native_tls: bool, cache: &Cache, @@ -183,7 +184,8 @@ impl FoundInterpreter { python_request, EnvironmentPreference::OnlySystem, toolchain_preference, - client_builder, + toolchain_fetch, + &client_builder, cache, ) .await? @@ -222,6 +224,7 @@ pub(crate) async fn get_or_init_environment( workspace: &Workspace, python: Option, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, connectivity: Connectivity, native_tls: bool, cache: &Cache, @@ -231,6 +234,7 @@ pub(crate) async fn get_or_init_environment( workspace, python, toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 49eb390b7..5b13757ca 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -7,7 +7,7 @@ use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode}; use uv_distribution::pyproject::DependencyType; use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_distribution::{ProjectWorkspace, VirtualProject, Workspace}; -use uv_toolchain::{ToolchainPreference, ToolchainRequest}; +use uv_toolchain::{ToolchainFetch, ToolchainPreference, ToolchainRequest}; use uv_warnings::{warn_user, warn_user_once}; use crate::commands::pip::operations::Modifications; @@ -23,6 +23,7 @@ pub(crate) async fn remove( package: Option, python: Option, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, preview: PreviewMode, connectivity: Connectivity, concurrency: Concurrency, @@ -86,6 +87,7 @@ pub(crate) async fn remove( project.workspace(), python.as_deref().map(ToolchainRequest::parse), toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 43c992c0e..9ae09be36 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -16,7 +16,7 @@ use uv_normalize::PackageName; use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_toolchain::{ request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain, - ToolchainPreference, ToolchainRequest, VersionRequest, + ToolchainFetch, ToolchainPreference, ToolchainRequest, VersionRequest, }; use uv_warnings::warn_user_once; @@ -38,6 +38,7 @@ pub(crate) async fn run( isolated: bool, preview: PreviewMode, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, connectivity: Connectivity, concurrency: Concurrency, native_tls: bool, @@ -89,7 +90,8 @@ pub(crate) async fn run( python_request, EnvironmentPreference::Any, toolchain_preference, - client_builder, + toolchain_fetch, + &client_builder, cache, ) .await? @@ -170,6 +172,7 @@ pub(crate) async fn run( project.workspace(), python.as_deref().map(ToolchainRequest::parse), toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, @@ -222,7 +225,8 @@ pub(crate) async fn run( // No opt-in is required for system environments, since we are not mutating it. EnvironmentPreference::Any, toolchain_preference, - client_builder, + toolchain_fetch, + &client_builder, cache, ) .await?; @@ -261,7 +265,8 @@ pub(crate) async fn run( python.as_deref().map(ToolchainRequest::parse), EnvironmentPreference::Any, toolchain_preference, - client_builder, + toolchain_fetch, + &client_builder, cache, ) .await? diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index fc327f344..6751c8094 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -7,7 +7,7 @@ use uv_dispatch::BuildDispatch; use uv_distribution::{VirtualProject, DEV_DEPENDENCIES}; use uv_installer::SitePackages; use uv_resolver::{FlatIndex, Lock}; -use uv_toolchain::{PythonEnvironment, ToolchainPreference, ToolchainRequest}; +use uv_toolchain::{PythonEnvironment, ToolchainFetch, ToolchainPreference, ToolchainRequest}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user_once; @@ -24,6 +24,7 @@ pub(crate) async fn sync( modifications: Modifications, python: Option, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, settings: InstallerSettings, preview: PreviewMode, connectivity: Connectivity, @@ -44,6 +45,7 @@ pub(crate) async fn sync( project.workspace(), python.as_deref().map(ToolchainRequest::parse), toolchain_preference, + toolchain_fetch, connectivity, native_tls, cache, diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 3ba7b1cb5..768892f2a 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -20,7 +20,8 @@ use uv_normalize::PackageName; use uv_requirements::RequirementsSpecification; use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint}; use uv_toolchain::{ - EnvironmentPreference, Interpreter, Toolchain, ToolchainPreference, ToolchainRequest, + EnvironmentPreference, Interpreter, Toolchain, ToolchainFetch, ToolchainPreference, + ToolchainRequest, }; use uv_warnings::warn_user_once; @@ -39,6 +40,7 @@ pub(crate) async fn install( settings: ResolverInstallerSettings, preview: PreviewMode, toolchain_preference: ToolchainPreference, + _toolchain_fetch: ToolchainFetch, connectivity: Connectivity, concurrency: Concurrency, native_tls: bool, @@ -49,6 +51,7 @@ pub(crate) async fn install( warn_user_once!("`uv tool install` is experimental and may change without warning."); } + // TODO(zanieb): Use `find_or_fetch` here let interpreter = Toolchain::find( &python .as_deref() diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 881ecf7bf..ddd4e2622 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -16,7 +16,8 @@ use uv_configuration::{Concurrency, PreviewMode}; use uv_normalize::PackageName; use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_toolchain::{ - EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainPreference, ToolchainRequest, + EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainFetch, ToolchainPreference, + ToolchainRequest, }; use uv_warnings::warn_user_once; @@ -35,6 +36,7 @@ pub(crate) async fn run( _isolated: bool, preview: PreviewMode, toolchain_preference: ToolchainPreference, + _toolchain_fetch: ToolchainFetch, connectivity: Connectivity, concurrency: Concurrency, native_tls: bool, @@ -76,6 +78,7 @@ pub(crate) async fn run( // Discover an interpreter. // Note we force preview on during `uv tool run` for now since the entire interface is in preview + // TODO(zanieb): We should use `find_or_fetch` here let interpreter = Toolchain::find( &python .as_deref() diff --git a/crates/uv/src/commands/toolchain/install.rs b/crates/uv/src/commands/toolchain/install.rs index d77b2a84e..371835ee8 100644 --- a/crates/uv/src/commands/toolchain/install.rs +++ b/crates/uv/src/commands/toolchain/install.rs @@ -1,6 +1,5 @@ use anyhow::Result; use futures::StreamExt; -use itertools::Itertools; use std::fmt::Write; use uv_cache::Cache; use uv_client::Connectivity; @@ -48,7 +47,7 @@ pub(crate) async fn install( let download_requests = requests .iter() - .map(|request| PythonDownloadRequest::from_request(request.clone())) + .map(PythonDownloadRequest::from_request) .collect::, downloads::Error>>()?; let installed_toolchains: Vec<_> = toolchains.find_all()?.collect(); @@ -105,8 +104,7 @@ pub(crate) async fn install( .into_iter() // Populate the download requests with defaults .map(PythonDownloadRequest::fill) - .map(|request| request.map(|inner| PythonDownload::from_request(&inner))) - .flatten_ok() + .map(|request| PythonDownload::from_request(&request)) .collect::, uv_toolchain::downloads::Error>>()?; // Construct a client diff --git a/crates/uv/src/commands/toolchain/list.rs b/crates/uv/src/commands/toolchain/list.rs index d9a144af6..926d31b8c 100644 --- a/crates/uv/src/commands/toolchain/list.rs +++ b/crates/uv/src/commands/toolchain/list.rs @@ -8,8 +8,8 @@ use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_toolchain::downloads::PythonDownloadRequest; use uv_toolchain::{ - find_toolchains, DiscoveryError, EnvironmentPreference, Toolchain, ToolchainNotFound, - ToolchainPreference, ToolchainRequest, ToolchainSource, + find_toolchains, DiscoveryError, EnvironmentPreference, Toolchain, ToolchainFetch, + ToolchainNotFound, ToolchainPreference, ToolchainRequest, ToolchainSource, }; use uv_warnings::warn_user_once; @@ -25,11 +25,13 @@ enum Kind { } /// List available toolchains. +#[allow(clippy::too_many_arguments)] pub(crate) async fn list( kinds: ToolchainListKinds, all_versions: bool, all_platforms: bool, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, preview: PreviewMode, cache: &Cache, printer: Printer, @@ -40,11 +42,18 @@ pub(crate) async fn list( let download_request = match kinds { ToolchainListKinds::Installed => None, - ToolchainListKinds::Default => Some(if all_platforms { - PythonDownloadRequest::default() - } else { - PythonDownloadRequest::from_env()? - }), + ToolchainListKinds::Default => { + if toolchain_fetch.is_automatic() { + Some(if all_platforms { + PythonDownloadRequest::default() + } else { + PythonDownloadRequest::from_env()? + }) + } else { + // If fetching is not automatic, then don't show downloads as available by default + None + } + } }; let downloads = download_request diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 752beaaf5..f9835291d 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -24,8 +24,8 @@ use uv_fs::Simplified; use uv_git::GitResolver; use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex}; use uv_toolchain::{ - request_from_version_file, EnvironmentPreference, Toolchain, ToolchainPreference, - ToolchainRequest, + request_from_version_file, EnvironmentPreference, Toolchain, ToolchainFetch, + ToolchainPreference, ToolchainRequest, }; use uv_types::{BuildContext, BuildIsolation, HashStrategy, InFlight}; @@ -43,6 +43,7 @@ pub(crate) async fn venv( path: &Path, python_request: Option<&str>, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, link_mode: LinkMode, index_locations: &IndexLocations, index_strategy: IndexStrategy, @@ -71,6 +72,7 @@ pub(crate) async fn venv( seed, preview, toolchain_preference, + toolchain_fetch, allow_existing, exclude_newer, native_tls, @@ -121,6 +123,7 @@ async fn venv_impl( seed: bool, preview: PreviewMode, toolchain_preference: ToolchainPreference, + toolchain_fetch: ToolchainFetch, allow_existing: bool, exclude_newer: Option, native_tls: bool, @@ -141,7 +144,8 @@ async fn venv_impl( interpreter_request, EnvironmentPreference::OnlySystem, toolchain_preference, - client_builder, + toolchain_fetch, + &client_builder, cache, ) .await diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 847ab2fc0..985b3a9d1 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -617,6 +617,7 @@ async fn run() -> Result { &args.name, args.settings.python.as_deref(), globals.toolchain_preference, + globals.toolchain_fetch, args.settings.link_mode, &args.settings.index_locations, args.settings.index_strategy, @@ -659,6 +660,7 @@ async fn run() -> Result { globals.isolated, globals.preview, globals.toolchain_preference, + globals.toolchain_fetch, globals.connectivity, Concurrency::default(), globals.native_tls, @@ -681,6 +683,7 @@ async fn run() -> Result { args.modifications, args.python, globals.toolchain_preference, + globals.toolchain_fetch, args.settings, globals.preview, globals.connectivity, @@ -704,6 +707,7 @@ async fn run() -> Result { args.settings, globals.preview, globals.toolchain_preference, + globals.toolchain_fetch, globals.connectivity, Concurrency::default(), globals.native_tls, @@ -733,6 +737,7 @@ async fn run() -> Result { args.python, args.settings, globals.toolchain_preference, + globals.toolchain_fetch, globals.preview, globals.connectivity, Concurrency::default(), @@ -756,6 +761,7 @@ async fn run() -> Result { args.package, args.python, globals.toolchain_preference, + globals.toolchain_fetch, globals.preview, globals.connectivity, Concurrency::default(), @@ -796,6 +802,7 @@ async fn run() -> Result { globals.isolated, globals.preview, globals.toolchain_preference, + globals.toolchain_fetch, globals.connectivity, Concurrency::default(), globals.native_tls, @@ -823,6 +830,7 @@ async fn run() -> Result { args.settings, globals.preview, globals.toolchain_preference, + globals.toolchain_fetch, globals.connectivity, Concurrency::default(), globals.native_tls, @@ -870,6 +878,7 @@ async fn run() -> Result { args.all_versions, args.all_platforms, globals.toolchain_preference, + globals.toolchain_fetch, globals.preview, &cache, printer, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7b907c229..5aafb30b9 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -31,7 +31,7 @@ use uv_settings::{ Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions, }; -use uv_toolchain::{Prefix, PythonVersion, Target, ToolchainPreference}; +use uv_toolchain::{Prefix, PythonVersion, Target, ToolchainFetch, ToolchainPreference}; use crate::commands::pip::operations::Modifications; @@ -48,6 +48,7 @@ pub(crate) struct GlobalSettings { pub(crate) show_settings: bool, pub(crate) preview: PreviewMode, pub(crate) toolchain_preference: ToolchainPreference, + pub(crate) toolchain_fetch: ToolchainFetch, } impl GlobalSettings { @@ -114,6 +115,10 @@ impl GlobalSettings { .toolchain_preference .combine(workspace.and_then(|workspace| workspace.globals.toolchain_preference)) .unwrap_or(default_toolchain_preference), + toolchain_fetch: args + .toolchain_fetch + .combine(workspace.and_then(|workspace| workspace.globals.toolchain_fetch)) + .unwrap_or_default(), } } } diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 1bce8bfb7..c6c44b9e5 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -711,7 +711,7 @@ pub fn python_toolchains_for_versions( if let Ok(toolchain) = Toolchain::find( &ToolchainRequest::parse(python_version), EnvironmentPreference::OnlySystem, - ToolchainPreference::PreferInstalledManaged, + ToolchainPreference::PreferManaged, &cache, ) { toolchain.into_interpreter().sys_executable().to_owned() diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 214817509..376e59418 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -2039,8 +2039,8 @@ fn lock_requires_python() -> Result<()> { .collect(); // Install from the lockfile. - // Note we need `--offline` otherwise we'll just fetch a 3.12 interpreter! - uv_snapshot!(filters, context38.sync().arg("--offline"), @r###" + // Note we need to disable toolchain fetches or we'll just download 3.12 + uv_snapshot!(filters, context38.sync().arg("--toolchain-fetch").arg("manual"), @r###" success: false exit_code: 2 ----- stdout ----- diff --git a/crates/uv/tests/pip_tree.rs b/crates/uv/tests/pip_tree.rs index e3eca62e1..d78f51563 100644 --- a/crates/uv/tests/pip_tree.rs +++ b/crates/uv/tests/pip_tree.rs @@ -448,7 +448,6 @@ fn prune() { └── threadpoolctl v3.4.0 ----- stderr ----- - "### ); @@ -472,7 +471,6 @@ fn prune() { └── threadpoolctl v3.4.0 ----- stderr ----- - "### ); @@ -495,7 +493,6 @@ fn prune() { └── threadpoolctl v3.4.0 ----- stderr ----- - "### ); } @@ -1350,7 +1347,6 @@ fn with_editable() { └── iniconfig v2.0.1.dev6+g9cae431 ----- stderr ----- - "### ); } diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index 323b38c83..74cb5eb13 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -58,6 +58,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -188,6 +189,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -319,6 +321,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -482,6 +485,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -614,6 +618,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -732,6 +737,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -887,6 +893,7 @@ fn resolve_index_url() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1042,6 +1049,7 @@ fn resolve_index_url() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1242,6 +1250,7 @@ fn resolve_find_links() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1391,6 +1400,7 @@ fn resolve_top_level() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1515,6 +1525,7 @@ fn resolve_top_level() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1667,6 +1678,7 @@ fn resolve_top_level() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1843,6 +1855,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -1957,6 +1970,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -2071,6 +2085,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -2187,6 +2202,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, @@ -2328,6 +2344,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { show_settings: true, preview: Disabled, toolchain_preference: OnlySystem, + toolchain_fetch: Automatic, } CacheSettings { no_cache: false, diff --git a/uv.schema.json b/uv.schema.json index d7c8b253a..655395124 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -235,6 +235,16 @@ "$ref": "#/definitions/Source" } }, + "toolchain-fetch": { + "anyOf": [ + { + "$ref": "#/definitions/ToolchainFetch" + }, + { + "type": "null" + } + ] + }, "toolchain-preference": { "anyOf": [ { @@ -1187,38 +1197,56 @@ } } }, + "ToolchainFetch": { + "oneOf": [ + { + "description": "Automatically fetch managed toolchains when needed.", + "type": "string", + "enum": [ + "automatic" + ] + }, + { + "description": "Do not automatically fetch managed toolchains; require explicit installation.", + "type": "string", + "enum": [ + "manual" + ] + } + ] + }, "ToolchainPreference": { "oneOf": [ { - "description": "Only use managed interpreters, never use system interpreters.", + "description": "Only use managed toolchains, never use system toolchains.", "type": "string", "enum": [ "only-managed" ] }, { - "description": "Prefer installed managed interpreters, but use system interpreters if not found. If neither can be found, download a managed interpreter.", + "description": "Prefer installed toolchains, only download managed toolchains if no system toolchain is found.", "type": "string", "enum": [ - "prefer-installed-managed" + "installed" ] }, { - "description": "Prefer managed interpreters, even if one needs to be downloaded, but use system interpreters if found.", + "description": "Prefer managed toolchains over system toolchains, even if one needs to be downloaded.", "type": "string", "enum": [ "prefer-managed" ] }, { - "description": "Prefer system interpreters, only use managed interpreters if no system interpreter is found.", + "description": "Prefer system toolchains, only use managed toolchains if no system toolchain is found.", "type": "string", "enum": [ "prefer-system" ] }, { - "description": "Only use system interpreters, never use managed interpreters.", + "description": "Only use system toolchains, never use managed toolchains.", "type": "string", "enum": [ "only-system"