Skip to content

Commit d764723

Browse files
pastewkaFFY00
authored andcommitted
Fix linking against libraries from Meson project on macOS
Signed-off-by: Filipe Laíns <[email protected]>
1 parent 207d812 commit d764723

File tree

5 files changed

+95
-23
lines changed

5 files changed

+95
-23
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ If you have a general question feel free to [start a discussion][new discussion]
2323
on Github. If you want to report a bug, request a feature, or propose an improvement, feel
2424
free to open an issue on our [bugtracker][bugtracker].
2525

26+
2627
## Contributing
2728

2829
If you are interested in contributing, please check out

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ endif
1414
py.install_sources(
1515
'mesonpy/__init__.py',
1616
'mesonpy/_compat.py',
17+
'mesonpy/_dylib.py',
1718
'mesonpy/_editable.py',
1819
'mesonpy/_elf.py',
1920
'mesonpy/_introspection.py',

mesonpy/__init__.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import tomllib
4242

4343
import mesonpy._compat
44+
import mesonpy._dylib
4445
import mesonpy._elf
4546
import mesonpy._introspection
4647
import mesonpy._tags
@@ -527,19 +528,32 @@ def _install_path(
527528
arcname = os.path.join(destination, os.path.relpath(path, origin).replace(os.path.sep, '/'))
528529
wheel_file.write(path, arcname)
529530
else:
530-
if self._has_internal_libs and platform.system() == 'Linux':
531-
# add .mesonpy.libs to the RPATH of ELF files
532-
if self._is_native(os.fspath(origin)):
533-
# copy ELF to our working directory to avoid Meson having to regenerate the file
534-
new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir)
535-
os.makedirs(new_origin.parent, exist_ok=True)
536-
shutil.copy2(origin, new_origin)
537-
origin = new_origin
538-
# add our in-wheel libs folder to the RPATH
539-
elf = mesonpy._elf.ELF(origin)
540-
libdir_path = f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
541-
if libdir_path not in elf.rpath:
542-
elf.rpath = [*elf.rpath, libdir_path]
531+
if self._has_internal_libs:
532+
if platform.system() == 'Linux' or platform.system() == 'Darwin':
533+
# add .mesonpy.libs to the RPATH of ELF files
534+
if self._is_native(os.fspath(origin)):
535+
# copy ELF to our working directory to avoid Meson having to regenerate the file
536+
new_origin = self._libs_build_dir / pathlib.Path(origin).relative_to(self._build_dir)
537+
os.makedirs(new_origin.parent, exist_ok=True)
538+
shutil.copy2(origin, new_origin)
539+
origin = new_origin
540+
# add our in-wheel libs folder to the RPATH
541+
if platform.system() == 'Linux':
542+
elf = mesonpy._elf.ELF(origin)
543+
libdir_path = \
544+
f'$ORIGIN/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
545+
if libdir_path not in elf.rpath:
546+
elf.rpath = [*elf.rpath, libdir_path]
547+
elif platform.system() == 'Darwin':
548+
dylib = mesonpy._dylib.Dylib(origin)
549+
libdir_path = \
550+
f'@loader_path/{os.path.relpath(f".{self._project.name}.mesonpy.libs", destination.parent)}'
551+
if libdir_path not in dylib.rpath:
552+
dylib.rpath = [*dylib.rpath, libdir_path]
553+
else:
554+
# Internal libraries are currently unsupported on this platform
555+
raise NotImplementedError("Bundling libraries in wheel is not supported on platform '{}'"
556+
.format(platform.system()))
543557

544558
wheel_file.write(origin, location)
545559

@@ -576,7 +590,6 @@ def build(self, directory: Path) -> pathlib.Path:
576590

577591
# install bundled libraries
578592
for destination, origin in self._wheel_files['mesonpy-libs']:
579-
assert platform.system() == 'Linux', 'Bundling libraries in wheel is currently only supported in POSIX!'
580593
destination = pathlib.Path(f'.{self._project.name}.mesonpy.libs', destination)
581594
self._install_path(whl, counter, origin, destination)
582595

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
@@ -147,7 +147,7 @@ def test_configure_data(wheel_configure_data):
147147
}
148148

149149

150-
@pytest.mark.skipif(platform.system() != 'Linux', reason='Unsupported on this platform for now')
150+
@pytest.mark.skipif(platform.system() not in ['Linux', 'Darwin'], reason='Unsupported on this platform for now')
151151
def test_local_lib(venv, wheel_link_against_local_lib):
152152
venv.pip('install', wheel_link_against_local_lib)
153153
output = venv.python('-c', 'import example; print(example.example_sum(1, 2))')
@@ -187,25 +187,32 @@ def test_detect_wheel_tag_script(wheel_executable):
187187
assert name.group('plat') == PLATFORM
188188

189189

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

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

198202

199-
@pytest.mark.skipif(platform.system() != 'Linux', reason='Unsupported on this platform for now')
203+
@pytest.mark.skipif(platform.system() not in ['Linux', 'Darwin'], reason='Unsupported on this platform for now')
200204
def test_uneeded_rpath(wheel_purelib_and_platlib, tmp_path):
201205
artifact = wheel.wheelfile.WheelFile(wheel_purelib_and_platlib)
202206
artifact.extractall(tmp_path)
203207

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

211218

0 commit comments

Comments
 (0)