Skip to content

Commit 4c0d817

Browse files
committed
EHN: add tool.meson-python.wheel.{exclude,include} settings
1 parent 8d50078 commit 4c0d817

File tree

6 files changed

+101
-40
lines changed

6 files changed

+101
-40
lines changed

docs/how-to-guides/shared-libraries.rst

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -187,38 +187,27 @@ strategies for folding a library built in a subproject into a wheel built with
187187
to be within the Python package's tree, or rely on ``meson-python`` to fold
188188
it into the wheel when it'd otherwise be installed to ``libdir``.
189189

190-
Option (1) tends to be easier, so unless the library of interest cannot be
191-
built as a static library or it would inflate the wheel size too much because
192-
it's needed by multiple Python extension modules, we recommend trying option
193-
(1) first.
194-
195-
A typical C or C++ project providing a library to link against tends to provide
196-
(a) one or more ``library()`` targets, which can be built as shared, static, or both,
197-
and (b) headers, pkg-config files, tests and perhaps other development targets
198-
that are needed to use the ``library()`` target(s). One of the challenges to use
199-
such projects as a subproject is that the headers and other installable targets
200-
are targeting system locations (e.g., ``<prefix>/include/``) which isn't supported
201-
by wheels and hence ``meson-python`` errors out when it encounters such an install
202-
target. This is perhaps the main issue one encounters with subproject usage,
203-
and the following two sections discuss how options (1) and (2) can work around
204-
that.
190+
Static linking tends to be easier, and it is the recommended solution, unless
191+
the library of interest cannot be built as a static library or it would
192+
inflate the wheel size too much because it's needed by multiple Python
193+
extension modules.
205194

206195
Static library from subproject
207196
------------------------------
208197

209-
The major advantage of building a library target as static and folding it directly
210-
into an extension module is that no targets from the subproject need to be installed.
211-
To configure the subproject for this use case, add the following to the
212-
``pyproject.toml`` file of your package:
198+
The major advantage of building a library target as static and folding it
199+
directly into an extension module is that the RPATH or the DLL search path do
200+
not need to be adjusted and no targets from the subproject need to be
201+
installed. To ensures that ``library()`` targets are built as static, and that
202+
no parts of the subprojects are installed, the following configuration can be
203+
added in ``pyproject.toml`` to ensure the relevant options are passed to Meson:
213204

214205
.. code-block:: toml
215206
216207
[tool.meson-python.args]
217208
setup = ['--default-library=static']
218209
install = ['--skip-subprojects']
219210
220-
This ensures that ``library`` targets are built as static, and nothing gets installed.
221-
222211
To then link against the static library in the subproject, say for a subproject
223212
named ``bar`` with the main library target contained in a ``bar_dep`` dependency,
224213
add this to your ``meson.build`` file:
@@ -235,30 +224,37 @@ add this to your ``meson.build`` file:
235224
install: true,
236225
)
237226
238-
That is all!
239-
240227
Shared library from subproject
241228
------------------------------
242229

243-
If we can't use the static library approach from the section above and we need
244-
a shared library, then we must have ``install: true`` for that shared library
245-
target. This can only work if we can pass some build option to the subproject
246-
that tells it to *only* install the shared library and not headers or other
247-
targets that we don't need. Install tags don't work per subproject, so
248-
this will look something like:
230+
Sometimes it may be necessary or preferable to use dynamic linking to a shared
231+
library provided in a subproject, for example to avoid inflating the wheel
232+
size having multiple copies of the same object code in different extension
233+
modules using the same library. In this case, the subproject needs to install
234+
the shared library in the usual location in ``libdir``. ``meson-python``
235+
will automatically include it into the wheel in
236+
``.<project-name>.mesonpy.libs`` just like an internal shared library.
237+
238+
Most projects, however, install more than the shared library and the extra
239+
components, such as header files or documentation, should not be included in
240+
the Python wheel. Projects may have configuration options to disable building
241+
and installing additional components, in this case, these options can be
242+
passed to the ``subproject()`` call:
249243

250244
.. code-block:: meson
251245
252246
foo_subproj = subproject('foo',
253247
default_options: {
254-
# This is a custom option - if it doesn't exist, can you add it
255-
# upstream or in WrapDB?
256-
'only_install_main_lib': true,
248+
'docs': 'disabled',
257249
})
258250
foo_dep = foo_subproj.get_variable('foo_dep')
259251
260-
Now we can use ``foo_dep`` like a normal dependency, ``meson-python`` will
261-
include it into the wheel in ``.<project-name>.mesonpy.libs`` just like an
262-
internal shared library that targets ``libdir`` (see
263-
:ref:`internal-shared-libraries`).
264-
*Remember: this method doesn't support Windows (yet)!*
252+
Install tags do not work per subproject, therefore to exclude other parts of
253+
the subproject form being included in the wheel, we need to resort to
254+
``meson-python`` install location filters using the
255+
:option:`tool.meson-python.wheel.exclude` build option:
256+
257+
.. code-block:: toml
258+
259+
[tool.meson-python.wheel]
260+
exclude = ['{prefix}/include/*']

docs/reference/pyproject-settings.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ use them and examples.
6363

6464
Extra arguments to be passed to the ``meson install`` command.
6565

66+
.. option:: tool.meson-python.wheel.exclude
67+
68+
List of glob patterns matching paths of files that must be excluded from
69+
the Python wheel. The accepted glob patterns are the ones implemented by
70+
the Python :mod:`fnmatch` with case sensitive matching. The paths to be
71+
matched are as they appear in the Meson introspection data, namely they are
72+
rooted in one of the Meson install locations: ``{bindir}``, ``{datadir}``,
73+
``{includedir}``, ``{libdir_shared}``, ``{libdir_static}``, et cetera.
74+
75+
This configuration setting is measure of last resort to exclude installed
76+
files from a Python wheel. It is to be used when the project includes
77+
subprojects that do not allow fine control on the installed files. Better
78+
solutions include the use of Meson install tags and excluding subprojects
79+
to be installed via :option:`tool.meson-python.args.install`.
80+
81+
.. option:: tool.meson-python.wheel.include
82+
83+
List of glob patterns matching paths of files that must not be excluded
84+
from the Python wheel. All files recorded for installation in the Meson
85+
project are included in the Python wheel unless matching an exclude glob
86+
pattern specified in :option:`tool.meson-python.wheel.exclude`. An include
87+
glob pattern is useful exclusively to limit the effect of an exclude
88+
pattern that matches too many files.
6689

6790
__ https://docs.python.org/3/c-api/stable.html?highlight=limited%20api#stable-application-binary-interface
6891
__ https://mesonbuild.com/Python-module.html#extension_module

mesonpy/__init__.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import contextlib
1717
import copy
1818
import difflib
19+
import fnmatch
1920
import functools
2021
import importlib.machinery
2122
import io
@@ -111,14 +112,32 @@ class InvalidLicenseExpression(Exception): # type: ignore[no-redef]
111112
}
112113

113114

114-
def _map_to_wheel(sources: Dict[str, Dict[str, Any]]) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
115+
def _compile_patterns(patterns: List[str]) -> Callable[[str], bool]:
116+
if not patterns:
117+
return lambda x: False
118+
func = re.compile('|'.join(fnmatch.translate(os.path.normpath(p)) for p in patterns)).match
119+
return typing.cast('Callable[[str], bool]', func)
120+
121+
122+
def _map_to_wheel(
123+
sources: Dict[str, Dict[str, Any]],
124+
exclude: List[str],
125+
include: List[str],
126+
) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
115127
"""Map files to the wheel, organized by wheel installation directory."""
116128
wheel_files: DefaultDict[str, List[Tuple[pathlib.Path, str]]] = collections.defaultdict(list)
117129
packages: Dict[str, str] = {}
130+
excluded = _compile_patterns(exclude)
131+
included = _compile_patterns(include)
118132

119133
for key, group in sources.items():
120134
for src, target in group.items():
121-
destination = pathlib.Path(target['destination'])
135+
target_destination = os.path.normpath(target['destination'])
136+
137+
if excluded(target_destination) and not included(target_destination):
138+
continue
139+
140+
destination = pathlib.Path(target_destination)
122141
anchor = destination.parts[0]
123142
dst = pathlib.Path(*destination.parts[1:])
124143

@@ -581,6 +600,10 @@ def _string_or_path(value: Any, name: str) -> str:
581600
'args': _table({
582601
name: _strings for name in _MESON_ARGS_KEYS
583602
}),
603+
'wheel': _table({
604+
'exclude': _strings,
605+
'include': _strings,
606+
}),
584607
})
585608

586609
table = pyproject.get('tool', {}).get('meson-python', {})
@@ -829,6 +852,10 @@ def __init__(
829852
# from the package, make sure the developers acknowledge this.
830853
self._allow_windows_shared_libs = pyproject_config.get('allow-windows-internal-shared-libs', False)
831854

855+
# Files to be excluded from the wheel
856+
self._excluded_files = pyproject_config.get('wheel', {}).get('exclude', [])
857+
self._included_files = pyproject_config.get('wheel', {}).get('include', [])
858+
832859
def _run(self, cmd: Sequence[str]) -> None:
833860
"""Invoke a subprocess."""
834861
# Flush the line to ensure that the log line with the executed
@@ -912,7 +939,7 @@ def _manifest(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
912939
sources[key][target] = details
913940

914941
# Map Meson installation locations to wheel paths.
915-
return _map_to_wheel(sources)
942+
return _map_to_wheel(sources, self._excluded_files, self._included_files)
916943

917944
@property
918945
def _meson_name(self) -> str:

tests/packages/subproject/pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,15 @@
55
[build-system]
66
build-backend = 'mesonpy'
77
requires = ['meson-python']
8+
9+
[tool.meson-python.wheel]
10+
exclude = [
11+
# Meson before version 1.3.0 install data files in
12+
# ``{datadir}/{project name}/``, later versions install
13+
# in the more correct ``{datadir}/{subproject name}/``.
14+
'{datadir}/*/data.txt',
15+
'{py_purelib}/dep.*',
16+
]
17+
include = [
18+
'{py_purelib}/dep.py',
19+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
excluded via tool.meson-python.wheel.exclude

tests/packages/subproject/subprojects/dep/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ project('dep')
77
py = import('python').find_installation()
88

99
py.install_sources('dep.py')
10+
11+
install_data('data.txt')

0 commit comments

Comments
 (0)