From 4de4287aadd81d8b6d7d2744a140af3a19d5d1d2 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Mon, 6 Jul 2026 12:38:04 -0700 Subject: [PATCH 1/2] ENH: add support for PEP 770 SBOM files Add a tool.meson-python.sbom-files setting listing glob patterns matched against the file installation paths as they appear in the Meson introspection data. Matching files are moved to the .dist-info/sboms/ directory in the wheel, the location defined by PEP 770 for SBOM documents describing the package contents. The default pattern is '{datadir}/{name}/sboms/*', with {name} the normalized project name. Anchoring the patterns in the data directory means that, when the Meson project is installed directly rather than through a Python build front-end, the SBOM files are installed as regular data files and no spurious .dist-info directory is created. Co-Authored-By: Claude Fable 5 --- CHANGELOG.rst | 9 ++++ docs/reference/pyproject-settings.rst | 14 ++++++ mesonpy/__init__.py | 26 ++++++++++- .../sbom-files-collision/a/dup.cdx.json | 6 +++ .../a/dup.cdx.json.license | 3 ++ .../sbom-files-collision/b/dup.cdx.json | 6 +++ .../b/dup.cdx.json.license | 3 ++ .../packages/sbom-files-collision/meson.build | 10 ++++ .../sbom-files-collision/pyproject.toml | 14 ++++++ tests/packages/sbom-files-custom/meson.build | 18 ++++++++ .../packages/sbom-files-custom/pyproject.toml | 14 ++++++ .../sbom-files-custom/sboms/custom.cdx.json | 7 +++ .../sboms/custom.cdx.json.license | 3 ++ .../sbom-files-custom/sboms/other.cdx.json | 7 +++ .../sboms/other.cdx.json.license | 3 ++ .../sbom-files-custom/sboms/readme.txt | 1 + .../sboms/readme.txt.license | 3 ++ tests/packages/sbom-files/data.txt | 1 + tests/packages/sbom-files/data.txt.license | 3 ++ tests/packages/sbom-files/generate.py | 17 +++++++ tests/packages/sbom-files/meson.build | 25 ++++++++++ tests/packages/sbom-files/pyproject.toml | 11 +++++ .../packages/sbom-files/sboms/static.cdx.json | 7 +++ .../sbom-files/sboms/static.cdx.json.license | 3 ++ tests/test_wheel.py | 46 +++++++++++++++++++ 25 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 tests/packages/sbom-files-collision/a/dup.cdx.json create mode 100644 tests/packages/sbom-files-collision/a/dup.cdx.json.license create mode 100644 tests/packages/sbom-files-collision/b/dup.cdx.json create mode 100644 tests/packages/sbom-files-collision/b/dup.cdx.json.license create mode 100644 tests/packages/sbom-files-collision/meson.build create mode 100644 tests/packages/sbom-files-collision/pyproject.toml create mode 100644 tests/packages/sbom-files-custom/meson.build create mode 100644 tests/packages/sbom-files-custom/pyproject.toml create mode 100644 tests/packages/sbom-files-custom/sboms/custom.cdx.json create mode 100644 tests/packages/sbom-files-custom/sboms/custom.cdx.json.license create mode 100644 tests/packages/sbom-files-custom/sboms/other.cdx.json create mode 100644 tests/packages/sbom-files-custom/sboms/other.cdx.json.license create mode 100644 tests/packages/sbom-files-custom/sboms/readme.txt create mode 100644 tests/packages/sbom-files-custom/sboms/readme.txt.license create mode 100644 tests/packages/sbom-files/data.txt create mode 100644 tests/packages/sbom-files/data.txt.license create mode 100644 tests/packages/sbom-files/generate.py create mode 100644 tests/packages/sbom-files/meson.build create mode 100644 tests/packages/sbom-files/pyproject.toml create mode 100644 tests/packages/sbom-files/sboms/static.cdx.json create mode 100644 tests/packages/sbom-files/sboms/static.cdx.json.license diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5da6e033d..fa0b1f10c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,15 @@ Changelog +++++++++ +Unreleased +========== + +- Add support for PEP 770 SBOM files. Files installed at a location matching + one of the glob patterns in the ``tool.meson-python.sbom-files`` setting, + by default the ``sboms/`` subdirectory of the project's data directory, + are moved to the ``.dist-info/sboms/`` directory in the wheel. + + 0.20.0 ====== diff --git a/docs/reference/pyproject-settings.rst b/docs/reference/pyproject-settings.rst index 0dab117b9..db6f09ce9 100644 --- a/docs/reference/pyproject-settings.rst +++ b/docs/reference/pyproject-settings.rst @@ -70,6 +70,19 @@ use them and examples. Extra arguments to be passed to the ``meson install`` command. +.. option:: tool.meson-python.sbom-files + + List of glob patterns matching paths of `PEP 770`_ SBOM files. Files + installed by the Meson project at a location matching one of the + patterns are moved to the ``sboms/`` subdirectory of the wheel's + ``.dist-info/`` directory. The accepted glob patterns and the paths + they are matched against are the same as for + :option:`tool.meson-python.wheel.exclude`. It defaults to + ``['{datadir}/{name}/sboms/*']``, with ``{name}`` the normalized + project name. Set it to an empty list to disable the special + handling of SBOM files. See the :ref:`how-to-guides-sboms` guide + for details. + .. option:: tool.meson-python.wheel.exclude List of glob patterns matching paths of files that must be excluded from @@ -99,6 +112,7 @@ use them and examples. pattern that matches too many files. .. _python limited api: https://docs.python.org/3/c-api/stable.html?highlight=limited%20api#stable-application-binary-interface +.. _pep 770: https://peps.python.org/pep-0770/ .. _extension_module(): `https://mesonbuild.com/Python-module.html#extension_module .. _meson introspection data: https://mesonbuild.com/IDE-integration.html#install-plan diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 3281bb1d5..282a92981 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -131,13 +131,15 @@ class _Entry(typing.NamedTuple): def _map_to_wheel( sources: Dict[str, Dict[str, Any]], - exclude: List[str], include: List[str] + exclude: List[str], include: List[str], + sbom_files: List[str], ) -> DefaultDict[str, List[_Entry]]: """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) included = _compile_patterns(include) + is_sbom = _compile_patterns(sbom_files) for key, group in sources.items(): for src, target in group.items(): @@ -182,8 +184,13 @@ def _map_to_wheel( relpath = os.path.relpath(filesrc, src) if relpath in exclude_files: continue + if is_sbom(os.path.join(target_destination, relpath)): + wheel_files['mesonpy-sboms'].append(_Entry(pathlib.Path(name), filesrc)) + continue filedst = dst / relpath wheel_files[path].append(_Entry(filedst, filesrc)) + elif is_sbom(target_destination): + wheel_files['mesonpy-sboms'].append(_Entry(pathlib.Path(destination.name), src)) else: wheel_files[path].append(_Entry(dst, src)) @@ -505,6 +512,7 @@ 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' + sboms: Dict[str, str] = {} for path, entries in self._manifest.items(): for dst, src in entries: @@ -515,6 +523,15 @@ 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 == 'mesonpy-sboms': + # PEP-770 SBOM files go into the .dist-info/sboms/ directory + dst = pathlib.Path(self._distinfo_dir, 'sboms', dst) + other = sboms.setdefault(dst.as_posix(), str(src)) + if other != str(src): + raise BuildError( + f'Two files matching "tool.meson-python.sbom-files" patterns ' + f'would be installed at {dst.as_posix()!r} in the wheel: ' + f'{other!r} and {str(src)!r}') else: dst = pathlib.Path(self._data_dir, path, dst) @@ -606,6 +623,7 @@ def _string_or_path(value: Any, name: str) -> str: 'limited-api': _bool, 'allow-windows-internal-shared-libs': _bool, 'args': _table(dict.fromkeys(_MESON_ARGS_KEYS, _strings)), + 'sbom-files': _strings, 'wheel': _table({ 'exclude': _strings, 'include': _strings, @@ -910,6 +928,10 @@ def __init__( self._excluded_files = pyproject_config.get('wheel', {}).get('exclude', []) self._included_files = pyproject_config.get('wheel', {}).get('include', []) + # PEP-770 SBOM files to be moved to the .dist-info/sboms/ directory in the wheel + self._sbom_files = pyproject_config.get( + 'sbom-files', ['{datadir}/' + self._metadata.canonical_name + '/sboms/*']) + def _run(self, cmd: Sequence[str]) -> None: """Invoke a subprocess.""" # Flush the line to ensure that the log line with the executed @@ -996,7 +1018,7 @@ def _manifest(self) -> DefaultDict[str, List[_Entry]]: sources[key][target] = details # Map Meson installation locations to wheel paths. - return _map_to_wheel(sources, self._excluded_files, self._included_files) + return _map_to_wheel(sources, self._excluded_files, self._included_files, self._sbom_files) @property def _meson_name(self) -> str: diff --git a/tests/packages/sbom-files-collision/a/dup.cdx.json b/tests/packages/sbom-files-collision/a/dup.cdx.json new file mode 100644 index 000000000..f3fb56c58 --- /dev/null +++ b/tests/packages/sbom-files-collision/a/dup.cdx.json @@ -0,0 +1,6 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "components": [] +} diff --git a/tests/packages/sbom-files-collision/a/dup.cdx.json.license b/tests/packages/sbom-files-collision/a/dup.cdx.json.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files-collision/a/dup.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files-collision/b/dup.cdx.json b/tests/packages/sbom-files-collision/b/dup.cdx.json new file mode 100644 index 000000000..d0c17909f --- /dev/null +++ b/tests/packages/sbom-files-collision/b/dup.cdx.json @@ -0,0 +1,6 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 2, + "components": [] +} diff --git a/tests/packages/sbom-files-collision/b/dup.cdx.json.license b/tests/packages/sbom-files-collision/b/dup.cdx.json.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files-collision/b/dup.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files-collision/meson.build b/tests/packages/sbom-files-collision/meson.build new file mode 100644 index 000000000..5bf048353 --- /dev/null +++ b/tests/packages/sbom-files-collision/meson.build @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('sbom-files-collision', version: '1.0.0') + +# Both files match a "tool.meson-python.sbom-files" pattern and have +# the same name: moving both to .dist-info/sboms/ must raise an error. +install_data('a/dup.cdx.json', install_dir: get_option('datadir') / 'a') +install_data('b/dup.cdx.json', install_dir: get_option('datadir') / 'b') diff --git a/tests/packages/sbom-files-collision/pyproject.toml b/tests/packages/sbom-files-collision/pyproject.toml new file mode 100644 index 000000000..c188c6fb3 --- /dev/null +++ b/tests/packages/sbom-files-collision/pyproject.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'sbom-files-collision' +version = '1.0.0' + +[tool.meson-python] +sbom-files = ['{datadir}/a/*', '{datadir}/b/*'] diff --git a/tests/packages/sbom-files-custom/meson.build b/tests/packages/sbom-files-custom/meson.build new file mode 100644 index 000000000..1c022899e --- /dev/null +++ b/tests/packages/sbom-files-custom/meson.build @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('sbom-files-custom', version: '1.0.0') + +# Matched by the "tool.meson-python.sbom-files" pattern declared in +# pyproject.toml and thus moved to .dist-info/sboms/ in the wheel. +install_data('sboms/custom.cdx.json', install_dir: get_option('datadir') / 'custom') + +# Not matched: setting "tool.meson-python.sbom-files" replaces the +# default pattern, thus files installed to the default location are +# not treated specially anymore. +install_data('sboms/other.cdx.json', + install_dir: get_option('datadir') / meson.project_name() / 'sboms') + +# Not matched: does not match the *.cdx.json glob. +install_data('sboms/readme.txt', install_dir: get_option('datadir') / 'custom') diff --git a/tests/packages/sbom-files-custom/pyproject.toml b/tests/packages/sbom-files-custom/pyproject.toml new file mode 100644 index 000000000..b7c0876cb --- /dev/null +++ b/tests/packages/sbom-files-custom/pyproject.toml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] + +[project] +name = 'sbom-files-custom' +version = '1.0.0' + +[tool.meson-python] +sbom-files = ['{datadir}/custom/*.cdx.json'] diff --git a/tests/packages/sbom-files-custom/sboms/custom.cdx.json b/tests/packages/sbom-files-custom/sboms/custom.cdx.json new file mode 100644 index 000000000..8a8fd07ca --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/custom.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/packages/sbom-files-custom/sboms/custom.cdx.json.license b/tests/packages/sbom-files-custom/sboms/custom.cdx.json.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/custom.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files-custom/sboms/other.cdx.json b/tests/packages/sbom-files-custom/sboms/other.cdx.json new file mode 100644 index 000000000..8a8fd07ca --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/other.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/packages/sbom-files-custom/sboms/other.cdx.json.license b/tests/packages/sbom-files-custom/sboms/other.cdx.json.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/other.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files-custom/sboms/readme.txt b/tests/packages/sbom-files-custom/sboms/readme.txt new file mode 100644 index 000000000..63b0eaeb7 --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/readme.txt @@ -0,0 +1 @@ +Not an SBOM file; must stay in the wheel data directory. diff --git a/tests/packages/sbom-files-custom/sboms/readme.txt.license b/tests/packages/sbom-files-custom/sboms/readme.txt.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files-custom/sboms/readme.txt.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files/data.txt b/tests/packages/sbom-files/data.txt new file mode 100644 index 000000000..d9938dfc2 --- /dev/null +++ b/tests/packages/sbom-files/data.txt @@ -0,0 +1 @@ +Regular data file that must stay in the wheel data directory. diff --git a/tests/packages/sbom-files/data.txt.license b/tests/packages/sbom-files/data.txt.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files/data.txt.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/sbom-files/generate.py b/tests/packages/sbom-files/generate.py new file mode 100644 index 000000000..e14a358c0 --- /dev/null +++ b/tests/packages/sbom-files/generate.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +"""Trivial SBOM generator for the sbom-files 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/sbom-files/meson.build b/tests/packages/sbom-files/meson.build new file mode 100644 index 000000000..c17aeb9a6 --- /dev/null +++ b/tests/packages/sbom-files/meson.build @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('sbom-files', version: '1.0.0') + +py = import('python').find_installation() + +sboms_dir = get_option('datadir') / meson.project_name() / 'sboms' + +# Static SBOM checked into the source tree. It is installed to the +# location matched by the default "tool.meson-python.sbom-files" +# pattern and thus moved to the .dist-info/sboms/ directory in the +# wheel. +install_data('sboms/static.cdx.json', install_dir: sboms_dir) + +# SBOM generated at build time. +custom_target('generated-sbom', + output: 'generated.cdx.json', + command: [py, files('generate.py'), '@OUTPUT@'], + install: true, + install_dir: sboms_dir) + +# Regular data file, not matched by the sbom-files patterns. +install_data('data.txt', install_dir: get_option('datadir') / meson.project_name()) diff --git a/tests/packages/sbom-files/pyproject.toml b/tests/packages/sbom-files/pyproject.toml new file mode 100644 index 000000000..584a21e81 --- /dev/null +++ b/tests/packages/sbom-files/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 = 'sbom-files' +version = '1.0.0' diff --git a/tests/packages/sbom-files/sboms/static.cdx.json b/tests/packages/sbom-files/sboms/static.cdx.json new file mode 100644 index 000000000..8a8fd07ca --- /dev/null +++ b/tests/packages/sbom-files/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/packages/sbom-files/sboms/static.cdx.json.license b/tests/packages/sbom-files/sboms/static.cdx.json.license new file mode 100644 index 000000000..775d000c4 --- /dev/null +++ b/tests/packages/sbom-files/sboms/static.cdx.json.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 2fdf7a5a0..ecade9ce2 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -148,6 +148,52 @@ def test_license_pep639(wheel_license_pep639): ''')) +def test_sbom_files(wheel_sbom_files): + artifact = wheel.wheelfile.WheelFile(wheel_sbom_files) + + assert wheel_contents(artifact) == { + 'sbom_files-1.0.0.data/data/sbom-files/data.txt', + 'sbom_files-1.0.0.dist-info/METADATA', + 'sbom_files-1.0.0.dist-info/RECORD', + 'sbom_files-1.0.0.dist-info/WHEEL', + 'sbom_files-1.0.0.dist-info/sboms/generated.cdx.json', + 'sbom_files-1.0.0.dist-info/sboms/static.cdx.json', + } + + # SBOM file content is preserved verbatim. + static_sbom = artifact.read('sbom_files-1.0.0.dist-info/sboms/static.cdx.json') + assert b'CycloneDX' in static_sbom + + # The SBOM generated at build time by the custom_target. + generated_sbom = artifact.read('sbom_files-1.0.0.dist-info/sboms/generated.cdx.json') + assert b'CycloneDX' in generated_sbom + + +def test_sbom_files_custom(wheel_sbom_files_custom): + artifact = wheel.wheelfile.WheelFile(wheel_sbom_files_custom) + + assert wheel_contents(artifact) == { + # matched by the sbom-files pattern declared in pyproject.toml + 'sbom_files_custom-1.0.0.dist-info/sboms/custom.cdx.json', + # the declared patterns replace the default one: files in the + # default location are not treated specially anymore + 'sbom_files_custom-1.0.0.data/data/sbom-files-custom/sboms/other.cdx.json', + # not matching the *.cdx.json glob + 'sbom_files_custom-1.0.0.data/data/custom/readme.txt', + 'sbom_files_custom-1.0.0.dist-info/METADATA', + 'sbom_files_custom-1.0.0.dist-info/RECORD', + 'sbom_files_custom-1.0.0.dist-info/WHEEL', + } + + +def test_sbom_files_collision(package_sbom_files_collision, tmp_path): + # Two files matching the sbom-files patterns and with the same + # name cannot both be moved to .dist-info/sboms/. + with pytest.raises(mesonpy.BuildError, match='sbom-files'): + 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 36c48186e1cc9ef3612d70142510ca167d823188 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Mon, 6 Jul 2026 12:38:04 -0700 Subject: [PATCH 2/2] DOC: add how-to guide for including SBOMs in wheels Co-Authored-By: Claude Fable 5 --- docs/how-to-guides/sboms.rst | 108 +++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 109 insertions(+) create mode 100644 docs/how-to-guides/sboms.rst diff --git a/docs/how-to-guides/sboms.rst b/docs/how-to-guides/sboms.rst new file mode 100644 index 000000000..940636c46 --- /dev/null +++ b/docs/how-to-guides/sboms.rst @@ -0,0 +1,108 @@ +.. SPDX-FileCopyrightText: 2026 The meson-python developers +.. +.. SPDX-License-Identifier: MIT + +.. _how-to-guides-sboms: + +*********************************** +Including SBOMs in wheels (PEP 770) +*********************************** + +`PEP 770`_ defines a standard location for Software Bill of Materials +(SBOM) documents describing the contents of Python packages: the +``sboms/`` subdirectory of the wheel's ``.dist-info/`` directory. + +``meson-python`` moves files installed by the Meson project at a +location matching one of the glob patterns in the +:option:`tool.meson-python.sbom-files` setting to the +``.dist-info/sboms/`` directory in the wheel. The default pattern is +``{datadir}/{name}/sboms/*``, with ``{name}`` the normalized project +name: files installed in the ``sboms/`` subdirectory of the project's +data directory are treated as SBOM files. When the Meson project is +installed directly, rather than through a Python build front-end, the +SBOM files are installed in the data directory, alongside the +project's other data files. + +.. _PEP 770: https://peps.python.org/pep-0770/ + +Static SBOM files +================= + +SBOM documents checked into the source tree, typically describing +source-vendored components, only need to be installed to the location +matched by the default pattern: + +.. code-block:: meson + + project('my-project', 'c', version: '1.0.0') + + install_data( + 'sboms/component1.cdx.json', + 'sboms/component2.cdx.json', + install_dir: get_option('datadir') / meson.project_name() / '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 +=========================== + +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 + + py = import('python').find_installation() + + custom_target('vendored-sbom', + output: 'vendored.cdx.json', + command: [py, files('scripts/generate_sbom.py'), '@OUTPUT@', + '--version', meson.project_version()], + install: true, + install_dir: get_option('datadir') / meson.project_name() / 'sboms', + ) + +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. + +Customizing the patterns +======================== + +The :option:`tool.meson-python.sbom-files` setting accepts a list of +glob patterns matched against the files' installation paths as they +appear in the Meson introspection data: + +.. code-block:: toml + + [tool.meson-python] + sbom-files = ['{datadir}/my-project/sboms/*.cdx.json'] + +Declaring the setting replaces the default pattern. Setting it to an +empty list disables the special handling of SBOM files entirely. + +File naming and validation +========================== + +* Files are placed in ``.dist-info/sboms/`` under their file name. + ``meson-python`` raises an error at build time when two matched + files have the same name. +* 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 +================= + +SBOM files are only placed in regular wheels (``pip install .`` or +``python -m build``). Editable wheels (``pip install -e .``) redirect +imports to the build directory and do not carry SBOM files. Since +SBOMs are distribution artifacts, this limitation does not affect +development workflows. diff --git a/docs/index.rst b/docs/index.rst index f56b7ebc0..0ec38fce3 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