Skip to content

Commit b3358ce

Browse files
rgommersdnicolodi
authored andcommitted
DOC: add documentation about using shared libraries
1 parent 2a9179d commit b3358ce

File tree

2 files changed

+252
-0
lines changed

2 files changed

+252
-0
lines changed
+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
.. SPDX-FileCopyrightText: 2024 The meson-python developers
2+
..
3+
.. SPDX-License-Identifier: MIT
4+
5+
.. _shared-libraries:
6+
7+
**********************
8+
Using shared libraries
9+
**********************
10+
11+
Python projects may build shared libraries as part of their project, or link
12+
with shared libraries from a dependency. This tends to be a common source of
13+
issues, hence this page aims to explain how to include shared libraries in
14+
wheels, any limitations and gotchas, and how support is implemented in
15+
``meson-python`` under the hood.
16+
17+
We distinguish between *internal* shared libraries that are built as part of
18+
the project, and *external* shared libraries that are provided by project
19+
dependencies and that are linked with the project build artifacts.
20+
For internal shared libraries, we also distinguish whether the shared library
21+
is being installed to its default system location (typically
22+
``/usr/local/lib`` on Unix-like systems, and ``C:\\lib`` on Windows - we call
23+
this ``libdir`` in this guide) or to a location in ``site-packages`` within the
24+
Python package install tree. All these scenarios are (or will be) supported,
25+
with some caveats:
26+
27+
+-----------------------+------------------+---------+-------+-------+
28+
| shared library source | install location | Windows | macOS | Linux |
29+
+=======================+==================+=========+=======+=======+
30+
| internal | libdir | no (1) |||
31+
+-----------------------+------------------+---------+-------+-------+
32+
| internal | site-packages ||||
33+
+-----------------------+------------------+---------+-------+-------+
34+
| external | n/a | ✓ (2) |||
35+
+-----------------------+------------------+---------+-------+-------+
36+
37+
.. TODO: add subproject as a source
38+
39+
1. Internal shared libraries on Windows cannot be automatically handled
40+
correctly, and currently ``meson-python`` therefore raises an error for them.
41+
`PR meson-python#551 <https://github.com/mesonbuild/meson-python/pull/551>`__
42+
may improve that situation in the near future.
43+
44+
2. External shared libraries require ``delvewheel`` usage on Windows (or some
45+
equivalent way, like amending the DLL search path to include the directory
46+
in which the external shared library is located). Due to the lack of
47+
`RPATH <https://en.wikipedia.org/wiki/Rpath>`__ support on Windows, there is
48+
no good way around this.
49+
50+
.. _internal-shared-libraries:
51+
52+
Internal shared libraries
53+
=========================
54+
55+
A shared library produced by ``library()`` or ``shared_library()`` built like this
56+
57+
.. code-block:: meson
58+
59+
example_lib = shared_library(
60+
'example',
61+
'examplelib.c',
62+
install: true,
63+
)
64+
65+
is installed to ``libdir`` by default. If the only reason the shared library exists
66+
is to be used inside the Python package being built, then it is best to modify
67+
the install location to be within the Python package itself:
68+
69+
.. code-block:: python
70+
71+
install_path: py.get_install_dir() / 'mypkg/subdir'
72+
73+
Then an extension module in the same install directory can link against the
74+
shared library in a portable manner by using ``install_rpath``:
75+
76+
.. code-block:: meson
77+
78+
py3.extension_module('_extmodule',
79+
'_extmodule.c',
80+
link_with: example_lib,
81+
install: true,
82+
subdir: 'mypkg/subdir',
83+
install_rpath: '$ORIGIN'
84+
)
85+
86+
The above method will work as advertised on macOS and Linux; ``meson-python`` does
87+
nothing special for this case. Windows needs some special handling though, due to
88+
the lack of RPATH support:
89+
90+
.. literalinclude:: ../../tests/packages/sharedlib-in-package/mypkg/__init__.py
91+
:start-after: start-literalinclude
92+
:end-before: end-literalinclude
93+
94+
If an internal shared library is not only used as part of a Python package, but
95+
for example also as a regular shared library in a C/C++ project or as a
96+
standalone library, then the method shown above won't work. The library is
97+
then marked for installation into the system default ``libdir`` location.
98+
Actually installing into ``libdir`` isn't possible with wheels, hence
99+
``meson-python`` will instead do the following *on platforms other than
100+
Windows*:
101+
102+
1. Install the shared library to ``<project-name>.mesonpy.libs`` (i.e., a
103+
top-level directory in the wheel, which on install will end up in
104+
``site-packages``).
105+
2. Rewrite RPATH entries for install targets that depend on the shared library
106+
to point to that new install location instead.
107+
108+
This will make the shared library work automatically, with no other action needed
109+
from the package author. *However*, currently an error is raised for this situation
110+
on Windows. This is documented also in :ref:`reference-limitations`.
111+
112+
113+
External shared libraries
114+
=========================
115+
116+
External shared libraries are installed somewhere on the build machine, and
117+
usually detected by a ``dependency()`` or ``compiler.find_library()`` call in a
118+
``meson.build`` file. When a Python extension module or executable uses the
119+
dependency, the shared library will be linked against at build time.
120+
121+
If the shared library is located in a directory on the loader search path,
122+
the wheel created by ``meson-python`` will work locally when installed.
123+
If it's in a non-standard location however, the shared library will go
124+
missing at runtime. The Python extension module linked against it needs an
125+
RPATH entry - and Meson will not automatically manage RPATH entries for you.
126+
Hence you'll need to add the needed RPATH yourself, for example by adding
127+
``-Wl,rpath=/path/to/dir/sharedlib/is/in`` to ``LDFLAGS`` before starting
128+
the build. In case you run into this problem after a wheel is built and
129+
installed, adding that same path to the ``LD_LIBRARY_PATH`` environment variable is a quick way of
130+
checking if that is indeed the problem.
131+
132+
On Windows, the solution is similar - the shared library can either be
133+
preloaded, or the directory that the library is located in added to the DLL
134+
search path with ``os.add_dll_directory``, or vendored into the wheel with
135+
``delvewheel`` in order to make the built Python package usable locally.
136+
137+
Publishing wheels which depend on external shared libraries
138+
-----------------------------------------------------------
139+
140+
On all platforms, wheels which depend on external shared libraries usually need
141+
post-processing to make them usable on machines other than the one on which
142+
they were built. This is because the RPATH entry for an external shared library
143+
contains a path specific to the build machine. This post-processing is done by
144+
tools like ``auditwheel`` (Linux), ``delvewheel`` (Windows), ``delocate``
145+
(macOS) or ``repair-wheel`` (any platform, wraps the other tools).
146+
147+
Running any of those tools on a wheel produced by ``meson-python`` will vendor
148+
the external shared library into the wheel and rewrite the RPATH entries (it
149+
may also do some other things, like symbol mangling).
150+
151+
On Windows, the package author may also have to add the preloading like shown
152+
above with ``_append_to_sharedlib_load_path()`` to the main ``__init__.py`` of
153+
the package, ``delvewheel`` may or may not take care of this (please check its
154+
documentation if your shared library goes missing at runtime).
155+
156+
Note that we don't cover using shared libraries contained in another wheel
157+
and depending on such a wheel at runtime in this guide. This is inherently
158+
complex and not recommended (you need to be in control of both packages, or
159+
upgrades may be impossible/breaking).
160+
161+
162+
Using libraries from a Meson subproject
163+
=======================================
164+
165+
It can often be useful to build a shared library in a
166+
`Meson subproject <https://mesonbuild.com/Subprojects.html>`__, for example as
167+
a fallback in case an external dependency isn't detected. There are two main
168+
strategies for folding a library built in a subproject into a wheel built with
169+
``meson-python``:
170+
171+
1. Build the library as a static library instead of a shared library, and
172+
link it into a Python extension module that needs it.
173+
2. Build the library as a shared library, and either change its install path
174+
to be within the Python package's tree, or rely on ``meson-python`` to fold
175+
it into the wheel when it'd otherwise be installed to ``libdir``.
176+
177+
Option (1) tends to be easier, so unless the library of interest cannot be
178+
built as a static library or it would inflate the wheel size too much because
179+
it's needed by multiple Python extension modules, we recommend trying option
180+
(1) first.
181+
182+
A typical C or C++ project providing a library to link against tends to provide
183+
(a) one or more ``library()`` targets, which can be built as shared, static, or both,
184+
and (b) headers, pkg-config files, tests and perhaps other development targets
185+
that are needed to use the ``library()`` target(s). One of the challenges to use
186+
such projects as a subproject is that the headers and other installable targets
187+
are targeting system locations (e.g., ``<prefix>/include/``) which isn't supported
188+
by wheels and hence ``meson-python`` errors out when it encounters such an install
189+
target. This is perhaps the main issue one encounters with subproject usage,
190+
and the following two sections discuss how options (1) and (2) can work around
191+
that.
192+
193+
Static library from subproject
194+
------------------------------
195+
196+
The major advantage of building a library target as static and folding it directly
197+
into an extension module is that no targets from the subproject need to be installed.
198+
To configure the subproject for this use case, add the following to the
199+
``pyproject.toml`` file of your package:
200+
201+
.. code-block:: toml
202+
203+
[tool.meson-python.args]
204+
setup = ['--default-library=static']
205+
install = ['--skip-subprojects']
206+
207+
This ensures that ``library`` targets are built as static, and nothing gets installed.
208+
209+
To then link against the static library in the subproject, say for a subproject
210+
named ``bar`` with the main library target contained in a ``bar_dep`` dependency,
211+
add this to your ``meson.build`` file:
212+
213+
.. code-block:: meson
214+
215+
bar_proj = subproject('bar')
216+
bar_dep = bar_proj.get_variable('bar_dep')
217+
218+
py.extension_module(
219+
'_example',
220+
'_examplemod.c',
221+
dependencies: bar_dep,
222+
install: true,
223+
)
224+
225+
That is all!
226+
227+
Shared library from subproject
228+
------------------------------
229+
230+
If we can't use the static library approach from the section above and we need
231+
a shared library, then we must have ``install: true`` for that shared library
232+
target. This can only work if we can pass some build option to the subproject
233+
that tells it to *only* install the shared library and not headers or other
234+
targets that we don't need. Install tags don't work per subproject, so
235+
this will look something like:
236+
237+
.. code-block:: meson
238+
239+
foo_subproj = subproject('foo',
240+
default_options: {
241+
# This is a custom option - if it doesn't exist, can you add it
242+
# upstream or in WrapDB?
243+
'only_install_main_lib': true,
244+
})
245+
foo_dep = foo_subproj.get_variable('foo_dep')
246+
247+
Now we can use ``foo_dep`` like a normal dependency, ``meson-python`` will
248+
include it into the wheel in ``<project-name>.mesonpy.libs`` just like an
249+
internal shared library that targets ``libdir`` (see
250+
:ref:`internal-shared-libraries`).
251+
*Remember: this method doesn't support Windows (yet)!*

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ the use of ``meson-python`` and Meson for Python packaging.
8282
how-to-guides/config-settings
8383
how-to-guides/meson-args
8484
how-to-guides/debug-builds
85+
how-to-guides/shared-libraries
8586
reference/limitations
8687
projects-using-meson-python
8788

0 commit comments

Comments
 (0)