Repository: scikit-build/scikit-build-core
Pull request #702 (2024-04-13 06:11:18)

feat: add metadata dir access
Mentioned in https://github.com/scikit-build/scikit-build-core/issues/666.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
Attention: Patch coverage is `88.88889%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 86.22%. Comparing base [(`c8c6f89`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/c8c6f89c5c3b2549c34dbeb94f53878a99d12260?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`9cae506`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

| [Files](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) | Patch % | Lines |
|---|---|---|
| [src/scikit\_build\_core/build/\_wheelfile.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?src=pr&el=tree&filepath=src%2Fscikit_build_core%2Fbuild%2F_wheelfile.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL2J1aWxkL193aGVlbGZpbGUucHk=) | 71.42% | [2 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #702      +/-   ##
==========================================
- Coverage   86.24%   86.22%   -0.02%     
==========================================
  Files          64       64              
  Lines        3322     3332      +10     
==========================================
+ Hits         2865     2873       +8     
- Misses        457      459       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/702?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).


Patch:
diff --git a/docs/cmakelists.md b/docs/cmakelists.md
index 44ce9cd..15065bf 100644
--- a/docs/cmakelists.md
+++ b/docs/cmakelists.md
@@ -106,6 +106,10 @@ configuration, with the variables:
   installed to Python's header directory.
 - `${SKBUILD_SCRIPTS_DIR}`: The scripts directory. Anything placed in here will
   go to `bin` (Unix) or `Scripts` (Windows).
+- `${SKBUILD_METADATA_DIR}`: The dist-info directory. Licenses go in the
+  `licenses` subdirectory. _Note that CMake is not run in the
+  `prepare_metadata_\*` hooks, so anything written to this directory will only
+  be present when writing wheels.\_
 - `${SKBUILD_NULL_DIR}`: Anything installed here will not be placed in the
   wheel.
 
diff --git a/src/scikit_build_core/_compat/importlib/metadata.py b/src/scikit_build_core/_compat/importlib/metadata.py
index e4fc907..209995d 100644
--- a/src/scikit_build_core/_compat/importlib/metadata.py
+++ b/src/scikit_build_core/_compat/importlib/metadata.py
@@ -5,8 +5,10 @@ import typing
 
 if sys.version_info < (3, 8):
     import importlib_metadata as _metadata
+    from importlib_metadata import PathDistribution
 else:
     import importlib.metadata as _metadata
+    from importlib.metadata import PathDistribution
 
 
 if typing.TYPE_CHECKING:
@@ -19,8 +21,7 @@ if typing.TYPE_CHECKING:
     else:
         from importlib.metadata import EntryPoints
 
-
-__all__ = ["entry_points"]
+__all__ = ["entry_points", "PathDistribution"]
 
 
 def entry_points(*, group: str) -> EntryPoints:
diff --git a/src/scikit_build_core/build/_wheelfile.py b/src/scikit_build_core/build/_wheelfile.py
index b3ceb80..777b8c4 100644
--- a/src/scikit_build_core/build/_wheelfile.py
+++ b/src/scikit_build_core/build/_wheelfile.py
@@ -76,7 +76,7 @@ class WheelWriter:
     folder: Path
     tags: Set[Tag]
     wheel_metadata: WheelMetadata
-    license_files: Mapping[Path, bytes] = dataclasses.field(default_factory=dict)
+    metadata_dir: Path | None
     _zipfile: zipfile.ZipFile | None = None
 
     @property
@@ -128,25 +128,36 @@ class WheelWriter:
         # Using deepcopy here because of a bug in pyproject-metadata
         # https://github.com/FFY00/python-pyproject-metadata/pull/49
         rfc822 = copy.deepcopy(self.metadata).as_rfc822()
-        for fp in self.license_files:
-            rfc822["License-File"] = f"{fp}"
 
-        license_entries = {
-            f"licenses/{fp}": data for fp, data in self.license_files.items()
+        metadata_files = self.metadata_dir.rglob("*") if self.metadata_dir else []
+        extra_metadata = {
+            str(f.relative_to(self.metadata_dir or Path())): f.read_bytes()
+            for f in metadata_files
+            if f.is_file()
         }
+        if {"METADATA", "WHEEL", "RECORD", "entry_points.txt"} & extra_metadata.keys():
+            msg = "Cannot have METADATA, WHEEL, RECORD, or entry_points.txt in metadata_dir"
+            raise ValueError(msg)
 
         return {
             "METADATA": bytes(rfc822),
             "WHEEL": self.wheel_metadata.as_bytes(),
             "entry_points.txt": entry_points.getvalue().encode("utf-8"),
-            **license_entries,
+            **extra_metadata,
         }
 
     def build(
         self, wheel_dirs: Mapping[str, Path], exclude: Sequence[str] = ()
     ) -> None:
         (targetlib,) = {"platlib", "purelib"} & set(wheel_dirs)
-        assert {targetlib, "data", "headers", "scripts", "null"} >= wheel_dirs.keys()
+        assert {
+            targetlib,
+            "data",
+            "headers",
+            "scripts",
+            "null",
+            "metadata",
+        } >= wheel_dirs.keys()
 
         # The "main" directory (platlib usually for us) will be handled specially below
         plans = {"": wheel_dirs[targetlib]}
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index 5f3bcdf..63d0451 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -201,6 +201,7 @@ def _build_wheel_impl(
             "headers": wheel_dir / "headers",
             "scripts": wheel_dir / "scripts",
             "null": wheel_dir / "null",
+            "metadata": wheel_dir / "metadata",
         }
 
         for d in wheel_dirs.values():
@@ -225,13 +226,17 @@ def _build_wheel_impl(
         if metadata.license and metadata.license.file:
             license_file_globs.append(str(metadata.license.file))
 
-        license_files = {
-            x: x.read_bytes()
-            for y in license_file_globs
-            for x in Path().glob(y)
-            if x.is_file()
-        }
-        if settings.wheel.license_files and not license_files:
+        for y in license_file_globs:
+            for x in Path().glob(y):
+                if x.is_file():
+                    path = wheel_dirs["metadata"] / "licenses" / x
+                    path.parent.mkdir(parents=True, exist_ok=True)
+                    shutil.copy(x, path)
+
+        if (
+            settings.wheel.license_files
+            and not (wheel_dirs["metadata"] / "licenses").is_dir()
+        ):
             logger.warning(
                 "No license files found, set wheel.license-files to [] to suppress this warning"
             )
@@ -254,7 +259,7 @@ def _build_wheel_impl(
                     root_is_purelib=targetlib == "purelib",
                     build_tag=settings.wheel.build_tag,
                 ),
-                license_files=license_files,
+                wheel_dirs["metadata"],
             )
             dist_info_contents = wheel.dist_info_contents()
             dist_info = Path(metadata_directory) / f"{wheel.name_ver}.dist-info"
@@ -363,7 +368,7 @@ def _build_wheel_impl(
                 root_is_purelib=targetlib == "purelib",
                 build_tag=settings.wheel.build_tag,
             ),
-            license_files=license_files,
+            wheel_dirs["metadata"],
         ) as wheel:
             wheel.build(wheel_dirs, exclude=settings.wheel.exclude)
 


Test patch:
diff --git a/tests/packages/simplest_c/CMakeLists.txt b/tests/packages/simplest_c/CMakeLists.txt
index f610e44..9d6caab 100644
--- a/tests/packages/simplest_c/CMakeLists.txt
+++ b/tests/packages/simplest_c/CMakeLists.txt
@@ -38,3 +38,11 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated_ignored.txt
 file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated_no_wheel.txt "Testing")
 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated_no_wheel.txt
         DESTINATION ${SKBUILD_PROJECT_NAME})
+
+# Testing metadata
+file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/metadata_file.txt "Testing")
+file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt "Testing")
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/metadata_file.txt
+        DESTINATION "${SKBUILD_METADATA_DIR}")
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt
+        DESTINATION "${SKBUILD_METADATA_DIR}/licenses")
diff --git a/tests/test_builder.py b/tests/test_builder.py
index e4a8476..c6092af 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -53,7 +53,13 @@ def test_get_python_include_dir():
     strict=False, reason="Doesn't matter if this fails, usually not used"
 )
 def test_get_python_library():
-    pprint.pprint(sysconfig.get_config_vars())
+    pprint.pprint(
+        {
+            k: v
+            for k, v in sysconfig.get_config_vars().items()
+            if isinstance(v, str) and 128 > len(v) > 2 and not k.startswith("MODULE")
+        }
+    )
 
     lib = get_python_library({})
     if sysconfig.get_platform().startswith("win"):
diff --git a/tests/test_pyproject_pep517.py b/tests/test_pyproject_pep517.py
index 231b66c..215ee3d 100644
--- a/tests/test_pyproject_pep517.py
+++ b/tests/test_pyproject_pep517.py
@@ -10,7 +10,13 @@ from pathlib import Path
 import build.util
 import pytest
 
-from scikit_build_core.build import _file_processor, build_sdist, build_wheel
+from scikit_build_core._compat.importlib.metadata import PathDistribution
+from scikit_build_core.build import (
+    _file_processor,
+    build_sdist,
+    build_wheel,
+    prepare_metadata_for_build_wheel,
+)
 
 ENTRYPOINTS = """\
 [one.two]
@@ -292,10 +298,31 @@ def test_prepare_metdata_for_build_wheel():
         "Requires-Python": ">=3.7",
         "Provides-Extra": "test",
         "Requires-Dist": 'pytest>=6.0; extra == "test"',
-        "License-File": "LICENSE",
     }
 
     for k, b in answer.items():
-        assert metadata[k] == b
+        assert metadata.get(k, None) == b
+
+    assert len(metadata) == len(answer)
+
+
+@pytest.mark.usefixtures("package_simple_pyproject_ext")
+def test_prepare_metdata_for_build_wheel_by_hand(tmp_path):
+    mddir = tmp_path / "dist"
+    mddir.mkdir()
+    out = prepare_metadata_for_build_wheel(str(mddir), {})
+    print("Metadata dir:", (mddir / out).resolve())
+    metadata = PathDistribution(mddir / out).metadata
+    answer = {
+        "Metadata-Version": "2.1",
+        "Name": "CMake.Example",
+        "Version": "0.0.1",
+        "Requires-Python": ">=3.7",
+        "Provides-Extra": "test",
+        "Requires-Dist": 'pytest>=6.0; extra == "test"',
+    }
+
+    for k, b in answer.items():
+        assert metadata.get(k, None) == b
 
     assert len(metadata) == len(answer)
diff --git a/tests/test_simplest_c.py b/tests/test_simplest_c.py
index cf6af48..4fbb9c7 100644
--- a/tests/test_simplest_c.py
+++ b/tests/test_simplest_c.py
@@ -124,25 +124,46 @@ def test_pep517_wheel_incexl(tmp_path, monkeypatch, virtualenv):
 
     virtualenv.install(wheel)
 
-    if sys.version_info >= (3, 8):
-        with wheel.open("rb") as f:
-            p = zipfile.Path(f)
-            file_names = {x.name for x in p.iterdir()}
-            simplest_pkg = {x.name for x in p.joinpath("simplest").iterdir()}
-            not_a_pkg = {x.name for x in p.joinpath("not_a_package").iterdir()}
-
-        filtered_pkg = {x for x in simplest_pkg if not x.startswith("_module")}
-
-        assert len(filtered_pkg) == len(simplest_pkg) - 2
-        assert {"simplest-0.0.1.dist-info", "simplest", "not_a_package"} == file_names
-        assert {
-            "__init__.py",
-            "data.txt",
-            "ignored_included.txt",
-            "generated.txt",
-            "generated_ignored.txt",
-        } == filtered_pkg
-        assert {"simple.txt"} == not_a_pkg
+    with wheel.open("rb") as f:
+        file_names = set(zipfile.ZipFile(f).namelist())
+
+    simplest_pkg = {
+        x.split("/", maxsplit=1)[-1] for x in file_names if x.startswith("simplest/")
+    }
+    not_a_pkg = {
+        x.split("/", maxsplit=1)[-1]
+        for x in file_names
+        if x.startswith("not_a_package/")
+    }
+    metadata_items = {
+        x.split("/", maxsplit=1)[-1]
+        for x in file_names
+        if x.startswith("simplest-0.0.1.dist-info/")
+    }
+
+    assert {
+        "licenses/LICENSE.txt",
+        "metadata_file.txt",
+        "RECORD",
+        "METADATA",
+        "WHEEL",
+        "entry_points.txt",
+    } == metadata_items
+
+    filtered_pkg = {x for x in simplest_pkg if not x.startswith("_module")}
+
+    assert len(filtered_pkg) == len(simplest_pkg) - 2
+    assert {"simplest-0.0.1.dist-info", "simplest", "not_a_package"} == {
+        x.split("/")[0] for x in file_names
+    }
+    assert {
+        "__init__.py",
+        "data.txt",
+        "ignored_included.txt",
+        "generated.txt",
+        "generated_ignored.txt",
+    } == filtered_pkg
+    assert {"simple.txt"} == not_a_pkg
 
     version = virtualenv.execute(
         "from simplest import square; print(square(2))",
diff --git a/tests/test_wheelfile_utils.py b/tests/test_wheelfile_utils.py
index 2e215bb..98bdbc7 100644
--- a/tests/test_wheelfile_utils.py
+++ b/tests/test_wheelfile_utils.py
@@ -33,6 +33,7 @@ def test_wheel_writer_simple(tmp_path, monkeypatch):
         out_dir,
         {Tag("py3", "none", "any")},
         scikit_build_core.build._wheelfile.WheelMetadata(),
+        None,
     )
     wheel.wheel_metadata.generator = "scikit-build-core 1.2.3"