diff --git a/Cargo.lock b/Cargo.lock index 77dfad413..f9316abc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5514,6 +5514,7 @@ dependencies = [ "tracing-test", "unicode-width 0.2.1", "url", + "uv-cache-key", "uv-fs", "uv-normalize", "uv-pep440", diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 9c83c61d3..1c2ecfeaf 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -103,7 +103,7 @@ mod resolver { ResolverEnvironment, ResolverOutput, }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; - use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; + use uv_workspace::WorkspaceCache; static MARKERS: LazyLock = LazyLock::new(|| { MarkerEnvironment::try_from(MarkerEnvironmentBuilder { @@ -141,7 +141,9 @@ mod resolver { universal: bool, ) -> Result { let build_isolation = BuildIsolation::default(); - let extra_build_dependencies = ExtraBuildDependencies::default(); + let extra_build_requires = uv_distribution::ExtraBuildRequires { + extra_build_dependencies: uv_workspace::pyproject::ExtraBuildDependencies::default(), + }; let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); @@ -188,7 +190,7 @@ mod resolver { &config_settings, &config_settings_package, build_isolation, - &extra_build_dependencies, + &extra_build_requires, LinkMode::default(), &build_options, &hashes, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 51b727406..e8c67bf43 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -40,7 +40,7 @@ use uv_types::{ HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; -use uv_workspace::pyproject::ExtraBuildDependencies; +use uv_distribution::ExtraBuildRequires; #[derive(Debug, Error)] pub enum BuildDispatchError { @@ -89,7 +89,7 @@ pub struct BuildDispatch<'a> { shared_state: SharedState, dependency_metadata: &'a DependencyMetadata, build_isolation: BuildIsolation<'a>, - extra_build_dependencies: &'a ExtraBuildDependencies, + extra_build_requires: &'a ExtraBuildRequires, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, config_settings: &'a ConfigSettings, @@ -118,7 +118,7 @@ impl<'a> BuildDispatch<'a> { config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, build_isolation: BuildIsolation<'a>, - extra_build_dependencies: &'a ExtraBuildDependencies, + extra_build_requires: &'a ExtraBuildRequires, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, hasher: &'a HashStrategy, @@ -141,7 +141,7 @@ impl<'a> BuildDispatch<'a> { config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + extra_build_requires, link_mode, build_options, hasher, @@ -223,6 +223,10 @@ impl BuildContext for BuildDispatch<'_> { &self.workspace_cache } + fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies { + &self.extra_build_requires.extra_build_dependencies + } + async fn resolve<'data>( &'data self, requirements: &'data [Requirement], @@ -456,7 +460,7 @@ impl BuildContext for BuildDispatch<'_> { self.workspace_cache(), config_settings, self.build_isolation, - self.extra_build_dependencies, + &self.extra_build_requires.extra_build_dependencies, &build_stack, build_kind, self.build_extra_env_vars.clone(), diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 07958f715..6371d58af 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -3,8 +3,8 @@ pub use download::LocalWheel; pub use error::Error; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use metadata::{ - ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata, - MetadataError, RequiresDist, SourcedDependencyGroups, + ArchiveMetadata, BuildRequires, ExtraBuildRequires, FlatRequiresDist, LoweredRequirement, + LoweringError, Metadata, MetadataError, RequiresDist, SourcedDependencyGroups, }; pub use reporter::Reporter; pub use source::prune; diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index 99b528017..7a746b5de 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -4,7 +4,8 @@ use std::path::Path; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::PackageName; -use uv_workspace::pyproject::ToolUvSources; +use uv_pypi_types::VerbatimParsedUrl; +use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache, }; @@ -203,3 +204,91 @@ impl BuildRequires { }) } } + +/// Lowered extra build dependencies with source resolution applied. +#[derive(Debug, Clone)] +pub struct ExtraBuildRequires { + pub extra_build_dependencies: ExtraBuildDependencies, +} + +impl ExtraBuildRequires { + /// Lower extra build dependencies from a workspace, applying source resolution. + pub fn from_workspace( + extra_build_dependencies: ExtraBuildDependencies, + workspace: &Workspace, + index_locations: &IndexLocations, + source_strategy: SourceStrategy, + ) -> Result { + match source_strategy { + SourceStrategy::Enabled => { + // Collect project sources and indexes + let project_indexes = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_deref()) + .unwrap_or(&[]); + + let empty_sources = BTreeMap::default(); + let project_sources = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .unwrap_or(&empty_sources); + + // Lower each package's extra build dependencies + let mut result = ExtraBuildDependencies::default(); + for (package_name, requirements) in extra_build_dependencies { + let lowered: Vec> = requirements + .into_iter() + .flat_map(|requirement| { + let requirement_name = requirement.name.clone(); + let extra = requirement.marker.top_level_extra_name(); + let group = None; + LoweredRequirement::from_requirement( + requirement, + None, + workspace.install_path(), + project_sources, + project_indexes, + extra.as_deref(), + group, + index_locations, + workspace, + None, + ) + .map(move |requirement| match requirement { + Ok(requirement) => Ok(requirement.into_inner().into()), + Err(err) => Err(MetadataError::LoweringError( + requirement_name.clone(), + Box::new(err), + )), + }) + }) + .collect::, _>>()?; + result.insert(package_name, lowered); + } + Ok(Self { + extra_build_dependencies: result, + }) + } + SourceStrategy::Disabled => { + // Without source resolution, just return the dependencies as-is + Ok(Self { + extra_build_dependencies, + }) + } + } + } + + /// Create from pre-lowered dependencies (for non-workspace contexts). + pub fn from_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self { + Self { + extra_build_dependencies, + } + } +} diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index a56a1c354..3375f9fe2 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -11,7 +11,7 @@ use uv_pypi_types::{HashDigests, ResolutionMetadata}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::{WorkspaceCache, WorkspaceError}; -pub use crate::metadata::build_requires::BuildRequires; +pub use crate::metadata::build_requires::{BuildRequires, ExtraBuildRequires}; pub use crate::metadata::dependency_groups::SourcedDependencyGroups; pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweringError; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 080a1e52d..14d9ca03c 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -390,6 +390,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } + /// Determine the extra build dependencies for the given package name. + fn extra_build_dependencies_for( + &self, + name: Option<&PackageName>, + ) -> &[uv_pep508::Requirement] { + name.and_then(|name| { + self.build_context + .extra_build_dependencies() + .get(name) + .map(|v| v.as_slice()) + }) + .unwrap_or(&[]) + } + /// Build a source distribution from a remote URL. async fn url<'data>( &self, @@ -423,12 +437,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = cache_shard.shard(revision.id()); let source_dist_entry = cache_shard.entry(SOURCE); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -596,12 +611,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we either need to build the metadata. @@ -795,12 +811,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = cache_shard.shard(revision.id()); let source_entry = cache_shard.entry(SOURCE); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -957,12 +974,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. @@ -1099,12 +1117,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -1287,12 +1306,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { )); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. @@ -1492,12 +1512,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?; - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -1795,12 +1816,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { )); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 223701692..bd1e0ffe2 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -114,6 +114,24 @@ impl Operator { pub fn is_star(self) -> bool { matches!(self, Self::EqualStar | Self::NotEqualStar) } + + /// Returns the string representation of this operator. + pub fn as_str(self) -> &'static str { + match self { + Self::Equal => "==", + // Beware, this doesn't print the star + Self::EqualStar => "==", + #[allow(deprecated)] + Self::ExactEqual => "===", + Self::NotEqual => "!=", + Self::NotEqualStar => "!=", + Self::TildeEqual => "~=", + Self::LessThan => "<", + Self::LessThanEqual => "<=", + Self::GreaterThan => ">", + Self::GreaterThanEqual => ">=", + } + } } impl FromStr for Operator { @@ -150,21 +168,7 @@ impl FromStr for Operator { impl std::fmt::Display for Operator { /// Note the `EqualStar` is also `==`. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let operator = match self { - Self::Equal => "==", - // Beware, this doesn't print the star - Self::EqualStar => "==", - #[allow(deprecated)] - Self::ExactEqual => "===", - Self::NotEqual => "!=", - Self::NotEqualStar => "!=", - Self::TildeEqual => "~=", - Self::LessThan => "<", - Self::LessThanEqual => "<=", - Self::GreaterThan => ">", - Self::GreaterThanEqual => ">=", - }; - + let operator = self.as_str(); write!(f, "{operator}") } } diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index e111c5118..13e2687cf 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -48,6 +48,11 @@ impl VersionSpecifiers { Self(Box::new([])) } + /// The number of specifiers. + pub fn len(&self) -> usize { + self.0.len() + } + /// Whether all specifiers match the given version. pub fn contains(&self, version: &Version) -> bool { self.iter().all(|specifier| specifier.contains(version)) diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index e9306da00..85b4e9c5d 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -19,6 +19,7 @@ doctest = false workspace = true [dependencies] +uv-cache-key = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index f63d46206..794981e67 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -26,6 +26,7 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use thiserror::Error; use url::Url; +use uv_cache_key::{CacheKey, CacheKeyHasher}; use cursor::Cursor; pub use marker::{ @@ -251,6 +252,52 @@ impl Serialize for Requirement { } } +impl CacheKey for Requirement +where + T: Display, +{ + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.name.as_str().cache_key(state); + + self.extras.len().cache_key(state); + for extra in &self.extras { + extra.as_str().cache_key(state); + } + + // TODO(zanieb): We inline cache key handling for the child types here, but we could + // move the implementations to the children. The intent here was to limit the scope of + // types exposing the `CacheKey` trait for now. + if let Some(version_or_url) = &self.version_or_url { + 1u8.cache_key(state); + match version_or_url { + VersionOrUrl::VersionSpecifier(spec) => { + 0u8.cache_key(state); + spec.len().cache_key(state); + for specifier in spec.iter() { + specifier.operator().as_str().cache_key(state); + specifier.version().to_string().cache_key(state); + } + } + VersionOrUrl::Url(url) => { + 1u8.cache_key(state); + url.to_string().cache_key(state); + } + } + } else { + 0u8.cache_key(state); + } + + if let Some(marker) = self.marker.contents() { + 1u8.cache_key(state); + marker.to_string().cache_key(state); + } else { + 0u8.cache_key(state); + } + + // `origin` is intentionally omitted + } +} + impl Requirement { /// Returns whether the markers apply for the given environment pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index e3f4ee012..875742da9 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -101,6 +101,9 @@ pub trait BuildContext { /// Workspace discovery caching. fn workspace_cache(&self) -> &WorkspaceCache; + /// Get the extra build dependencies. + fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies; + /// Resolve the given requirements into a ready-to-install set of package versions. fn resolve<'a>( &'a self, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index a367b7a2e..7c62a30f7 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -564,6 +564,9 @@ async fn build_package( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -577,7 +580,7 @@ async fn build_package( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &hasher, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 036c43435..94cc2b388 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -476,6 +476,9 @@ pub(crate) async fn pip_compile( .map(|constraint| constraint.requirement.clone()), ); + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -489,7 +492,7 @@ pub(crate) async fn pip_compile( &config_settings, &config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hashes, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 548a2b14e..f558e7355 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -421,6 +421,9 @@ pub(crate) async fn pip_install( let state = SharedState::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -434,7 +437,7 @@ pub(crate) async fn pip_install( config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 963fd0659..5bbf8b071 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -354,6 +354,9 @@ pub(crate) async fn pip_sync( let state = SharedState::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -367,7 +370,7 @@ pub(crate) async fn pip_sync( config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 42cba36f6..611edc05b 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -431,6 +431,18 @@ pub(crate) async fn add( }; // Create a build dispatch. + let extra_build_requires = if let AddTarget::Project(project, _) = &target { + uv_distribution::ExtraBuildRequires::from_workspace( + settings.resolver.extra_build_dependencies.clone(), + project.workspace(), + &settings.resolver.index_locations, + settings.resolver.sources, + )? + } else { + uv_distribution::ExtraBuildRequires::from_lowered( + settings.resolver.extra_build_dependencies.clone(), + ) + }; let build_dispatch = BuildDispatch::new( &client, cache, @@ -444,7 +456,7 @@ pub(crate) async fn add( &settings.resolver.config_setting, &settings.resolver.config_settings_package, build_isolation, - &settings.resolver.extra_build_dependencies, + &extra_build_requires, settings.resolver.link_mode, &settings.resolver.build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 9d2b33ba5..73b89d084 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -671,6 +671,17 @@ async fn do_lock( }; // Create a build dispatch. + let extra_build_requires = match &target { + LockTarget::Workspace(workspace) => uv_distribution::ExtraBuildRequires::from_workspace( + extra_build_dependencies.clone(), + workspace, + index_locations, + *sources, + )?, + LockTarget::Script(_) => uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ), + }; let build_dispatch = BuildDispatch::new( &client, cache, @@ -684,7 +695,7 @@ async fn do_lock( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 6c5ea1e6f..f2a03a51d 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1733,6 +1733,9 @@ pub(crate) async fn resolve_names( let build_hasher = HashStrategy::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -1746,7 +1749,7 @@ pub(crate) async fn resolve_names( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, @@ -1943,6 +1946,9 @@ pub(crate) async fn resolve_environment( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let resolve_dispatch = BuildDispatch::new( &client, cache, @@ -1956,7 +1962,7 @@ pub(crate) async fn resolve_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, @@ -2083,6 +2089,9 @@ pub(crate) async fn sync_environment( }; // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2096,7 +2105,7 @@ pub(crate) async fn sync_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &build_hasher, @@ -2309,6 +2318,9 @@ pub(crate) async fn update_environment( }; // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2322,7 +2334,7 @@ pub(crate) async fn update_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 89721fb93..91989ac50 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -18,24 +18,23 @@ use uv_configuration::{ }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, IndexLocations, Requirement, Resolution, ResolvedDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; -use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; +use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Installable, Lock}; use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; -use uv_workspace::pyproject::Source; +use uv_workspace::pyproject::{ExtraBuildDependencies, Source}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache, - pyproject::ExtraBuildDependencies, }; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -547,60 +546,6 @@ impl Deref for SyncEnvironment { } } -/// Lower extra build dependencies using workspace sources. -/// -/// This ensures that extra build dependencies respect source configurations -/// from the project's `tool.uv.sources` table. -#[allow(clippy::result_large_err)] -fn lower_extra_build_dependencies( - extra_build_dependencies: &ExtraBuildDependencies, - workspace: &Workspace, - index_locations: &IndexLocations, -) -> Result { - use std::collections::BTreeMap; - use uv_configuration::SourceStrategy; - - let mut lowered_dependencies = BTreeMap::new(); - - for (package_name, requirements) in extra_build_dependencies { - // Use BuildRequires to lower the requirements - let metadata = uv_distribution::BuildRequires::from_workspace( - uv_pypi_types::BuildRequires { - name: Some(package_name.clone()), - requires_dist: requirements.clone(), - }, - workspace, - index_locations, - SourceStrategy::Enabled, - )?; - - // Extract the lowered requirements and convert them - let lowered_requirements: Vec> = - metadata.requires_dist.into_iter().map(Into::into).collect(); - lowered_dependencies.insert(package_name.clone(), lowered_requirements); - } - - Ok(ExtraBuildDependencies::from(lowered_dependencies)) -} - -/// Lower extra build dependencies using script sources. -/// -/// This ensures that extra build dependencies respect source configurations -/// from the script's metadata. -fn lower_extra_build_dependencies_for_script( - extra_build_dependencies: &ExtraBuildDependencies, - _script: &Pep723Script, - _index_locations: &IndexLocations, -) -> ExtraBuildDependencies { - // Scripts don't have extra build dependencies per se, but we still need to handle - // the case for consistency. Since scripts don't define extra build dependencies - // for other packages, we just return the dependencies as-is. - // - // If in the future scripts support defining extra build dependencies for packages - // they depend on, we would need to implement proper lowering here using the - // script's sources. - extra_build_dependencies.clone() -} /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] @@ -650,20 +595,24 @@ pub(super) async fn do_sync( ); } - // Lower extra build dependencies to apply source configurations - let extra_build_dependencies = match &target { + // Lower the extra build dependencies with source resolution + let extra_build_requires = match &target { InstallTarget::Workspace { workspace, .. } | InstallTarget::Project { workspace, .. } | InstallTarget::NonProjectWorkspace { workspace, .. } => { - lower_extra_build_dependencies(extra_build_dependencies, workspace, index_locations)? + uv_distribution::ExtraBuildRequires::from_workspace( + extra_build_dependencies.clone(), + workspace, + index_locations, + sources, + )? } - InstallTarget::Script { script, .. } => lower_extra_build_dependencies_for_script( - extra_build_dependencies, - script, - index_locations, - ), + InstallTarget::Script { .. } => uv_distribution::ExtraBuildRequires { + extra_build_dependencies: ExtraBuildDependencies::default(), + }, }; + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) @@ -792,7 +741,7 @@ pub(super) async fn do_sync( config_setting, config_settings_package, build_isolation, - &extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 116dc0553..77477afe8 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -275,7 +275,9 @@ pub(crate) async fn venv( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); - let extra_build_dependencies = ExtraBuildDependencies::default(); + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + ExtraBuildDependencies::default(), + ); // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -290,7 +292,7 @@ pub(crate) async fn venv( &config_settings, &config_settings_package, BuildIsolation::Isolated, - &extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5f87bbbb5..b2dff78ab 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1573,8 +1573,9 @@ fn sync_extra_build_dependencies() -> Result<()> { child = { path = "child" } "#})?; + context.venv().arg("--clear").assert().success(); // Running `uv sync` should fail due to missing build-dependencies - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1607,7 +1608,8 @@ fn sync_extra_build_dependencies() -> Result<()> { child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1620,7 +1622,8 @@ fn sync_extra_build_dependencies() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) "); - // Adding `extra-build-dependencies` with the wrong name should not + // Adding `extra-build-dependencies` with the wrong name should fail the build + // (the cache is invalidated when extra build dependencies change) pyproject_toml.write_str(indoc! {r#" [project] name = "parent" @@ -1635,7 +1638,8 @@ fn sync_extra_build_dependencies() -> Result<()> { wrong_name = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1703,7 +1707,8 @@ fn sync_extra_build_dependencies() -> Result<()> { "#})?; // Confirm that `bad_child` fails if anyio is provided - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1738,7 +1743,8 @@ fn sync_extra_build_dependencies() -> Result<()> { child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1747,10 +1753,9 @@ fn sync_extra_build_dependencies() -> Result<()> { warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] Installed [N] packages in [TIME] + bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child) - ~ child==0.1.0 (from file://[TEMP_DIR]/child) + + child==0.1.0 (from file://[TEMP_DIR]/child) "); Ok(()) @@ -1815,7 +1820,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { })?; // Running `uv sync` should succeed, as `anyio` is provided as a source - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1828,6 +1833,9 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) "); + // TODO(zanieb): We want to test with `--no-sources` too but unfortunately that's not easy + // because it'll disable the `child` path source too! + Ok(()) }