diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 49cbdc72098a5..9f7309bbd270f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -19,7 +19,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::Requirement; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; -use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode}; +use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_static::EnvVars; pub mod comma; @@ -4045,6 +4045,24 @@ pub struct ToolUpgradeArgs { #[arg(long, hide = true)] pub pre: bool, + /// The strategy to use when selecting multiple versions of a given package across Python + /// versions and platforms. + /// + /// By default, uv will optimize for selecting the latest version of each package for each + /// supported Python version (`requires-python`), while minimizing the number of selected + /// versions across platforms. + /// + /// Under `fewest`, uv will minimize the number of + /// selected versions for each package, preferring older versions that are compatible with a + /// wider range of supported Python versions or platforms. + #[arg( + long, + value_enum, + env = EnvVars::UV_FORK_STRATEGY, + help_heading = "Resolver options" + )] + pub fork_strategy: Option, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, @@ -4834,6 +4852,24 @@ pub struct ResolverArgs { #[arg(long, hide = true, help_heading = "Resolver options")] pub pre: bool, + /// The strategy to use when selecting multiple versions of a given package across Python + /// versions and platforms. + /// + /// By default, uv will optimize for selecting the latest version of each package for each + /// supported Python version (`requires-python`), while minimizing the number of selected + /// versions across platforms. + /// + /// Under `fewest`, uv will minimize the number of + /// selected versions for each package, preferring older versions that are compatible with a + /// wider range of supported Python versions or platforms. + #[arg( + long, + value_enum, + env = EnvVars::UV_FORK_STRATEGY, + help_heading = "Resolver options" + )] + pub fork_strategy: Option, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, @@ -5006,6 +5042,24 @@ pub struct ResolverInstallerArgs { #[arg(long, hide = true)] pub pre: bool, + /// The strategy to use when selecting multiple versions of a given package across Python + /// versions and platforms. + /// + /// By default, uv will optimize for selecting the latest version of each package for each + /// supported Python version (`requires-python`), while minimizing the number of selected + /// versions across platforms. + /// + /// Under `fewest`, uv will minimize the number of + /// selected versions for each package, preferring older versions that are compatible with a + /// wider range of supported Python versions or platforms. + #[arg( + long, + value_enum, + env = EnvVars::UV_FORK_STRATEGY, + help_heading = "Resolver options" + )] + pub fork_strategy: Option, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg( long, diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index a431e8643ec25..e15d56e834c52 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -43,6 +43,7 @@ impl From for PipOptions { resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -58,6 +59,7 @@ impl From for PipOptions { index_strategy, keyring_provider, resolution, + fork_strategy, prerelease: if pre { Some(PrereleaseMode::Allow) } else { @@ -126,6 +128,7 @@ impl From for PipOptions { resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -150,6 +153,7 @@ impl From for PipOptions { } else { prerelease }, + fork_strategy, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), no_build_isolation: flag(no_build_isolation, build_isolation), @@ -235,6 +239,7 @@ pub fn resolver_options( resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -291,6 +296,7 @@ pub fn resolver_options( } else { prerelease }, + fork_strategy, dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), @@ -324,6 +330,7 @@ pub fn resolver_installer_options( resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -392,6 +399,7 @@ pub fn resolver_installer_options( } else { prerelease }, + fork_strategy, dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index b64f6ba6f7b78..b0a64d0688397 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -50,7 +50,9 @@ impl schemars::JsonSchema for PythonVersion { ..schemars::schema::StringValidation::default() })), metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("A Python version specifier, e.g. `3.7` or `3.8.0`.".to_string()), + description: Some( + "A Python version specifier, e.g. `3.11` or `3.12.4`.".to_string(), + ), ..schemars::schema::Metadata::default() })), ..schemars::schema::SchemaObject::default() diff --git a/crates/uv-resolver/src/fork_strategy.rs b/crates/uv-resolver/src/fork_strategy.rs new file mode 100644 index 0000000000000..dd7a13936b96e --- /dev/null +++ b/crates/uv-resolver/src/fork_strategy.rs @@ -0,0 +1,23 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum ForkStrategy { + /// Optimize for selecting the fewest number of versions for each package. Older versions may + /// be preferred if they are compatible with a wider range of supported Python versions or + /// platforms. + Fewest, + /// Optimize for selecting latest supported version of each package, for each supported Python + /// version. + #[default] + RequiresPython, +} + +impl std::fmt::Display for ForkStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Fewest => write!(f, "fewest"), + Self::RequiresPython => write!(f, "requires-python"), + } + } +} diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index bce29ec2eff34..d9d1930b94b01 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -3,6 +3,7 @@ pub use error::{NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange}; pub use exclude_newer::ExcludeNewer; pub use exclusions::Exclusions; pub use flat_index::{FlatDistributions, FlatIndex}; +pub use fork_strategy::ForkStrategy; pub use lock::{ InstallTarget, Lock, LockError, LockVersion, PackageMap, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, VERSION, @@ -41,6 +42,7 @@ mod exclude_newer; mod exclusions; mod flat_index; mod fork_indexes; +mod fork_strategy; mod fork_urls; mod graph_ops; mod lock; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 0ab758f278530..0b52a22fbd5d2 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -13,6 +13,7 @@ use std::sync::{Arc, LazyLock}; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use url::Url; +use crate::fork_strategy::ForkStrategy; pub use crate::lock::map::PackageMap; pub use crate::lock::requirements_txt::RequirementsTxtExport; pub use crate::lock::target::InstallTarget; @@ -239,6 +240,7 @@ impl Lock { let options = ResolverOptions { resolution_mode: resolution.options.resolution_mode, prerelease_mode: resolution.options.prerelease_mode, + fork_strategy: resolution.options.fork_strategy, exclude_newer: resolution.options.exclude_newer, }; let lock = Self::new( @@ -548,6 +550,11 @@ impl Lock { self.options.prerelease_mode } + /// Returns the multi-version mode used to generate this lock. + pub fn fork_strategy(&self) -> ForkStrategy { + self.options.fork_strategy + } + /// Returns the exclude newer setting used to generate this lock. pub fn exclude_newer(&self) -> Option { self.options.exclude_newer @@ -675,6 +682,12 @@ impl Lock { value(self.options.prerelease_mode.to_string()), ); } + if self.options.fork_strategy != ForkStrategy::default() { + options_table.insert( + "fork-strategy", + value(self.options.fork_strategy.to_string()), + ); + } if let Some(exclude_newer) = self.options.exclude_newer { options_table.insert("exclude-newer", value(exclude_newer.to_string())); } @@ -1317,6 +1330,9 @@ struct ResolverOptions { /// The [`PrereleaseMode`] used to generate this lock. #[serde(default)] prerelease_mode: PrereleaseMode, + /// The [`ForkStrategy`] used to generate this lock. + #[serde(default)] + fork_strategy: ForkStrategy, /// The [`ExcludeNewer`] used to generate this lock. exclude_newer: Option, } diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 40ded19b4e83d..345a9ee9ef539 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index fede0baf574f3..2758a29f126fc 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 172e1b9020360..77278a22683c8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index 205f784d82e46..1f8f9344630da 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index 205f784d82e46..1f8f9344630da 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index 205f784d82e46..1f8f9344630da 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 388c6214a11b3..0749d1d185e18 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index 55115287ab418..390ef30a247c5 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index 336e6f52f340f..dee02dd0fbf0e 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index 465eefcc143f4..4d2a25d4cf821 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -1,5 +1,5 @@ --- -source: crates/uv-resolver/src/lock/tests.rs +source: crates/uv-resolver/src/lock/mod.rs expression: result --- Ok( @@ -33,6 +33,7 @@ Ok( options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, exclude_newer: None, }, packages: [ diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index 7f2c1010e2cbd..8add30092f72d 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -1,5 +1,6 @@ use uv_configuration::IndexStrategy; +use crate::fork_strategy::ForkStrategy; use crate::{DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; /// Options for resolving a manifest. @@ -8,6 +9,7 @@ pub struct Options { pub resolution_mode: ResolutionMode, pub prerelease_mode: PrereleaseMode, pub dependency_mode: DependencyMode, + pub fork_strategy: ForkStrategy, pub exclude_newer: Option, pub index_strategy: IndexStrategy, pub flexibility: Flexibility, @@ -19,6 +21,7 @@ pub struct OptionsBuilder { resolution_mode: ResolutionMode, prerelease_mode: PrereleaseMode, dependency_mode: DependencyMode, + fork_strategy: ForkStrategy, exclude_newer: Option, index_strategy: IndexStrategy, flexibility: Flexibility, @@ -51,6 +54,13 @@ impl OptionsBuilder { self } + /// Sets the multi-version mode. + #[must_use] + pub fn fork_strategy(mut self, fork_strategy: ForkStrategy) -> Self { + self.fork_strategy = fork_strategy; + self + } + /// Sets the exclusion date. #[must_use] pub fn exclude_newer(mut self, exclude_newer: Option) -> Self { @@ -78,6 +88,7 @@ impl OptionsBuilder { resolution_mode: self.resolution_mode, prerelease_mode: self.prerelease_mode, dependency_mode: self.dependency_mode, + fork_strategy: self.fork_strategy, exclude_newer: self.exclude_newer, index_strategy: self.index_strategy, flexibility: self.flexibility, diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index ec84b535b2b88..7c6b3b99b7aa1 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -298,23 +298,6 @@ impl RequiresPython { } } - /// Returns the [`RequiresPythonBound`] truncated to the major and minor version. - pub fn bound_major_minor(&self) -> LowerBound { - match self.range.lower().as_ref() { - // Ex) `>=3.10.1` -> `>=3.10` - Bound::Included(version) => LowerBound(Bound::Included(Version::new( - version.release().iter().take(2), - ))), - // Ex) `>3.10.1` -> `>=3.10` - // This is unintuitive, but `>3.10.1` does indicate that _some_ version of Python 3.10 - // is supported. - Bound::Excluded(version) => LowerBound(Bound::Included(Version::new( - version.release().iter().take(2), - ))), - Bound::Unbounded => LowerBound(Bound::Unbounded), - } - } - /// Returns the [`Range`] bounding the `Requires-Python` specifier. pub fn range(&self) -> &RequiresPythonRange { &self.range diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index eda0d9ba288e9..942ee5c43db51 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -43,6 +43,7 @@ use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::dependency_provider::UvDependencyProvider; use crate::error::{NoSolutionError, ResolveError}; use crate::fork_indexes::ForkIndexes; +use crate::fork_strategy::ForkStrategy; use crate::fork_urls::ForkUrls; use crate::manifest::Manifest; use crate::pins::FilePins; @@ -1124,22 +1125,24 @@ impl ResolverState>() - .join(", ") - ); - return Ok(Some(ResolverVersion::Forked(forks))); + if matches!(self.options.fork_strategy, ForkStrategy::RequiresPython) { + if env.marker_environment().is_none() { + let forks = fork_python_requirement(requires_python, python_requirement, env); + if !forks.is_empty() { + debug!( + "Forking Python requirement `{}` on `{}` for {}=={} ({})", + python_requirement.target(), + requires_python, + name, + candidate.version(), + forks + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + ); + return Ok(Some(ResolverVersion::Forked(forks))); + } } } diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 95c1597a485d1..d333144740d2b 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -10,7 +10,7 @@ use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipInd use uv_install_wheel::linker::LinkMode; use uv_pypi_types::{SchemaConflicts, SupportedEnvironments}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; -use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode}; +use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use crate::{FilesystemOptions, Options, PipOptions}; @@ -78,6 +78,7 @@ impl_combine_or!(IndexStrategy); impl_combine_or!(IndexUrl); impl_combine_or!(KeyringProviderType); impl_combine_or!(LinkMode); +impl_combine_or!(ForkStrategy); impl_combine_or!(NonZeroUsize); impl_combine_or!(PathBuf); impl_combine_or!(PipExtraIndex); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 1fbb96f1184e2..ccc7a6b6482bc 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -15,7 +15,7 @@ use uv_normalize::{ExtraName, PackageName}; use uv_pep508::Requirement; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; -use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode}; +use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_static::EnvVars; /// A `pyproject.toml` with an (optional) `[tool.uv]` section. @@ -309,6 +309,7 @@ pub struct ResolverOptions { pub keyring_provider: Option, pub resolution: Option, pub prerelease: Option, + pub fork_strategy: Option, pub dependency_metadata: Option>, pub config_settings: Option, pub exclude_newer: Option, @@ -485,6 +486,25 @@ pub struct ResolverInstallerOptions { possible_values = true )] pub prerelease: Option, + /// The strategy to use when selecting multiple versions of a given package across Python + /// versions and platforms. + /// + /// By default, uv will optimize for selecting the latest version of each package for each + /// supported Python version (`requires-python`), while minimizing the number of selected + /// versions across platforms. + /// + /// Under `fewest`, uv will minimize the number of + /// selected versions for each package, preferring older versions that are compatible with a + /// wider range of supported Python versions or platforms. + #[option( + default = "\"fewest\"", + value_type = "str", + example = r#" + fork-strategy = "fewest" + "#, + possible_values = true + )] + pub fork_strategy: Option, /// Pre-defined static metadata for dependencies of the project (direct or transitive). When /// provided, enables the resolver to use the specified metadata instead of querying the /// registry or building the relevant package from source. @@ -1068,6 +1088,25 @@ pub struct PipOptions { possible_values = true )] pub prerelease: Option, + /// The strategy to use when selecting multiple versions of a given package across Python + /// versions and platforms. + /// + /// By default, uv will optimize for selecting the latest version of each package for each + /// supported Python version (`requires-python`), while minimizing the number of selected + /// versions across platforms. + /// + /// Under `fewest`, uv will minimize the number of + /// selected versions for each package, preferring older versions that are compatible with a + /// wider range of supported Python versions or platforms. + #[option( + default = "\"fewest\"", + value_type = "str", + example = r#" + fork-strategy = "fewest" + "#, + possible_values = true + )] + pub fork_strategy: Option, /// Pre-defined static metadata for dependencies of the project (direct or transitive). When /// provided, enables the resolver to use the specified metadata instead of querying the /// registry or building the relevant package from source. @@ -1432,6 +1471,7 @@ impl From for ResolverOptions { keyring_provider: value.keyring_provider, resolution: value.resolution, prerelease: value.prerelease, + fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, exclude_newer: value.exclude_newer, @@ -1494,6 +1534,7 @@ pub struct ToolOptions { pub keyring_provider: Option, pub resolution: Option, pub prerelease: Option, + pub fork_strategy: Option, pub dependency_metadata: Option>, pub config_settings: Option, pub no_build_isolation: Option, @@ -1520,6 +1561,7 @@ impl From for ToolOptions { keyring_provider: value.keyring_provider, resolution: value.resolution, prerelease: value.prerelease, + fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, no_build_isolation: value.no_build_isolation, @@ -1548,6 +1590,7 @@ impl From for ResolverInstallerOptions { keyring_provider: value.keyring_provider, resolution: value.resolution, prerelease: value.prerelease, + fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, no_build_isolation: value.no_build_isolation, @@ -1598,6 +1641,7 @@ pub struct OptionsWire { allow_insecure_host: Option>, resolution: Option, prerelease: Option, + fork_strategy: Option, dependency_metadata: Option>, config_settings: Option, no_build_isolation: Option, @@ -1677,6 +1721,7 @@ impl From for Options { allow_insecure_host, resolution, prerelease, + fork_strategy, dependency_metadata, config_settings, no_build_isolation, @@ -1737,6 +1782,7 @@ impl From for Options { keyring_provider, resolution, prerelease, + fork_strategy, dependency_metadata, config_settings, no_build_isolation, diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 1960024ce2444..354b160c94d30 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -46,6 +46,10 @@ impl EnvVars { /// `allow`, uv will allow pre-release versions for all dependencies. pub const UV_PRERELEASE: &'static str = "UV_PRERELEASE"; + /// Equivalent to the `--fork-strategy` argument. Controls version selection during universal + /// resolution. + pub const UV_FORK_STRATEGY: &'static str = "UV_FORK_STRATEGY"; + /// Equivalent to the `--system` command-line argument. If set to `true`, uv will /// use the first Python interpreter found in the system `PATH`. /// WARNING: `UV_SYSTEM_PYTHON=true` is intended for use in continuous integration (CI) diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index b6969699eebd8..7684c3c7128a1 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -205,6 +205,7 @@ async fn build_impl( keyring_provider, resolution: _, prerelease: _, + fork_strategy: _, dependency_metadata, config_setting, no_build_isolation, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 65b437be0d05f..ffb0066d8e8e8 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -31,7 +31,7 @@ use uv_requirements::{ upgrade::read_requirements_txt, RequirementsSource, RequirementsSpecification, }; use uv_resolver::{ - AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, + AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, ForkStrategy, InMemoryIndex, OptionsBuilder, PrereleaseMode, PythonRequirement, RequiresPython, ResolutionMode, ResolverEnvironment, }; @@ -57,6 +57,7 @@ pub(crate) async fn pip_compile( output_file: Option<&Path>, resolution_mode: ResolutionMode, prerelease_mode: PrereleaseMode, + fork_strategy: ForkStrategy, dependency_mode: DependencyMode, upgrade: Upgrade, generate_hashes: bool, @@ -361,6 +362,7 @@ pub(crate) async fn pip_compile( let options = OptionsBuilder::new() .resolution_mode(resolution_mode) .prerelease_mode(prerelease_mode) + .fork_strategy(fork_strategy) .dependency_mode(dependency_mode) .exclude_newer(exclude_newer) .index_strategy(index_strategy) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index f7afd49b8138c..fa7e64b999b0d 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -316,6 +316,7 @@ async fn do_lock( keyring_provider, resolution, prerelease, + fork_strategy, dependency_metadata, config_setting, no_build_isolation, @@ -468,6 +469,7 @@ async fn do_lock( let options = OptionsBuilder::new() .resolution_mode(resolution) .prerelease_mode(prerelease) + .fork_strategy(fork_strategy) .exclude_newer(exclude_newer) .index_strategy(index_strategy) .build(); @@ -750,6 +752,15 @@ impl ValidatedLock { ); return Ok(Self::Unusable(lock)); } + if lock.fork_strategy() != options.fork_strategy { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to change in fork strategy: `{}` vs. `{}`", + lock.fork_strategy().cyan(), + options.fork_strategy.cyan() + ); + return Ok(Self::Unusable(lock)); + } match (lock.exclude_newer(), options.exclude_newer) { (None, None) => (), (Some(existing), Some(provided)) if existing == provided => (), diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 99ef9e6aa2deb..c2d060db8b04c 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -921,6 +921,7 @@ pub(crate) async fn resolve_names( keyring_provider, resolution: _, prerelease: _, + fork_strategy: _, dependency_metadata, config_setting, no_build_isolation, @@ -1057,6 +1058,7 @@ pub(crate) async fn resolve_environment<'a>( keyring_provider, resolution, prerelease, + fork_strategy, dependency_metadata, config_setting, no_build_isolation, @@ -1117,6 +1119,7 @@ pub(crate) async fn resolve_environment<'a>( let options = OptionsBuilder::new() .resolution_mode(resolution) .prerelease_mode(prerelease) + .fork_strategy(fork_strategy) .exclude_newer(exclude_newer) .index_strategy(index_strategy) .build(); @@ -1386,6 +1389,7 @@ pub(crate) async fn update_environment( keyring_provider, resolution, prerelease, + fork_strategy, dependency_metadata, config_setting, no_build_isolation, @@ -1471,6 +1475,7 @@ pub(crate) async fn update_environment( let options = OptionsBuilder::new() .resolution_mode(*resolution) .prerelease_mode(*prerelease) + .fork_strategy(*fork_strategy) .exclude_newer(*exclude_newer) .index_strategy(*index_strategy) .build(); diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 77bffa57a1fc2..b62ef2b548017 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -169,6 +169,7 @@ pub(crate) async fn tree( keyring_provider, resolution: _, prerelease: _, + fork_strategy: _, dependency_metadata: _, config_setting: _, no_build_isolation: _, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 8b326f5cb9bba..0ac73dc4f3018 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -339,6 +339,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.output_file.as_deref(), args.settings.resolution, args.settings.prerelease, + args.settings.fork_strategy, args.settings.dependency_mode, args.settings.upgrade, args.settings.generate_hashes, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 914497f5eeef5..2e677eeabf4ae 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -32,7 +32,9 @@ use uv_normalize::PackageName; use uv_pep508::{ExtraName, RequirementOrigin}; use uv_pypi_types::{Requirement, SupportedEnvironments}; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; -use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode}; +use uv_resolver::{ + AnnotationStyle, DependencyMode, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode, +}; use uv_settings::{ Combine, FilesystemOptions, Options, PipOptions, PublishOptions, PythonInstallMirrors, ResolverInstallerOptions, ResolverOptions, @@ -574,6 +576,7 @@ impl ToolUpgradeSettings { resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -607,6 +610,7 @@ impl ToolUpgradeSettings { resolution, prerelease, pre, + fork_strategy, config_setting, no_build_isolation, no_build_isolation_package, @@ -2215,6 +2219,7 @@ pub(crate) struct ResolverSettings { pub(crate) keyring_provider: KeyringProviderType, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) fork_strategy: ForkStrategy, pub(crate) dependency_metadata: DependencyMetadata, pub(crate) config_setting: ConfigSettings, pub(crate) no_build_isolation: bool, @@ -2233,6 +2238,7 @@ pub(crate) struct ResolverSettingsRef<'a> { pub(crate) keyring_provider: KeyringProviderType, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) fork_strategy: ForkStrategy, pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, pub(crate) no_build_isolation: bool, @@ -2264,6 +2270,7 @@ impl ResolverSettings { keyring_provider: self.keyring_provider, resolution: self.resolution, prerelease: self.prerelease, + fork_strategy: self.fork_strategy, dependency_metadata: &self.dependency_metadata, config_setting: &self.config_setting, no_build_isolation: self.no_build_isolation, @@ -2298,6 +2305,7 @@ impl From for ResolverSettings { ), resolution: value.resolution.unwrap_or_default(), prerelease: value.prerelease.unwrap_or_default(), + fork_strategy: value.fork_strategy.unwrap_or_default(), dependency_metadata: DependencyMetadata::from_entries( value.dependency_metadata.into_iter().flatten(), ), @@ -2333,6 +2341,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> { pub(crate) keyring_provider: KeyringProviderType, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) fork_strategy: ForkStrategy, pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, pub(crate) no_build_isolation: bool, @@ -2359,6 +2368,7 @@ pub(crate) struct ResolverInstallerSettings { pub(crate) keyring_provider: KeyringProviderType, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) fork_strategy: ForkStrategy, pub(crate) dependency_metadata: DependencyMetadata, pub(crate) config_setting: ConfigSettings, pub(crate) no_build_isolation: bool, @@ -2395,6 +2405,7 @@ impl ResolverInstallerSettings { keyring_provider: self.keyring_provider, resolution: self.resolution, prerelease: self.prerelease, + fork_strategy: self.fork_strategy, dependency_metadata: &self.dependency_metadata, config_setting: &self.config_setting, no_build_isolation: self.no_build_isolation, @@ -2431,6 +2442,7 @@ impl From for ResolverInstallerSettings { ), resolution: value.resolution.unwrap_or_default(), prerelease: value.prerelease.unwrap_or_default(), + fork_strategy: value.fork_strategy.unwrap_or_default(), dependency_metadata: DependencyMetadata::from_entries( value.dependency_metadata.into_iter().flatten(), ), @@ -2489,6 +2501,7 @@ pub(crate) struct PipSettings { pub(crate) dependency_mode: DependencyMode, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) fork_strategy: ForkStrategy, pub(crate) dependency_metadata: DependencyMetadata, pub(crate) output_file: Option, pub(crate) no_strip_extras: bool, @@ -2555,6 +2568,7 @@ impl PipSettings { allow_empty_requirements, resolution, prerelease, + fork_strategy, dependency_metadata, output_file, no_strip_extras, @@ -2596,6 +2610,7 @@ impl PipSettings { keyring_provider: top_level_keyring_provider, resolution: top_level_resolution, prerelease: top_level_prerelease, + fork_strategy: top_level_fork_strategy, dependency_metadata: top_level_dependency_metadata, config_settings: top_level_config_settings, no_build_isolation: top_level_no_build_isolation, @@ -2627,6 +2642,7 @@ impl PipSettings { let keyring_provider = keyring_provider.combine(top_level_keyring_provider); let resolution = resolution.combine(top_level_resolution); let prerelease = prerelease.combine(top_level_prerelease); + let fork_strategy = fork_strategy.combine(top_level_fork_strategy); let dependency_metadata = dependency_metadata.combine(top_level_dependency_metadata); let config_settings = config_settings.combine(top_level_config_settings); let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation); @@ -2672,6 +2688,10 @@ impl PipSettings { }, resolution: args.resolution.combine(resolution).unwrap_or_default(), prerelease: args.prerelease.combine(prerelease).unwrap_or_default(), + fork_strategy: args + .fork_strategy + .combine(fork_strategy) + .unwrap_or_default(), dependency_metadata: DependencyMetadata::from_entries( args.dependency_metadata .combine(dependency_metadata) @@ -2813,6 +2833,7 @@ impl<'a> From> for ResolverSettingsRef<'a> { keyring_provider: settings.keyring_provider, resolution: settings.resolution, prerelease: settings.prerelease, + fork_strategy: settings.fork_strategy, dependency_metadata: settings.dependency_metadata, config_setting: settings.config_setting, no_build_isolation: settings.no_build_isolation, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 38ae207c15899..409867e247e40 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -4980,6 +4980,115 @@ fn lock_requires_python_maximum_version() -> Result<()> { Ok(()) } +#[test] +fn lock_requires_python_fewest_versions() -> Result<()> { + let context = TestContext::new("3.11"); + + let lockfile = context.temp_dir.join("uv.lock"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["numpy"] + + [tool.uv] + fork-strategy = "fewest" + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(&lockfile).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.8" + + [options] + fork-strategy = "fewest" + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "numpy" + version = "1.24.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a4/9b/027bec52c633f6556dba6b722d9a0befb40498b9ceddd29cbe67a45a127c/numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463", size = 10911229 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/80/6cdfb3e275d95155a34659163b83c09e3a3ff9f1456880bec6cc63d71083/numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64", size = 19789140 }, + { url = "https://files.pythonhosted.org/packages/64/5f/3f01d753e2175cfade1013eea08db99ba1ee4bdb147ebcf3623b75d12aa7/numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1", size = 13854297 }, + { url = "https://files.pythonhosted.org/packages/5a/b3/2f9c21d799fa07053ffa151faccdceeb69beec5a010576b8991f614021f7/numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4", size = 13995611 }, + { url = "https://files.pythonhosted.org/packages/10/be/ae5bf4737cb79ba437879915791f6f26d92583c738d7d960ad94e5c36adf/numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6", size = 17282357 }, + { url = "https://files.pythonhosted.org/packages/c0/64/908c1087be6285f40e4b3e79454552a701664a079321cff519d8c7051d06/numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc", size = 12429222 }, + { url = "https://files.pythonhosted.org/packages/22/55/3d5a7c1142e0d9329ad27cece17933b0e2ab4e54ddc5c1861fbfeb3f7693/numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e", size = 14841514 }, + { url = "https://files.pythonhosted.org/packages/a9/cc/5ed2280a27e5dab12994c884f1f4d8c3bd4d885d02ae9e52a9d213a6a5e2/numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810", size = 19775508 }, + { url = "https://files.pythonhosted.org/packages/c0/bc/77635c657a3668cf652806210b8662e1aff84b818a55ba88257abf6637a8/numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254", size = 13840033 }, + { url = "https://files.pythonhosted.org/packages/a7/4c/96cdaa34f54c05e97c1c50f39f98d608f96f0677a6589e64e53104e22904/numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7", size = 13991951 }, + { url = "https://files.pythonhosted.org/packages/22/97/dfb1a31bb46686f09e68ea6ac5c63fdee0d22d7b23b8f3f7ea07712869ef/numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5", size = 17278923 }, + { url = "https://files.pythonhosted.org/packages/35/e2/76a11e54139654a324d107da1d98f99e7aa2a7ef97cfd7c631fba7dbde71/numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d", size = 12422446 }, + { url = "https://files.pythonhosted.org/packages/d8/ec/ebef2f7d7c28503f958f0f8b992e7ce606fb74f9e891199329d5f5f87404/numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694", size = 14834466 }, + { url = "https://files.pythonhosted.org/packages/11/10/943cfb579f1a02909ff96464c69893b1d25be3731b5d3652c2e0cf1281ea/numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61", size = 19780722 }, + { url = "https://files.pythonhosted.org/packages/a7/ae/f53b7b265fdc701e663fbb322a8e9d4b14d9cb7b2385f45ddfabfc4327e4/numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f", size = 13843102 }, + { url = "https://files.pythonhosted.org/packages/25/6f/2586a50ad72e8dbb1d8381f837008a0321a3516dfd7cb57fc8cf7e4bb06b/numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e", size = 14039616 }, + { url = "https://files.pythonhosted.org/packages/98/5d/5738903efe0ecb73e51eb44feafba32bdba2081263d40c5043568ff60faf/numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc", size = 17316263 }, + { url = "https://files.pythonhosted.org/packages/d1/57/8d328f0b91c733aa9aa7ee540dbc49b58796c862b4fbcb1146c701e888da/numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2", size = 12455660 }, + { url = "https://files.pythonhosted.org/packages/69/65/0d47953afa0ad569d12de5f65d964321c208492064c38fe3b0b9744f8d44/numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706", size = 14868112 }, + { url = "https://files.pythonhosted.org/packages/9a/cd/d5b0402b801c8a8b56b04c1e85c6165efab298d2f0ab741c2406516ede3a/numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400", size = 19816549 }, + { url = "https://files.pythonhosted.org/packages/14/27/638aaa446f39113a3ed38b37a66243e21b38110d021bfcb940c383e120f2/numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f", size = 13879950 }, + { url = "https://files.pythonhosted.org/packages/8f/27/91894916e50627476cff1a4e4363ab6179d01077d71b9afed41d9e1f18bf/numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9", size = 14030228 }, + { url = "https://files.pythonhosted.org/packages/7a/7c/d7b2a0417af6428440c0ad7cb9799073e507b1a465f827d058b826236964/numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d", size = 17311170 }, + { url = "https://files.pythonhosted.org/packages/18/9d/e02ace5d7dfccee796c37b995c63322674daf88ae2f4a4724c5dd0afcc91/numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835", size = 12454918 }, + { url = "https://files.pythonhosted.org/packages/63/38/6cc19d6b8bfa1d1a459daf2b3fe325453153ca7019976274b6f33d8b5663/numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8", size = 14867441 }, + { url = "https://files.pythonhosted.org/packages/a4/fd/8dff40e25e937c94257455c237b9b6bf5a30d42dd1cc11555533be099492/numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef", size = 19156590 }, + { url = "https://files.pythonhosted.org/packages/42/e7/4bf953c6e05df90c6d351af69966384fed8e988d0e8c54dad7103b59f3ba/numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a", size = 16705744 }, + { url = "https://files.pythonhosted.org/packages/fc/dd/9106005eb477d022b60b3817ed5937a43dad8fd1f20b0610ea8a32fcb407/numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2", size = 14734290 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "numpy" }, + ] + + [package.metadata] + requires-dist = [{ name = "numpy" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + Ok(()) +} + /// Ensure that `python_version >= '3.10' or python_version < '3.10'` is correctly collapsed to /// the full version range. This is _not_ the case under standard PEP 440 semantics, but Python /// requirements are evaluated using release-only semantics. diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 8d50670d06606..f721da120222a 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -147,6 +147,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -302,6 +303,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -458,6 +460,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -646,6 +649,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -773,6 +777,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -939,6 +944,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -1148,6 +1154,7 @@ fn resolve_index_url() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -1365,6 +1372,7 @@ fn resolve_index_url() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -1545,6 +1553,7 @@ fn resolve_find_links() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -1694,6 +1703,7 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -1895,6 +1905,7 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2079,6 +2090,7 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2228,6 +2240,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2360,6 +2373,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2492,6 +2506,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2626,6 +2641,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2759,6 +2775,7 @@ fn resolve_tool() -> anyhow::Result<()> { LowestDirect, ), prerelease: None, + fork_strategy: None, dependency_metadata: None, config_settings: None, no_build_isolation: None, @@ -2792,6 +2809,7 @@ fn resolve_tool() -> anyhow::Result<()> { keyring_provider: Disabled, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -2943,6 +2961,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -3133,6 +3152,7 @@ fn resolve_both() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -3411,6 +3431,7 @@ fn resolve_config_file() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -3482,7 +3503,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `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`, `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`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `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`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` "### ); @@ -3637,6 +3658,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -3772,6 +3794,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -3926,6 +3949,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -4133,6 +4157,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -4319,6 +4344,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -4511,6 +4537,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -4698,6 +4725,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -4892,6 +4920,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5079,6 +5108,7 @@ fn index_priority() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5219,6 +5249,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5345,6 +5376,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5469,6 +5501,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5595,6 +5628,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5719,6 +5753,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), @@ -5844,6 +5879,7 @@ fn verify_hashes() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + fork_strategy: RequiresPython, dependency_metadata: DependencyMetadata( {}, ), diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 245bdf08c66e7..9363e39738dc0 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -82,6 +82,11 @@ use this space-separated list of URLs as additional indexes when searching for p Equivalent to the `--find-links` command-line argument. If set, uv will use this comma-separated list of additional locations to search for packages. +### `UV_FORK_STRATEGY` + +Equivalent to the `--fork-strategy` argument. Controls version selection during universal +resolution. + ### `UV_FROZEN` Equivalent to the `--frozen` command-line argument. If set, uv will run without diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 987a18c5c979c..3b125335a164f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -175,6 +175,20 @@ uv run [OPTIONS] [COMMAND]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--frozen

Run without updating the uv.lock file.

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

@@ -816,6 +830,20 @@ uv add [OPTIONS] >

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--frozen

Add dependencies without re-locking the project.

The project environment will not be synced.

@@ -1160,6 +1188,20 @@ uv remove [OPTIONS] ...

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--frozen

Remove dependencies without re-locking the project.

The project environment will not be synced.

@@ -1502,6 +1544,20 @@ uv sync [OPTIONS]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--frozen

Sync without updating the uv.lock file.

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

@@ -1866,6 +1922,20 @@ uv lock [OPTIONS]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -2168,6 +2238,20 @@ uv export [OPTIONS]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--format format

The format to which uv.lock should be exported.

At present, only requirements-txt is supported.

@@ -2529,6 +2613,20 @@ uv tree [OPTIONS]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--frozen

Display the requirements without locking the project.

If the lockfile is missing, uv will exit with an error.

@@ -2983,6 +3081,20 @@ uv tool run [OPTIONS] [COMMAND]

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--from from

Use the given package to provide the command.

By default, the package name is assumed to match the command name.

@@ -3301,6 +3413,20 @@ uv tool install [OPTIONS]

Will replace any existing entry points with the same name in the executable directory.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -3609,6 +3735,20 @@ uv tool upgrade [OPTIONS] ...

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -5390,6 +5530,20 @@ uv pip compile [OPTIONS] ...

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--generate-hashes

Include distribution hashes in the output file

--help, -h

Display the concise help for this command

@@ -6236,6 +6390,20 @@ uv pip install [OPTIONS] |--editable If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -7881,6 +8049,20 @@ uv build [OPTIONS] [SRC]

By default, uv won’t create a PEP 517 build environment for packages using the uv build backend, but use a fast path that calls into the build backend directly. This option forces always using PEP 517.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • + +
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index cc39a1a274d00..237880f1d683e 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -764,6 +764,42 @@ formats described above. --- +### [`fork-strategy`](#fork-strategy) {: #fork-strategy } + +The strategy to use when selecting multiple versions of a given package across Python +versions and platforms. + +By default, uv will optimize for selecting the latest version of each package for each +supported Python version (`requires-python`), while minimizing the number of selected +versions across platforms. + +Under `fewest`, uv will minimize the number of +selected versions for each package, preferring older versions that are compatible with a +wider range of supported Python versions or platforms. + +**Default value**: `"fewest"` + +**Possible values**: + +- `"fewest"`: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms +- `"requires-python"`: Optimize for selecting latest supported version of each package, for each supported Python version + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + fork-strategy = "fewest" + ``` +=== "uv.toml" + + ```toml + fork-strategy = "fewest" + ``` + +--- + ### [`index`](#index) {: #index } The package indexes to use when resolving dependencies. @@ -2108,6 +2144,44 @@ formats described above. --- +#### [`fork-strategy`](#pip_fork-strategy) {: #pip_fork-strategy } + + +The strategy to use when selecting multiple versions of a given package across Python +versions and platforms. + +By default, uv will optimize for selecting the latest version of each package for each +supported Python version (`requires-python`), while minimizing the number of selected +versions across platforms. + +Under `fewest`, uv will minimize the number of +selected versions for each package, preferring older versions that are compatible with a +wider range of supported Python versions or platforms. + +**Default value**: `"fewest"` + +**Possible values**: + +- `"fewest"`: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms +- `"requires-python"`: Optimize for selecting latest supported version of each package, for each supported Python version + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv.pip] + fork-strategy = "fewest" + ``` +=== "uv.toml" + + ```toml + [pip] + fork-strategy = "fewest" + ``` + +--- + #### [`generate-hashes`](#pip_generate-hashes) {: #pip_generate-hashes } diff --git a/uv.schema.json b/uv.schema.json index d49eaeb56b619..1b02f35666771 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -179,6 +179,17 @@ "$ref": "#/definitions/IndexUrl" } }, + "fork-strategy": { + "description": "The strategy to use when selecting multiple versions of a given package across Python versions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each supported Python version (`requires-python`), while minimizing the number of selected versions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.", + "anyOf": [ + { + "$ref": "#/definitions/ForkStrategy" + }, + { + "type": "null" + } + ] + }, "index": { "description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined index has the highest priority. Further, the indexes provided by this setting are given higher priority than any indexes specified via [`index_url`](#index-url) or [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains a given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for those dependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is given the lowest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.", "type": [ @@ -626,6 +637,24 @@ "description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. For example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: - - ", "type": "string" }, + "ForkStrategy": { + "oneOf": [ + { + "description": "Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms.", + "type": "string", + "enum": [ + "fewest" + ] + }, + { + "description": "Optimize for selecting latest supported version of each package, for each supported Python version.", + "type": "string", + "enum": [ + "requires-python" + ] + } + ] + }, "GitPattern": { "anyOf": [ { @@ -947,6 +976,17 @@ "$ref": "#/definitions/IndexUrl" } }, + "fork-strategy": { + "description": "The strategy to use when selecting multiple versions of a given package across Python versions and platforms.\n\nBy default, uv will optimize for selecting the latest version of each package for each supported Python version (`requires-python`), while minimizing the number of selected versions across platforms.\n\nUnder `fewest`, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.", + "anyOf": [ + { + "$ref": "#/definitions/ForkStrategy" + }, + { + "type": "null" + } + ] + }, "generate-hashes": { "description": "Include distribution hashes in the output file.", "type": [ @@ -1352,7 +1392,7 @@ ] }, "PythonVersion": { - "description": "A Python version specifier, e.g. `3.7` or `3.8.0`.", + "description": "A Python version specifier, e.g. `3.11` or `3.12.4`.", "type": "string", "pattern": "^3\\.\\d+(\\.\\d+)?$" },