diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index c1f08455e..0fc406e4e 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 76c8396a5..df43a5beb 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 e93260581..fa40840d2 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 d90af673d..0f0983e42 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,