Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
======

Expand Down
108 changes: 108 additions & 0 deletions docs/how-to-guides/sboms.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/pyproject-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
26 changes: 24 additions & 2 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions tests/packages/sbom-files-collision/a/dup.cdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": []
}
3 changes: 3 additions & 0 deletions tests/packages/sbom-files-collision/a/dup.cdx.json.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
6 changes: 6 additions & 0 deletions tests/packages/sbom-files-collision/b/dup.cdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 2,
"components": []
}
3 changes: 3 additions & 0 deletions tests/packages/sbom-files-collision/b/dup.cdx.json.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
10 changes: 10 additions & 0 deletions tests/packages/sbom-files-collision/meson.build
Original file line number Diff line number Diff line change
@@ -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')
14 changes: 14 additions & 0 deletions tests/packages/sbom-files-collision/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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/*']
18 changes: 18 additions & 0 deletions tests/packages/sbom-files-custom/meson.build
Original file line number Diff line number Diff line change
@@ -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')
14 changes: 14 additions & 0 deletions tests/packages/sbom-files-custom/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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']
7 changes: 7 additions & 0 deletions tests/packages/sbom-files-custom/sboms/custom.cdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
7 changes: 7 additions & 0 deletions tests/packages/sbom-files-custom/sboms/other.cdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": []
}
3 changes: 3 additions & 0 deletions tests/packages/sbom-files-custom/sboms/other.cdx.json.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
1 change: 1 addition & 0 deletions tests/packages/sbom-files-custom/sboms/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not an SBOM file; must stay in the wheel data directory.
3 changes: 3 additions & 0 deletions tests/packages/sbom-files-custom/sboms/readme.txt.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
1 change: 1 addition & 0 deletions tests/packages/sbom-files/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Regular data file that must stay in the wheel data directory.
3 changes: 3 additions & 0 deletions tests/packages/sbom-files/data.txt.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2026 The meson-python developers
#
# SPDX-License-Identifier: MIT
17 changes: 17 additions & 0 deletions tests/packages/sbom-files/generate.py
Original file line number Diff line number Diff line change
@@ -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)
Loading