mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
uv build: Catch version mismatch between sdist and wheel (#9633)
When building a wheel from a source distribution or both a source distribution and a wheel, the versions in their filenames must be the same. By inspecting the filenames, we also assert that the filenames from the build a valid (we don't enforce normalization though, just that uv can parse them). Note that we're not yet checking that also the `pyproject.toml` version, if declared, and METADATA version matches.
This commit is contained in:
parent
42a35b59cd
commit
566c178276
3 changed files with 94 additions and 18 deletions
|
@ -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;
|
||||
|
|
|
@ -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<BuildMessage, Error> {
|
||||
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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue