Skip to content

Commit 8ef3b2e

Browse files
Enable extra build dependencies to 'match runtime' versions (#15036)
## Summary This is an alternative to #14944 that functions a little differently. Rather than adding separate strategies, you can instead say: ```toml [tool.uv.extra-build-dependencies] child = [{ requirement = "anyio", match-runtime = true }] ``` Which will then enforce that `anyio` uses the same version as in the lockfile.
1 parent b2c382f commit 8ef3b2e

File tree

11 files changed

+566
-81
lines changed

11 files changed

+566
-81
lines changed

crates/uv-build-frontend/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ impl SourceBuild {
328328
.and_then(|name| extra_build_requires.get(name).cloned())
329329
.unwrap_or_default()
330330
.into_iter()
331+
.map(Requirement::from)
331332
.collect();
332333

333334
// Create a virtual environment, or install into the shared environment if requested.
Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
use std::collections::BTreeMap;
22

3+
use uv_cache_key::{CacheKey, CacheKeyHasher};
34
use uv_normalize::PackageName;
45

5-
use crate::Requirement;
6+
use crate::{Name, Requirement, RequirementSource, Resolution};
7+
8+
#[derive(Debug, thiserror::Error)]
9+
pub enum ExtraBuildRequiresError {
10+
#[error(
11+
"`{0}` was declared as an extra build dependency with `match-runtime = true`, but was not found in the resolution"
12+
)]
13+
NotFound(PackageName),
14+
}
615

716
/// Lowered extra build dependencies with source resolution applied.
817
#[derive(Debug, Clone, Default)]
9-
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<Requirement>>);
18+
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<ExtraBuildRequirement>>);
1019

1120
impl std::ops::Deref for ExtraBuildRequires {
12-
type Target = BTreeMap<PackageName, Vec<Requirement>>;
21+
type Target = BTreeMap<PackageName, Vec<ExtraBuildRequirement>>;
1322

1423
fn deref(&self) -> &Self::Target {
1524
&self.0
@@ -23,16 +32,78 @@ impl std::ops::DerefMut for ExtraBuildRequires {
2332
}
2433

2534
impl IntoIterator for ExtraBuildRequires {
26-
type Item = (PackageName, Vec<Requirement>);
27-
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<Requirement>>;
35+
type Item = (PackageName, Vec<ExtraBuildRequirement>);
36+
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildRequirement>>;
2837

2938
fn into_iter(self) -> Self::IntoIter {
3039
self.0.into_iter()
3140
}
3241
}
3342

34-
impl FromIterator<(PackageName, Vec<Requirement>)> for ExtraBuildRequires {
35-
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<Requirement>)>>(iter: T) -> Self {
43+
impl FromIterator<(PackageName, Vec<ExtraBuildRequirement>)> for ExtraBuildRequires {
44+
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildRequirement>)>>(
45+
iter: T,
46+
) -> Self {
3647
Self(iter.into_iter().collect())
3748
}
3849
}
50+
51+
#[derive(Debug, Clone, PartialEq, Eq)]
52+
pub struct ExtraBuildRequirement {
53+
/// The underlying [`Requirement`] for the build requirement.
54+
pub requirement: Requirement,
55+
/// Whether this build requirement should match the runtime environment.
56+
pub match_runtime: bool,
57+
}
58+
59+
impl From<ExtraBuildRequirement> for Requirement {
60+
fn from(value: ExtraBuildRequirement) -> Self {
61+
value.requirement
62+
}
63+
}
64+
65+
impl CacheKey for ExtraBuildRequirement {
66+
fn cache_key(&self, state: &mut CacheKeyHasher) {
67+
self.requirement.cache_key(state);
68+
self.match_runtime.cache_key(state);
69+
}
70+
}
71+
72+
impl ExtraBuildRequires {
73+
/// Apply runtime constraints from a resolution to the extra build requirements.
74+
pub fn match_runtime(
75+
self,
76+
resolution: &Resolution,
77+
) -> Result<ExtraBuildRequires, ExtraBuildRequiresError> {
78+
self.into_iter()
79+
.map(|(name, requirements)| {
80+
let requirements = requirements
81+
.into_iter()
82+
.map(|requirement| match requirement {
83+
ExtraBuildRequirement {
84+
requirement,
85+
match_runtime: true,
86+
} => {
87+
let dist = resolution
88+
.distributions()
89+
.find(|dist| dist.name() == &requirement.name)
90+
.ok_or_else(|| {
91+
ExtraBuildRequiresError::NotFound(requirement.name.clone())
92+
})?;
93+
let requirement = Requirement {
94+
source: RequirementSource::from(dist),
95+
..requirement
96+
};
97+
Ok::<_, ExtraBuildRequiresError>(ExtraBuildRequirement {
98+
requirement,
99+
match_runtime: true,
100+
})
101+
}
102+
requirement => Ok(requirement),
103+
})
104+
.collect::<Result<Vec<_>, _>>()?;
105+
Ok::<_, ExtraBuildRequiresError>((name, requirements))
106+
})
107+
.collect::<Result<ExtraBuildRequires, _>>()
108+
}
109+
}

crates/uv-distribution/src/index/built_wheel_index.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use uv_cache_info::CacheInfo;
55
use uv_cache_key::cache_digest;
66
use uv_configuration::{ConfigSettings, PackageConfigSettings};
77
use uv_distribution_types::{
8-
DirectUrlSourceDist, DirectorySourceDist, ExtraBuildRequires, GitSourceDist, Hashed,
9-
PathSourceDist, Requirement,
8+
DirectUrlSourceDist, DirectorySourceDist, ExtraBuildRequirement, ExtraBuildRequires,
9+
GitSourceDist, Hashed, PathSourceDist,
1010
};
1111
use uv_normalize::PackageName;
1212
use uv_platform_tags::Tags;
@@ -267,8 +267,8 @@ impl<'a> BuiltWheelIndex<'a> {
267267
}
268268
}
269269

270-
/// Determine the extra build dependencies for the given package name.
271-
fn extra_build_requires_for(&self, name: &PackageName) -> &[Requirement] {
270+
/// Determine the extra build requirements for the given package name.
271+
fn extra_build_requires_for(&self, name: &PackageName) -> &[ExtraBuildRequirement] {
272272
self.extra_build_requires
273273
.get(name)
274274
.map(Vec::as_slice)

crates/uv-distribution/src/metadata/build_requires.rs

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use std::collections::BTreeMap;
22
use std::path::Path;
33

44
use uv_configuration::SourceStrategy;
5-
use uv_distribution_types::{ExtraBuildRequires, IndexLocations, Requirement};
5+
use uv_distribution_types::{
6+
ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
7+
};
68
use uv_normalize::PackageName;
7-
use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources};
9+
use uv_workspace::pyproject::{ExtraBuildDependencies, ExtraBuildDependency, ToolUvSources};
810
use uv_workspace::{
911
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
1012
};
@@ -248,50 +250,48 @@ impl LoweredExtraBuildDependencies {
248250
// Lower each package's extra build dependencies
249251
let mut build_requires = ExtraBuildRequires::default();
250252
for (package_name, requirements) in extra_build_dependencies {
251-
let lowered: Vec<Requirement> = requirements
253+
let lowered: Vec<ExtraBuildRequirement> = requirements
252254
.into_iter()
253-
.flat_map(|requirement| {
254-
let requirement_name = requirement.name.clone();
255-
let extra = requirement.marker.top_level_extra_name();
256-
let group = None;
257-
LoweredRequirement::from_requirement(
258-
requirement,
259-
None,
260-
workspace.install_path(),
261-
project_sources,
262-
project_indexes,
263-
extra.as_deref(),
264-
group,
265-
index_locations,
266-
workspace,
267-
None,
268-
)
269-
.map(
270-
move |requirement| match requirement {
271-
Ok(requirement) => Ok(requirement.into_inner()),
272-
Err(err) => Err(MetadataError::LoweringError(
273-
requirement_name.clone(),
274-
Box::new(err),
275-
)),
276-
},
277-
)
278-
})
255+
.flat_map(
256+
|ExtraBuildDependency {
257+
requirement,
258+
match_runtime,
259+
}| {
260+
let requirement_name = requirement.name.clone();
261+
let extra = requirement.marker.top_level_extra_name();
262+
let group = None;
263+
LoweredRequirement::from_requirement(
264+
requirement,
265+
None,
266+
workspace.install_path(),
267+
project_sources,
268+
project_indexes,
269+
extra.as_deref(),
270+
group,
271+
index_locations,
272+
workspace,
273+
None,
274+
)
275+
.map(move |requirement| {
276+
match requirement {
277+
Ok(requirement) => Ok(ExtraBuildRequirement {
278+
requirement: requirement.into_inner(),
279+
match_runtime,
280+
}),
281+
Err(err) => Err(MetadataError::LoweringError(
282+
requirement_name.clone(),
283+
Box::new(err),
284+
)),
285+
}
286+
})
287+
},
288+
)
279289
.collect::<Result<Vec<_>, _>>()?;
280290
build_requires.insert(package_name, lowered);
281291
}
282292
Ok(Self(build_requires))
283293
}
284-
SourceStrategy::Disabled => Ok(Self(
285-
extra_build_dependencies
286-
.into_iter()
287-
.map(|(name, requirements)| {
288-
(
289-
name,
290-
requirements.into_iter().map(Requirement::from).collect(),
291-
)
292-
})
293-
.collect(),
294-
)),
294+
SourceStrategy::Disabled => Ok(Self::from_non_lowered(extra_build_dependencies)),
295295
}
296296
}
297297

@@ -308,7 +308,20 @@ impl LoweredExtraBuildDependencies {
308308
.map(|(name, requirements)| {
309309
(
310310
name,
311-
requirements.into_iter().map(Requirement::from).collect(),
311+
requirements
312+
.into_iter()
313+
.map(
314+
|ExtraBuildDependency {
315+
requirement,
316+
match_runtime,
317+
}| {
318+
ExtraBuildRequirement {
319+
requirement: requirement.into(),
320+
match_runtime,
321+
}
322+
},
323+
)
324+
.collect::<Vec<_>>(),
312325
)
313326
})
314327
.collect(),

crates/uv-distribution/src/source/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ use uv_client::{
3232
use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy};
3333
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
3434
use uv_distribution_types::{
35-
BuildableSource, DirectorySourceUrl, GitSourceUrl, HashPolicy, Hashed, IndexUrl, PathSourceUrl,
36-
Requirement, SourceDist, SourceUrl,
35+
BuildableSource, DirectorySourceUrl, ExtraBuildRequirement, GitSourceUrl, HashPolicy, Hashed,
36+
IndexUrl, PathSourceUrl, SourceDist, SourceUrl,
3737
};
3838
use uv_extract::hash::Hasher;
3939
use uv_fs::{rename_with_retry, write_atomic};
@@ -405,7 +405,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
405405
}
406406

407407
/// Determine the extra build dependencies for the given package name.
408-
fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[Requirement] {
408+
fn extra_build_dependencies_for(&self, name: Option<&PackageName>) -> &[ExtraBuildRequirement] {
409409
name.and_then(|name| {
410410
self.build_context
411411
.extra_build_requires()

crates/uv-scripts/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use uv_pypi_types::VerbatimParsedUrl;
1616
use uv_redacted::DisplaySafeUrl;
1717
use uv_settings::{GlobalOptions, ResolverInstallerOptions};
1818
use uv_warnings::warn_user;
19-
use uv_workspace::pyproject::Sources;
19+
use uv_workspace::pyproject::{ExtraBuildDependency, Sources};
2020

2121
static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"));
2222

@@ -428,8 +428,7 @@ pub struct ToolUv {
428428
pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
429429
pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
430430
pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
431-
pub extra_build_dependencies:
432-
Option<BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>>,
431+
pub extra_build_dependencies: Option<BTreeMap<PackageName, Vec<ExtraBuildDependency>>>,
433432
pub sources: Option<BTreeMap<PackageName, Sources>>,
434433
}
435434

0 commit comments

Comments
 (0)