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()) {

View File

@ -480,7 +480,7 @@ impl BuildContext for BuildDispatch<'_> {
self.workspace_cache(),
config_settings,
self.build_isolation,
self.extra_build_requires(),
self.extra_build_requires,
&build_stack,
build_kind,
environment_variables,

View File

@ -13089,3 +13089,36 @@ fn sync_extra_build_variables() -> Result<()> {
Ok(())
}
#[test]
fn reject_unmatched_runtime() -> Result<()> {
let context = TestContext::new("3.12").with_exclude_newer("2025-01-01T00:00Z");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["source-distribution", "iniconfig"]
[tool.uv.extra-build-dependencies]
source-distribution = [{ requirement = "iniconfig", match-runtime = true }]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
× Failed to download and build `source-distribution==0.0.3`
Extra build requirement `iniconfig` was declared with `match-runtime = true`, but `source-distribution` does not declare static metadata, making runtime-matching impossible
help: `source-distribution` (v0.0.3) was included because `foo` (v0.1.0) depends on `source-distribution`
");
Ok(())
}