diff --git a/crates/uv-distribution-filename/src/lib.rs b/crates/uv-distribution-filename/src/lib.rs index 99b916478..5edd1454a 100644 --- a/crates/uv-distribution-filename/src/lib.rs +++ b/crates/uv-distribution-filename/src/lib.rs @@ -6,7 +6,7 @@ use uv_pep440::Version; pub use build_tag::{BuildTag, BuildTagError}; pub use egg::{EggInfoFilename, EggInfoFilenameError}; pub use extension::{DistExtension, ExtensionError, SourceDistExtension}; -pub use source_dist::SourceDistFilename; +pub use source_dist::{SourceDistFilename, SourceDistFilenameError}; pub use wheel::{WheelFilename, WheelFilenameError}; mod build_tag; diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 3c4cf8121..42c67bd9f 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::fmt::Write as _; use std::io::Write as _; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::{fmt, io}; use anyhow::{Context, Result}; @@ -25,11 +26,14 @@ use uv_configuration::{ TrustedHost, }; use uv_dispatch::{BuildDispatch, SharedState}; -use uv_distribution_filename::SourceDistExtension; +use uv_distribution_filename::{ + DistFilename, SourceDistExtension, SourceDistFilename, WheelFilename, +}; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, SourceDist}; use uv_fs::{relative_to, Simplified}; use uv_install_wheel::linker::LinkMode; use uv_normalize::PackageName; +use uv_pep440::Version; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, @@ -76,6 +80,12 @@ enum Error { distribution ending in one of: {1}." )] InvalidSourceDistExt(String, uv_distribution_filename::ExtensionError), + #[error("The built source distribution has an invalid filename")] + 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 version {0}, but the wheel declares version {1}")] + VersionMismatch(Version, Version), } /// Build source distributions and wheels. @@ -649,7 +659,7 @@ async fn build_package( build_results.push(sdist_build.clone()); // Extract the source distribution into a temporary directory. - let path = output_dir.join(sdist_build.filename()); + let path = output_dir.join(sdist_build.filename().to_string()); let reader = fs_err::tokio::File::open(&path).await?; let ext = SourceDistExtension::from_path(path.as_path()) .map_err(|err| Error::InvalidSourceDistExt(path.user_display().to_string(), err))?; @@ -676,6 +686,7 @@ async fn build_package( subdirectory, version_id, build_output, + Some(sdist_build.filename().version()), ) .await?; build_results.push(wheel_build); @@ -712,6 +723,7 @@ async fn build_package( subdirectory, version_id, build_output, + None, ) .await?; build_results.push(wheel_build); @@ -732,7 +744,6 @@ async fn build_package( build_output, ) .await?; - build_results.push(sdist_build); let wheel_build = build_wheel( source.path(), @@ -747,8 +758,10 @@ async fn build_package( subdirectory, version_id, build_output, + Some(sdist_build.filename().version()), ) .await?; + build_results.push(sdist_build); build_results.push(wheel_build); } BuildPlan::WheelFromSdist => { @@ -760,6 +773,14 @@ async fn build_package( let temp_dir = tempfile::tempdir_in(&output_dir)?; uv_extract::stream::archive(reader, ext, temp_dir.path()).await?; + // If the source distribution has a version in its filename, check the version. + let version = source + .path() + .file_name() + .and_then(|filename| filename.to_str()) + .and_then(|filename| SourceDistFilename::parsed_normalized_filename(filename).ok()) + .map(|filename| filename.version); + // Extract the top-level directory from the archive. let extracted = match uv_extract::strip_component(temp_dir.path()) { Ok(top_level) => top_level, @@ -780,6 +801,7 @@ async fn build_package( subdirectory, version_id, build_output, + version.as_ref(), ) .await?; build_results.push(wheel_build); @@ -837,7 +859,7 @@ async fn build_sdist( .await??; BuildMessage::List { - filename: filename.to_string(), + filename: DistFilename::SourceDistFilename(filename), source_tree: source_tree.to_path_buf(), file_list, } @@ -866,7 +888,10 @@ async fn build_sdist( .to_string(); BuildMessage::Build { - filename, + filename: DistFilename::SourceDistFilename( + SourceDistFilename::parsed_normalized_filename(&filename) + .map_err(Error::InvalidBuiltSourceDistFilename)?, + ), output_dir: output_dir.to_path_buf(), } } @@ -896,7 +921,10 @@ async fn build_sdist( .map_err(Error::BuildDispatch)?; let filename = builder.build(output_dir).await?; BuildMessage::Build { - filename, + filename: DistFilename::SourceDistFilename( + SourceDistFilename::parsed_normalized_filename(&filename) + .map_err(Error::InvalidBuiltSourceDistFilename)?, + ), output_dir: output_dir.to_path_buf(), } } @@ -920,16 +948,18 @@ async fn build_wheel( subdirectory: Option<&Path>, version_id: Option<&str>, build_output: BuildOutput, + // Used for checking version consistency + version: Option<&Version>, ) -> Result { let build_message = match action { BuildAction::List => { let source_tree_ = source_tree.to_path_buf(); - let (name, file_list) = tokio::task::spawn_blocking(move || { + let (filename, file_list) = tokio::task::spawn_blocking(move || { uv_build_backend::list_wheel(&source_tree_, uv_version::version()) }) .await??; BuildMessage::List { - filename: name.to_string(), + filename: DistFilename::WheelFilename(filename), source_tree: source_tree.to_path_buf(), file_list, } @@ -955,11 +985,10 @@ async fn build_wheel( uv_version::version(), ) }) - .await?? - .to_string(); + .await??; BuildMessage::Build { - filename, + filename: DistFilename::WheelFilename(filename), output_dir: output_dir.to_path_buf(), } } @@ -989,11 +1018,19 @@ async fn build_wheel( .map_err(Error::BuildDispatch)?; let filename = builder.build(output_dir).await?; BuildMessage::Build { - filename, + filename: DistFilename::WheelFilename( + WheelFilename::from_str(&filename).map_err(Error::InvalidBuiltWheelFilename)?, + ), output_dir: output_dir.to_path_buf(), } } }; + if let Some(expected) = version { + let actual = build_message.filename().version(); + if expected != actual { + return Err(Error::VersionMismatch(expected.clone(), actual.clone())); + } + } Ok(build_message) } @@ -1086,19 +1123,19 @@ impl<'a> Source<'a> { /// We run all builds in parallel, so we wait until all builds are done to show the success messages /// in order. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] enum BuildMessage { /// A built wheel or source distribution. Build { /// The name of the built distribution. - filename: String, + filename: DistFilename, /// The location of the built distribution. output_dir: PathBuf, }, /// Show the list of files that would be included in a distribution. List { /// The name of the build distribution. - filename: String, + filename: DistFilename, // All source files are relative to the source tree. source_tree: PathBuf, // Included file and source file, if not generated. @@ -1108,7 +1145,7 @@ enum BuildMessage { impl BuildMessage { /// The filename of the wheel or source distribution. - fn filename(&self) -> &str { + fn filename(&self) -> &DistFilename { match self { BuildMessage::Build { filename: name, .. } => name, BuildMessage::List { filename: name, .. } => name, @@ -1124,7 +1161,11 @@ impl BuildMessage { writeln!( printer.stderr(), "Successfully built {}", - output_dir.join(filename).user_display().bold().cyan() + output_dir + .join(filename.to_string()) + .user_display() + .bold() + .cyan() )?; } BuildMessage::List { diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index de0dd61d8..3765b14d7 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1,5 +1,6 @@ use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use fs_err::File; use indoc::indoc; @@ -2334,3 +2335,37 @@ fn list_files_errors() -> Result<()> { "###); Ok(()) } + +#[test] +fn version_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + let anyio_local = current_dir()?.join("../../scripts/packages/anyio_local"); + context + .build() + .arg("--sdist") + .arg("--out-dir") + .arg(context.temp_dir.path()) + .arg(anyio_local) + .assert() + .success(); + let wrong_source_dist = context.temp_dir.child("anyio-1.2.3.tar.gz"); + fs_err::rename( + context.temp_dir.child("anyio-4.3.0+foo.tar.gz"), + &wrong_source_dist, + )?; + uv_snapshot!(context.filters(), context.build() + .arg(wrong_source_dist.path()) + .arg("--wheel") + .arg("--out-dir") + .arg(context.temp_dir.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel from source distribution... + × Failed to build `[TEMP_DIR]/anyio-1.2.3.tar.gz` + ╰─▶ The source distribution declares version 1.2.3, but the wheel declares version 4.3.0+foo + "###); + Ok(()) +}