Skip to content

Commit

Permalink
macos: Rewrite install_name for dependent built libraries on install
Browse files Browse the repository at this point in the history
On macOS, we set the install_name for built libraries to
@rpath/libfoo.dylib, and when linking to the library, we set the RPATH
to its path in the build directory. This allows all built binaries to
be run as-is from the build directory (uninstalled).

However, on install, we have to strip all the RPATHs because they
point to the build directory, and we change the install_name of all
built libraries to the absolute path to the library. This causes the
install name in binaries to be out of date.

We now change that install name to point to the absolute path to each
built library after installation.

Fixes #3038
Fixes #3077

With this, the default workflow on macOS matches what everyone seems
to do, including Autotools and CMake. The next step is providing a way
for build files to override the install_name that is used after
installation for use with, f.ex., private libraries when combined with
the install_rpath: kwarg on targets.
  • Loading branch information
nirbheek committed Jun 8, 2018
1 parent d51552e commit 02eb35e
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 84 deletions.
10 changes: 10 additions & 0 deletions mesonbuild/backend/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ def __init__(self, source_dir, build_dir, prefix, strip_bin,
self.install_subdirs = []
self.mesonintrospect = mesonintrospect

class TargetInstallData:
def __init__(self, fname, outdir, aliases, strip, install_name_mappings, install_rpath, install_mode):
self.fname = fname
self.outdir = outdir
self.aliases = aliases
self.strip = strip
self.install_name_mappings = install_name_mappings
self.install_rpath = install_rpath
self.install_mode = install_mode

class ExecutableSerialisation:
def __init__(self, name, fname, cmd_args, env, is_cross, exe_wrapper,
workdir, extra_paths, capture):
Expand Down
105 changes: 66 additions & 39 deletions mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
from .. import mlog
from .. import dependencies
from .. import compilers
from ..compilers import CompilerArgs
from ..compilers import CompilerArgs, get_macos_dylib_install_name
from ..linkers import ArLinker
from ..mesonlib import File, MesonException, OrderedSet
from ..mesonlib import get_compiler_for_source, has_path_sep
from .backends import CleanTrees, InstallData
from .backends import CleanTrees, InstallData, TargetInstallData
from ..build import InvalidArguments

if mesonlib.is_windows():
Expand Down Expand Up @@ -699,34 +699,61 @@ def generate_install(self, outfile):
with open(install_data_file, 'wb') as ofile:
pickle.dump(d, ofile)

def get_target_install_dirs(self, t):
# Find the installation directory.
if isinstance(t, build.SharedModule):
default_install_dir = self.environment.get_shared_module_dir()
elif isinstance(t, build.SharedLibrary):
default_install_dir = self.environment.get_shared_lib_dir()
elif isinstance(t, build.StaticLibrary):
default_install_dir = self.environment.get_static_lib_dir()
elif isinstance(t, build.Executable):
default_install_dir = self.environment.get_bindir()
elif isinstance(t, build.CustomTarget):
default_install_dir = None
else:
assert(isinstance(t, build.BuildTarget))
# XXX: Add BuildTarget-specific install dir cases here
default_install_dir = self.environment.get_libdir()
outdirs = t.get_custom_install_dir()
if outdirs[0] is not None and outdirs[0] != default_install_dir and outdirs[0] is not True:
# Either the value is set to a non-default value, or is set to
# False (which means we want this specific output out of many
# outputs to not be installed).
custom_install_dir = True
else:
custom_install_dir = False
outdirs[0] = default_install_dir
return outdirs, custom_install_dir

def get_target_link_deps_mappings(self, t, prefix):
'''
On macOS, we need to change the install names of all built libraries
that a target depends on using install_name_tool so that the target
continues to work after installation. For this, we need a dictionary
mapping of the install_name value to the new one, so we can change them
on install.
'''
result = {}
if isinstance(t, build.StaticLibrary):
return result
for ld in t.get_all_link_deps():
if ld is t or not isinstance(ld, build.SharedLibrary):
continue
old = get_macos_dylib_install_name(ld.prefix, ld.name, ld.suffix, ld.soversion)
if old in result:
continue
fname = ld.get_filename()
outdirs, _ = self.get_target_install_dirs(ld)
new = os.path.join(prefix, outdirs[0], fname)
result.update({old: new})
return result

def generate_target_install(self, d):
for t in self.build.get_targets().values():
if not t.should_install():
continue
# Find the installation directory.
if isinstance(t, build.SharedModule):
default_install_dir = self.environment.get_shared_module_dir()
elif isinstance(t, build.SharedLibrary):
default_install_dir = self.environment.get_shared_lib_dir()
elif isinstance(t, build.StaticLibrary):
default_install_dir = self.environment.get_static_lib_dir()
elif isinstance(t, build.Executable):
default_install_dir = self.environment.get_bindir()
elif isinstance(t, build.CustomTarget):
default_install_dir = None
else:
assert(isinstance(t, build.BuildTarget))
# XXX: Add BuildTarget-specific install dir cases here
default_install_dir = self.environment.get_libdir()
outdirs = t.get_custom_install_dir()
if outdirs[0] is not None and outdirs[0] != default_install_dir and outdirs[0] is not True:
# Either the value is set to a non-default value, or is set to
# False (which means we want this specific output out of many
# outputs to not be installed).
custom_install_dir = True
else:
custom_install_dir = False
outdirs[0] = default_install_dir
outdirs, custom_install_dir = self.get_target_install_dirs(t)
# Sanity-check the outputs and install_dirs
num_outdirs, num_out = len(outdirs), len(t.get_outputs())
if num_outdirs != 1 and num_outdirs != num_out:
Expand All @@ -741,8 +768,10 @@ def generate_target_install(self, d):
# Install primary build output (library/executable/jar, etc)
# Done separately because of strip/aliases/rpath
if outdirs[0] is not False:
i = [self.get_target_filename(t), outdirs[0],
t.get_aliases(), should_strip, t.install_rpath, install_mode]
mappings = self.get_target_link_deps_mappings(t, d.prefix)
i = TargetInstallData(self.get_target_filename(t), outdirs[0],
t.get_aliases(), should_strip, mappings,
t.install_rpath, install_mode)
d.targets.append(i)
# On toolchains/platforms that use an import library for
# linking (separate from the shared library with all the
Expand All @@ -756,11 +785,8 @@ def generate_target_install(self, d):
else:
implib_install_dir = self.environment.get_import_lib_dir()
# Install the import library.
i = [self.get_target_filename_for_linking(t),
implib_install_dir,
# It has no aliases, should not be stripped, and
# doesn't have an install_rpath
{}, False, '', install_mode]
i = TargetInstallData(self.get_target_filename_for_linking(t),
implib_install_dir, {}, False, {}, '', install_mode)
d.targets.append(i)
# Install secondary outputs. Only used for Vala right now.
if num_outdirs > 1:
Expand All @@ -769,7 +795,8 @@ def generate_target_install(self, d):
if outdir is False:
continue
f = os.path.join(self.get_target_dir(t), output)
d.targets.append([f, outdir, {}, False, None, install_mode])
i = TargetInstallData(f, outdir, {}, False, {}, None, install_mode)
d.targets.append(i)
elif isinstance(t, build.CustomTarget):
# If only one install_dir is specified, assume that all
# outputs will be installed into it. This is for
Expand All @@ -781,14 +808,16 @@ def generate_target_install(self, d):
if num_outdirs == 1 and num_out > 1:
for output in t.get_outputs():
f = os.path.join(self.get_target_dir(t), output)
d.targets.append([f, outdirs[0], {}, False, None, install_mode])
i = TargetInstallData(f, outdirs[0], {}, False, {}, None, install_mode)
d.targets.append(i)
else:
for output, outdir in zip(t.get_outputs(), outdirs):
# User requested that we not install this output
if outdir is False:
continue
f = os.path.join(self.get_target_dir(t), output)
d.targets.append([f, outdir, {}, False, None, install_mode])
i = TargetInstallData(f, outdir, {}, False, {}, None, install_mode)
d.targets.append(i)

def generate_custom_install_script(self, d):
result = []
Expand Down Expand Up @@ -2392,7 +2421,6 @@ def get_cross_stdlib_link_args(self, target, linker):
return linker.get_no_stdlib_link_args()

def get_target_type_link_args(self, target, linker):
abspath = os.path.join(self.environment.get_build_dir(), target.subdir)
commands = []
if isinstance(target, build.Executable):
# Currently only used with the Swift compiler to add '-emit-executable'
Expand All @@ -2416,8 +2444,7 @@ def get_target_type_link_args(self, target, linker):
commands += linker.get_pic_args()
# Add -Wl,-soname arguments on Linux, -install_name on OS X
commands += linker.get_soname_args(target.prefix, target.name, target.suffix,
abspath, target.soversion,
isinstance(target, build.SharedModule))
target.soversion, isinstance(target, build.SharedModule))
# This is only visited when building for Windows using either GCC or Visual Studio
if target.vs_module_defs and hasattr(linker, 'gen_vs_module_defs_args'):
commands += linker.gen_vs_module_defs_args(target.vs_module_defs.rel_to_builddir(self.build_to_src))
Expand Down
2 changes: 2 additions & 0 deletions mesonbuild/compilers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'clike_langs',
'c_suffixes',
'cpp_suffixes',
'get_macos_dylib_install_name',
'get_base_compile_args',
'get_base_link_args',
'is_assembly',
Expand Down Expand Up @@ -105,6 +106,7 @@
clike_langs,
c_suffixes,
cpp_suffixes,
get_macos_dylib_install_name,
get_base_compile_args,
get_base_link_args,
is_header,
Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/compilers/c.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def get_no_warn_args(self):
# Almost every compiler uses this for disabling warnings
return ['-w']

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, *args):
return []

def split_shlib_to_parts(self, fname):
Expand Down
32 changes: 19 additions & 13 deletions mesonbuild/compilers/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,9 @@ def build_osx_rpath_args(self, build_dir, rpath_paths, build_rpath):
abs_rpaths = [os.path.join(build_dir, p) for p in rpath_paths]
if build_rpath != '':
abs_rpaths.append(build_rpath)
args = ['-Wl,-rpath,' + rp for rp in abs_rpaths]
# Ensure that there is enough space for large RPATHs
args = ['-Wl,-headerpad_max_install_names']
args += ['-Wl,-rpath,' + rp for rp in abs_rpaths]
return args

def build_unix_rpath_args(self, build_dir, from_dir, rpath_paths, build_rpath, install_rpath):
Expand Down Expand Up @@ -1056,7 +1058,14 @@ def language_stdlib_only_link_flags(self):
GNU_LD_AS_NEEDED = '-Wl,--as-needed'
APPLE_LD_AS_NEEDED = '-Wl,-dead_strip_dylibs'

def get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_macos_dylib_install_name(prefix, shlib_name, suffix, soversion):
install_name = prefix + shlib_name
if soversion is not None:
install_name += '.' + soversion
install_name += '.dylib'
return '@rpath/' + install_name

def get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, is_shared_module):
if soversion is None:
sostr = ''
else:
Expand All @@ -1069,11 +1078,8 @@ def get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, path, soversion, i
elif gcc_type == GCC_OSX:
if is_shared_module:
return []
install_name = prefix + shlib_name
if soversion is not None:
install_name += '.' + soversion
install_name += '.dylib'
return ['-install_name', os.path.join('@rpath', install_name)]
name = get_macos_dylib_install_name(prefix, shlib_name, suffix, soversion)
return ['-install_name', name]
else:
raise RuntimeError('Not implemented yet.')

Expand Down Expand Up @@ -1213,8 +1219,8 @@ def get_pch_suffix(self):
def split_shlib_to_parts(self, fname):
return os.path.dirname(fname), fname

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
return get_gcc_soname_args(self.gcc_type, prefix, shlib_name, suffix, path, soversion, is_shared_module)
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
return get_gcc_soname_args(self.gcc_type, prefix, shlib_name, suffix, soversion, is_shared_module)

def get_std_shared_lib_link_args(self):
return ['-shared']
Expand Down Expand Up @@ -1330,7 +1336,7 @@ def get_pch_use_args(self, pch_dir, header):
# so it might change semantics at any time.
return ['-include-pch', os.path.join(pch_dir, self.get_pch_name(header))]

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
if self.clang_type == CLANG_STANDARD:
gcc_type = GCC_STANDARD
elif self.clang_type == CLANG_OSX:
Expand All @@ -1339,7 +1345,7 @@ def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared
gcc_type = GCC_MINGW
else:
raise MesonException('Unreachable code when converting clang type to gcc type.')
return get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, path, soversion, is_shared_module)
return get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, is_shared_module)

def has_multi_arguments(self, args, env):
myargs = ['-Werror=unknown-warning-option', '-Werror=unused-command-line-argument']
Expand Down Expand Up @@ -1422,7 +1428,7 @@ def get_pch_name(self, header_name):
def split_shlib_to_parts(self, fname):
return os.path.dirname(fname), fname

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
if self.icc_type == ICC_STANDARD:
gcc_type = GCC_STANDARD
elif self.icc_type == ICC_OSX:
Expand All @@ -1431,7 +1437,7 @@ def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared
gcc_type = GCC_MINGW
else:
raise MesonException('Unreachable code when converting icc type to gcc type.')
return get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, path, soversion, is_shared_module)
return get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, is_shared_module)

# TODO: centralise this policy more globally, instead
# of fragmenting it into GnuCompiler and ClangCompiler
Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/compilers/cs.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def get_output_args(self, fname):
def get_link_args(self, fname):
return ['-r:' + fname]

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, *args):
return []

def get_werror_args(self):
Expand Down
4 changes: 2 additions & 2 deletions mesonbuild/compilers/d.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ def get_pic_args(self):
def get_std_shared_lib_link_args(self):
return ['-shared']

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
# FIXME: Make this work for Windows, MacOS and cross-compiling
return get_gcc_soname_args(GCC_STANDARD, prefix, shlib_name, suffix, path, soversion, is_shared_module)
return get_gcc_soname_args(GCC_STANDARD, prefix, shlib_name, suffix, soversion, is_shared_module)

def get_feature_args(self, kwargs, build_to_src):
res = []
Expand Down
8 changes: 4 additions & 4 deletions mesonbuild/compilers/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ def get_warn_args(self, level):
def get_no_warn_args(self):
return CCompiler.get_no_warn_args(self)

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
return CCompiler.get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module)
def get_soname_args(self, *args):
return CCompiler.get_soname_args(self, *args)

def split_shlib_to_parts(self, fname):
return CCompiler.split_shlib_to_parts(self, fname)

def build_rpath_args(self, build_dir, from_dir, rpath_paths, build_rpath, install_rpath):
return CCompiler.build_rpath_args(self, build_dir, from_dir, rpath_paths, build_rpath, install_rpath)
def build_rpath_args(self, *args):
return CCompiler.build_rpath_args(self, *args)

def get_dependency_gen_args(self, outtarget, outfile):
return []
Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/compilers/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, exelist, version):
self.id = 'unknown'
self.javarunner = 'java'

def get_soname_args(self, prefix, shlib_name, suffix, path, soversion, is_shared_module):
def get_soname_args(self, *args):
return []

def get_werror_args(self):
Expand Down
11 changes: 5 additions & 6 deletions mesonbuild/mintro.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ def buildparser():
def determine_installed_path(target, installdata):
install_target = None
for i in installdata.targets:
if os.path.basename(i[0]) == target.get_filename(): # FIXME, might clash due to subprojects.
if os.path.basename(i.fname) == target.get_filename(): # FIXME, might clash due to subprojects.
install_target = i
break
if install_target is None:
raise RuntimeError('Something weird happened. File a bug.')
fname = i[0]
outdir = i[1]
outname = os.path.join(installdata.prefix, outdir, os.path.basename(fname))
outname = os.path.join(installdata.prefix, i.outdir, os.path.basename(i.fname))
# Normalize the path by using os.path.sep consistently, etc.
# Does not change the effective path.
return str(pathlib.PurePath(outname))
Expand All @@ -69,8 +67,9 @@ def determine_installed_path(target, installdata):
def list_installed(installdata):
res = {}
if installdata is not None:
for path, installdir, aliases, *unknown in installdata.targets:
res[os.path.join(installdata.build_dir, path)] = os.path.join(installdata.prefix, installdir, os.path.basename(path))
for t in installdata.targets:
res[os.path.join(installdata.build_dir, t.fname)] = \
os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname))
for path, installpath, unused_prefix in installdata.data:
res[path] = os.path.join(installdata.prefix, installpath)
for path, installdir in installdata.headers:
Expand Down
Loading

0 comments on commit 02eb35e

Please sign in to comment.