Enable extra build dependencies to 'match runtime' versions (#15036)

## Summary

This is an alternative to https://github.com/astral-sh/uv/pull/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.
This commit is contained in:
Charlie Marsh
2025-08-05 19:00:44 +01:00
committed by GitHub
parent b2c382f7c1
commit 8ef3b2eb8e
11 changed files with 566 additions and 81 deletions
@@ -1,15 +1,24 @@
use std::collections::BTreeMap;
use uv_cache_key::{CacheKey, CacheKeyHasher};
use uv_normalize::PackageName;
use crate::Requirement;
use crate::{Name, Requirement, RequirementSource, Resolution};
#[derive(Debug, thiserror::Error)]
pub enum ExtraBuildRequiresError {
#[error(
"`{0}` was declared as an extra build dependency with `match-runtime = true`, but was not found in the resolution"
)]
NotFound(PackageName),
}
/// Lowered extra build dependencies with source resolution applied.
#[derive(Debug, Clone, Default)]
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<Requirement>>);
pub struct ExtraBuildRequires(BTreeMap<PackageName, Vec<ExtraBuildRequirement>>);
impl std::ops::Deref for ExtraBuildRequires {
type Target = BTreeMap<PackageName, Vec<Requirement>>;
type Target = BTreeMap<PackageName, Vec<ExtraBuildRequirement>>;
fn deref(&self) -> &Self::Target {
&self.0
@@ -23,16 +32,78 @@ impl std::ops::DerefMut for ExtraBuildRequires {
}
impl IntoIterator for ExtraBuildRequires {
type Item = (PackageName, Vec<Requirement>);
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<Requirement>>;
type Item = (PackageName, Vec<ExtraBuildRequirement>);
type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildRequirement>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<(PackageName, Vec<Requirement>)> for ExtraBuildRequires {
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<Requirement>)>>(iter: T) -> Self {
impl FromIterator<(PackageName, Vec<ExtraBuildRequirement>)> for ExtraBuildRequires {
fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildRequirement>)>>(
iter: T,
) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtraBuildRequirement {
/// The underlying [`Requirement`] for the build requirement.
pub requirement: Requirement,
/// Whether this build requirement should match the runtime environment.
pub match_runtime: bool,
}
impl From<ExtraBuildRequirement> for Requirement {
fn from(value: ExtraBuildRequirement) -> Self {
value.requirement
}
}
impl CacheKey for ExtraBuildRequirement {
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.requirement.cache_key(state);
self.match_runtime.cache_key(state);
}
}
impl ExtraBuildRequires {
/// Apply runtime constraints from a resolution to the extra build requirements.
pub fn match_runtime(
self,
resolution: &Resolution,
) -> Result<ExtraBuildRequires, ExtraBuildRequiresError> {
self.into_iter()
.map(|(name, requirements)| {
let requirements = requirements
.into_iter()
.map(|requirement| match requirement {
ExtraBuildRequirement {
requirement,
match_runtime: true,
} => {
let dist = resolution
.distributions()
.find(|dist| dist.name() == &requirement.name)
.ok_or_else(|| {
ExtraBuildRequiresError::NotFound(requirement.name.clone())
})?;
let requirement = Requirement {
source: RequirementSource::from(dist),
..requirement
};
Ok::<_, ExtraBuildRequiresError>(ExtraBuildRequirement {
requirement,
match_runtime: true,
})
}
requirement => Ok(requirement),
})
.collect::<Result<Vec<_>, _>>()?;
Ok::<_, ExtraBuildRequiresError>((name, requirements))
})
.collect::<Result<ExtraBuildRequires, _>>()
}
}