From c6d89a10f51814b3bf785d4911d7f510298ae6e6 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Wed, 22 Apr 2026 13:18:20 -0700 Subject: [PATCH 01/15] Route {py_distinfo} placeholder to wheel .dist-info// Recognise the {py_distinfo} install path placeholder emitted by Meson's python.dist_info_install_dir() helper (added in Meson 1.12.0). Files installed to that placeholder are routed into the wheel's -.dist-info// directory. This is the recommended mechanism for placing PEP 770 SBOMs and other dist-info-bound metadata files in the wheel, replacing post-build wheel surgery that projects like pandas have been doing in custom scripts. Both static install_data() and dynamic custom_target() inputs are supported. A duplicate-basename check in the wheel writer surfaces collisions clearly rather than silently clobbering files. Editable wheels (pip install -e .) do not include dist-info-bound files since they redirect imports to the build directory; SBOMs are intended for distribution artefacts so this is acceptable for v1. Includes a tests/packages/dist-info-sboms/ test fixture exercising both the install_data and custom_target patterns, plus a how-to guide and changelog entry. --- CHANGELOG.rst | 12 ++ docs/how-to-guides/sboms.rst | 103 ++++++++++++++++++ docs/index.rst | 1 + mesonpy/__init__.py | 22 ++++ tests/packages/dist-info-sboms/generate.py | 16 +++ tests/packages/dist-info-sboms/meson.build | 24 ++++ tests/packages/dist-info-sboms/pyproject.toml | 11 ++ .../dist-info-sboms/sboms/license-extra.txt | 1 + .../dist-info-sboms/sboms/static.cdx.json | 7 ++ tests/test_wheel.py | 21 ++++ 10 files changed, 218 insertions(+) create mode 100644 docs/how-to-guides/sboms.rst create mode 100644 tests/packages/dist-info-sboms/generate.py create mode 100644 tests/packages/dist-info-sboms/meson.build create mode 100644 tests/packages/dist-info-sboms/pyproject.toml create mode 100644 tests/packages/dist-info-sboms/sboms/license-extra.txt create mode 100644 tests/packages/dist-info-sboms/sboms/static.cdx.json diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ad9a70c..71c98f1a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,18 @@ Changelog +++++++++ +Unreleased +========== + +- Recognize the ``{py_distinfo}`` install path placeholder emitted by Meson's + ``python.dist_info_install_dir()`` helper (added in Meson 1.12.0). Files + installed to that placeholder are routed into the wheel's + ``-.dist-info//`` directory. This is the + recommended mechanism for placing PEP 770 SBOMs and other dist-info-bound + metadata files in the wheel. Routing is enforced for non-editable wheels; + editable wheels do not include dist-info-bound files (a known limitation). + + 0.19.0 ====== diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst new file mode 100644 index 00000000..af003cc0 --- /dev/null +++ b/docs/how-to-guides/sboms.rst @@ -0,0 +1,103 @@ +.. SPDX-FileCopyrightText: 2026 The meson-python developers +.. +.. SPDX-License-Identifier: MIT + +.. _how-to-guides-sboms: + +*********************************** +Including SBOMs in wheels (PEP 770) +*********************************** + +`PEP 770`_ specifies that wheels may carry Software Bill of Materials +(SBOM) documents under the ``.dist-info/sboms/`` directory. ``meson-python`` +supports placing arbitrary files into the ``.dist-info//`` of the +generated wheel via the +``python.dist_info_install_dir()`` helper introduced in Meson 1.12.0. + +This page walks through the static and dynamic patterns that scientific +Python projects (pandas, NumPy, SciPy, scikit-learn) use to ship SBOMs +in their wheels. + +.. _PEP 770: https://peps.python.org/pep-0770/ + +Static SBOM files +================= + +For SBOMs that are checked into the source tree (typically describing +source-vendored components): + +.. code-block:: meson + + project('my-project', version: '1.0.0', meson_version: '>=1.12.0') + + py = import('python').find_installation() + + install_data( + 'sboms/component1.cdx.json', + 'sboms/component2.cdx.json', + install_dir: py.dist_info_install_dir('sboms'), + ) + +The files end up in the wheel at +``my_project-1.0.0.dist-info/sboms/component1.cdx.json`` and +``component2.cdx.json``. + +Dynamically generated SBOMs +=========================== + +Many projects generate the SBOM at build time from a TOML manifest of +vendored components. Use a ``custom_target`` that produces the SBOM file +and installs it via the same helper: + +.. code-block:: meson + + custom_target('vendored-sbom', + output: 'vendored.cdx.json', + command: [py, files('scripts/generate_sbom.py'), '@OUTPUT@', + '--version', meson.project_version()], + install: true, + install_dir: py.dist_info_install_dir('sboms'), + ) + +The generator runs during the build, and ``meson-python`` injects the +output into ``my_project-1.0.0.dist-info/sboms/vendored.cdx.json``. + +Other ``.dist-info`` subdirectories +=================================== + +The same helper accepts any subdirectory name. PEP 639 license files +land in ``licenses``, for example: + +.. code-block:: meson + + install_data('LICENSES/extra.txt', + install_dir: py.dist_info_install_dir('licenses')) + +PEP 639 license files declared via ``project.license-files`` in +``pyproject.toml`` are handled separately (no ``meson.build`` change +needed). Use ``dist_info_install_dir('licenses')`` only for additional +license files outside the standard ``project.license-files`` list. + +File naming and validation +========================== + +* Files installed via ``dist_info_install_dir()`` must have unique + basenames within their subdirectory. ``meson-python`` will raise an + error at build time if two files would write to the same path inside + ``.dist-info//``. +* The recommended file extensions are ``.cdx.json`` for CycloneDX and + ``.spdx.json`` for SPDX, per the PSF + `SBOMs for Python packages`_ proposal. + +.. _SBOMs for Python packages: https://github.com/psf/sboms-for-python-packages + +Editable installs +================= + +Files installed via ``dist_info_install_dir()`` are placed in the wheel +when ``meson-python`` builds a regular wheel (``pip install .`` or +``python -m build``). They are **not** included in editable wheels +(``pip install -e .``), because editable wheels redirect imports to the +build directory rather than carrying project files. SBOMs are intended +for distribution artefacts, so this limitation generally does not affect +development workflows. diff --git a/docs/index.rst b/docs/index.rst index f56b7ebc..0ec38fce 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -83,6 +83,7 @@ the use of ``meson-python`` and Meson for Python packaging. how-to-guides/meson-args how-to-guides/debug-builds how-to-guides/shared-libraries + how-to-guides/sboms reference/limitations projects-using-meson-python diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index e0328d1a..8c894387 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -108,6 +108,10 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef] '{moduledir_shared}': 'platlib', '{includedir}': 'headers', '{datadir}': 'data', + # files routed by Meson's python.dist_info_install_dir() helper into + # the wheel's .dist-info// directory (PEP 770 SBOMs, etc.). + # Requires meson >= 1.12.0. + '{py_distinfo}': 'distinfo', # custom location '{libdir}': 'mesonpy-libs', '{libdir_shared}': 'mesonpy-libs', @@ -495,6 +499,10 @@ def build(self, directory: Path) -> pathlib.Path: with _clicounter(sum(len(x) for x in self._manifest.values())) as counter: root = 'purelib' if self._pure else 'platlib' + # Track basenames written under each .dist-info// to + # surface collisions (two files with the same basename would + # silently clobber each other in the wheel). + distinfo_seen: Dict[str, str] = {} for path, entries in self._manifest.items(): for dst, src in entries: @@ -505,6 +513,20 @@ def build(self, directory: Path) -> pathlib.Path: elif path == 'mesonpy-libs': # custom installation path for bundled libraries dst = pathlib.Path(self._libs_dir, dst) + elif path == 'distinfo': + # files routed by Meson's + # python.dist_info_install_dir() helper. + target = pathlib.Path(self._distinfo_dir, dst).as_posix() + previous = distinfo_seen.get(target) + if previous is not None: + raise BuildError( + f'Two files would be installed to {target!r} ' + f'in the wheel: {previous!r} and {os.fspath(src)!r}. ' + f'Files placed in .dist-info/ via ' + f'python.dist_info_install_dir() must have ' + f'unique basenames within their subdirectory.') + distinfo_seen[target] = os.fspath(src) + dst = pathlib.Path(self._distinfo_dir, dst) else: dst = pathlib.Path(self._data_dir, path, dst) diff --git a/tests/packages/dist-info-sboms/generate.py b/tests/packages/dist-info-sboms/generate.py new file mode 100644 index 00000000..56635572 --- /dev/null +++ b/tests/packages/dist-info-sboms/generate.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +"""Trivial SBOM generator for the dist-info-sboms test package.""" + +import json +import sys + +with open(sys.argv[1], 'w') as f: + json.dump({ + 'bomFormat': 'CycloneDX', + 'specVersion': '1.6', + 'version': 1, + 'components': [], + }, f) diff --git a/tests/packages/dist-info-sboms/meson.build b/tests/packages/dist-info-sboms/meson.build new file mode 100644 index 00000000..93c9ab5b --- /dev/null +++ b/tests/packages/dist-info-sboms/meson.build @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('dist-info-sboms', version: '1.0.0', meson_version: '>=1.12.0') + +py = import('python').find_installation() + +# Static SBOM file installed via the helper. +install_data('sboms/static.cdx.json', + install_dir: py.dist_info_install_dir('sboms')) + +# Dynamic SBOM produced by a custom_target. This is the pattern +# scientific-Python projects (pandas, NumPy, SciPy) use to inject +# build-time-generated SBOMs into the wheel. +custom_target('generated-sbom', + output: 'generated.cdx.json', + command: [py, files('generate.py'), '@OUTPUT@'], + install: true, + install_dir: py.dist_info_install_dir('sboms')) + +# Different subdirectory — proves the helper's parameterization works. +install_data('sboms/license-extra.txt', + install_dir: py.dist_info_install_dir('licenses')) diff --git a/tests/packages/dist-info-sboms/pyproject.toml b/tests/packages/dist-info-sboms/pyproject.toml new file mode 100644 index 00000000..e9421b72 --- /dev/null +++ b/tests/packages/dist-info-sboms/pyproject.toml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'dist-info-sboms' +version = '1.0.0' diff --git a/tests/packages/dist-info-sboms/sboms/license-extra.txt b/tests/packages/dist-info-sboms/sboms/license-extra.txt new file mode 100644 index 00000000..58a5c659 --- /dev/null +++ b/tests/packages/dist-info-sboms/sboms/license-extra.txt @@ -0,0 +1 @@ +Sample license file routed via py.dist_info_install_dir('licenses'). diff --git a/tests/packages/dist-info-sboms/sboms/static.cdx.json b/tests/packages/dist-info-sboms/sboms/static.cdx.json new file mode 100644 index 00000000..8a8fd07c --- /dev/null +++ b/tests/packages/dist-info-sboms/sboms/static.cdx.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "components": [] +} diff --git a/tests/test_wheel.py b/tests/test_wheel.py index e3c93559..ae727ec3 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -158,6 +158,27 @@ def test_license_pep639(wheel_license_pep639): ''')) +def test_dist_info_sboms(wheel_dist_info_sboms): + artifact = wheel.wheelfile.WheelFile(wheel_dist_info_sboms) + + assert wheel_contents(artifact) == { + 'dist_info_sboms-1.0.0.dist-info/METADATA', + 'dist_info_sboms-1.0.0.dist-info/RECORD', + 'dist_info_sboms-1.0.0.dist-info/WHEEL', + 'dist_info_sboms-1.0.0.dist-info/sboms/static.cdx.json', + 'dist_info_sboms-1.0.0.dist-info/sboms/generated.cdx.json', + 'dist_info_sboms-1.0.0.dist-info/licenses/license-extra.txt', + } + + # Static SBOM file content preserved verbatim. + static_sbom = artifact.read('dist_info_sboms-1.0.0.dist-info/sboms/static.cdx.json') + assert b'CycloneDX' in static_sbom + + # Dynamically-generated SBOM produced by the custom_target ran. + generated_sbom = artifact.read('dist_info_sboms-1.0.0.dist-info/sboms/generated.cdx.json') + assert b'CycloneDX' in generated_sbom + + @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') def test_contents(package_library, wheel_library): artifact = wheel.wheelfile.WheelFile(wheel_library) From 739b0fbcb2046a6ac4a6a3dd464554760b931458 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Wed, 22 Apr 2026 13:50:53 -0700 Subject: [PATCH 02/15] Extend .dist-info/ collision check to cover license-files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on the {py_distinfo} placeholder routing: The original duplicate-basename check in _WheelBuilder.build() only tracked files going through the `distinfo` manifest path. PEP 639 license-files (written by _wheel_write_metadata as part of the wheel's metadata) and pre-PEP-639 license files bypassed the check. A user declaring `license-files = ['LICENSE.txt']` in pyproject.toml and separately routing `duplicate/LICENSE.txt` via py.dist_info_install_dir('licenses') would produce a wheel with two zip entries at the same path (silent clobber, no error). Fix: thread the seen-set through _wheel_write_metadata so both sources share the same collision registry, and error out with a clear message pointing at the two likely sources. The editable wheel builder still calls _wheel_write_metadata without a seen-set; this is acceptable since editable wheels have no manifest loop and no distinfo routing — collisions within license-files alone would have existed before this change and remain a pyproject_metadata concern. Also remove a redundant os.fspath(src) (src is already str) and add a tests/packages/dist-info-sboms-collision/ fixture that exercises the cross-source collision path via pytest.raises(BuildError). --- mesonpy/__init__.py | 46 ++++++++++++++----- .../dist-info-sboms-collision/LICENSE.txt | 1 + .../duplicate/LICENSE.txt | 2 + .../dist-info-sboms-collision/meson.build | 14 ++++++ .../dist-info-sboms-collision/pyproject.toml | 13 ++++++ tests/test_wheel.py | 11 +++++ 6 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 tests/packages/dist-info-sboms-collision/LICENSE.txt create mode 100644 tests/packages/dist-info-sboms-collision/duplicate/LICENSE.txt create mode 100644 tests/packages/dist-info-sboms-collision/meson.build create mode 100644 tests/packages/dist-info-sboms-collision/pyproject.toml diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 8c894387..066bcfcc 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -472,37 +472,60 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, if not os.fspath(origin).endswith('.pdb'): raise - def _wheel_write_metadata(self, whl: mesonpy._wheelfile.WheelFile) -> None: + def _wheel_write_metadata( + self, + whl: mesonpy._wheelfile.WheelFile, + distinfo_seen: Optional[Dict[str, str]] = None, + ) -> None: # add metadata whl.writestr(f'{self._distinfo_dir}/METADATA', bytes(self._metadata.as_rfc822())) whl.writestr(f'{self._distinfo_dir}/WHEEL', self.wheel) if self.entrypoints_txt: whl.writestr(f'{self._distinfo_dir}/entry_points.txt', self.entrypoints_txt) + def _record(target: str, origin: str) -> None: + if distinfo_seen is None: + return + previous = distinfo_seen.get(target) + if previous is not None: + raise BuildError( + f'Two files would be installed to {target!r} in the ' + f'wheel: {previous!r} and {origin!r}. Files placed in ' + f'.dist-info/ must have unique basenames within their ' + f'subdirectory; check your project.license-files list ' + f'and any python.dist_info_install_dir() uses.') + distinfo_seen[target] = origin + # Add pre-PEP-639 license files. if isinstance(self._metadata.license, pyproject_metadata.License): license_file = self._metadata.license.file if license_file: - whl.write(license_file, f'{self._distinfo_dir}/{os.path.basename(license_file)}') + target = f'{self._distinfo_dir}/{os.path.basename(license_file)}' + _record(target, str(license_file)) + whl.write(license_file, target) # Add PEP-639 license-files. Use ``getattr()`` for compatibility with pyproject-metadata < 0.9.0. license_files = getattr(self._metadata, 'license_files', None) if license_files: for f in license_files: - whl.write(f, f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}') + target = f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}' + _record(target, str(f)) + whl.write(f, target) def build(self, directory: Path) -> pathlib.Path: wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: - self._wheel_write_metadata(whl) + # Track files written under .dist-info/ to surface collisions + # (two files at the same wheel path would silently clobber + # each other in the archive). Populated by both + # _wheel_write_metadata (license files) and the manifest loop + # below (files routed via python.dist_info_install_dir()). + distinfo_seen: Dict[str, str] = {} + self._wheel_write_metadata(whl, distinfo_seen) with _clicounter(sum(len(x) for x in self._manifest.values())) as counter: root = 'purelib' if self._pure else 'platlib' - # Track basenames written under each .dist-info// to - # surface collisions (two files with the same basename would - # silently clobber each other in the wheel). - distinfo_seen: Dict[str, str] = {} for path, entries in self._manifest.items(): for dst, src in entries: @@ -521,11 +544,10 @@ def build(self, directory: Path) -> pathlib.Path: if previous is not None: raise BuildError( f'Two files would be installed to {target!r} ' - f'in the wheel: {previous!r} and {os.fspath(src)!r}. ' - f'Files placed in .dist-info/ via ' - f'python.dist_info_install_dir() must have ' + f'in the wheel: {previous!r} and {str(src)!r}. ' + f'Files placed in .dist-info/ must have ' f'unique basenames within their subdirectory.') - distinfo_seen[target] = os.fspath(src) + distinfo_seen[target] = str(src) dst = pathlib.Path(self._distinfo_dir, dst) else: dst = pathlib.Path(self._data_dir, path, dst) diff --git a/tests/packages/dist-info-sboms-collision/LICENSE.txt b/tests/packages/dist-info-sboms-collision/LICENSE.txt new file mode 100644 index 00000000..7bc53ae7 --- /dev/null +++ b/tests/packages/dist-info-sboms-collision/LICENSE.txt @@ -0,0 +1 @@ +Top-level LICENSE.txt referenced by project.license-files in pyproject.toml. diff --git a/tests/packages/dist-info-sboms-collision/duplicate/LICENSE.txt b/tests/packages/dist-info-sboms-collision/duplicate/LICENSE.txt new file mode 100644 index 00000000..36ce06ec --- /dev/null +++ b/tests/packages/dist-info-sboms-collision/duplicate/LICENSE.txt @@ -0,0 +1,2 @@ +Second LICENSE.txt routed via py.dist_info_install_dir('licenses'). +Colliding basename should trigger a BuildError. diff --git a/tests/packages/dist-info-sboms-collision/meson.build b/tests/packages/dist-info-sboms-collision/meson.build new file mode 100644 index 00000000..a53345ce --- /dev/null +++ b/tests/packages/dist-info-sboms-collision/meson.build @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('dist-info-sboms-collision', version: '1.0.0', meson_version: '>=1.12.0') + +py = import('python').find_installation() + +# This install_data targets the same wheel path as the PEP 639 +# license-files entry in pyproject.toml (.dist-info/licenses/LICENSE.txt). +# meson-python should surface the collision as a BuildError rather than +# silently clobber. +install_data('duplicate/LICENSE.txt', + install_dir: py.dist_info_install_dir('licenses')) diff --git a/tests/packages/dist-info-sboms-collision/pyproject.toml b/tests/packages/dist-info-sboms-collision/pyproject.toml new file mode 100644 index 00000000..704c461e --- /dev/null +++ b/tests/packages/dist-info-sboms-collision/pyproject.toml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'dist-info-sboms-collision' +version = '1.0.0' +license = 'MIT' +license-files = ['LICENSE.txt'] diff --git a/tests/test_wheel.py b/tests/test_wheel.py index ae727ec3..5563e2d2 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -179,6 +179,17 @@ def test_dist_info_sboms(wheel_dist_info_sboms): assert b'CycloneDX' in generated_sbom +@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') +def test_dist_info_sboms_collision(package_dist_info_sboms_collision, tmp_path): + # Verifies the collision check catches the case where a file routed + # via python.dist_info_install_dir('licenses') targets the same wheel + # path as a PEP 639 license-files entry. Before the shared seen-set + # fix, this silently produced a wheel with duplicate zip entries. + with pytest.raises(mesonpy.BuildError, match='Two files would be installed to'): + with mesonpy._project() as project: + project.wheel(tmp_path) + + @pytest.mark.skipif(sys.platform in {'win32', 'cygwin'}, reason='requires RPATH support') def test_contents(package_library, wheel_library): artifact = wheel.wheelfile.WheelFile(wheel_library) From ded08d9f28e30488ddb81728837d0d36afed8a4b Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Wed, 22 Apr 2026 23:54:08 -0700 Subject: [PATCH 03/15] Also check license-files collisions in editable wheels Follow-up to the shared seen-set refactor: _EditableWheelBuilder.build() was still calling _wheel_write_metadata() without a registry, so duplicate license-files entries in pyproject.toml would silently clobber in the editable wheel even though they'd now raise in regular wheels. Pass an empty registry from editable too, so the collision check is consistent across both wheel kinds. Editable wheels still don't carry dist_info_install_dir()-routed files (no manifest loop), so no behavior change for that code path. --- mesonpy/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 066bcfcc..ebad290d 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -580,7 +580,10 @@ def build(self, directory: Path, source_dir: pathlib.Path, build_dir: pathlib.Pa wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: - self._wheel_write_metadata(whl) + # Pass a registry so duplicate license-files in pyproject + # still surface clearly even though editable wheels have no + # manifest loop and no dist_info_install_dir() routing. + self._wheel_write_metadata(whl, distinfo_seen={}) whl.writestr( f'{self._distinfo_dir}/direct_url.json', source_dir.as_uri().encode('utf-8')) From c4b09ba17c393273e5300da1967141e262f47993 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 15:09:26 -0700 Subject: [PATCH 04/15] Use neutral subdir name in dist-info-sboms test fixture The tests/packages/dist-info-sboms fixture was routing a license-extra.txt file to py.dist_info_install_dir('licenses'). That unintentionally suggested this is an alternative mechanism for PEP 639 license-files, which it is not -- license-files still flow entirely through pyproject_metadata and the existing _wheel_write_metadata code path. Rename the second subdir to 'extras' (and the payload file to extra.txt) so the test exercises parameterisation without the PEP 639 overtone. The dist-info-sboms-collision fixture deliberately keeps 'licenses' because its whole purpose is demonstrating the cross-source collision check against PEP 639 license-files. --- tests/packages/dist-info-sboms/meson.build | 10 +++++++--- tests/packages/dist-info-sboms/sboms/extra.txt | 1 + tests/packages/dist-info-sboms/sboms/license-extra.txt | 1 - tests/test_wheel.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 tests/packages/dist-info-sboms/sboms/extra.txt delete mode 100644 tests/packages/dist-info-sboms/sboms/license-extra.txt diff --git a/tests/packages/dist-info-sboms/meson.build b/tests/packages/dist-info-sboms/meson.build index 93c9ab5b..f9917dad 100644 --- a/tests/packages/dist-info-sboms/meson.build +++ b/tests/packages/dist-info-sboms/meson.build @@ -19,6 +19,10 @@ custom_target('generated-sbom', install: true, install_dir: py.dist_info_install_dir('sboms')) -# Different subdirectory — proves the helper's parameterization works. -install_data('sboms/license-extra.txt', - install_dir: py.dist_info_install_dir('licenses')) +# Different subdirectory — proves the helper's parameterization works +# and that files routed to two subdirs land in distinct wheel paths. +# 'extras' (rather than 'licenses') avoids suggesting this is a PEP 639 +# alternative; PEP 639 license-files still flow through +# pyproject_metadata + the existing _wheel_write_metadata path. +install_data('sboms/extra.txt', + install_dir: py.dist_info_install_dir('extras')) diff --git a/tests/packages/dist-info-sboms/sboms/extra.txt b/tests/packages/dist-info-sboms/sboms/extra.txt new file mode 100644 index 00000000..6720832b --- /dev/null +++ b/tests/packages/dist-info-sboms/sboms/extra.txt @@ -0,0 +1 @@ +Sample extras file routed via py.dist_info_install_dir('extras'). diff --git a/tests/packages/dist-info-sboms/sboms/license-extra.txt b/tests/packages/dist-info-sboms/sboms/license-extra.txt deleted file mode 100644 index 58a5c659..00000000 --- a/tests/packages/dist-info-sboms/sboms/license-extra.txt +++ /dev/null @@ -1 +0,0 @@ -Sample license file routed via py.dist_info_install_dir('licenses'). diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 5563e2d2..4889635f 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -167,7 +167,7 @@ def test_dist_info_sboms(wheel_dist_info_sboms): 'dist_info_sboms-1.0.0.dist-info/WHEEL', 'dist_info_sboms-1.0.0.dist-info/sboms/static.cdx.json', 'dist_info_sboms-1.0.0.dist-info/sboms/generated.cdx.json', - 'dist_info_sboms-1.0.0.dist-info/licenses/license-extra.txt', + 'dist_info_sboms-1.0.0.dist-info/extras/extra.txt', } # Static SBOM file content preserved verbatim. From b365ecb1c1e259ea75197d0f3d46c31e2a57e983 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 20:33:57 -0700 Subject: [PATCH 05/15] Drop {py_distinfo} placeholder; detect dist-info prefix in install paths Reviewer feedback on the paired meson PR (mesonbuild/meson#15738) pointed out that the proposed python.dist_info_install_dir() helper was effectively just computing '-.dist-info//' and emitting a placeholder -- wrapping validation we'd invented and a fallback that could silently mismatch the wheel's actual distinfo name. The helper as proposed doesn't carry its weight. Rework meson-python to do the routing entirely on its side: detect files staged under {py_purelib}//... in the install plan and reroute them into the wheel's own .dist-info/ at pack time. Users compose the path themselves from primitives that already exist in meson: distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' install_data('sboms/foo.cdx.json', install_dir: py.get_install_dir() / distinfo / 'sboms') Effects: - No new meson API surface, no new placeholder, no wait for a meson release. Works with meson versions already shipped. - Authority for the dist-info dir is the PEP 621 pyproject name, canonicalised so the user's meson.project_name() can carry either hyphens or underscores without breaking routing. - If the user's composed name doesn't match the wheel's actual distinfo, the file stays in purelib rather than getting silently renamed -- the mismatch surfaces as an unexpected directory in the wheel. - Existing collision check still fires (covers both metadata-layer license-files and files staged via this mechanism). Tests updated to use the new composition pattern. meson_version pins on the fixtures dropped since no meson feature is needed. --- CHANGELOG.rst | 15 +++-- docs/how-to-guides/sboms.rst | 56 ++++++++++--------- mesonpy/__init__.py | 55 ++++++++++++++---- .../dist-info-sboms-collision/meson.build | 6 +- tests/packages/dist-info-sboms/meson.build | 25 +++++---- 5 files changed, 103 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71c98f1a..dbb14d9c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,13 +14,16 @@ Changelog Unreleased ========== -- Recognize the ``{py_distinfo}`` install path placeholder emitted by Meson's - ``python.dist_info_install_dir()`` helper (added in Meson 1.12.0). Files - installed to that placeholder are routed into the wheel's - ``-.dist-info//`` directory. This is the +- Route files staged under ``{py_purelib}/-.dist-info/...`` + into the wheel's ``.dist-info/`` directory at pack time. This is the recommended mechanism for placing PEP 770 SBOMs and other dist-info-bound - metadata files in the wheel. Routing is enforced for non-editable wheels; - editable wheels do not include dist-info-bound files (a known limitation). + metadata files in the wheel, removing the need for post-build wheel + surgery. Works with both static ``install_data()`` and dynamic + ``custom_target(install: true)``; requires no new meson API. The collision + check surfaces overlaps between files routed here and PEP 639 + ``license-files`` written by the metadata layer. Routing is enforced for + non-editable wheels; editable wheels do not include dist-info-bound files + (a known limitation). 0.19.0 diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst index af003cc0..e0ab0fcf 100644 --- a/docs/how-to-guides/sboms.rst +++ b/docs/how-to-guides/sboms.rst @@ -9,14 +9,16 @@ Including SBOMs in wheels (PEP 770) *********************************** `PEP 770`_ specifies that wheels may carry Software Bill of Materials -(SBOM) documents under the ``.dist-info/sboms/`` directory. ``meson-python`` -supports placing arbitrary files into the ``.dist-info//`` of the -generated wheel via the -``python.dist_info_install_dir()`` helper introduced in Meson 1.12.0. +(SBOM) documents under the ``.dist-info/sboms/`` directory. +``meson-python`` places any file installed under +``{py_purelib}/-.dist-info//...`` into the +wheel's ``.dist-info//`` at pack time. This is the mechanism +scientific-Python projects (pandas, NumPy, SciPy, scikit-learn) use to +ship SBOMs in their wheels without post-build wheel surgery. -This page walks through the static and dynamic patterns that scientific -Python projects (pandas, NumPy, SciPy, scikit-learn) use to ship SBOMs -in their wheels. +The ``-.dist-info`` directory name is recognised +regardless of whether the user wrote the project name with hyphens or +underscores — the comparison is canonicalised. .. _PEP 770: https://peps.python.org/pep-0770/ @@ -28,14 +30,15 @@ source-vendored components): .. code-block:: meson - project('my-project', version: '1.0.0', meson_version: '>=1.12.0') + project('my-project', version: '1.0.0') py = import('python').find_installation() + distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' install_data( 'sboms/component1.cdx.json', 'sboms/component2.cdx.json', - install_dir: py.dist_info_install_dir('sboms'), + install_dir: py.get_install_dir() / distinfo / 'sboms', ) The files end up in the wheel at @@ -47,7 +50,7 @@ Dynamically generated SBOMs Many projects generate the SBOM at build time from a TOML manifest of vendored components. Use a ``custom_target`` that produces the SBOM file -and installs it via the same helper: +and installs it to the same location: .. code-block:: meson @@ -56,7 +59,7 @@ and installs it via the same helper: command: [py, files('scripts/generate_sbom.py'), '@OUTPUT@', '--version', meson.project_version()], install: true, - install_dir: py.dist_info_install_dir('sboms'), + install_dir: py.get_install_dir() / distinfo / 'sboms', ) The generator runs during the build, and ``meson-python`` injects the @@ -65,26 +68,29 @@ output into ``my_project-1.0.0.dist-info/sboms/vendored.cdx.json``. Other ``.dist-info`` subdirectories =================================== -The same helper accepts any subdirectory name. PEP 639 license files -land in ``licenses``, for example: +Any subdirectory under the ``.dist-info`` directory works the same way. +PEP 639 license files can be placed under ``licenses``, for example: .. code-block:: meson install_data('LICENSES/extra.txt', - install_dir: py.dist_info_install_dir('licenses')) + install_dir: py.get_install_dir() / distinfo / 'licenses') PEP 639 license files declared via ``project.license-files`` in -``pyproject.toml`` are handled separately (no ``meson.build`` change -needed). Use ``dist_info_install_dir('licenses')`` only for additional -license files outside the standard ``project.license-files`` list. +``pyproject.toml`` are handled separately and go into +``.dist-info/licenses/`` automatically (no ``meson.build`` change +needed). Use the pattern above only for additional license files +outside the standard ``project.license-files`` list. File naming and validation ========================== -* Files installed via ``dist_info_install_dir()`` must have unique - basenames within their subdirectory. ``meson-python`` will raise an - error at build time if two files would write to the same path inside - ``.dist-info//``. +* Files installed under ``-.dist-info//`` + must have unique basenames within their subdirectory. ``meson-python`` + raises an error at build time if two files would write to the same + path inside ``.dist-info//``. The collision check covers + files routed via this mechanism and PEP 639 license-files from + ``project.license-files``. * The recommended file extensions are ``.cdx.json`` for CycloneDX and ``.spdx.json`` for SPDX, per the PSF `SBOMs for Python packages`_ proposal. @@ -94,10 +100,10 @@ File naming and validation Editable installs ================= -Files installed via ``dist_info_install_dir()`` are placed in the wheel -when ``meson-python`` builds a regular wheel (``pip install .`` or +Files staged via this mechanism are placed in the wheel when +``meson-python`` builds a regular wheel (``pip install .`` or ``python -m build``). They are **not** included in editable wheels (``pip install -e .``), because editable wheels redirect imports to the build directory rather than carrying project files. SBOMs are intended -for distribution artefacts, so this limitation generally does not affect -development workflows. +for distribution artefacts, so this limitation generally does not +affect development workflows. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index ebad290d..cce91397 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -108,10 +108,6 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef] '{moduledir_shared}': 'platlib', '{includedir}': 'headers', '{datadir}': 'data', - # files routed by Meson's python.dist_info_install_dir() helper into - # the wheel's .dist-info// directory (PEP 770 SBOMs, etc.). - # Requires meson >= 1.12.0. - '{py_distinfo}': 'distinfo', # custom location '{libdir}': 'mesonpy-libs', '{libdir_shared}': 'mesonpy-libs', @@ -130,15 +126,36 @@ class _Entry(typing.NamedTuple): src: str +def _canonicalize_distinfo(dir_name: str) -> str: + """Canonicalize a `-.dist-info` directory for equality + comparison. Collapses runs of ``-``, ``_``, ``.`` to a single ``-`` and + lowercases, so that e.g. ``dist-info-sboms-1.0.dist-info`` and + ``dist_info_sboms-1.0.dist-info`` compare equal. PEP 491 uses ``_`` as + the separator while users typing ``meson.project_name()`` often carry + hyphens from their original name. + """ + return re.sub(r'[-_.]+', '-', dir_name).lower() + + def _map_to_wheel( sources: Dict[str, Dict[str, Any]], - exclude: List[str], include: List[str] + exclude: List[str], include: List[str], + distinfo_dir: str, ) -> DefaultDict[str, List[_Entry]]: - """Map files to the wheel, organized by wheel installation directory.""" + """Map files to the wheel, organized by wheel installation directory. + + ``distinfo_dir`` is the ``-.dist-info`` directory name this + wheel will carry, derived from PEP 621 metadata. Files staged under + ``{py_purelib}//...`` are rerouted into the wheel's + ``.dist-info/`` at pack time — this is the mechanism projects use to + place PEP 770 SBOMs and other dist-info-bound metadata files in the + wheel. + """ wheel_files: DefaultDict[str, List[_Entry]] = collections.defaultdict(list) packages: Dict[str, str] = {} excluded = _compile_patterns(exclude) included = _compile_patterns(include) + canonical_distinfo = _canonicalize_distinfo(distinfo_dir) for key, group in sources.items(): for src, target in group.items(): @@ -155,7 +172,19 @@ def _map_to_wheel( if path is None: raise BuildError(f'Could not map installation path to an equivalent wheel directory: {str(destination)!r}') - if path == 'purelib' or path == 'platlib': + # Files staged under {py_purelib}//... are routed + # into the wheel's .dist-info/ at pack time. Authority for the + # distinfo dir name is the PEP 621 metadata (distinfo_dir); the + # user's meson.build can write the name with either hyphens or + # underscores and we compare canonically. + if ( + path == 'purelib' + and dst.parts + and _canonicalize_distinfo(dst.parts[0]) == canonical_distinfo + ): + path = 'distinfo' + dst = pathlib.Path(*dst.parts[1:]) + elif path == 'purelib' or path == 'platlib': package = destination.parts[1] other = packages.setdefault(package, path) if other != path: @@ -537,8 +566,8 @@ def build(self, directory: Path) -> pathlib.Path: # custom installation path for bundled libraries dst = pathlib.Path(self._libs_dir, dst) elif path == 'distinfo': - # files routed by Meson's - # python.dist_info_install_dir() helper. + # files detected under {purelib}//... in + # the install plan; see _map_to_wheel(). target = pathlib.Path(self._distinfo_dir, dst).as_posix() previous = distinfo_seen.get(target) if previous is not None: @@ -1019,8 +1048,12 @@ def _manifest(self) -> DefaultDict[str, List[_Entry]]: continue sources[key][target] = details - # Map Meson installation locations to wheel paths. - return _map_to_wheel(sources, self._excluded_files, self._included_files) + # Map Meson installation locations to wheel paths. Pass the + # wheel's dist-info directory name so files staged under + # {purelib}/-.dist-info/... get rerouted to the + # wheel's own .dist-info/ at pack time. + distinfo_dir = f'{self._metadata.distribution_name}-{self._metadata.version}.dist-info' + return _map_to_wheel(sources, self._excluded_files, self._included_files, distinfo_dir) @property def _meson_name(self) -> str: diff --git a/tests/packages/dist-info-sboms-collision/meson.build b/tests/packages/dist-info-sboms-collision/meson.build index a53345ce..33017fd8 100644 --- a/tests/packages/dist-info-sboms-collision/meson.build +++ b/tests/packages/dist-info-sboms-collision/meson.build @@ -2,13 +2,15 @@ # # SPDX-License-Identifier: MIT -project('dist-info-sboms-collision', version: '1.0.0', meson_version: '>=1.12.0') +project('dist-info-sboms-collision', version: '1.0.0') py = import('python').find_installation() +distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' + # This install_data targets the same wheel path as the PEP 639 # license-files entry in pyproject.toml (.dist-info/licenses/LICENSE.txt). # meson-python should surface the collision as a BuildError rather than # silently clobber. install_data('duplicate/LICENSE.txt', - install_dir: py.dist_info_install_dir('licenses')) + install_dir: py.get_install_dir() / distinfo / 'licenses') diff --git a/tests/packages/dist-info-sboms/meson.build b/tests/packages/dist-info-sboms/meson.build index f9917dad..36567a4f 100644 --- a/tests/packages/dist-info-sboms/meson.build +++ b/tests/packages/dist-info-sboms/meson.build @@ -2,13 +2,18 @@ # # SPDX-License-Identifier: MIT -project('dist-info-sboms', version: '1.0.0', meson_version: '>=1.12.0') +project('dist-info-sboms', version: '1.0.0') py = import('python').find_installation() -# Static SBOM file installed via the helper. +# Compose the wheel's .dist-info/ directory name. meson-python detects +# this prefix under {py_purelib}/... in the install plan and routes the +# file into the wheel's own .dist-info/. +distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' + +# Static SBOM file. install_data('sboms/static.cdx.json', - install_dir: py.dist_info_install_dir('sboms')) + install_dir: py.get_install_dir() / distinfo / 'sboms') # Dynamic SBOM produced by a custom_target. This is the pattern # scientific-Python projects (pandas, NumPy, SciPy) use to inject @@ -17,12 +22,12 @@ custom_target('generated-sbom', output: 'generated.cdx.json', command: [py, files('generate.py'), '@OUTPUT@'], install: true, - install_dir: py.dist_info_install_dir('sboms')) + install_dir: py.get_install_dir() / distinfo / 'sboms') -# Different subdirectory — proves the helper's parameterization works -# and that files routed to two subdirs land in distinct wheel paths. -# 'extras' (rather than 'licenses') avoids suggesting this is a PEP 639 -# alternative; PEP 639 license-files still flow through -# pyproject_metadata + the existing _wheel_write_metadata path. +# Different subdirectory — proves the routing handles arbitrary +# subdir names, not just 'sboms'. 'extras' (rather than 'licenses') +# avoids suggesting this is a PEP 639 alternative; PEP 639 +# license-files still flow through pyproject_metadata + the existing +# _wheel_write_metadata path. install_data('sboms/extra.txt', - install_dir: py.dist_info_install_dir('extras')) + install_dir: py.get_install_dir() / distinfo / 'extras') From a384103deae2482dd4c9dfc36ef9c7120839dbf9 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 20:54:15 -0700 Subject: [PATCH 06/15] Detect dist-info prefix under platlib too, not only purelib The previous commit's detection only fired for destinations under {py_purelib}/..., but projects shipping C extensions (pandas, numpy, scipy) call find_installation(pure: false) and their install paths come through {py_platlib}/... instead. For those projects files landed at the right wheel location by coincidence (the wheel root IS platlib, so path == root, so dst passes through unchanged) but our collision-check branch was never hit -- a PEP 639 license-file clash with a user-installed file would silently clobber. Extend the detection to accept both roots. Update the how-to guide to use `pure: false` (matching the tutorial's running example and real-world scientific-Python projects), and add a tests/packages/dist-info-sboms-platlib/ fixture that builds with `pure: false` so the platlib path is exercised in CI. --- docs/how-to-guides/sboms.rst | 9 +++++++-- mesonpy/__init__.py | 15 +++++++++------ .../packages/dist-info-sboms-platlib/meson.build | 15 +++++++++++++++ .../dist-info-sboms-platlib/pyproject.toml | 11 +++++++++++ .../dist-info-sboms-platlib/sboms/static.cdx.json | 7 +++++++ tests/test_wheel.py | 14 ++++++++++++++ 6 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 tests/packages/dist-info-sboms-platlib/meson.build create mode 100644 tests/packages/dist-info-sboms-platlib/pyproject.toml create mode 100644 tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst index e0ab0fcf..9fee75df 100644 --- a/docs/how-to-guides/sboms.rst +++ b/docs/how-to-guides/sboms.rst @@ -30,9 +30,9 @@ source-vendored components): .. code-block:: meson - project('my-project', version: '1.0.0') + project('my-project', 'c', version: '1.0.0') - py = import('python').find_installation() + py = import('python').find_installation(pure: false) distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' install_data( @@ -41,6 +41,11 @@ source-vendored components): install_dir: py.get_install_dir() / distinfo / 'sboms', ) +``py.get_install_dir()`` returns a path under ``{py_purelib}`` for +``pure: true`` projects and ``{py_platlib}`` for ``pure: false`` +(the common case when shipping C extensions). ``meson-python`` +recognises the distinfo prefix under either root. + The files end up in the wheel at ``my_project-1.0.0.dist-info/sboms/component1.cdx.json`` and ``component2.cdx.json``. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index cce91397..ce7aa044 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -172,13 +172,16 @@ def _map_to_wheel( if path is None: raise BuildError(f'Could not map installation path to an equivalent wheel directory: {str(destination)!r}') - # Files staged under {py_purelib}//... are routed - # into the wheel's .dist-info/ at pack time. Authority for the - # distinfo dir name is the PEP 621 metadata (distinfo_dir); the - # user's meson.build can write the name with either hyphens or - # underscores and we compare canonically. + # Files staged under {py_purelib}//... or + # {py_platlib}//... are routed into the wheel's + # .dist-info/ at pack time. Both roots are recognised because a + # project built with pure: false installs into platlib (pandas, + # numpy, scipy) while a pure-Python project uses purelib. + # Authority for the distinfo dir name is the PEP 621 metadata; + # the user's meson.build can write the name with either hyphens + # or underscores and we compare canonically. if ( - path == 'purelib' + path in ('purelib', 'platlib') and dst.parts and _canonicalize_distinfo(dst.parts[0]) == canonical_distinfo ): diff --git a/tests/packages/dist-info-sboms-platlib/meson.build b/tests/packages/dist-info-sboms-platlib/meson.build new file mode 100644 index 00000000..d5a140d4 --- /dev/null +++ b/tests/packages/dist-info-sboms-platlib/meson.build @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('dist-info-sboms-platlib', version: '1.0.0') + +# pure: false exercises the platlib routing path. Projects shipping +# C extensions (pandas, numpy, scipy) all use this; the distinfo +# prefix detection must work for both purelib and platlib roots. +py = import('python').find_installation(pure: false) + +distinfo = meson.project_name() + '-' + meson.project_version() + '.dist-info' + +install_data('sboms/static.cdx.json', + install_dir: py.get_install_dir() / distinfo / 'sboms') diff --git a/tests/packages/dist-info-sboms-platlib/pyproject.toml b/tests/packages/dist-info-sboms-platlib/pyproject.toml new file mode 100644 index 00000000..1525f4e4 --- /dev/null +++ b/tests/packages/dist-info-sboms-platlib/pyproject.toml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'dist-info-sboms-platlib' +version = '1.0.0' diff --git a/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json b/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json new file mode 100644 index 00000000..8a8fd07c --- /dev/null +++ b/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "components": [] +} diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 4889635f..fce268f0 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -179,6 +179,20 @@ def test_dist_info_sboms(wheel_dist_info_sboms): assert b'CycloneDX' in generated_sbom +def test_dist_info_sboms_platlib(wheel_dist_info_sboms_platlib): + # Covers `pure: false` projects (pandas, numpy, scipy): meson's + # install plan emits {py_platlib}//... rather than + # {py_purelib}/..., and the distinfo-prefix detection has to fire + # for both roots. Without that coverage the file can land at the + # right wheel path by accident but bypass the collision check. + artifact = wheel.wheelfile.WheelFile(wheel_dist_info_sboms_platlib) + names = wheel_contents(artifact) + # The wheel is a platlib wheel (cpNN-abi-plat tag); its .dist-info + # directory name may vary with python interpreter tag, so check + # the payload by suffix. + assert any(n.endswith('.dist-info/sboms/static.cdx.json') for n in names), names + + @pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression') def test_dist_info_sboms_collision(package_dist_info_sboms_collision, tmp_path): # Verifies the collision check catches the case where a file routed From 9cec754e15338dd28068cf9c1e8107dcc8622a7e Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 22:07:38 -0700 Subject: [PATCH 07/15] Update changelog entry to reflect platlib support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier draft only mentioned {py_purelib} staging; the actual routing handles both {py_purelib} and {py_platlib}, which is what real scientific-Python projects (pandas, numpy, scipy — all pure: false) rely on. Also mention the canonical-name comparison and the pure-Python case. --- CHANGELOG.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbb14d9c..70a02446 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,12 +15,16 @@ Unreleased ========== - Route files staged under ``{py_purelib}/-.dist-info/...`` - into the wheel's ``.dist-info/`` directory at pack time. This is the - recommended mechanism for placing PEP 770 SBOMs and other dist-info-bound - metadata files in the wheel, removing the need for post-build wheel - surgery. Works with both static ``install_data()`` and dynamic - ``custom_target(install: true)``; requires no new meson API. The collision - check surfaces overlaps between files routed here and PEP 639 + or ``{py_platlib}/-.dist-info/...`` into the wheel's + ``.dist-info/`` directory at pack time. This is the recommended mechanism + for placing PEP 770 SBOMs and other dist-info-bound metadata files in the + wheel, removing the need for post-build wheel surgery. Works with both + static ``install_data()`` and dynamic ``custom_target(install: true)``, + and with ``pure: false`` projects (which stage under ``{py_platlib}``) + as well as pure-Python packages. Requires no new meson API. The distinfo + directory name is compared canonically, so hyphens vs. underscores in the + user's ``meson.project_name()`` don't break routing. The existing + collision check surfaces overlaps between files routed here and PEP 639 ``license-files`` written by the metadata layer. Routing is enforced for non-editable wheels; editable wheels do not include dist-info-bound files (a known limitation). From 372f344d1d71a0ceb3f44535be8f216c892d1f3f Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 22:22:05 -0700 Subject: [PATCH 08/15] Polish SBOMs how-to for consistency with the rest of docs/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stylistic pass so the new page reads like its neighbors: - Condense the 4-paragraph intro to one paragraph. - Move the "files end up in the wheel at…" sentence to right after the static-files code block, where it belongs in the reader's flow. - Fold the canonical-name-match note into the "File naming and validation" bullets rather than giving it its own standalone paragraph. - Drop the scientific-Python project name-drop; the page is meant to be generic, other how-to guides don't namecheck downstream users. - Normalize "artefact" / "recognised" to the American spelling the rest of docs/ use. --- docs/how-to-guides/sboms.rst | 83 +++++++++++++++++------------------- mesonpy/__init__.py | 2 +- 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst index 9fee75df..90d7f0e6 100644 --- a/docs/how-to-guides/sboms.rst +++ b/docs/how-to-guides/sboms.rst @@ -8,25 +8,22 @@ Including SBOMs in wheels (PEP 770) *********************************** -`PEP 770`_ specifies that wheels may carry Software Bill of Materials -(SBOM) documents under the ``.dist-info/sboms/`` directory. -``meson-python`` places any file installed under -``{py_purelib}/-.dist-info//...`` into the -wheel's ``.dist-info//`` at pack time. This is the mechanism -scientific-Python projects (pandas, NumPy, SciPy, scikit-learn) use to -ship SBOMs in their wheels without post-build wheel surgery. - -The ``-.dist-info`` directory name is recognised -regardless of whether the user wrote the project name with hyphens or -underscores — the comparison is canonicalised. +`PEP 770`_ defines a location for Software Bill of Materials (SBOM) +files inside the wheel's ``.dist-info/sboms/`` directory. +``meson-python`` routes any file installed under +``{py_purelib}/-.dist-info//...`` or +``{py_platlib}/-.dist-info//...`` into the +wheel's own ``.dist-info//`` at pack time, giving projects a +way to ship SBOMs and other dist-info-bound metadata files without +post-build wheel surgery. .. _PEP 770: https://peps.python.org/pep-0770/ Static SBOM files ================= -For SBOMs that are checked into the source tree (typically describing -source-vendored components): +For SBOMs that are checked into the source tree, typically describing +source-vendored components: .. code-block:: meson @@ -41,21 +38,17 @@ source-vendored components): install_dir: py.get_install_dir() / distinfo / 'sboms', ) -``py.get_install_dir()`` returns a path under ``{py_purelib}`` for -``pure: true`` projects and ``{py_platlib}`` for ``pure: false`` -(the common case when shipping C extensions). ``meson-python`` -recognises the distinfo prefix under either root. - The files end up in the wheel at ``my_project-1.0.0.dist-info/sboms/component1.cdx.json`` and -``component2.cdx.json``. +``component2.cdx.json``. ``py.get_install_dir()`` returns a path under +``{py_purelib}`` for ``pure: true`` projects and ``{py_platlib}`` for +``pure: false`` projects; both roots are recognized. Dynamically generated SBOMs =========================== -Many projects generate the SBOM at build time from a TOML manifest of -vendored components. Use a ``custom_target`` that produces the SBOM file -and installs it to the same location: +When the SBOM is generated at build time, use a ``custom_target`` that +writes the file and installs it to the same location: .. code-block:: meson @@ -67,36 +60,38 @@ and installs it to the same location: install_dir: py.get_install_dir() / distinfo / 'sboms', ) -The generator runs during the build, and ``meson-python`` injects the -output into ``my_project-1.0.0.dist-info/sboms/vendored.cdx.json``. +The generator runs during the build, and the output is routed into +``my_project-1.0.0.dist-info/sboms/vendored.cdx.json``. Other ``.dist-info`` subdirectories =================================== -Any subdirectory under the ``.dist-info`` directory works the same way. -PEP 639 license files can be placed under ``licenses``, for example: +Any subdirectory works the same way. Additional PEP 639 license files, +for example, can go under ``licenses``: .. code-block:: meson install_data('LICENSES/extra.txt', install_dir: py.get_install_dir() / distinfo / 'licenses') -PEP 639 license files declared via ``project.license-files`` in -``pyproject.toml`` are handled separately and go into -``.dist-info/licenses/`` automatically (no ``meson.build`` change -needed). Use the pattern above only for additional license files -outside the standard ``project.license-files`` list. +License files declared via ``project.license-files`` in +``pyproject.toml`` are already placed in ``.dist-info/licenses/`` +automatically and do not need a ``meson.build`` entry. Use the pattern +above only for additional files outside the standard +``project.license-files`` list. File naming and validation ========================== -* Files installed under ``-.dist-info//`` - must have unique basenames within their subdirectory. ``meson-python`` - raises an error at build time if two files would write to the same - path inside ``.dist-info//``. The collision check covers - files routed via this mechanism and PEP 639 license-files from - ``project.license-files``. -* The recommended file extensions are ``.cdx.json`` for CycloneDX and +* Files installed under ``-.dist-info//`` must + have unique basenames within their subdirectory. ``meson-python`` + raises a ``BuildError`` at build time if two files would write to + the same path, including collisions between files routed through + this mechanism and files written from ``project.license-files``. +* The ``-.dist-info`` directory name is matched + canonically, so hyphens and underscores in the user's project name + do not break routing. +* Recommended file extensions are ``.cdx.json`` for CycloneDX and ``.spdx.json`` for SPDX, per the PSF `SBOMs for Python packages`_ proposal. @@ -105,10 +100,8 @@ File naming and validation Editable installs ================= -Files staged via this mechanism are placed in the wheel when -``meson-python`` builds a regular wheel (``pip install .`` or -``python -m build``). They are **not** included in editable wheels -(``pip install -e .``), because editable wheels redirect imports to the -build directory rather than carrying project files. SBOMs are intended -for distribution artefacts, so this limitation generally does not -affect development workflows. +Files staged via this mechanism are only placed in non-editable wheels +(``pip install .`` or ``python -m build``). Editable wheels +(``pip install -e .``) redirect imports to the build directory and do +not carry dist-info-bound payloads. Since SBOMs are distribution +artifacts, this limitation does not affect development workflows. diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index ce7aa044..71bfa2a0 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -174,7 +174,7 @@ def _map_to_wheel( # Files staged under {py_purelib}//... or # {py_platlib}//... are routed into the wheel's - # .dist-info/ at pack time. Both roots are recognised because a + # .dist-info/ at pack time. Both roots are recognized because a # project built with pure: false installs into platlib (pandas, # numpy, scipy) while a pure-Python project uses purelib. # Authority for the distinfo dir name is the PEP 621 metadata; From ec05306e210d4272384700a17dcb0cd48300bd06 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 22:29:16 -0700 Subject: [PATCH 09/15] Trim changelog entry to match neighbor style --- CHANGELOG.rst | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70a02446..5919e943 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,20 +14,10 @@ Changelog Unreleased ========== -- Route files staged under ``{py_purelib}/-.dist-info/...`` - or ``{py_platlib}/-.dist-info/...`` into the wheel's - ``.dist-info/`` directory at pack time. This is the recommended mechanism - for placing PEP 770 SBOMs and other dist-info-bound metadata files in the - wheel, removing the need for post-build wheel surgery. Works with both - static ``install_data()`` and dynamic ``custom_target(install: true)``, - and with ``pure: false`` projects (which stage under ``{py_platlib}``) - as well as pure-Python packages. Requires no new meson API. The distinfo - directory name is compared canonically, so hyphens vs. underscores in the - user's ``meson.project_name()`` don't break routing. The existing - collision check surfaces overlaps between files routed here and PEP 639 - ``license-files`` written by the metadata layer. Routing is enforced for - non-editable wheels; editable wheels do not include dist-info-bound files - (a known limitation). +- Add support for including PEP 770 SBOMs and other dist-info-bound metadata + files in the wheel. Files installed to ``-.dist-info//`` + under ``{py_purelib}`` or ``{py_platlib}`` are routed into the wheel's + ``.dist-info//`` at pack time. 0.19.0 From 38c25dd42c308af643ace5f538176369c6b0c3a4 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 22:36:37 -0700 Subject: [PATCH 10/15] Inline the license-file collision check, drop the _record closure Minor cleanup. The nested _record helper saved a handful of lines of duplication but read as extra structure for something already small. Write the check inline at each of the two sites (pre-PEP-639 license file, PEP-639 license-files loop). Also drop the Optional typing on distinfo_seen since no caller passes None, and update the error message to no longer reference the python.dist_info_install_dir() helper (which was removed earlier in this branch). --- mesonpy/__init__.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 71bfa2a0..1503abba 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -507,7 +507,7 @@ def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, def _wheel_write_metadata( self, whl: mesonpy._wheelfile.WheelFile, - distinfo_seen: Optional[Dict[str, str]] = None, + distinfo_seen: Dict[str, str], ) -> None: # add metadata whl.writestr(f'{self._distinfo_dir}/METADATA', bytes(self._metadata.as_rfc822())) @@ -515,25 +515,17 @@ def _wheel_write_metadata( if self.entrypoints_txt: whl.writestr(f'{self._distinfo_dir}/entry_points.txt', self.entrypoints_txt) - def _record(target: str, origin: str) -> None: - if distinfo_seen is None: - return - previous = distinfo_seen.get(target) - if previous is not None: - raise BuildError( - f'Two files would be installed to {target!r} in the ' - f'wheel: {previous!r} and {origin!r}. Files placed in ' - f'.dist-info/ must have unique basenames within their ' - f'subdirectory; check your project.license-files list ' - f'and any python.dist_info_install_dir() uses.') - distinfo_seen[target] = origin - # Add pre-PEP-639 license files. if isinstance(self._metadata.license, pyproject_metadata.License): license_file = self._metadata.license.file if license_file: target = f'{self._distinfo_dir}/{os.path.basename(license_file)}' - _record(target, str(license_file)) + if target in distinfo_seen: + raise BuildError( + f'Two files would be installed to {target!r} in the wheel: ' + f'{distinfo_seen[target]!r} and {str(license_file)!r}. ' + f'Files placed in .dist-info/ must have unique paths.') + distinfo_seen[target] = str(license_file) whl.write(license_file, target) # Add PEP-639 license-files. Use ``getattr()`` for compatibility with pyproject-metadata < 0.9.0. @@ -541,7 +533,12 @@ def _record(target: str, origin: str) -> None: if license_files: for f in license_files: target = f'{self._distinfo_dir}/licenses/{pathlib.Path(f).as_posix()}' - _record(target, str(f)) + if target in distinfo_seen: + raise BuildError( + f'Two files would be installed to {target!r} in the wheel: ' + f'{distinfo_seen[target]!r} and {str(f)!r}. ' + f'Files placed in .dist-info/ must have unique paths.') + distinfo_seen[target] = str(f) whl.write(f, target) def build(self, directory: Path) -> pathlib.Path: From a9b42868662c16085d263c5e1fa0f05adcbd6959 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 22:53:26 -0700 Subject: [PATCH 11/15] Simplify docstrings and comments Trim the explanatory prose around the dist-info routing to the minimum the code doesn't already convey: - Revert _map_to_wheel's docstring to the original one-liner; the distinfo_dir parameter is self-evident from the signature and usage. - Shrink _canonicalize_distinfo's docstring to one line. - Compress the "route /... under purelib/platlib" comment from eight lines to two. - Compress the "collision tracker" comment in build() from five lines to one, dropping the now-stale reference to the removed python.dist_info_install_dir() helper. - Align the collision error message in the distinfo branch with the one in _wheel_write_metadata ("unique paths" rather than "unique basenames within their subdirectory"). --- mesonpy/__init__.py | 46 +++++++++------------------------------------ 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 1503abba..6df2338c 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -127,13 +127,7 @@ class _Entry(typing.NamedTuple): def _canonicalize_distinfo(dir_name: str) -> str: - """Canonicalize a `-.dist-info` directory for equality - comparison. Collapses runs of ``-``, ``_``, ``.`` to a single ``-`` and - lowercases, so that e.g. ``dist-info-sboms-1.0.dist-info`` and - ``dist_info_sboms-1.0.dist-info`` compare equal. PEP 491 uses ``_`` as - the separator while users typing ``meson.project_name()`` often carry - hyphens from their original name. - """ + """Canonical form for .dist-info directory equality comparison.""" return re.sub(r'[-_.]+', '-', dir_name).lower() @@ -142,15 +136,7 @@ def _map_to_wheel( exclude: List[str], include: List[str], distinfo_dir: str, ) -> DefaultDict[str, List[_Entry]]: - """Map files to the wheel, organized by wheel installation directory. - - ``distinfo_dir`` is the ``-.dist-info`` directory name this - wheel will carry, derived from PEP 621 metadata. Files staged under - ``{py_purelib}//...`` are rerouted into the wheel's - ``.dist-info/`` at pack time — this is the mechanism projects use to - place PEP 770 SBOMs and other dist-info-bound metadata files in the - wheel. - """ + """Map files to the wheel, organized by wheel installation directory.""" wheel_files: DefaultDict[str, List[_Entry]] = collections.defaultdict(list) packages: Dict[str, str] = {} excluded = _compile_patterns(exclude) @@ -172,14 +158,8 @@ def _map_to_wheel( if path is None: raise BuildError(f'Could not map installation path to an equivalent wheel directory: {str(destination)!r}') - # Files staged under {py_purelib}//... or - # {py_platlib}//... are routed into the wheel's - # .dist-info/ at pack time. Both roots are recognized because a - # project built with pure: false installs into platlib (pandas, - # numpy, scipy) while a pure-Python project uses purelib. - # Authority for the distinfo dir name is the PEP 621 metadata; - # the user's meson.build can write the name with either hyphens - # or underscores and we compare canonically. + # Route /... staged under purelib or platlib into + # the wheel's .dist-info/. if ( path in ('purelib', 'platlib') and dst.parts @@ -544,11 +524,7 @@ def _wheel_write_metadata( def build(self, directory: Path) -> pathlib.Path: wheel_file = pathlib.Path(directory, f'{self.name}.whl') with mesonpy._wheelfile.WheelFile(wheel_file, 'w') as whl: - # Track files written under .dist-info/ to surface collisions - # (two files at the same wheel path would silently clobber - # each other in the archive). Populated by both - # _wheel_write_metadata (license files) and the manifest loop - # below (files routed via python.dist_info_install_dir()). + # Collision tracker for files written under .dist-info/. distinfo_seen: Dict[str, str] = {} self._wheel_write_metadata(whl, distinfo_seen) @@ -566,16 +542,12 @@ def build(self, directory: Path) -> pathlib.Path: # custom installation path for bundled libraries dst = pathlib.Path(self._libs_dir, dst) elif path == 'distinfo': - # files detected under {purelib}//... in - # the install plan; see _map_to_wheel(). target = pathlib.Path(self._distinfo_dir, dst).as_posix() - previous = distinfo_seen.get(target) - if previous is not None: + if target in distinfo_seen: raise BuildError( - f'Two files would be installed to {target!r} ' - f'in the wheel: {previous!r} and {str(src)!r}. ' - f'Files placed in .dist-info/ must have ' - f'unique basenames within their subdirectory.') + f'Two files would be installed to {target!r} in the wheel: ' + f'{distinfo_seen[target]!r} and {str(src)!r}. ' + f'Files placed in .dist-info/ must have unique paths.') distinfo_seen[target] = str(src) dst = pathlib.Path(self._distinfo_dir, dst) else: From bc3c11ffbaba552a612374712831c4ef4c62c7e8 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 23:00:14 -0700 Subject: [PATCH 12/15] Deduplicate the purelib/platlib branch check The distinfo-reroute and same-package-split check both needed path in ('purelib', 'platlib'), done twice in back-to-back branches. Collapse into one outer check with two sub-branches so the condition is stated once. --- mesonpy/__init__.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 6df2338c..2a1eb7e8 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -158,26 +158,22 @@ def _map_to_wheel( if path is None: raise BuildError(f'Could not map installation path to an equivalent wheel directory: {str(destination)!r}') - # Route /... staged under purelib or platlib into - # the wheel's .dist-info/. - if ( - path in ('purelib', 'platlib') - and dst.parts - and _canonicalize_distinfo(dst.parts[0]) == canonical_distinfo - ): - path = 'distinfo' - dst = pathlib.Path(*dst.parts[1:]) - elif path == 'purelib' or path == 'platlib': - package = destination.parts[1] - other = packages.setdefault(package, path) - if other != path: - this = os.fspath(pathlib.Path(path, *destination.parts[1:])) - module = next(entry.dst for entry in wheel_files[other] if entry.dst.parts[0] == destination.parts[1]) - that = os.fspath(other / module) - raise BuildError( - f'The {package} package is split between {path} and {other}: ' - f'{this!r} and {that!r}, a "pure: false" argument may be missing in meson.build. ' - f'It is recommended to set it in "import(\'python\').find_installation()"') + if path in ('purelib', 'platlib'): + if dst.parts and _canonicalize_distinfo(dst.parts[0]) == canonical_distinfo: + # Route /... into the wheel's .dist-info/. + path = 'distinfo' + dst = pathlib.Path(*dst.parts[1:]) + else: + package = destination.parts[1] + other = packages.setdefault(package, path) + if other != path: + this = os.fspath(pathlib.Path(path, *destination.parts[1:])) + module = next(entry.dst for entry in wheel_files[other] if entry.dst.parts[0] == destination.parts[1]) + that = os.fspath(other / module) + raise BuildError( + f'The {package} package is split between {path} and {other}: ' + f'{this!r} and {that!r}, a "pure: false" argument may be missing in meson.build. ' + f'It is recommended to set it in "import(\'python\').find_installation()"') if key == 'install_subdirs' or key == 'targets' and os.path.isdir(src): exclude_files = {os.path.normpath(x) for x in target.get('exclude_files', [])} From b24415b933361841720053ce32dc16c8b502466c Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Thu, 23 Apr 2026 23:51:31 -0700 Subject: [PATCH 13/15] Clarify where the SBOM generator script lives The Dynamically generated SBOMs section showed ``files('scripts/generate_sbom.py')`` without explaining that the script is project-authored (not provided by meson-python) and that its location is arbitrary. Add a short note pointing users at the PSF SBOMs for Python packages proposal for guidance on writing generators, and noting ``scripts/`` as a common convention. --- docs/how-to-guides/sboms.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst index 90d7f0e6..f3f7349d 100644 --- a/docs/how-to-guides/sboms.rst +++ b/docs/how-to-guides/sboms.rst @@ -63,6 +63,13 @@ writes the file and installs it to the same location: The generator runs during the build, and the output is routed into ``my_project-1.0.0.dist-info/sboms/vendored.cdx.json``. +The generator is provided by the project; ``meson-python`` does not +ship one. It can be a script checked into the source tree +(``scripts/`` is a common convention) or a third-party generator +installed via ``[build-system] requires``. For guidance on generator +implementations and the SBOM format itself, see the PSF +`SBOMs for Python packages`_ proposal. + Other ``.dist-info`` subdirectories =================================== From f32ed4f0d78d43ed1734f37b5a95b9bb024ca714 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Fri, 24 Apr 2026 00:23:56 -0700 Subject: [PATCH 14/15] Fix pre-commit failures: ruff import spacing + SPDX sidecars pre-commit.ci on #843 surfaced two issues: - ruff I001: generate.py was missing the blank line required by the repo's lines-after-imports = 2 isort setting. - reuse lint: extra.txt had no copyright/licensing information. Adding sidecar .license files is the repo's established pattern for plain-text fixtures (see tests/packages/install-data/data.txt.license). Also add sidecars for the two static.cdx.json fixtures for good measure -- CI didn't flag them but local reuse runs do. Both are one-line sidecars matching the rest of the repo's style. --- .../dist-info-sboms-platlib/sboms/static.cdx.json.license | 3 +++ tests/packages/dist-info-sboms/generate.py | 1 + tests/packages/dist-info-sboms/sboms/extra.txt.license | 3 +++ tests/packages/dist-info-sboms/sboms/static.cdx.json.license | 3 +++ 4 files changed, 10 insertions(+) create mode 100644 tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json.license create mode 100644 tests/packages/dist-info-sboms/sboms/extra.txt.license create mode 100644 tests/packages/dist-info-sboms/sboms/static.cdx.json.license diff --git a/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json.license b/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json.license new file mode 100644 index 00000000..775d000c --- /dev/null +++ b/tests/packages/dist-info-sboms-platlib/sboms/static.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/dist-info-sboms/generate.py b/tests/packages/dist-info-sboms/generate.py index 56635572..ffea484c 100644 --- a/tests/packages/dist-info-sboms/generate.py +++ b/tests/packages/dist-info-sboms/generate.py @@ -7,6 +7,7 @@ import json import sys + with open(sys.argv[1], 'w') as f: json.dump({ 'bomFormat': 'CycloneDX', diff --git a/tests/packages/dist-info-sboms/sboms/extra.txt.license b/tests/packages/dist-info-sboms/sboms/extra.txt.license new file mode 100644 index 00000000..775d000c --- /dev/null +++ b/tests/packages/dist-info-sboms/sboms/extra.txt.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/dist-info-sboms/sboms/static.cdx.json.license b/tests/packages/dist-info-sboms/sboms/static.cdx.json.license new file mode 100644 index 00000000..775d000c --- /dev/null +++ b/tests/packages/dist-info-sboms/sboms/static.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT From 2aac893cb4a64f8669b68fe9e6bea142deb31d9c Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Sun, 26 Apr 2026 21:58:28 -0700 Subject: [PATCH 15/15] update doc --- CHANGELOG.rst | 6 +++--- docs/how-to-guides/sboms.rst | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5919e943..75ace44b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,9 +15,9 @@ Unreleased ========== - Add support for including PEP 770 SBOMs and other dist-info-bound metadata - files in the wheel. Files installed to ``-.dist-info//`` - under ``{py_purelib}`` or ``{py_platlib}`` are routed into the wheel's - ``.dist-info//`` at pack time. + files in the wheel. Files installed to + ``py.get_install_dir() / -.dist-info / `` are + routed into the wheel's ``.dist-info//`` at pack time. 0.19.0 diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst index f3f7349d..bb7147c9 100644 --- a/docs/how-to-guides/sboms.rst +++ b/docs/how-to-guides/sboms.rst @@ -10,11 +10,10 @@ Including SBOMs in wheels (PEP 770) `PEP 770`_ defines a location for Software Bill of Materials (SBOM) files inside the wheel's ``.dist-info/sboms/`` directory. -``meson-python`` routes any file installed under -``{py_purelib}/-.dist-info//...`` or -``{py_platlib}/-.dist-info//...`` into the -wheel's own ``.dist-info//`` at pack time, giving projects a -way to ship SBOMs and other dist-info-bound metadata files without +``meson-python`` routes any file installed to +``py.get_install_dir() / -.dist-info / `` into +the wheel's own ``.dist-info//`` at pack time, giving projects +a way to ship SBOMs and other dist-info-bound metadata files without post-build wheel surgery. .. _PEP 770: https://peps.python.org/pep-0770/ @@ -40,9 +39,7 @@ source-vendored components: The files end up in the wheel at ``my_project-1.0.0.dist-info/sboms/component1.cdx.json`` and -``component2.cdx.json``. ``py.get_install_dir()`` returns a path under -``{py_purelib}`` for ``pure: true`` projects and ``{py_platlib}`` for -``pure: false`` projects; both roots are recognized. +``component2.cdx.json``. Dynamically generated SBOMs ===========================