Skip to content

Commit

Permalink
Allow configuring the toolchain fetch strategy (#4601)
Browse files Browse the repository at this point in the history
Adds a `toolchain-fetch` option alongside `toolchain-preference` with
`automatic` (default) and `manual` values allowing automatic toolchain
fetches to be disabled (replaces
#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
#4601 (comment) and
#4601 (comment)
  • Loading branch information
zanieb authored Jul 2, 2024
1 parent ec2723a commit 6799cc8
Show file tree
Hide file tree
Showing 25 changed files with 219 additions and 89 deletions.
8 changes: 6 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ToolchainPreference>,

/// Whether to automatically download Python toolchains when required.
#[arg(global = true, long)]
pub toolchain_fetch: Option<ToolchainFetch>,

/// 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,
Expand Down
3 changes: 2 additions & 1 deletion crates/uv-settings/src/combine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<T> Combine for Option<Vec<T>> {
Expand Down
3 changes: 2 additions & 1 deletion crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -60,6 +60,7 @@ pub struct GlobalOptions {
pub cache_dir: Option<PathBuf>,
pub preview: Option<bool>,
pub toolchain_preference: Option<ToolchainPreference>,
pub toolchain_fetch: Option<ToolchainFetch>,
}

/// Settings relevant to all installer operations.
Expand Down
46 changes: 28 additions & 18 deletions crates/uv-toolchain/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
38 changes: 22 additions & 16 deletions crates/uv-toolchain/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Self::from_request(request).ok()
}

/// Construct a new [`PythonDownloadRequest`] from a [`ToolchainRequest`].
pub fn from_request(request: ToolchainRequest) -> Result<Self, Error> {
let result = match request {
ToolchainRequest::Version(version) => Self::default().with_version(version),
pub fn from_request(request: &ToolchainRequest) -> Result<Self, Error> {
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<Self, Error> {
#[must_use]
pub fn fill(mut self) -> Self {
if self.implementation.is_none() {
self.implementation = Some(ImplementationName::CPython);
}
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-toolchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 29 additions & 10 deletions crates/uv-toolchain/src/toolchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -79,33 +81,50 @@ impl Toolchain {
request: Option<ToolchainRequest>,
environments: EnvironmentPreference,
preference: ToolchainPreference,
client_builder: BaseClientBuilder<'a>,
toolchain_fetch: ToolchainFetch,
client_builder: &BaseClientBuilder<'a>,
cache: &Cache,
) -> Result<Self, Error> {
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),
}
}

/// 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<Self, Error> {
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();

Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -39,6 +39,7 @@ pub(crate) async fn add(
python: Option<String>,
settings: ResolverInstallerSettings,
toolchain_preference: ToolchainPreference,
toolchain_fetch: ToolchainFetch,
preview: PreviewMode,
connectivity: Connectivity,
concurrency: Concurrency,
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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,
Expand All @@ -44,6 +45,7 @@ pub(crate) async fn lock(
&workspace,
python.as_deref().map(ToolchainRequest::parse),
toolchain_preference,
toolchain_fetch,
connectivity,
native_tls,
cache,
Expand Down
8 changes: 6 additions & 2 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -127,6 +127,7 @@ impl FoundInterpreter {
workspace: &Workspace,
python_request: Option<ToolchainRequest>,
toolchain_preference: ToolchainPreference,
toolchain_fetch: ToolchainFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
Expand Down Expand Up @@ -183,7 +184,8 @@ impl FoundInterpreter {
python_request,
EnvironmentPreference::OnlySystem,
toolchain_preference,
client_builder,
toolchain_fetch,
&client_builder,
cache,
)
.await?
Expand Down Expand Up @@ -222,6 +224,7 @@ pub(crate) async fn get_or_init_environment(
workspace: &Workspace,
python: Option<ToolchainRequest>,
toolchain_preference: ToolchainPreference,
toolchain_fetch: ToolchainFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
Expand All @@ -231,6 +234,7 @@ pub(crate) async fn get_or_init_environment(
workspace,
python,
toolchain_preference,
toolchain_fetch,
connectivity,
native_tls,
cache,
Expand Down
Loading

0 comments on commit 6799cc8

Please sign in to comment.