diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index 24bfa972a..580690600 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -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(_) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 20d7c7cd3..430de4f65 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -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 = 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::, _>>()?; // 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()) { diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 8711e62d8..2817b7054 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -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, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index c0e9dfb18..cd398a82d 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -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(()) +}