Reject match-runtime = true for dynamic packages (#15292)

## Summary

If `match-runtime = true`, but we can't resolve a package's metadata
statically, then we can't _know_ what the runtime version of the package
will be -- because we can't resolve without building it. This PR makes
that footgun clearer by raising an error.

Closes https://github.com/astral-sh/uv/issues/15264.
This commit is contained in:
Charlie Marsh 2025-08-15 10:18:11 +01:00 committed by GitHub
parent 7eb076aaef
commit 627c062cab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 6 deletions

View file

@ -91,6 +91,10 @@ pub enum Error {
NoSourceDistBuilds,
#[error("Cyclic build dependency detected for `{0}`")]
CyclicBuildDependency(PackageName),
#[error(
"Extra build requirement `{0}` was declared with `match-runtime = true`, but `{1}` does not declare static metadata, making runtime-matching impossible"
)]
UnmatchedRuntime(PackageName, PackageName),
}
impl IsBuildBackendError for Error {
@ -106,7 +110,8 @@ impl IsBuildBackendError for Error {
| Self::Virtualenv(_)
| Self::NoSourceDistBuild(_)
| Self::NoSourceDistBuilds
| Self::CyclicBuildDependency(_) => false,
| Self::CyclicBuildDependency(_)
| Self::UnmatchedRuntime(_, _) => false,
Self::CommandFailed(_, _)
| Self::BuildBackend(_)
| Self::MissingHeader(_)

View file

@ -34,7 +34,8 @@ use uv_configuration::Preview;
use uv_configuration::{BuildKind, BuildOutput, SourceStrategy};
use uv_distribution::BuildRequires;
use uv_distribution_types::{
ConfigSettings, ExtraBuildRequires, IndexLocations, Requirement, Resolution,
ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
Resolution,
};
use uv_fs::LockedFile;
use uv_fs::{PythonExt, Simplified};
@ -326,13 +327,28 @@ impl SourceBuild {
.or(fallback_package_version)
.cloned();
let extra_build_dependencies: Vec<Requirement> = package_name
let extra_build_dependencies = package_name
.as_ref()
.and_then(|name| extra_build_requires.get(name).cloned())
.unwrap_or_default()
.into_iter()
.map(Requirement::from)
.collect();
.map(|requirement| {
match requirement {
ExtraBuildRequirement {
requirement,
match_runtime: true,
} if requirement.source.is_empty() => {
Err(Error::UnmatchedRuntime(
requirement.name.clone(),
// SAFETY: if `package_name` is `None`, the iterator is empty.
package_name.clone().unwrap(),
))
}
requirement => Ok(requirement),
}
})
.map_ok(Requirement::from)
.collect::<Result<Vec<_>, _>>()?;
// Create a virtual environment, or install into the shared environment if requested.
let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) {