Build backend: Fix pre-PEP 639 license files (#9965)

We were not copying the license file from a pre-PEP 639 declaration to
the source distribution.

Fixes #9947
This commit is contained in:
konsti 2024-12-17 15:19:59 +01:00 committed by GitHub
parent a78e7468a7
commit 654ff8015a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 9 deletions

View file

@ -180,9 +180,10 @@ mod tests {
use super::*; use super::*;
use flate2::bufread::GzDecoder; use flate2::bufread::GzDecoder;
use fs_err::File; use fs_err::File;
use indoc::indoc;
use insta::assert_snapshot; use insta::assert_snapshot;
use itertools::Itertools; use itertools::Itertools;
use std::io::BufReader; use std::io::{BufReader, Read};
use tempfile::TempDir; use tempfile::TempDir;
use uv_fs::{copy_dir_all, relative_to}; 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() 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
"###);
}
} }

View file

@ -119,8 +119,31 @@ impl PyProjectToml {
self.project.readme.as_ref() self.project.readme.as_ref()
} }
pub(crate) fn license_files(&self) -> Option<&[String]> { /// The license files that need to be included in the source distribution.
self.project.license_files.as_deref() pub(crate) fn license_files_source_dist(&self) -> impl Iterator<Item = &str> {
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<Item = &str> {
// 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> { pub(crate) fn settings(&self) -> Option<&BuildBackendSettings> {
@ -682,10 +705,20 @@ pub(crate) enum License {
}, },
File { File {
/// The file containing the license text. /// 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 /// A `project.authors` or `project.maintainers` entry as specified in
/// <https://packaging.python.org/en/latest/specifications/pyproject-toml/#authors-maintainers>. /// <https://packaging.python.org/en/latest/specifications/pyproject-toml/#authors-maintainers>.
/// ///

View file

@ -95,7 +95,7 @@ fn source_dist_matcher(
} }
// Include the license files // 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}`"); trace!("Including license files at: `{license_files}`");
let glob = parse_portable_glob(license_files).map_err(|err| Error::PortableGlob { let glob = parse_portable_glob(license_files).map_err(|err| Error::PortableGlob {
field: "project.license-files".to_string(), field: "project.license-files".to_string(),

View file

@ -175,7 +175,7 @@ fn write_wheel(
debug!("Visited {files_visited} files for wheel build"); debug!("Visited {files_visited} files for wheel build");
// Add the license files // 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"); debug!("Adding license files");
let license_dir = format!( let license_dir = format!(
"{}-{}.dist-info/licenses/", "{}-{}.dist-info/licenses/",
@ -186,7 +186,7 @@ fn write_wheel(
wheel_subdir_from_globs( wheel_subdir_from_globs(
source_tree, source_tree,
&license_dir, &license_dir,
license_files, pyproject_toml.license_files_wheel(),
&mut wheel_writer, &mut wheel_writer,
"project.license-files", "project.license-files",
)?; )?;
@ -429,14 +429,15 @@ pub(crate) fn build_exclude_matcher(
fn wheel_subdir_from_globs( fn wheel_subdir_from_globs(
src: &Path, src: &Path,
target: &str, target: &str,
globs: &[String], globs: impl IntoIterator<Item = impl AsRef<str>>,
wheel_writer: &mut impl DirectoryWriter, wheel_writer: &mut impl DirectoryWriter,
// For error messages // For error messages
globs_field: &str, globs_field: &str,
) -> Result<(), Error> { ) -> Result<(), Error> {
let license_files_globs: Vec<_> = globs let license_files_globs: Vec<_> = globs
.iter() .into_iter()
.map(|license_files| { .map(|license_files| {
let license_files = license_files.as_ref();
trace!( trace!(
"Including {} at `{}` with `{}`", "Including {} at `{}` with `{}`",
globs_field, globs_field,