diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index b0a9f4a2d..b9c24d17b 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -97,6 +97,10 @@ pub enum Error { UnmatchedRuntime(PackageName, PackageName), #[error("Build requires for `{0}` missing for metadata declared in `pyproject.toml`")] MissingBuildRequirementForMetadata(PackageName), + #[error( + "Build requirement `{0}` was declared with `match-runtime = true`, but there is no runtime environment to match against" + )] + UnmatchedRuntimeMetadata(PackageName), } impl IsBuildBackendError for Error { @@ -114,6 +118,7 @@ impl IsBuildBackendError for Error { | Self::NoSourceDistBuilds | Self::CyclicBuildDependency(_) | Self::UnmatchedRuntime(_, _) + | Self::UnmatchedRuntimeMetadata(_) | Self::MissingBuildRequirementForMetadata(_) => false, Self::CommandFailed(_, _) | Self::BuildBackend(_) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 2be1096f5..6e4091259 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -404,6 +404,7 @@ impl SourceBuild { }; let resolved_requirements = Self::get_resolved_requirements( + build_kind, build_context, source_build_context, &DEFAULT_BACKEND, @@ -528,10 +529,25 @@ impl SourceBuild { } fn apply_build_metadata<'a>( + build_kind: BuildKind, metadata: &BTreeMap, build_requires: impl Iterator, - top_level_resolution: &Resolution, + build_context: &impl BuildContext, ) -> Result, Box> { + let top_level_resolution = build_context.top_level_resolution(); + if top_level_resolution.is_none() { + for (package, metadata) in metadata { + if let Some(true) = metadata.match_runtime { + if !matches!(build_kind, BuildKind::Sdist) { + return Err(Box::new(Error::UnmatchedRuntimeMetadata(package.clone()))); + } + } + } + // Nothing needs runtime matching, carry on + return Ok(Vec::new()); + } + // SAFETY: check above guarantees that top_level_resolution is Some + let top_level_resolution = top_level_resolution.unwrap(); let dists = top_level_resolution.distributions(); let dists_by_name = dists .filter_map(|dist| metadata.get(dist.name()).map(|_| (dist.name(), dist))) @@ -578,6 +594,7 @@ impl SourceBuild { } async fn get_resolved_requirements( + build_kind: BuildKind, build_context: &impl BuildContext, source_build_context: SourceBuildContext, default_backend: &Pep517Backend, @@ -622,16 +639,15 @@ impl SourceBuild { }; if let Some(build_dep_metadata) = build_dep_metadata { - if let Some(resolution) = build_context.top_level_resolution() { - let mut reqs_with_metadata = Self::apply_build_metadata( - &build_dep_metadata.0, - requirements.iter(), - resolution, - ) - .map_err(|err| *err)?; - reqs_with_metadata.extend(requirements.into_owned().into_iter()); - requirements = Cow::Owned(reqs_with_metadata); - } + let mut reqs_with_metadata = Self::apply_build_metadata( + build_kind, + &build_dep_metadata.0, + requirements.iter(), + build_context, + ) + .map_err(|err| *err)?; + reqs_with_metadata.extend(requirements.into_owned().into_iter()); + requirements = Cow::Owned(reqs_with_metadata); } build_context diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 138a218bb..bc644299b 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -2084,3 +2084,56 @@ fn venv_included_in_sdist() -> Result<()> { Ok(()) } + +/// Runtime matching causes building a wheel to fail +#[test] +fn runtime_matching() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + project + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + project.child("README").touch()?; + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [tool.uv.build-dependencies-metadata.hatchling] + match-runtime = true + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#})?; + + uv_snapshot!(context.filters(), context.build().current_dir(&project), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + Building wheel from source distribution... + × Failed to build `[TEMP_DIR]/project` + ╰─▶ Build requirement `hatchling` was declared with `match-runtime = true`, but there is no runtime environment to match against + "); + + // But building a sdist is fine + uv_snapshot!(context.filters(), context.build().arg("--sdist").current_dir(&project), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + Successfully built dist/project-0.1.0.tar.gz + "); + + Ok(()) +} diff --git a/docs/reference/settings.md b/docs/reference/settings.md index c91137b9b..d6c8eff23 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -30,6 +30,26 @@ build-constraint-dependencies = ["setuptools==60.0.0"] --- +### [`build-dependencies-metadata`](#build-dependencies-metadata) {: #build-dependencies-metadata } + +Metadata for build dependencies. + +This allows specifying metadata for build dependencies, such as runtime matching. + +**Default value**: `None` + +**Type**: `dict` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +[build-dependencies-metadata.package1] +match-runtime = true +``` + +--- + ### [`conflicts`](#conflicts) {: #conflicts } Declare collections of extras or dependency groups that are conflicting diff --git a/uv.schema.json b/uv.schema.json index 6deddd4be..c519a1fe4 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -46,6 +46,17 @@ "type": "string" } }, + "build-dependencies-metadata": { + "description": "Metadata for build dependencies.\n\nThis allows specifying metadata for build dependencies, such as runtime matching.", + "anyOf": [ + { + "$ref": "#/definitions/BuildDependenciesMetadata" + }, + { + "type": "null" + } + ] + }, "cache-dir": { "description": "Path to the cache directory.\n\nDefaults to `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux and macOS, and\n`%LOCALAPPDATA%\\uv\\cache` on Windows.", "type": [ @@ -759,6 +770,23 @@ } } }, + "BuildDependenciesMetadata": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/BuildDependencyMetadata" + } + }, + "BuildDependencyMetadata": { + "type": "object", + "properties": { + "match_runtime": { + "type": [ + "boolean", + "null" + ] + } + } + }, "CacheKey": { "anyOf": [ {