From bd8fb7e9b94976133faf36e9cd8feaed6264105a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 14 Jul 2026 15:31:47 -0500 Subject: [PATCH] Validate built wheel project identity --- crates/uv-build-frontend/src/lib.rs | 12 +++ crates/uv/src/commands/build_frontend.rs | 42 +++++++-- crates/uv/tests/build/build.rs | 108 +++++++++++++++++++++++ 3 files changed, 156 insertions(+), 6 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 4b1ba09e25d36..eeb7bd14a95fb 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -884,6 +884,18 @@ impl SourceBuild { Ok(filename) } + /// Return the statically declared project name, if available. + pub fn project_name(&self) -> Option<&PackageName> { + self.project.as_ref().map(|project| &project.name) + } + + /// Return the statically declared project version, if available. + pub fn project_version(&self) -> Option<&Version> { + self.project + .as_ref() + .and_then(|project| project.version.as_ref()) + } + /// Perform a PEP 517 build for a wheel or source distribution (sdist). async fn pep517_build(&self, output_dir: &Path) -> Result { // Lock the source tree, if necessary. diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 6780b5ca69c02..ad98f00385c72 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -27,7 +27,7 @@ use uv_distribution_types::{ PackageConfigSettings, Requirement, SourceDist, }; use uv_errors::{ErrorOptions, Hint, Hints, write_error_chain_with_options}; -use uv_fs::{Simplified, normalize_path, relative_to}; +use uv_fs::{Simplified, normalize_path, relative_to, rename_with_retry}; use uv_install_wheel::LinkMode; use uv_normalize::PackageName; use uv_pep440::Version; @@ -98,6 +98,10 @@ pub(crate) enum Error { InvalidBuiltWheelFilename(#[source] uv_distribution_filename::WheelFilenameError), #[error("The source distribution declares version {0}, but the wheel declares version {1}")] VersionMismatch(Version, Version), + #[error("The project declares name `{0}`, but the wheel declares name `{1}`, which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] + ProjectNameMismatch(PackageName, PackageName), + #[error("The project declares version {0}, but the wheel declares version {1}, which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] + ProjectVersionMismatch(Version, Version), } impl Hint for Error { @@ -1145,12 +1149,38 @@ async fn build_wheel( ) .await .map_err(|err| Error::BuildDispatch(err.into()))?; - let filename = builder.build(output_dir).await?; + // Keep the built wheel out of the output directory until its identity is validated. + let temp_dir = tempfile::tempdir_in(output_dir)?; + let raw_filename = builder.build(temp_dir.path()).await?; + let filename = + WheelFilename::from_str(&raw_filename).map_err(Error::InvalidBuiltWheelFilename)?; + if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) { + if let Some(expected) = builder.project_name() + && expected != &filename.name + { + return Err(Error::ProjectNameMismatch( + expected.clone(), + filename.name.clone(), + )); + } + if let Some(expected) = builder.project_version() + && expected != &filename.version + && expected != &filename.version.clone().without_local() + { + return Err(Error::ProjectVersionMismatch( + expected.clone(), + filename.version.clone(), + )); + } + } + rename_with_retry( + temp_dir.path().join(&raw_filename), + output_dir.join(&raw_filename), + ) + .await?; BuildMessage::Build { - normalized_filename: DistFilename::WheelFilename( - WheelFilename::from_str(&filename).map_err(Error::InvalidBuiltWheelFilename)?, - ), - raw_filename: filename, + normalized_filename: DistFilename::WheelFilename(filename), + raw_filename, output_dir: output_dir.to_path_buf(), } } diff --git a/crates/uv/tests/build/build.rs b/crates/uv/tests/build/build.rs index 7ea3c115ca9cc..6d4f15315720e 100644 --- a/crates/uv/tests/build/build.rs +++ b/crates/uv/tests/build/build.rs @@ -2666,6 +2666,114 @@ fn build_version_mismatch() -> Result<()> { Ok(()) } +#[test] +fn build_wheel_project_identity_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#" + [project] + name = "configured-name" + version = "1.0.0" + requires-python = ">=3.12" + + [build-system] + requires = [] + build-backend = "backend" + backend-path = ["."] + "#})?; + project.child("backend.py").write_str(indoc! {r#" + import os + import pathlib + import zipfile + + + def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + wheel_name = os.environ["UV_TEST_WHEEL_NAME"] + name, version, *_ = wheel_name.split("-") + dist_info = f"{name}-{version}.dist-info" + records = [ + (f"{name}/__init__.py", b""), + ( + f"{dist_info}/METADATA", + f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n".encode(), + ), + ( + f"{dist_info}/WHEEL", + b"Wheel-Version: 1.0\nGenerator: uv-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n", + ), + ] + + with zipfile.ZipFile(pathlib.Path(wheel_directory, wheel_name), "w") as wheel: + for path, contents in records: + wheel.writestr(path, contents) + record = "\n".join(f"{path},," for path, _ in records) + wheel.writestr(f"{dist_info}/RECORD", f"{record}\n{dist_info}/RECORD,,\n") + + return wheel_name + "#})?; + + let dist = project.child("dist"); + dist.create_dir_all()?; + let existing = dist.child("existing.txt"); + existing.write_binary(b"existing artifact")?; + + uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel").env("UV_TEST_WHEEL_NAME", "different_name-1.0.0-py3-none-any.whl"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel... + error: Failed to build `[TEMP_DIR]/project` + Caused by: The project declares name `configured-name`, but the wheel declares name `different-name`, which indicates a malformed wheel. If this is intentional, set `UV_SKIP_WHEEL_FILENAME_CHECK=1`. + "); + dist.child("different_name-1.0.0-py3-none-any.whl") + .assert(predicate::path::missing()); + assert_eq!(fs_err::read(existing.path())?, b"existing artifact"); + + uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel").env("UV_TEST_WHEEL_NAME", "configured_name-9.0.0-py3-none-any.whl"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel... + error: Failed to build `[TEMP_DIR]/project` + Caused by: The project declares version 1.0.0, but the wheel declares version 9.0.0, which indicates a malformed wheel. If this is intentional, set `UV_SKIP_WHEEL_FILENAME_CHECK=1`. + "); + dist.child("configured_name-9.0.0-py3-none-any.whl") + .assert(predicate::path::missing()); + assert_eq!(fs_err::read(existing.path())?, b"existing artifact"); + + uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel").env("UV_TEST_WHEEL_NAME", "different_name-9.0.0-py3-none-any.whl").env(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building wheel... + Successfully built project/dist/different_name-9.0.0-py3-none-any.whl + "); + project + .child("dist/different_name-9.0.0-py3-none-any.whl") + .assert(predicate::path::is_file()); + + uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel").env("UV_TEST_WHEEL_NAME", "configured_name-1.0.0+local-py3-none-any.whl"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building wheel... + Successfully built project/dist/configured_name-1.0.0+local-py3-none-any.whl + "); + project + .child("dist/configured_name-1.0.0+local-py3-none-any.whl") + .assert(predicate::path::is_file()); + + Ok(()) +} + #[cfg(unix)] // Symlinks aren't universally available on windows. #[test] fn build_with_symlink() -> Result<()> {