Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/uv-build-frontend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
// Lock the source tree, if necessary.
Expand Down
42 changes: 36 additions & 6 deletions crates/uv/src/commands/build_frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
}
}
Expand Down
108 changes: 108 additions & 0 deletions crates/uv/tests/build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
Loading