Skip to content

Commit c70d6c5

Browse files
committed
Reimplement @rpath support for macOS
1 parent f24041f commit c70d6c5

11 files changed

Lines changed: 188 additions & 78 deletions

File tree

‎CHANGES.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
result of `find_files` hasn't actually changed
1010
- Add support for system headers when compiling with MSVC
1111
- New `--verbose` option for configuration
12+
- The `install_name` for libraries on macOS now (again) uses `@rpath`-based
13+
paths prior to installation
1214

1315
### Bug fixes
1416
- Automatically update dependencies via `mopack` if the toolchain file has

‎bfg9000/builtins/pkg_config.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ..safe_str import literal, shell_literal
1515
from ..shell import posix as pshell
1616
from ..shell.syntax import Syntax, Writer
17-
from ..tools.install_name_tool import darwin_install_name
17+
from ..tools.install_name_tool import install_name as darwin_install_name
1818
from ..tools.pkg_config import GeneratedPkgConfigPackage, PkgConfigPackage
1919
from ..versioning import simplify_specifiers, Specifier, SpecifierSet
2020

@@ -423,7 +423,7 @@ def _write(self, out, data, installed):
423423
if builder.object_format == 'mach-o':
424424
all_libs = flatten(i.all for i in data['libs'])
425425
install_names = filter(None, (darwin_install_name(
426-
installify_fn(i), env, strict=False
426+
env, installify_fn(i), strict=False
427427
) for i in all_libs))
428428
self._write_variable(out, 'install_names', install_names,
429429
Syntax.shell)

‎bfg9000/tools/cc/linker.py‎

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -119,33 +119,38 @@ def _always_flags(self):
119119
return []
120120

121121
def _local_rpath(self, library, output):
122-
if ( not isinstance(library, Library) or
123-
self.builder.object_format != 'elf' ):
122+
if not isinstance(library, Library):
124123
return None, []
125-
126-
rpath = patchelf.local_rpath(self.env, library, output)
127-
if rpath is None:
124+
elif self.builder.object_format == 'elf':
125+
rpath = patchelf.local_rpath(self.env, library, output)
126+
if rpath is None:
127+
return None, []
128+
129+
# Prior to binutils 2.28, GNU's BFD-based ld doesn't correctly
130+
# respect $ORIGIN in a shared library's DT_RPATH/DT_RUNPATH field.
131+
# This results in ld being unable to find other shared libraries
132+
# needed by the directly-linked library. For more information, see:
133+
# <https://sourceware.org/bugzilla/show_bug.cgi?id=20535>.
134+
try:
135+
ld = self.builder.linker('raw')
136+
fix_rpath = (ld.brand == 'bfd' and ld.version in
137+
SpecifierSet('<2.28'))
138+
except KeyError:
139+
fix_rpath = False
140+
141+
rpath_link = []
142+
if output and fix_rpath:
143+
rpath_link = [
144+
i.path.parent() for i in
145+
recursive_walk(library.runtime_file, 'runtime_deps')
146+
]
147+
148+
return rpath, rpath_link
149+
elif self.builder.object_format == 'mach-o':
150+
return install_name_tool.local_rpath(self.env, library, output), []
151+
else:
128152
return None, []
129153

130-
# Prior to binutils 2.28, GNU's BFD-based ld doesn't correctly respect
131-
# $ORIGIN in a shared library's DT_RPATH/DT_RUNPATH field. This results
132-
# in ld being unable to find other shared libraries needed by the
133-
# directly-linked library. For more information, see:
134-
# <https://sourceware.org/bugzilla/show_bug.cgi?id=20535>.
135-
try:
136-
ld = self.builder.linker('raw')
137-
fix_rpath = (ld.brand == 'bfd' and ld.version in
138-
SpecifierSet('<2.28'))
139-
except KeyError:
140-
fix_rpath = False
141-
142-
rpath_link = []
143-
if output and fix_rpath:
144-
rpath_link = [i.path.parent() for i in
145-
recursive_walk(library.runtime_file, 'runtime_deps')]
146-
147-
return rpath, rpath_link
148-
149154
def always_libs(self, primary):
150155
# XXX: Don't just asssume that these are the right libraries to use.
151156
# For instance, clang users might want to use libc++ instead.
@@ -280,8 +285,13 @@ def flags(self, options, global_options=None, output=None, mode='normal'):
280285

281286
flags.extend('-L' + i for i in uniques(lib_dirs))
282287
if rpaths:
283-
flags.append('-Wl,-rpath,' + safe_str.join(rpaths, ':'))
288+
rpaths = uniques(rpaths)
289+
if self.builder.linker('raw').brand == 'apple':
290+
flags.extend('-Wl,-rpath,' + i for i in rpaths)
291+
else:
292+
flags.append('-Wl,-rpath,' + safe_str.join(rpaths, ':'))
284293
if rpath_links:
294+
rpath_links = uniques(rpath_links)
285295
flags.append('-Wl,-rpath-link,' + safe_str.join(rpath_links, ':'))
286296
return flags
287297

@@ -380,9 +390,8 @@ def _always_flags(self):
380390

381391
def _soname(self, library):
382392
if self.env.target_platform.genus == 'darwin':
383-
return ['-install_name', install_name_tool.darwin_install_name(
384-
library, self.env
385-
)]
393+
install_name = install_name_tool.install_name(self.env, library)
394+
return ['-install_name', install_name]
386395
else:
387396
return ['-Wl,-soname,' + library.runtime_file.path.basename()]
388397

‎bfg9000/tools/install_name_tool.py‎

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import os
2+
13
from . import tool
24
from .. import file_types, options as opts
35
from .common import SimpleCommand
4-
from ..iterutils import flatten, listify
6+
from ..iterutils import flatten
7+
from ..path import BasePath, Path, Root, InstallRoot
58

69

710
@tool('install_name_tool')
@@ -14,37 +17,100 @@ def __init__(self, env):
1417
default='install_name_tool'
1518
)
1619

17-
def _call(self, cmd, file, *, id=None, changes=[]):
20+
def _call(self, cmd, file, *, id=None, changes=[], rpaths=[]):
1821
args = []
1922
if id:
2023
args += ['-id', id]
21-
args += flatten(['-change'] + listify(i) for i in changes)
24+
args += flatten(['-change', old, new] for old, new in changes)
25+
for old, new in rpaths:
26+
if old and new:
27+
args += ['-rpath', old, new]
28+
elif new:
29+
args += ['-add_rpath', new]
30+
elif old:
31+
args += ['-delete_rpath', old]
2232

2333
if args:
2434
return cmd + args + [file]
2535

2636

27-
def darwin_install_name(library, env, strict=True):
28-
while isinstance(library, file_types.LinkLibrary):
29-
library = library.library
37+
def install_name(env, library, *, strict=True):
38+
if library.runtime_file:
39+
path = library.runtime_file.path
40+
if path.root in InstallRoot:
41+
return path.cross(env)
42+
else:
43+
return os.path.join('@rpath', path.suffix)
44+
elif not strict:
45+
return None
46+
47+
raise TypeError('unable to create install_name') # pragma: no cover
48+
49+
50+
def local_rpath(env, library, output):
51+
if not library.runtime_file:
52+
return None
53+
54+
rpath = library.runtime_file.path.parent().cross(env)
55+
if rpath.root != Root.absolute and rpath.root not in InstallRoot:
56+
if not output:
57+
raise ValueError('unable to construct runtime search path')
58+
# This should almost always be true, except for when linking to a
59+
# shared library stored in the srcdir.
60+
if rpath.root == output.path.root:
61+
rpath = Path('.', rpath.root).relpath(output.path.parent(),
62+
prefix='@loader_path')
63+
return rpath
64+
3065

31-
if isinstance(library, file_types.VersionedSharedLibrary):
32-
return library.soname.path.string(env.base_dirs)
33-
elif isinstance(library, file_types.SharedLibrary):
34-
return library.path.string(env.base_dirs)
35-
elif strict: # pragma: no cover
36-
raise TypeError('unable to create darwin install_name')
37-
else:
66+
def installed_rpath(env, library, install_db):
67+
if not library.runtime_file:
3868
return None
69+
try:
70+
return install_db.target[library.runtime_file].path.parent()
71+
except KeyError:
72+
if library.runtime_file.path.root == Root.absolute:
73+
return library.runtime_file.path.parent()
74+
raise
3975

4076

4177
def post_install(env, options, output, install_db, *, is_library=False):
78+
new_id = None
79+
if is_library:
80+
installed_id = install_name(env, install_db.host[output])
81+
if installed_id != install_name(env, output):
82+
new_id = installed_id
83+
4284
change_opts = options.filter(opts.install_name_change)
4385
changes = ([(i.old, i.new) for i in change_opts] +
44-
[(darwin_install_name(i, env), install_db.target[i].path)
86+
[(install_name(env, i), install_name(env, install_db.target[i]))
4587
for i in output.runtime_deps])
4688

89+
# Compute all the changed rpaths, preserving the original order. To do
90+
# this, add all the entries into a dict with the key being the old rpath;
91+
# for new entries, set the key to a sentinel value for uniqueness, which
92+
# we'll remove at the end.
93+
new_rpaths = {}
94+
sentinel = 1
95+
for i in options:
96+
if isinstance(i, opts.lib):
97+
if not isinstance(i.library, file_types.Library):
98+
continue
99+
local = local_rpath(env, i.library, output)
100+
if local is not None:
101+
installed = installed_rpath(env, i.library, install_db)
102+
if not isinstance(local, BasePath) or local != installed:
103+
new_rpaths[local] = installed
104+
elif isinstance(i, opts.rpath_dir):
105+
if i.when == opts.RpathWhen.installed:
106+
new_rpaths[sentinel] = i.path
107+
sentinel += 1
108+
elif i.when == opts.RpathWhen.uninstalled:
109+
new_rpaths[i.path] = None
110+
new_rpaths = ((k if not isinstance(k, int) else None, v)
111+
for k, v in new_rpaths.items())
112+
47113
path = install_db.host[output].path
48114
return env.tool('install_name_tool')(
49-
path, id=path.cross(env) if is_library else None, changes=changes
115+
path, id=new_id, changes=changes, rpaths=new_rpaths
50116
)

‎bfg9000/tools/pkg_config.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,8 @@ def link_options(self, linker, *, raw=False):
235235
# should check if there are any install_names set that we need
236236
# to update when installing. For more information, see the
237237
# pkg-config builtin.
238-
extra_opts = self._get_install_name_changes()
238+
extra_opts = (self._get_rpaths() +
239+
self._get_install_name_changes())
239240

240241
return flags + libdirs + libs + extra_opts
241242

‎doc/about/changes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ in progress
1111
result of `find_files` hasn't actually changed
1212
- Add support for system headers when compiling with MSVC
1313
- New `--verbose` option for configuration
14+
- The `install_name` for libraries on macOS now (again) uses `@rpath`-based
15+
paths prior to installation
1416

1517
### Bug fixes
1618
- Automatically update dependencies via `mopack` if the toolchain file has

‎test/unit/tools/cc/test_cc.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ def check_package(self, pkg):
273273
self.assertEqual(pkg.link_options(self.linker), option_list(
274274
'-pthread', opts.lib_dir(Directory(Path('/path'))),
275275
opts.lib_literal('-lfoo'),
276-
(opts.rpath_dir(Path('/path')) if self.platform_name == 'linux'
277-
else None)
276+
(opts.rpath_dir(Path('/path'))
277+
if self.platform_name in ['linux', 'macos'] else None)
278278
))
279279

280280
def test_resolve_pkg_config(self):

‎test/unit/tools/cc/test_linker.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def test_flags_empty(self):
9797
self.assertEqual(self.linker.flags(opts.option_list()), [])
9898

9999
def test_flags_lib_dir(self):
100-
libdir = self.Path('/path/to/lib')
100+
libdir = self.Path('/path/to/lib/')
101101
lib = self.Path('/path/to/lib/libfoo.a')
102102
srclibdir = self.Path('.', Root.srcdir)
103103
srclib = self.Path('libfoo.a', Root.srcdir)
@@ -107,15 +107,14 @@ def test_flags_lib_dir(self):
107107
if self.shared:
108108
output = SharedLibrary(self.Path('out'), 'native')
109109
if self.env.target_platform.genus == 'darwin':
110-
soname = ['-install_name',
111-
self.Path('out').string(self.env.base_dirs)]
110+
soname = ['-install_name', os.path.join('@rpath', 'out')]
112111
else:
113112
soname = ['-Wl,-soname,out']
114113
else:
115114
output = Executable(self.Path('exe'), 'native')
116115
soname = []
117116

118-
if self.env.target_platform.genus == 'linux':
117+
if self.env.target_platform.genus in ['linux', 'darwin']:
119118
rpath = ['-Wl,-rpath,' + libdir]
120119
srcdir_rpath = ['-Wl,-rpath,' + srclibdir]
121120
else:
@@ -493,8 +492,10 @@ def test_post_installed_macos(self):
493492
self.assertEqual(fn(install_outputs), (
494493
[install_name_tool] +
495494
(['-id', installed.cross(self.env)] if self.shared else []) +
496-
['-change', deplib.path.string(self.env.base_dirs),
497-
installify(deplib, cross=self.env).path, installed]
495+
['-change', os.path.join('@rpath', deplib.path.suffix),
496+
installify(deplib, cross=self.env).path,
497+
'-rpath', '@loader_path', self.Path('.', InstallRoot.libdir),
498+
installed]
498499
))
499500

500501

‎test/unit/tools/msvc/test_msvc.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ def check_package(self, pkg):
172172
self.assertEqual(pkg.link_options(self.linker), option_list(
173173
'/DEBUG', opts.lib_dir(Directory(Path('/path'))),
174174
opts.lib_literal('foo.lib'),
175-
(opts.rpath_dir(Path('/path')) if self.platform_name == 'linux'
176-
else None)
175+
(opts.rpath_dir(Path('/path'))
176+
if self.platform_name in ['linux', 'macos'] else None)
177177
))
178178

179179
def test_lang(self):

0 commit comments

Comments
 (0)