Skip to content

Commit 7fd9c3c

Browse files
committed
Fix linking against libraries from Meson project on macOS
1 parent f6cc2e0 commit 7fd9c3c

File tree

4 files changed

+92
-23
lines changed

4 files changed

+92
-23
lines changed

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ endif
1010
py.install_sources(
1111
'mesonpy/__init__.py',
1212
'mesonpy/_compat.py',
13+
'mesonpy/_dylib.py',
1314
'mesonpy/_editable.py',
1415
'mesonpy/_elf.py',
1516
'mesonpy/_introspection.py',

mesonpy/__init__.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545

4646
import mesonpy._compat
47+
import mesonpy._dylib
4748
import mesonpy._elf
4849
import mesonpy._introspection
4950
import mesonpy._tags
@@ -509,19 +510,28 @@ def _install_path(
509510
arcname = os.path.join(destination, os.path.relpath(path, origin).replace(os.path.sep, '/'))
510511
wheel_file.write(path, arcname)
511512
else:
512-
if self._has_internal_libs and platform.system() == 'Linux':
513-
# add .mesonpy.libs to the RPATH of ELF files
514-
if self._is_native(os.fspath(origin)):
515-
# copy ELF to our working directory to avoid Meson having to regenerate the file
516-
new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir)
517-
os.makedirs(new_origin.parent, exist_ok=True)
518-
shutil.copy2(origin, new_origin)
519-
origin = new_origin
520-
# add our in-wheel libs folder to the RPATH
521-
elf = mesonpy._elf.ELF(origin)
522-
libdir_path = f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
523-
if libdir_path not in elf.rpath:
524-
elf.rpath = [*elf.rpath, libdir_path]
513+
if self._has_internal_libs:
514+
if platform.system() == 'Linux' or platform.system() == 'Darwin':
515+
# add .mesonpy.libs to the RPATH of ELF files
516+
if self._is_native(os.fspath(origin)):
517+
# copy ELF to our working directory to avoid Meson having to regenerate the file
518+
new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir)
519+
os.makedirs(new_origin.parent, exist_ok=True)
520+
shutil.copy2(origin, new_origin)
521+
origin = new_origin
522+
# add our in-wheel libs folder to the RPATH
523+
if platform.system() == 'Linux':
524+
elf = mesonpy._elf.ELF(origin)
525+
libdir_path = \
526+
f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
527+
if libdir_path not in elf.rpath:
528+
elf.rpath = [*elf.rpath, libdir_path]
529+
else: # 'Darwin'
530+
dylib = mesonpy._dylib.Dylib(origin)
531+
libdir_path = \
532+
f'@loader_path/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
533+
if libdir_path not in dylib.rpath:
534+
dylib.rpath = [*dylib.rpath, libdir_path]
525535

526536
wheel_file.write(origin, location)
527537

@@ -558,7 +568,8 @@ def build(self, directory: Path) -> pathlib.Path:
558568

559569
# install bundled libraries
560570
for destination, origin in self._wheel_files['mesonpy-libs']:
561-
assert platform.system() == 'Linux', 'Bundling libraries in wheel is currently only supported in POSIX!'
571+
assert platform.system() == 'Linux' or platform.system() == 'Darwin', \
572+
'Bundling libraries in wheel is currently only supported in POSIX!'
562573
destination = pathlib.Path(f'.{self._project.name}.mesonpy.libs', destination)
563574
self._install_path(whl, counter, origin, destination)
564575

mesonpy/_dylib.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-License-Identifier: MIT
2+
# SPDX-FileCopyrightText: 2023 Lars Pastewka <[email protected]>
3+
4+
import os
5+
import subprocess
6+
7+
from typing import Optional
8+
9+
from mesonpy._compat import Collection, Path
10+
11+
12+
# This class is modeled after the ELF class in _elf.py
13+
class Dylib:
14+
def __init__(self, path: Path) -> None:
15+
self._path = os.fspath(path)
16+
self._rpath: Optional[Collection[str]] = None
17+
self._needed: Optional[Collection[str]] = None
18+
19+
def _otool(self, *args: str) -> str:
20+
return subprocess.check_output(['otool', *args, self._path], stderr=subprocess.STDOUT).decode()
21+
22+
def _install_name_tool(self, *args: str) -> str:
23+
return subprocess.check_output(['install_name_tool', *args, self._path], stderr=subprocess.STDOUT).decode()
24+
25+
@property
26+
def rpath(self) -> Collection[str]:
27+
if self._rpath is None:
28+
self._rpath = []
29+
# Run otool -l to get the load commands
30+
otool_output = self._otool('-l').strip()
31+
# Manually parse the output for LC_RPATH
32+
rpath_tag = False
33+
for line in [x.split() for x in otool_output.split('\n')]:
34+
if line == ['cmd', 'LC_RPATH']:
35+
rpath_tag = True
36+
elif len(line) >= 2 and line[0] == 'path' and rpath_tag:
37+
self._rpath += [line[1]]
38+
rpath_tag = False
39+
return frozenset(self._rpath)
40+
41+
@rpath.setter
42+
def rpath(self, value: Collection[str]) -> None:
43+
# We clear all LC_RPATH load commands
44+
if self._rpath:
45+
for rpath in self._rpath:
46+
self._install_name_tool('-delete_rpath', rpath)
47+
# We then rewrite the new load commands
48+
for rpath in value:
49+
self._install_name_tool('-add_rpath', rpath)
50+
self._rpath = value

tests/test_wheel.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_configure_data(wheel_configure_data):
144144
}
145145

146146

147-
@pytest.mark.skipif(platform.system() != 'Linux', reason='Unsupported on this platform for now')
147+
@pytest.mark.skipif(platform.system() not in ['Linux', 'Darwin'], reason='Unsupported on this platform for now')
148148
def test_local_lib(venv, wheel_link_against_local_lib):
149149
venv.pip('install', wheel_link_against_local_lib)
150150
output = venv.python('-c', 'import example; print(example.example_sum(1, 2))')
@@ -184,25 +184,32 @@ def test_detect_wheel_tag_script(wheel_executable):
184184
assert name.group('plat') == PLATFORM
185185

186186

187-
@pytest.mark.skipif(platform.system() != 'Linux', reason='Unsupported on this platform for now')
187+
@pytest.mark.skipif(platform.system() not in ['Linux', 'Darwin'], reason='Unsupported on this platform for now')
188188
def test_rpath(wheel_link_against_local_lib, tmp_path):
189189
artifact = wheel.wheelfile.WheelFile(wheel_link_against_local_lib)
190190
artifact.extractall(tmp_path)
191191

192-
elf = mesonpy._elf.ELF(tmp_path / f'example{EXT_SUFFIX}')
193-
assert '$ORIGIN/.link_against_local_lib.mesonpy.libs' in elf.rpath
192+
if platform.system() == 'Linux':
193+
elf = mesonpy._elf.ELF(tmp_path / f'example{EXT_SUFFIX}')
194+
assert '$ORIGIN/.link_against_local_lib.mesonpy.libs' in elf.rpath
195+
else: # 'Darwin'
196+
dylib = mesonpy._dylib.Dylib(tmp_path / f'example{EXT_SUFFIX}')
197+
assert '@loader_path/.link_against_local_lib.mesonpy.libs' in dylib.rpath
194198

195199

196-
@pytest.mark.skipif(platform.system() != 'Linux', reason='Unsupported on this platform for now')
200+
@pytest.mark.skipif(platform.system() not in ['Linux', 'Darwin'], reason='Unsupported on this platform for now')
197201
def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path):
198202
artifact = wheel.wheelfile.WheelFile(wheel_purelib_and_platlib)
199203
artifact.extractall(tmp_path)
200204

201-
elf = mesonpy._elf.ELF(tmp_path / f'plat{EXT_SUFFIX}')
202-
if elf.rpath:
203-
# elf.rpath is a frozenset, so iterate over it. An rpath may be
205+
if platform.system() == 'Linux':
206+
shared_lib = mesonpy._elf.ELF(tmp_path / f'plat{EXT_SUFFIX}')
207+
else: # 'Darwin'
208+
shared_lib = mesonpy._dylib.Dylib(tmp_path / f'plat{EXT_SUFFIX}')
209+
if shared_lib.rpath:
210+
# shared_lib.rpath is a frozenset, so iterate over it. An rpath may be
204211
# present, e.g. when conda is used (rpath will be <conda-prefix>/lib/)
205-
for rpath in elf.rpath:
212+
for rpath in shared_lib.rpath:
206213
assert 'mesonpy.libs' not in rpath
207214

208215

0 commit comments

Comments
 (0)