diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index d2e651a19..ce3d8621a 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -354,6 +354,7 @@ pub fn resolver_options( no_binary: flag(no_binary, binary, "binary"), no_binary_package: Some(no_binary_package), no_sources: if no_sources { Some(true) } else { None }, + build_dependency_strategy: None, } } @@ -480,5 +481,6 @@ pub fn resolver_installer_options( Some(no_binary_package) }, no_sources: if no_sources { Some(true) } else { None }, + build_dependency_strategy: None, } } diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index ffc15c2d3..158c64255 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -1,4 +1,5 @@ pub use authentication::*; +pub use build_dependency_strategy::*; pub use build_options::*; pub use concurrency::*; pub use config_settings::*; @@ -24,6 +25,7 @@ pub use trusted_publishing::*; pub use vcs::*; mod authentication; +mod build_dependency_strategy; mod build_options; mod concurrency; mod config_settings; diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 738b00ffe..b8d2e3c28 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use url::Url; use uv_configuration::{ - ConfigSettings, ExportFormat, IndexStrategy, KeyringProviderType, PackageConfigSettings, - RequiredVersion, TargetTriple, TrustedPublishing, + BuildDependencyStrategy, ConfigSettings, ExportFormat, IndexStrategy, KeyringProviderType, + PackageConfigSettings, RequiredVersion, TargetTriple, TrustedPublishing, }; use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex}; use uv_install_wheel::LinkMode; @@ -77,6 +77,7 @@ macro_rules! impl_combine_or { impl_combine_or!(AddBoundsKind); impl_combine_or!(AnnotationStyle); +impl_combine_or!(BuildDependencyStrategy); impl_combine_or!(ExcludeNewer); impl_combine_or!(ExportFormat); impl_combine_or!(ForkStrategy); diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 0e3f13f71..d56d512e0 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -329,6 +329,7 @@ fn warn_uv_toml_masked_fields(options: &Options) { no_build_package, no_binary, no_binary_package, + build_dependency_strategy: _, }, install_mirrors: PythonInstallMirrors { diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 9eb765a1e..830438cad 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize}; use uv_cache_info::CacheKey; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, PackageConfigSettings, - PackageNameSpecifier, RequiredVersion, TargetTriple, TrustedHost, TrustedPublishing, + BuildDependencyStrategy, ConfigSettings, IndexStrategy, KeyringProviderType, + PackageConfigSettings, PackageNameSpecifier, RequiredVersion, TargetTriple, TrustedHost, + TrustedPublishing, }; use uv_distribution_types::{ Index, IndexUrl, IndexUrlError, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata, @@ -373,6 +374,7 @@ pub struct ResolverOptions { pub no_build_isolation: Option, pub no_build_isolation_package: Option>, pub no_sources: Option, + pub build_dependency_strategy: Option, } /// Shared settings, relevant to all operations that must resolve and install dependencies. The @@ -509,6 +511,23 @@ pub struct ResolverInstallerOptions { "# )] pub keyring_provider: Option, + /// The strategy to use when resolving build dependencies for source distributions. + /// + /// - `latest`: Use the latest compatible version of each build dependency. + /// - `prefer-locked`: Prefer the versions pinned in the lockfile, if available. + /// + /// When set to `prefer-locked`, uv will use the locked versions of packages specified in the + /// lockfile as preferences when resolving build dependencies during source builds. This helps + /// ensure that build environments are consistent with the project's resolved dependencies. + #[option( + default = "\"latest\"", + value_type = "str", + example = r#" + build-dependency-strategy = "prefer-locked" + "#, + possible_values = true + )] + pub build_dependency_strategy: Option, /// The strategy to use when selecting between the different compatible versions for a given /// package requirement. /// @@ -1686,6 +1705,7 @@ impl From for ResolverOptions { no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, no_sources: value.no_sources, + build_dependency_strategy: value.build_dependency_strategy, } } } @@ -1811,6 +1831,7 @@ impl From for ResolverInstallerOptions { no_build_package: value.no_build_package, no_binary: value.no_binary, no_binary_package: value.no_binary_package, + build_dependency_strategy: None, } } } @@ -1864,6 +1885,7 @@ pub struct OptionsWire { no_build_package: Option>, no_binary: Option, no_binary_package: Option>, + build_dependency_strategy: Option, // #[serde(flatten)] // install_mirror: PythonInstallMirrors, @@ -1954,6 +1976,7 @@ impl From for Options { no_build_package, no_binary, no_binary_package, + build_dependency_strategy, pip, cache_keys, override_dependencies, @@ -2021,6 +2044,7 @@ impl From for Options { no_build_package, no_binary, no_binary_package, + build_dependency_strategy, }, pip, cache_keys, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index b613cf30e..c817bc3e5 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( upgrade: _, build_options, sources, + build_dependency_strategy: _, } = settings; let client_builder = BaseClientBuilder::default() diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 509ac8f75..7ccb4876a 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::collections::hash_map::Entry; use std::fmt::Write; use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::FromStr; use std::sync::Arc; @@ -17,9 +17,9 @@ use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DevMode, DryRun, - EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, InstallOptions, Preview, - PreviewFeatures, SourceStrategy, + BuildDependencyStrategy, Concurrency, Constraints, DependencyGroups, + DependencyGroupsWithDefaults, DevMode, DryRun, EditableMode, ExtrasSpecification, + ExtrasSpecificationWithDefaults, InstallOptions, Preview, PreviewFeatures, SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -27,7 +27,7 @@ use uv_distribution_types::{ Index, IndexName, IndexUrl, IndexUrls, NameRequirementSpecification, Requirement, RequirementSource, UnresolvedRequirement, VersionId, }; -use uv_fs::{LockedFile, Simplified}; +use uv_fs::{CWD, LockedFile, Simplified}; use uv_git::GIT_STORE; use uv_git_types::GitReference; use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, PackageName}; @@ -427,31 +427,27 @@ pub(crate) async fn add( FlatIndex::from_entries(entries, None, &hasher, &settings.resolver.build_options) }; - // Load preferences from the existing lockfile if available. - let preferences = if let Ok(Some(lock)) = LockTarget::from(&target).read().await { - Preferences::from_iter( - lock.packages() - .iter() - .filter_map(|package| { - Preference::from_lock( - package, - match &target { - AddTarget::Script(_, _) => Path::new(".") - .canonicalize() - .unwrap_or_else(|_| PathBuf::from(".")), - AddTarget::Project(project, _) => { - project.workspace().install_path().clone() - } - } - .as_path(), - ) - .transpose() - }) - .collect::, _>>()?, - &ResolverEnvironment::specific(target.interpreter().markers().clone().into()), - ) - } else { - Preferences::default() + // Load preferences from the existing lockfile if available and if configured to do so. + let preferences = match settings.resolver.build_dependency_strategy { + BuildDependencyStrategy::PreferLocked => { + if let Ok(Some(lock)) = LockTarget::from(&target).read().await { + Preferences::from_iter( + lock.packages() + .iter() + .filter_map(|package| { + Preference::from_lock(package, &target.install_path()) + .transpose() + }) + .collect::, _>>()?, + &ResolverEnvironment::specific( + target.interpreter().markers().clone().into(), + ), + ) + } else { + Preferences::default() + } + } + BuildDependencyStrategy::Latest => Preferences::default(), }; // Create a build dispatch. @@ -1345,6 +1341,14 @@ impl AddTarget { } } + /// Return the parent path of the target. + pub(crate) fn install_path(&self) -> &Path { + match self { + Self::Script(script, _) => script.path.parent().unwrap_or(&*CWD), + Self::Project(project, _) => project.root(), + } + } + /// Write the updated content to the target. /// /// Returns `true` if the content was modified. diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 86a3e7014..1754eb6ff 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -12,8 +12,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview, - Reinstall, Upgrade, + BuildDependencyStrategy, Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, + ExtrasSpecification, Preview, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -440,6 +440,7 @@ async fn do_lock( upgrade, build_options, sources, + build_dependency_strategy, } = settings; // Collect the requirements, etc. @@ -663,24 +664,25 @@ async fn do_lock( FlatIndex::from_entries(entries, None, &hasher, build_options) }; + // Extract preferences and git refs from the existing lockfile if available for build dispatch. // We extract preferences before validation because validation may need to build source - // distributions to get their metadata. - let preferences = existing_lock - .as_ref() - .map(|existing_lock| -> Result { - let locked = read_lock_requirements(existing_lock, target.install_path(), upgrade)?; - // Populate the Git resolver. - for ResolvedRepositoryReference { reference, sha } in &locked.git { - debug!("Inserting Git reference into resolver: `{reference:?}` at `{sha}`"); - state.git().insert(reference.clone(), *sha); - } - Ok(Preferences::from_iter( - locked.preferences, - &ResolverEnvironment::universal(vec![]), - )) - }) - .transpose()? - .unwrap_or_default(); + // distributions to get their metadata, and those builds should use the lockfile's preferences + // for accuracy. While the lockfile hasn't been validated yet, using its preferences is still + // better than using defaults, as most lockfiles are valid and this gives more accurate results. + let preferences = match build_dependency_strategy { + BuildDependencyStrategy::PreferLocked => existing_lock + .as_ref() + .map(|existing_lock| -> Result { + Ok(Preferences::from_iter( + read_lock_requirements(existing_lock, target.install_path(), upgrade)? + .preferences, + &ResolverEnvironment::universal(vec![]), + )) + }) + .transpose()? + .unwrap_or_default(), + BuildDependencyStrategy::Latest => Preferences::default(), + }; // Create a build dispatch. let build_dispatch = BuildDispatch::new( diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 5051fe868..d39d360cb 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1696,6 +1696,7 @@ pub(crate) async fn resolve_names( resolution: _, sources, upgrade: _, + build_dependency_strategy: _, }, compile_bytecode: _, reinstall: _, @@ -1851,6 +1852,7 @@ pub(crate) async fn resolve_environment( upgrade: _, build_options, sources, + build_dependency_strategy: _, } = settings; // Respect all requirements from the provided sources. @@ -2201,6 +2203,7 @@ pub(crate) async fn update_environment( resolution, sources, upgrade, + build_dependency_strategy: _, }, compile_bytecode, reinstall, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 1d594bd53..521bf188e 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -208,6 +208,7 @@ pub(crate) async fn tree( upgrade: _, build_options: _, sources: _, + build_dependency_strategy: _, } = &settings; let capabilities = IndexCapabilities::default(); diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b563b0b8e..121bfba94 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -21,11 +21,11 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, DependencyGroups, DryRun, EditableMode, - ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, - KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, Preview, ProjectBuildBackend, - Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, - Upgrade, VersionControlSystem, + BuildDependencyStrategy, BuildOptions, Concurrency, ConfigSettings, DependencyGroups, DryRun, + EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, + InstallOptions, KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, Preview, + ProjectBuildBackend, Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, + TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl, Requirement}; use uv_install_wheel::LinkMode; @@ -2738,6 +2738,7 @@ pub(crate) struct ResolverSettings { pub(crate) resolution: ResolutionMode, pub(crate) sources: SourceStrategy, pub(crate) upgrade: Upgrade, + pub(crate) build_dependency_strategy: BuildDependencyStrategy, } impl ResolverSettings { @@ -2802,6 +2803,7 @@ impl From for ResolverSettings { NoBinary::from_args(value.no_binary, value.no_binary_package.unwrap_or_default()), NoBuild::from_args(value.no_build, value.no_build_package.unwrap_or_default()), ), + build_dependency_strategy: value.build_dependency_strategy.unwrap_or_default(), } } } @@ -2887,6 +2889,7 @@ impl From for ResolverInstallerSettings { .map(Requirement::from) .collect(), ), + build_dependency_strategy: value.build_dependency_strategy.unwrap_or_default(), }, compile_bytecode: value.compile_bytecode.unwrap_or_default(), reinstall: Reinstall::from_args( @@ -3054,6 +3057,7 @@ impl PipSettings { no_build_package: top_level_no_build_package, no_binary: top_level_no_binary, no_binary_package: top_level_no_binary_package, + build_dependency_strategy: _, } = top_level; // Merge the top-level options (`tool.uv`) with the pip-specific options (`tool.uv.pip`), diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index ccab4b540..cc8521ce3 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -13655,6 +13655,9 @@ fn add_build_dependencies_respect_locked_versions() -> Result<()> { version = "0.1.0" requires-python = ">=3.9" dependencies = ["anyio<4.1"] + + [tool.uv] + build-dependency-strategy = "prefer-locked" "#})?; // Create a lockfile with anyio 4.0.0 @@ -13717,6 +13720,9 @@ fn add_build_dependencies_respect_locked_versions() -> Result<()> { requires-python = ">=3.9" dependencies = ["anyio<3.8", "child"] + [tool.uv] + build-dependency-strategy = "prefer-locked" + [tool.uv.sources] child = { workspace = true } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5b43c7c6f..125bb6d97 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11457,7 +11457,7 @@ fn sync_build_dependencies_respect_locked_versions() -> Result<()> { Resolved [N] packages in [TIME] "); - // Now add the child dependency + // Now add the child dependency with build-dependency-strategy = "prefer-locked" pyproject_toml.write_str(indoc! {r#" [project] name = "parent" @@ -11465,6 +11465,9 @@ fn sync_build_dependencies_respect_locked_versions() -> Result<()> { requires-python = ">=3.9" dependencies = ["anyio<4.1", "child"] + [tool.uv] + build-dependency-strategy = "prefer-locked" + [tool.uv.sources] child = { path = "child" } "#})?; @@ -11512,6 +11515,9 @@ fn sync_build_dependencies_respect_locked_versions() -> Result<()> { requires-python = ">=3.9" dependencies = ["anyio<3.8", "child"] + [tool.uv] + build-dependency-strategy = "prefer-locked" + [tool.uv.sources] child = { path = "child" } "#})?; diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 6469a584b..31dd1157b 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -749,6 +749,40 @@ bypasses SSL verification and could expose you to MITM attacks. --- +### [`build-dependency-strategy`](#build-dependency-strategy) {: #build-dependency-strategy } + +The strategy to use when resolving build dependencies for source distributions. + +- `latest`: Use the latest compatible version of each build dependency. +- `prefer-locked`: Prefer the versions pinned in the lockfile, if available. + +When set to `prefer-locked`, uv will use the locked versions of packages specified in the +lockfile as preferences when resolving build dependencies during source builds. This helps +ensure that build environments are consistent with the project's resolved dependencies. + +**Default value**: `"latest"` + +**Possible values**: + +- `"latest"`: Use the latest compatible version of each build dependency +- `"prefer-locked"`: Prefer the versions pinned in the lockfile, if available + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + build-dependency-strategy = "prefer-locked" + ``` +=== "uv.toml" + + ```toml + build-dependency-strategy = "prefer-locked" + ``` + +--- + ### [`cache-dir`](#cache-dir) {: #cache-dir } Path to the cache directory. diff --git a/uv.schema.json b/uv.schema.json index 7ca04d4f8..d592eca73 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -46,6 +46,17 @@ "type": "string" } }, + "build-dependency-strategy": { + "description": "The strategy to use when resolving build dependencies for source distributions.\n\n- `latest`: Use the latest compatible version of each build dependency.\n- `prefer-locked`: Prefer the versions pinned in the lockfile, if available.\n\nWhen set to `prefer-locked`, uv will use the locked versions of packages specified in the\nlockfile as preferences when resolving build dependencies during source builds. This helps\nensure that build environments are consistent with the project's resolved dependencies.", + "anyOf": [ + { + "$ref": "#/definitions/BuildDependencyStrategy" + }, + { + "type": "null" + } + ] + }, "cache-dir": { "description": "Path to the cache directory.\n\nDefaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux and macOS, and\n`%LOCALAPPDATA%\\uv\\cache` on Windows.", "type": [ @@ -726,6 +737,20 @@ } } }, + "BuildDependencyStrategy": { + "oneOf": [ + { + "description": "Use the latest compatible version of each build dependency.", + "type": "string", + "const": "latest" + }, + { + "description": "Prefer the versions pinned in the lockfile, if available.", + "type": "string", + "const": "prefer-locked" + } + ] + }, "CacheKey": { "anyOf": [ {