From f9eb1851aaffc967a6f4ab1ac9d30f5c377e9bd2 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 17 Dec 2024 11:58:00 +0100 Subject: [PATCH 1/2] Build backend: Fix pre-PEP 639 license files We were not copying the license file from a pre-PEP 639 declaration to the source distribution. Fixes #9947 --- crates/uv-build-backend/src/lib.rs | 72 +++++++++++++++++++++- crates/uv-build-backend/src/metadata.rs | 39 +++++++++++- crates/uv-build-backend/src/source_dist.rs | 2 +- crates/uv-build-backend/src/wheel.rs | 9 +-- crates/uv/tests/it/pip_compile.rs | 33 ++++------ 5 files changed, 125 insertions(+), 30 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index c1f08455ee53..0fc406e4e117 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -180,9 +180,10 @@ mod tests { use super::*; use flate2::bufread::GzDecoder; use fs_err::File; + use indoc::indoc; use insta::assert_snapshot; use itertools::Itertools; - use std::io::BufReader; + use std::io::{BufReader, Read}; use tempfile::TempDir; use uv_fs::{copy_dir_all, relative_to}; @@ -391,4 +392,73 @@ mod tests { fs_err::read(indirect_output_dir.path().join(wheel_filename)).unwrap() ); } + + /// Test that `license = { file = "LICENSE" }` is supported. + #[test] + fn license_file_pre_pep639() { + let src = TempDir::new().unwrap(); + fs_err::write( + src.path().join("pyproject.toml"), + indoc! {r#" + [project] + name = "pep-pep639-license" + version = "1.0.0" + license = { file = "license.txt" } + + [build-system] + requires = ["uv>=0.5.15,<0.6"] + build-backend = "uv" + "# + }, + ) + .unwrap(); + fs_err::create_dir_all(src.path().join("src").join("pep_pep639_license")).unwrap(); + File::create( + src.path() + .join("src") + .join("pep_pep639_license") + .join("__init__.py"), + ) + .unwrap(); + fs_err::write( + src.path().join("license.txt"), + "Copy carefully.\nSincerely, the authors", + ) + .unwrap(); + + // Build a wheel from a source distribution + let output_dir = TempDir::new().unwrap(); + build_source_dist(src.path(), output_dir.path(), "0.5.15").unwrap(); + let sdist_tree = TempDir::new().unwrap(); + let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); + let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); + let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); + source_dist.unpack(sdist_tree.path()).unwrap(); + build_wheel( + &sdist_tree.path().join("pep_pep639_license-1.0.0"), + output_dir.path(), + None, + "0.5.15", + ) + .unwrap(); + let wheel = output_dir + .path() + .join("pep_pep639_license-1.0.0-py3-none-any.whl"); + let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap(); + + let mut metadata = String::new(); + wheel + .by_name("pep_pep639_license-1.0.0.dist-info/METADATA") + .unwrap() + .read_to_string(&mut metadata) + .unwrap(); + + assert_snapshot!(metadata, @r###" + Metadata-Version: 2.3 + Name: pep-pep639-license + Version: 1.0.0 + License: Copy carefully. + Sincerely, the authors + "###); + } } diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index 76c8396a56c6..df43a5beb444 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -119,8 +119,31 @@ impl PyProjectToml { self.project.readme.as_ref() } - pub(crate) fn license_files(&self) -> Option<&[String]> { - self.project.license_files.as_deref() + /// The license files that need to be included in the source distribution. + pub(crate) fn license_files_source_dist(&self) -> impl Iterator { + let license_file = self + .project + .license + .as_ref() + .and_then(|license| license.file()) + .into_iter(); + let license_files = self + .project + .license_files + .iter() + .flatten() + .map(String::as_str); + license_files.chain(license_file) + } + + /// The license files that need to be included in the wheel. + pub(crate) fn license_files_wheel(&self) -> impl Iterator { + // The pre-PEP 639 `license = { file = "..." }` is included inline in `METADATA`. + self.project + .license_files + .iter() + .flatten() + .map(String::as_str) } pub(crate) fn settings(&self) -> Option<&BuildBackendSettings> { @@ -682,10 +705,20 @@ pub(crate) enum License { }, File { /// The file containing the license text. - file: PathBuf, + file: String, }, } +impl License { + fn file(&self) -> Option<&str> { + if let Self::File { file } = self { + Some(file) + } else { + None + } + } +} + /// A `project.authors` or `project.maintainers` entry as specified in /// . /// diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index e93260581970..fa40840d2ed2 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -95,7 +95,7 @@ fn source_dist_matcher( } // Include the license files - for license_files in pyproject_toml.license_files().into_iter().flatten() { + for license_files in pyproject_toml.license_files_source_dist() { trace!("Including license files at: `{license_files}`"); let glob = parse_portable_glob(license_files).map_err(|err| Error::PortableGlob { field: "project.license-files".to_string(), diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index d90af673d1e5..0f0983e42836 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -175,7 +175,7 @@ fn write_wheel( debug!("Visited {files_visited} files for wheel build"); // Add the license files - if let Some(license_files) = &pyproject_toml.license_files() { + if pyproject_toml.license_files_wheel().next().is_some() { debug!("Adding license files"); let license_dir = format!( "{}-{}.dist-info/licenses/", @@ -186,7 +186,7 @@ fn write_wheel( wheel_subdir_from_globs( source_tree, &license_dir, - license_files, + pyproject_toml.license_files_wheel(), &mut wheel_writer, "project.license-files", )?; @@ -429,14 +429,15 @@ pub(crate) fn build_exclude_matcher( fn wheel_subdir_from_globs( src: &Path, target: &str, - globs: &[String], + globs: impl IntoIterator>, wheel_writer: &mut impl DirectoryWriter, // For error messages globs_field: &str, ) -> Result<(), Error> { let license_files_globs: Vec<_> = globs - .iter() + .into_iter() .map(|license_files| { + let license_files = license_files.as_ref(); trace!( "Including {} at `{}` with `{}`", globs_field, diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index a5bb819b2043..558963b15fb2 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -10990,30 +10990,21 @@ fn unnamed_git_requirement() -> Result<()> { uv_snapshot!(context.filters(), context.pip_compile() .arg("requirements.in"), @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] requirements.in - blinker==1.7.0 - # via flask - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@735a4701d6d5e848241e7d7535db898efb62d400 - # via -r requirements.in - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask ----- stderr ----- - Resolved 7 packages in [TIME] + error: Git operation failed + Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/5a70990eb5793a40 + Caused by: failed to fetch branch or tag `3.0.0` + Caused by: process didn't exit successfully: `/usr/bin/git fetch --force --update-head-ok 'https://github.com/pallets/flask.git' '+refs/tags/3.0.0:refs/remotes/origin/tags/3.0.0'` (exit status: 128) + --- stderr + error: RPC failed; curl 92 HTTP/2 stream 5 was not closed cleanly: CANCEL (err 8) + error: 5192 bytes of body are still expected + fetch-pack: unexpected disconnect while reading sideband packet + fatal: early EOF + fatal: fetch-pack: invalid index-pack output "###); Ok(()) From 6b2ee0db7f0fea909c0e88dd9864c916a10de926 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 17 Dec 2024 15:11:20 +0100 Subject: [PATCH 2/2] Fix test --- crates/uv/tests/it/pip_compile.rs | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 558963b15fb2..a5bb819b2043 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -10990,21 +10990,30 @@ fn unnamed_git_requirement() -> Result<()> { uv_snapshot!(context.filters(), context.pip_compile() .arg("requirements.in"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + blinker==1.7.0 + # via flask + click==8.1.7 + # via flask + flask @ git+https://github.com/pallets/flask.git@735a4701d6d5e848241e7d7535db898efb62d400 + # via -r requirements.in + itsdangerous==2.1.2 + # via flask + jinja2==3.1.3 + # via flask + markupsafe==2.1.5 + # via + # jinja2 + # werkzeug + werkzeug==3.0.1 + # via flask ----- stderr ----- - error: Git operation failed - Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/5a70990eb5793a40 - Caused by: failed to fetch branch or tag `3.0.0` - Caused by: process didn't exit successfully: `/usr/bin/git fetch --force --update-head-ok 'https://github.com/pallets/flask.git' '+refs/tags/3.0.0:refs/remotes/origin/tags/3.0.0'` (exit status: 128) - --- stderr - error: RPC failed; curl 92 HTTP/2 stream 5 was not closed cleanly: CANCEL (err 8) - error: 5192 bytes of body are still expected - fetch-pack: unexpected disconnect while reading sideband packet - fatal: early EOF - fatal: fetch-pack: invalid index-pack output + Resolved 7 packages in [TIME] "###); Ok(())