Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

ENH: support shared libraries on Windows when explicitly enabled #551

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 12, 2025
Merged
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
4 changes: 2 additions & 2 deletions 4 .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ opensuse-15_task:

freebsd_task:
freebsd_instance:
image_family: freebsd-14-0
install_script: pkg install -y git ninja
image_family: freebsd-14-2
install_script: pkg install -y git ninja patchelf
<< : *test

macos-arm64_task:
Expand Down
2 changes: 1 addition & 1 deletion 2 ci/alpine-3.docker
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
#
# SPDX-License-Identifier: MIT

# 20240817
# 20250212
FROM alpine:3
RUN apk add --no-cache python3-dev py3-pip build-base ninja-is-really-ninja git patchelf
2 changes: 1 addition & 1 deletion 2 ci/debian-11.docker
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
#
# SPDX-License-Identifier: MIT

# 20240204
# 20250212
FROM debian:bullseye
RUN apt-get update && apt-get install -y git ninja-build patchelf python3-pip python3-venv && rm -rf /var/lib/apt/lists/*
59 changes: 31 additions & 28 deletions 59 docs/how-to-guides/shared-libraries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,22 @@ this ``libdir`` in this guide) or to a location in ``site-packages`` within the
Python package install tree. All these scenarios are (or will be) supported,
with some caveats:

+-----------------------+------------------+---------+-------+-------+
| shared library source | install location | Windows | macOS | Linux |
+=======================+==================+=========+=======+=======+
| internal | libdir | no (1) | ✓ | ✓ |
+-----------------------+------------------+---------+-------+-------+
| internal | site-packages | ✓ | ✓ | ✓ |
+-----------------------+------------------+---------+-------+-------+
| external | n/a | ✓ (2) | ✓ | ✓ |
+-----------------------+------------------+---------+-------+-------+
+-----------------------+------------------+------------+-------+-------+
| shared library source | install location | Windows | macOS | Linux |
+=======================+==================+============+=======+=======+
| internal | libdir | ✓ :sup:`1` | ✓ | ✓ |
+-----------------------+------------------+------------+-------+-------+
| internal | site-packages | ✓ | ✓ | ✓ |
+-----------------------+------------------+------------+-------+-------+
| external | --- | ✓ :sup:`2` | ✓ | ✓ |
+-----------------------+------------------+------------+-------+-------+

.. TODO: add subproject as a source

1. Internal shared libraries on Windows cannot be automatically handled
correctly, and currently ``meson-python`` therefore raises an error for them.
`PR meson-python#551 <https://github.com/mesonbuild/meson-python/pull/551>`__
may improve that situation in the near future.
1. Support for internal shared libraries on Windows is enabled with the
:option:`tool.meson-python.allow-windows-internal-shared-libs` option and
requires cooperation from the package to extend the DLL search path or
preload the required libraries. See below for more details.

2. External shared libraries require ``delvewheel`` usage on Windows (or some
equivalent way, like amending the DLL search path to include the directory
Expand Down Expand Up @@ -91,23 +91,26 @@ the lack of RPATH support:
:start-after: start-literalinclude
:end-before: end-literalinclude

If an internal shared library is not only used as part of a Python package, but
for example also as a regular shared library in a C/C++ project or as a
standalone library, then the method shown above won't work. The library is
then marked for installation into the system default ``libdir`` location.
Actually installing into ``libdir`` isn't possible with wheels, hence
``meson-python`` will instead do the following *on platforms other than
Windows*:
If an internal shared library is not only used as part of a Python package,
but for example also as a regular shared library then the method shown above
won't work. The library is then marked for installation into the system
default ``libdir`` location. Actually installing into ``libdir`` isn't
possible with wheels, hence ``meson-python`` will instead do the following:

1. Install the shared library to ``<project-name>.mesonpy.libs`` (i.e., a
1. Install the shared library to the ``.<project-name>.mesonpy.libs``
top-level directory in the wheel, which on install will end up in
``site-packages``).
2. Rewrite RPATH entries for install targets that depend on the shared library
to point to that new install location instead.
``site-packages``.
2. On platforms other than Windows, rewrite RPATH entries for install targets
that depend on the shared library to point to that new install location
instead.

This will make the shared library work automatically, with no other action needed
from the package author. *However*, currently an error is raised for this situation
on Windows. This is documented also in :ref:`reference-limitations`.
On platforms other than Windows, this will make the shared library work
automatically, with no other action needed from the package author. On
Windows, due to the lack of RPATH support, the ``.<project-name>.mesonpy.libs``
location search path needs to be added to the DLL search path, with code
similar to the one presented above. For this reason, handling of internal
shared libraries on Windows is conditional to setting the
:option:`tool.meson-python.allow-windows-internal-shared-libs` option.


External shared libraries
Expand Down Expand Up @@ -245,7 +248,7 @@ this will look something like:
foo_dep = foo_subproj.get_variable('foo_dep')

Now we can use ``foo_dep`` like a normal dependency, ``meson-python`` will
include it into the wheel in ``<project-name>.mesonpy.libs`` just like an
include it into the wheel in ``.<project-name>.mesonpy.libs`` just like an
internal shared library that targets ``libdir`` (see
:ref:`internal-shared-libraries`).
*Remember: this method doesn't support Windows (yet)!*
15 changes: 0 additions & 15 deletions 15 docs/reference/limitations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,7 @@ run-time inside the package alongside the Python code, and use
access them.


Shared libraries on Windows
===========================

On Windows, ``meson-python`` cannot encapsulate shared libraries
installed as part of the Meson project into the Python wheel for
Python extension modules or executables, in a way suitable for them to
be found at run-time.

This limitation can be overcome with static linking or using
`delvewheel`_ to post-process the Python wheel to bundle the required
shared libraries and include the setup code to properly set the
library search path.


.. _install_data(): https://mesonbuild.com/Reference-manual_functions.html#install_data
.. _importlib-resources: https://importlib-resources.readthedocs.io/en/latest/index.html
.. _delvewheel: https://github.com/adang1345/delvewheel

.. |install_data()| replace:: ``install_data()``
13 changes: 13 additions & 0 deletions 13 docs/reference/pyproject-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ This page lists the configuration settings supported by
:ref:`how-to-guides-meson-args` guide for for information on how to
use them and examples.

.. option:: tool.meson-python.allow-windows-internal-shared-libs

Enable support for relocating internal shared libraries that would be
installed into the system shared library location to the
``.<package-name>.mesonpy.libs`` folder also on Windows. The relocation can
be done transparently on UNIX platforms and on macOS, where the shared
library load path can be adjusted via RPATH or equivalent mechanisms.
Windows lacks a similar facility, thus the Python package is responsible to
extend the DLL load path to include this directory or to preload the
shared libraries. See :ref:`here <internal-shared-libraries>` for detailed
documentation. This option ensures that the package authors are aware of
this requirement.

.. option:: tool.meson-python.limited-api

A boolean indicating whether the extension modules contained in the
Expand Down
17 changes: 15 additions & 2 deletions 17 mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,12 @@
metadata: Metadata,
manifest: Dict[str, List[Tuple[pathlib.Path, str]]],
limited_api: bool,
allow_windows_shared_libs: bool,
) -> None:
self._metadata = metadata
self._manifest = manifest
self._limited_api = limited_api
self._allow_windows_shared_libs = allow_windows_shared_libs

@property
def _has_internal_libs(self) -> bool:
Expand Down Expand Up @@ -422,6 +424,12 @@

if self._has_internal_libs:
if _is_native(origin):
if sys.platform == 'win32' and not self._allow_windows_shared_libs:
raise NotImplementedError(

Check warning on line 428 in mesonpy/__init__.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/__init__.py#L428

Added line #L428 was not covered by tests
'Loading shared libraries bundled in the Python wheel on Windows requires '
'setting the DLL load path or preloading. See the documentation for '
'the "tool.meson-python.allow-windows-internal-shared-libs" option.')

# When an executable, libray, or Python extension module is
# dynamically linked to a library built as part of the project,
# Meson adds a library load path to it pointing to the build
Expand Down Expand Up @@ -567,6 +575,7 @@
scheme = _table({
'meson': _string_or_path,
'limited-api': _bool,
'allow-windows-internal-shared-libs': _bool,
'args': _table({
name: _strings for name in _MESON_ARGS_KEYS
}),
Expand Down Expand Up @@ -784,6 +793,10 @@
'The package targets Python\'s Limited API, which is not supported by free-threaded CPython. '
'The "python.allow_limited_api" Meson build option may be used to override the package default.')

# Shared library support on Windows requires collaboration
# from the package, make sure the developers acknowledge this.
self._allow_windows_shared_libs = pyproject_config.get('allow-windows-internal-shared-libs', False)

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 @@ -988,13 +1001,13 @@
def wheel(self, directory: Path) -> pathlib.Path:
"""Generates a wheel in the specified directory."""
self.build()
builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api)
builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs)
return builder.build(directory)

def editable(self, directory: Path) -> pathlib.Path:
"""Generates an editable wheel in the specified directory."""
self.build()
builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api)
builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs)
return builder.build(directory, self._source_dir, self._build_dir, self._build_command, self._editable_verbose)


Expand Down
36 changes: 18 additions & 18 deletions 36 mesonpy/_rpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,10 @@
from mesonpy._compat import Iterable, Path


if sys.platform == 'linux':

def _get_rpath(filepath: Path) -> List[str]:
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
return r.stdout.strip().split(':')

def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True)
if sys.platform == 'win32' or sys.platform == 'cygwin':

def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
old_rpath = _get_rpath(filepath)
new_rpath = []
for path in old_rpath:
if path.startswith('$ORIGIN/'):
path = '$ORIGIN/' + libs_relative_path
new_rpath.append(path)
if new_rpath != old_rpath:
_set_rpath(filepath, new_rpath)

pass

elif sys.platform == 'darwin':

Expand Down Expand Up @@ -85,6 +70,21 @@ def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
_set_rpath(filepath, new_rpath)

else:
# Assume that any other platform uses ELF binaries.

def _get_rpath(filepath: Path) -> List[str]:
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
return r.stdout.strip().split(':')

def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True)

def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
raise NotImplementedError(f'Bundling libraries in wheel is not supported on {sys.platform}')
old_rpath = _get_rpath(filepath)
new_rpath = []
for path in old_rpath:
if path.startswith('$ORIGIN/'):
path = '$ORIGIN/' + libs_relative_path
new_rpath.append(path)
if new_rpath != old_rpath:
_set_rpath(filepath, new_rpath)
20 changes: 20 additions & 0 deletions 20 tests/packages/link-against-local-lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2025 The meson-python developers
#
# SPDX-License-Identifier: MIT

import os
import sys


_path = os.path.join(os.path.dirname(__file__), '..', '.link_against_local_lib.mesonpy.libs')
if os.name == 'nt':
os.add_dll_directory(_path)
elif sys.platform == 'cygwin':
os.environ['PATH'] = os.pathsep.join((os.environ['PATH'], _path))
del _path


from ._example import example_sum # noqa: E402


__all__ = ['example_sum']
4 changes: 2 additions & 2 deletions 4 tests/packages/link-against-local-lib/examplemod.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ static PyMethodDef methods[] = {

static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"example",
"_example",
NULL,
-1,
methods,
};

PyMODINIT_FUNC PyInit_example(void)
PyMODINIT_FUNC PyInit__example(void)
{
return PyModule_Create(&module);
}
2 changes: 2 additions & 0 deletions 2 tests/packages/link-against-local-lib/lib/examplelib.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: MIT

#include "examplelib.h"

int sum(int a, int b) {
return a + b;
}
13 changes: 12 additions & 1 deletion 13 tests/packages/link-against-local-lib/lib/examplelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
//
// SPDX-License-Identifier: MIT

int sum(int a, int b);
#pragma once

#if defined(EXAMPLE_DLL_EXPORTS)
#define EXAMPLE_DLL __declspec(dllexport)
#elif defined(BAR_DLL_IMPORTS)
#define EXAMPLE_DLL __declspec(dllimport)
#else
#define EXAMPLE_DLL
#endif


EXAMPLE_DLL int sum(int a, int b);
1 change: 1 addition & 0 deletions 1 tests/packages/link-against-local-lib/lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
example_lib = shared_library(
'example',
'examplelib.c',
c_args: lib_compile_args,
install: true,
)
20 changes: 17 additions & 3 deletions 20 tests/packages/link-against-local-lib/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@

project('link-against-local-lib', 'c', version: '1.0.0')

if meson.get_compiler('c').get_id() in ['msvc', 'clang-cl', 'intel-cl']
lib_compile_args = ['-DEXAMPLE_DLL_EXPORTS']
link_args = ['-DEXAMPLE_DLL_IMPORTS']
else
lib_compile_args = []
link_args = ['-Wl,-rpath,custom-rpath']
endif

subdir('lib')

py = import('python').find_installation()
py = import('python').find_installation(pure: false)

py.install_sources(
'__init__.py',
subdir: 'example',
)

py.extension_module(
'example',
'_example',
'examplemod.c',
link_with: example_lib,
link_args: ['-Wl,-rpath,custom-rpath'],
link_args: link_args,
install: true,
subdir: 'example',
)
3 changes: 3 additions & 0 deletions 3 tests/packages/link-against-local-lib/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[tool.meson-python]
allow-windows-internal-shared-libs = true
7 changes: 3 additions & 4 deletions 7 tests/packages/sharedlib-in-package/mypkg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@

# start-literalinclude
def _append_to_sharedlib_load_path():
"""
Ensure the shared libraries in this package can be loaded on Windows.
"""Ensure the shared libraries in this package can be loaded on Windows.

Windows lacks a concept equivalent to RPATH: Python extension modules
cannot find DLLs installed outside the DLL search path. This function
ensures that the location of the shared libraries distributed inside this
Python package is in the DLL search path of the process.

The Windows DLL search path includes the object depending on it is located:
the DLL search path needs to be augmented only when the Python extension
The Windows DLL search path includes the path to the object attempting
to load the DLL: it needs to be augmented only when the Python extension
modules and the DLLs they require are installed in separate directories.
Cygwin does not have the same default library search path: all locations
where the shared libraries are installed need to be added to the search
Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.