diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 6780b5ca69c..0ee05ee803d 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -96,6 +96,8 @@ pub(crate) enum Error { InvalidBuiltSourceDistFilename(#[source] uv_distribution_filename::SourceDistFilenameError), #[error("The built wheel has an invalid filename")] InvalidBuiltWheelFilename(#[source] uv_distribution_filename::WheelFilenameError), + #[error("The source distribution declares name {0}, but the wheel declares name {1}")] + NameMismatch(PackageName, PackageName), #[error("The source distribution declares version {0}, but the wheel declares version {1}")] VersionMismatch(Version, Version), } @@ -793,7 +795,7 @@ async fn build_package( subdirectory, version_id, build_output, - Some(sdist_build.normalized_filename().version()), + Some(sdist_build.normalized_filename()), ) .await?; build_results.push(wheel_build); @@ -865,7 +867,7 @@ async fn build_package( subdirectory, version_id, build_output, - Some(sdist_build.normalized_filename().version()), + Some(sdist_build.normalized_filename()), ) .await?; build_results.push(sdist_build); @@ -881,13 +883,13 @@ async fn build_package( uv_extract::stream::archive(source.path().display(), reader, ext, temp_dir.path()) .await?; - // If the source distribution has a version in its filename, check the version. - let version = source + // If the source distribution has a normalized filename, check its identity. + let source_dist = source .path() .file_name() .and_then(|filename| filename.to_str()) .and_then(|filename| SourceDistFilename::parsed_normalized_filename(filename).ok()) - .map(|filename| filename.version); + .map(DistFilename::SourceDistFilename); // Extract the top-level directory from the archive. let extracted = match uv_extract::strip_component(temp_dir.path()) { @@ -909,7 +911,7 @@ async fn build_package( subdirectory, version_id, build_output, - version.as_ref(), + source_dist.as_ref(), ) .await?; build_results.push(wheel_build); @@ -1068,8 +1070,8 @@ async fn build_wheel( subdirectory: Option<&Path>, version_id: Option<&str>, build_output: BuildOutput, - // Used for checking version consistency - version: Option<&Version>, + // Used for checking source distribution and wheel consistency + source_dist: Option<&DistFilename>, ) -> Result { let build_message = match action { BuildAction::List => { @@ -1155,10 +1157,19 @@ async fn build_wheel( } } }; - if let Some(expected) = version { - let actual = build_message.normalized_filename().version(); - if expected != actual { - return Err(Error::VersionMismatch(expected.clone(), actual.clone())); + if let Some(expected) = source_dist { + let actual = build_message.normalized_filename(); + if expected.name() != actual.name() { + return Err(Error::NameMismatch( + expected.name().clone(), + actual.name().clone(), + )); + } + if expected.version() != actual.version() { + return Err(Error::VersionMismatch( + expected.version().clone(), + actual.version().clone(), + )); } } Ok(build_message) diff --git a/crates/uv/tests/build/build.rs b/crates/uv/tests/build/build.rs index 7ea3c115ca9..e591257cbcd 100644 --- a/crates/uv/tests/build/build.rs +++ b/crates/uv/tests/build/build.rs @@ -2666,6 +2666,46 @@ fn build_version_mismatch() -> Result<()> { Ok(()) } +/// A backend must not return an sdist and wheel for different projects. +#[test] +fn build_name_mismatch() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let project = context.temp_dir.child("project"); + project.child("pyproject.toml").write_str(indoc! {r#" + [build-system] + requires = [] + build-backend = "backend" + backend-path = ["."] + "#})?; + project.child("backend.py").write_str(indoc! {r#" + from pathlib import Path + + def build_sdist(sdist_directory, config_settings=None): + filename = "alpha-1.0.0.tar.gz" + Path(sdist_directory, filename).touch() + return filename + + def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + filename = "beta-1.0.0-py3-none-any.whl" + Path(wheel_directory, filename).touch() + return filename + "#})?; + + uv_snapshot!(context.filters(), context.build().arg("--sdist").arg("--wheel").current_dir(&project), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + Building wheel... + error: Failed to build `[TEMP_DIR]/project` + Caused by: The source distribution declares name alpha, but the wheel declares name beta + "); + + Ok(()) +} + #[cfg(unix)] // Symlinks aren't universally available on windows. #[test] fn build_with_symlink() -> Result<()> { @@ -2763,16 +2803,16 @@ fn build_workspace_virtual_root() -> Result<()> { "#})?; uv_snapshot!(context.filters(), context.build().arg("--no-build-logs"), @" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- Building source distribution... warning: `[TEMP_DIR]/` appears to be a workspace root without a Python project; consider using `uv sync` to install the workspace, or add a `[build-system]` table to `pyproject.toml` Building wheel from source distribution... - Successfully built dist/cache-0.0.0.tar.gz - Successfully built dist/UNKNOWN-0.0.0-py3-none-any.whl + error: Failed to build `[TEMP_DIR]/` + Caused by: The source distribution declares name cache, but the wheel declares name unknown "); Ok(()) } @@ -2792,16 +2832,16 @@ fn build_pyproject_toml_not_a_project() -> Result<()> { "})?; uv_snapshot!(context.filters(), context.build().arg("--no-build-logs"), @" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- ----- stderr ----- Building source distribution... warning: `[TEMP_DIR]/` does not appear to be a Python project, as the `pyproject.toml` does not include a `[build-system]` table, and neither `setup.py` nor `setup.cfg` are present in the directory Building wheel from source distribution... - Successfully built dist/cache-0.0.0.tar.gz - Successfully built dist/UNKNOWN-0.0.0-py3-none-any.whl + error: Failed to build `[TEMP_DIR]/` + Caused by: The source distribution declares name cache, but the wheel declares name unknown "); Ok(()) }