Skip to content

Commit 8dfbe80

Browse files
committed
ENH: simplify and extend rpath handling
1 parent 4f1874f commit 8dfbe80

File tree

7 files changed

+87
-172
lines changed

7 files changed

+87
-172
lines changed

meson.build

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ py = import('python').find_installation()
99
py.install_sources(
1010
'mesonpy/__init__.py',
1111
'mesonpy/_compat.py',
12-
'mesonpy/_dylib.py',
1312
'mesonpy/_editable.py',
14-
'mesonpy/_elf.py',
13+
'mesonpy/_rpath.py',
1514
'mesonpy/_tags.py',
1615
'mesonpy/_util.py',
1716
'mesonpy/_wheelfile.py',

mesonpy/__init__.py

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@
4646
import pyproject_metadata
4747

4848
import mesonpy._compat
49-
import mesonpy._dylib
50-
import mesonpy._elf
49+
import mesonpy._rpath
5150
import mesonpy._tags
5251
import mesonpy._util
5352
import mesonpy._wheelfile
@@ -285,8 +284,6 @@ def __init__(
285284
self._build_dir = build_dir
286285
self._sources = sources
287286

288-
self._libs_build_dir = self._build_dir / 'mesonpy-wheel-libs'
289-
290287
@cached_property
291288
def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
292289
return _map_to_wheel(self._sources)
@@ -438,49 +435,22 @@ def _is_native(self, file: Union[str, pathlib.Path]) -> bool:
438435
return True
439436
return False
440437

441-
def _install_path(
442-
self,
443-
wheel_file: mesonpy._wheelfile.WheelFile,
444-
origin: Path,
445-
destination: pathlib.Path,
446-
) -> None:
447-
""""Install" file or directory into the wheel
448-
and do the necessary processing before doing so.
449-
450-
Some files might need to be fixed up to set the RPATH to the internal
451-
library directory on Linux wheels for eg.
452-
"""
453-
location = destination.as_posix()
438+
def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path) -> None:
439+
"""Add a file to the wheel."""
454440

455441
if self._has_internal_libs:
456-
if platform.system() == 'Linux' or platform.system() == 'Darwin':
457-
# add .mesonpy.libs to the RPATH of ELF files
458-
if self._is_native(os.fspath(origin)):
459-
# copy ELF to our working directory to avoid Meson having to regenerate the file
460-
new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir)
461-
os.makedirs(new_origin.parent, exist_ok=True)
462-
shutil.copy2(origin, new_origin)
463-
origin = new_origin
464-
# add our in-wheel libs folder to the RPATH
465-
if platform.system() == 'Linux':
466-
elf = mesonpy._elf.ELF(origin)
467-
libdir_path = \
468-
f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
469-
if libdir_path not in elf.rpath:
470-
elf.rpath = [*elf.rpath, libdir_path]
471-
elif platform.system() == 'Darwin':
472-
dylib = mesonpy._dylib.Dylib(origin)
473-
libdir_path = \
474-
f'@loader_path/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
475-
if libdir_path not in dylib.rpath:
476-
dylib.rpath = [*dylib.rpath, libdir_path]
477-
else:
478-
# Internal libraries are currently unsupported on this platform
479-
raise NotImplementedError("Bundling libraries in wheel is not supported on platform '{}'"
480-
.format(platform.system()))
442+
if self._is_native(os.fspath(origin)):
443+
# When an executable, libray, or Python extension module is
444+
# dynamically linked to a library built as part of the project,
445+
# Meson adds a library load path to it pointing to the build
446+
# directory, in the form of a relative RPATH entry. meson-python
447+
# relocates the shared libraries to the $project.mesonpy.libs
448+
# folder. Rewrite the RPATH to point to that folder instead.
449+
libspath = os.path.relpath(f'.{self._project.name}.mesonpy.libs', destination.parent)
450+
mesonpy._rpath.fix_rpath(origin, libspath)
481451

482452
try:
483-
wheel_file.write(origin, location)
453+
wheel_file.write(origin, destination.as_posix())
484454
except FileNotFoundError:
485455
# work around for Meson bug, see https://github.com/mesonbuild/meson/pull/11655
486456
if not os.fspath(origin).endswith('.pdb'):

mesonpy/_dylib.py

Lines changed: 0 additions & 55 deletions
This file was deleted.

mesonpy/_elf.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

mesonpy/_rpath.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SPDX-FileCopyrightText: 2023 The meson-python developers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from __future__ import annotations
6+
7+
import os
8+
import subprocess
9+
import sys
10+
import typing
11+
12+
13+
if typing.TYPE_CHECKING:
14+
from typing import List
15+
16+
from mesonpy._compat import Iterable, Path
17+
18+
19+
if sys.platform == 'linux':
20+
21+
def _get_rpath(filepath: Path) -> List[str]:
22+
r = subprocess.run(['patchelf', '--print-rpath', os.fspath(filepath)], capture_output=True, text=True)
23+
return r.stdout.strip().split(':')
24+
25+
def _set_rpath(filepath: Path, rpath: Iterable[str]) -> None:
26+
subprocess.run(['patchelf','--set-rpath', ':'.join(rpath), os.fspath(filepath)], check=True)
27+
28+
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
29+
rpath = _get_rpath(filepath)
30+
if '$ORIGIN/' in rpath:
31+
rpath = [('$ORIGIN/' + libs_relative_path if path == '$ORIGIN/' else path) for path in rpath]
32+
_set_rpath(filepath, rpath)
33+
34+
35+
elif sys.platform == 'darwin':
36+
37+
def _get_rpath(filepath: Path) -> List[str]:
38+
rpath = []
39+
r = subprocess.run(['otool', '-l', os.fspath(filepath)], capture_output=True, text=True)
40+
rpath_tag = False
41+
for line in [x.split() for x in r.stdout.split('\n')]:
42+
if line == ['cmd', 'LC_RPATH']:
43+
rpath_tag = True
44+
elif len(line) >= 2 and line[0] == 'path' and rpath_tag:
45+
rpath.append(line[1])
46+
rpath_tag = False
47+
return rpath
48+
49+
def _replace_rpath(filepath: Path, old: str, new: str) -> None:
50+
subprocess.run(['install_name_tool', '-rpath', old, new, os.fspath(filepath)], check=True)
51+
52+
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
53+
rpath = _get_rpath(filepath)
54+
if '@loader_path/' in rpath:
55+
_replace_rpath(filepath, '@loader_path/', '@loader_path/' + libs_relative_path)
56+
57+
else:
58+
59+
def fix_rpath(filepath: Path, libs_relative_path: str) -> None:
60+
raise NotImplementedError(f'Bundling libraries in wheel is not supported on {sys.platform}')

tests/packages/link-against-local-lib/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ py.extension_module(
1616
'example',
1717
'examplemod.c',
1818
link_with: example_lib,
19+
link_args: ['-Wl,-rpath,custom-rpath'],
1920
install: true,
2021
)

tests/test_wheel.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# SPDX-License-Identifier: MIT
44

55
import os
6-
import platform
76
import re
87
import shutil
98
import stat
@@ -166,28 +165,25 @@ def test_rpath(wheel_link_against_local_lib, tmp_path):
166165
artifact = wheel.wheelfile.WheelFile(wheel_link_against_local_lib)
167166
artifact.extractall(tmp_path)
168167

169-
if platform.system() == 'Linux':
170-
elf = mesonpy._elf.ELF(tmp_path / f'example{EXT_SUFFIX}')
171-
assert '$ORIGIN/.link_against_local_lib.mesonpy.libs' in elf.rpath
172-
else: # 'Darwin'
173-
dylib = mesonpy._dylib.Dylib(tmp_path / f'example{EXT_SUFFIX}')
174-
assert '@loader_path/.link_against_local_lib.mesonpy.libs' in dylib.rpath
168+
origin = {'linux': '$ORIGIN', 'darwin': '@loader_path'}[sys.platform]
169+
expected = {f'{origin}/.link_against_local_lib.mesonpy.libs', 'custom-rpath',}
170+
171+
rpath = set(mesonpy._rpath._get_rpath(tmp_path / f'example{EXT_SUFFIX}'))
172+
# Verify that rpath is a superset of the expected one: linking to
173+
# the Python runtime may require additional rpath entries.
174+
assert rpath >= expected
175175

176176

177177
@pytest.mark.skipif(sys.platform not in {'linux', 'darwin'}, reason='Not supported on this platform')
178178
def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path):
179179
artifact = wheel.wheelfile.WheelFile(wheel_purelib_and_platlib)
180180
artifact.extractall(tmp_path)
181181

182-
if platform.system() == 'Linux':
183-
shared_lib = mesonpy._elf.ELF(tmp_path / f'plat{EXT_SUFFIX}')
184-
else: # 'Darwin'
185-
shared_lib = mesonpy._dylib.Dylib(tmp_path / f'plat{EXT_SUFFIX}')
186-
if shared_lib.rpath:
187-
# shared_lib.rpath is a frozenset, so iterate over it. An rpath may be
188-
# present, e.g. when conda is used (rpath will be <conda-prefix>/lib/)
189-
for rpath in shared_lib.rpath:
190-
assert 'mesonpy.libs' not in rpath
182+
origin = {'linux': '$ORIGIN', 'darwin': '@loader_path'}[sys.platform]
183+
184+
rpath = mesonpy._rpath._get_rpath(tmp_path / f'plat{EXT_SUFFIX}')
185+
for path in rpath:
186+
assert origin not in path
191187

192188

193189
@pytest.mark.skipif(sys.platform not in {'linux', 'darwin'}, reason='Not supported on this platform')

0 commit comments

Comments
 (0)