Improve support for macOS dylib versioning

We now use the soversion to set compatibility_version and
current_version by default. This is the only sane thing we can do by
default because of the restrictions on the values that can be used for
compatibility and current version.

Users can override this value with the `darwin_versions:` kwarg, which
can be a single value or a two-element list of values. The first one
is the compatibility version and the second is the current version.

Fixes https://github.com/mesonbuild/meson/issues/3555
Fixes https://github.com/mesonbuild/meson/issues/1451
pull/4099/head
Nirbheek Chauhan 6 years ago committed by Nirbheek Chauhan
parent b86f2fd17f
commit bead8287a5
  1. 11
      docs/markdown/Reference-manual.md
  2. 9
      docs/markdown/snippets/shared_library_darwin_versions.md
  3. 3
      mesonbuild/backend/ninjabackend.py
  4. 49
      mesonbuild/build.py
  5. 30
      mesonbuild/compilers/compilers.py
  6. 6
      mesonbuild/compilers/d.py
  7. 129
      run_unittests.py
  8. 2
      test cases/osx/2 library versions/installed_files.txt
  9. 17
      test cases/osx/2 library versions/meson.build

@ -1183,13 +1183,20 @@ extra keyword arguments.
`soversion` is `4`, a Windows DLL will be called `foo-4.dll` and one
of the aliases of the Linux shared library would be
`libfoo.so.4`. If this is not specified, the first part of `version`
is used instead. For example, if `version` is `3.6.0` and
is used instead (see below). For example, if `version` is `3.6.0` and
`soversion` is not defined, it is set to `3`.
- `version` a string specifying the version of this shared library,
such as `1.1.0`. On Linux and OS X, this is used to set the shared
library version in the filename, such as `libfoo.so.1.1.0` and
`libfoo.1.1.0.dylib`. If this is not specified, `soversion` is used
instead (see below).
instead (see above).
- `darwin_versions` *(added 0.48)* an integer, string, or a list of
versions to use for setting dylib `compatibility version` and
`current version` on macOS. If a list is specified, it must be
either zero, one, or two elements. If only one element is specified
or if it's not a list, the specified value will be used for setting
both compatibility version and current version. If unspecified, the
`soversion` will be used as per the aforementioned rules.
- `vs_module_defs` a string, a File object, or Custom Target for a
Microsoft module definition file for controlling symbol exports,
etc., on platforms where that is possible (e.g. Windows).

@ -0,0 +1,9 @@
## `shared_library()` now supports setting dylib compatibility and current version
Now, by default `shared_library()` sets `-compatibility_version` and
`-current_version` of a macOS dylib using the `soversion`.
This can be overriden by using the `darwin_versions:` kwarg to
[`shared_library()`](Reference-manual.md#shared_library). As usual, you can
also pass this kwarg to `library()` or `build_target()` and it will be used in
the appropriate circumstances.

@ -2257,7 +2257,8 @@ rule FORTRAN_DEP_HACK%s
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,
target.soversion, isinstance(target, build.SharedModule))
target.soversion, target.darwin_versions,
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))

@ -83,7 +83,7 @@ known_build_target_kwargs = (
cs_kwargs)
known_exe_kwargs = known_build_target_kwargs | {'implib', 'export_dynamic'}
known_shlib_kwargs = known_build_target_kwargs | {'version', 'soversion', 'vs_module_defs'}
known_shlib_kwargs = known_build_target_kwargs | {'version', 'soversion', 'vs_module_defs', 'darwin_versions'}
known_shmod_kwargs = known_build_target_kwargs
known_stlib_kwargs = known_build_target_kwargs | {'pic'}
known_jar_kwargs = known_exe_kwargs | {'main_class'}
@ -1392,6 +1392,8 @@ class SharedLibrary(BuildTarget):
def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs):
self.soversion = None
self.ltversion = None
# Max length 2, first element is compatibility_version, second is current_version
self.darwin_versions = []
self.vs_module_defs = None
# The import library this target will generate
self.import_filename = None
@ -1518,6 +1520,44 @@ class SharedLibrary(BuildTarget):
self.filename = self.filename_tpl.format(self)
self.outputs = [self.filename]
@staticmethod
def _validate_darwin_versions(darwin_versions):
try:
if isinstance(darwin_versions, int):
darwin_versions = str(darwin_versions)
if isinstance(darwin_versions, str):
darwin_versions = 2 * [darwin_versions]
if not isinstance(darwin_versions, list):
raise InvalidArguments('Shared library darwin_versions: must be a string, integer,'
'or a list, not {!r}'.format(darwin_versions))
if len(darwin_versions) > 2:
raise InvalidArguments('Shared library darwin_versions: list must contain 2 or fewer elements')
if len(darwin_versions) == 1:
darwin_versions = 2 * darwin_versions
for i, v in enumerate(darwin_versions[:]):
if isinstance(v, int):
v = str(v)
if not isinstance(v, str):
raise InvalidArguments('Shared library darwin_versions: list elements '
'must be strings or integers, not {!r}'.format(v))
if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', v):
raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z where '
'X, Y, Z are numbers, and Y and Z are optional')
parts = v.split('.')
if len(parts) in (1, 2, 3) and int(parts[0]) > 65535:
raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
'where X is [0, 65535] and Y, Z are optional')
if len(parts) in (2, 3) and int(parts[1]) > 255:
raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
'where Y is [0, 255] and Y, Z are optional')
if len(parts) == 3 and int(parts[2]) > 255:
raise InvalidArguments('Shared library darwin_versions: must be X.Y.Z '
'where Z is [0, 255] and Y, Z are optional')
darwin_versions[i] = v
except ValueError:
raise InvalidArguments('Shared library darwin_versions: value is invalid')
return darwin_versions
def process_kwargs(self, kwargs, environment):
super().process_kwargs(kwargs, environment)
@ -1546,6 +1586,13 @@ class SharedLibrary(BuildTarget):
# We replicate what Autotools does here and take the first
# number of the version by default.
self.soversion = self.ltversion.split('.')[0]
# macOS and iOS dylib compatibility_version and current_version
if 'darwin_versions' in kwargs:
self.darwin_versions = self._validate_darwin_versions(kwargs['darwin_versions'])
elif self.soversion:
# If unspecified, pick the soversion
self.darwin_versions = 2 * [self.soversion]
# Visual Studio module-definitions file
if 'vs_module_defs' in kwargs:
path = kwargs['vs_module_defs']

@ -1056,14 +1056,14 @@ class Compiler:
return None
def build_osx_rpath_args(self, build_dir, rpath_paths, build_rpath):
# Ensure that there is enough space for large RPATHs and install_name
args = ['-Wl,-headerpad_max_install_names']
if not rpath_paths and not build_rpath:
return []
return args
# On OSX, rpaths must be absolute.
abs_rpaths = [os.path.join(build_dir, p) for p in rpath_paths]
if build_rpath != '':
abs_rpaths.append(build_rpath)
# Ensure that there is enough space for large RPATHs
args = ['-Wl,-headerpad_max_install_names']
# Need to deduplicate abs_rpaths, as rpath_paths and
# build_rpath are not guaranteed to be disjoint sets
args += ['-Wl,-rpath,' + rp for rp in OrderedSet(abs_rpaths)]
@ -1165,12 +1165,9 @@ def get_macos_dylib_install_name(prefix, shlib_name, suffix, 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:
sostr = '.' + soversion
def get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, darwin_versions, is_shared_module):
if gcc_type == GCC_STANDARD:
sostr = '' if soversion is None else '.' + soversion
return ['-Wl,-soname,%s%s.%s%s' % (prefix, shlib_name, suffix, sostr)]
elif gcc_type in (GCC_MINGW, GCC_CYGWIN):
# For PE/COFF the soname argument has no effect with GNU LD
@ -1179,7 +1176,10 @@ def get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, is_shar
if is_shared_module:
return []
name = get_macos_dylib_install_name(prefix, shlib_name, suffix, soversion)
return ['-install_name', name]
args = ['-install_name', name]
if darwin_versions:
args += ['-compatibility_version', darwin_versions[0], '-current_version', darwin_versions[1]]
return args
else:
raise RuntimeError('Not implemented yet.')
@ -1325,8 +1325,8 @@ class GnuCompiler:
def split_shlib_to_parts(self, fname):
return os.path.dirname(fname), fname
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_soname_args(self, *args):
return get_gcc_soname_args(self.gcc_type, *args)
def get_std_shared_lib_link_args(self):
return ['-shared']
@ -1452,7 +1452,7 @@ class ClangCompiler:
# 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, soversion, is_shared_module):
def get_soname_args(self, *args):
if self.clang_type == CLANG_STANDARD:
gcc_type = GCC_STANDARD
elif self.clang_type == CLANG_OSX:
@ -1461,7 +1461,7 @@ class ClangCompiler:
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, soversion, is_shared_module)
return get_gcc_soname_args(gcc_type, *args)
def has_multi_arguments(self, args, env):
myargs = ['-Werror=unknown-warning-option', '-Werror=unused-command-line-argument']
@ -1620,7 +1620,7 @@ class IntelCompiler:
def split_shlib_to_parts(self, fname):
return os.path.dirname(fname), fname
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
def get_soname_args(self, *args):
if self.icc_type == ICC_STANDARD:
gcc_type = GCC_STANDARD
elif self.icc_type == ICC_OSX:
@ -1629,7 +1629,7 @@ class IntelCompiler:
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, soversion, is_shared_module)
return get_gcc_soname_args(gcc_type, *args)
# TODO: centralise this policy more globally, instead
# of fragmenting it into GnuCompiler and ClangCompiler

@ -118,16 +118,14 @@ class DCompiler(Compiler):
def get_std_shared_lib_link_args(self):
return ['-shared']
def get_soname_args(self, prefix, shlib_name, suffix, soversion, is_shared_module):
def get_soname_args(self, *args):
# FIXME: Make this work for cross-compiling
gcc_type = GCC_STANDARD
if is_windows():
gcc_type = GCC_CYGWIN
if is_osx():
gcc_type = GCC_OSX
return get_gcc_soname_args(gcc_type, prefix, shlib_name, suffix, soversion, is_shared_module)
return get_gcc_soname_args(gcc_type, *args)
def get_feature_args(self, kwargs, build_to_src):
res = []

@ -2980,10 +2980,88 @@ class WindowsTests(BasePlatformTests):
self.utime(os.path.join(testdir, 'res', 'resource.h'))
self.assertRebuiltTarget('prog_1')
class DarwinTests(BasePlatformTests):
'''
Tests that should run on macOS
'''
def setUp(self):
super().setUp()
self.platform_test_dir = os.path.join(self.src_root, 'test cases/osx')
def test_apple_bitcode(self):
'''
Test that -fembed-bitcode is correctly added while compiling and
-bitcode_bundle is added while linking when b_bitcode is true and not
when it is false. This can't be an ordinary test case because we need
to inspect the compiler database.
'''
testdir = os.path.join(self.common_test_dir, '4 shared')
# Try with bitcode enabled
out = self.init(testdir, extra_args='-Db_bitcode=true')
# Warning was printed
self.assertRegex(out, 'WARNING:.*b_bitcode')
# Compiler options were added
compdb = self.get_compdb()
self.assertIn('-fembed-bitcode', compdb[0]['command'])
build_ninja = os.path.join(self.builddir, 'build.ninja')
# Linker options were added
with open(build_ninja, 'r', encoding='utf-8') as f:
contents = f.read()
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
self.assertIsNotNone(m, msg=contents)
# Try with bitcode disabled
self.setconf('-Db_bitcode=false')
# Regenerate build
self.build()
compdb = self.get_compdb()
self.assertNotIn('-fembed-bitcode', compdb[0]['command'])
build_ninja = os.path.join(self.builddir, 'build.ninja')
with open(build_ninja, 'r', encoding='utf-8') as f:
contents = f.read()
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
self.assertIsNone(m, msg=contents)
def test_apple_bitcode_modules(self):
'''
Same as above, just for shared_module()
'''
testdir = os.path.join(self.common_test_dir, '153 shared module resolving symbol in executable')
# Ensure that it builds even with bitcode enabled
self.init(testdir, extra_args='-Db_bitcode=true')
self.build()
self.run_tests()
def _get_darwin_versions(self, fname):
fname = os.path.join(self.builddir, fname)
out = subprocess.check_output(['otool', '-L', fname], universal_newlines=True)
m = re.match(r'.*version (.*), current version (.*)\)', out.split('\n')[1])
self.assertIsNotNone(m, msg=out)
return m.groups()
def test_library_versioning(self):
'''
Ensure that compatibility_version and current_version are set correctly
'''
testdir = os.path.join(self.platform_test_dir, '2 library versions')
self.init(testdir)
self.build()
targets = {}
for t in self.introspect('--targets'):
targets[t['name']] = t['filename']
self.assertEqual(self._get_darwin_versions(targets['some']), ('7.0.0', '7.0.0'))
self.assertEqual(self._get_darwin_versions(targets['noversion']), ('0.0.0', '0.0.0'))
self.assertEqual(self._get_darwin_versions(targets['onlyversion']), ('1.0.0', '1.0.0'))
self.assertEqual(self._get_darwin_versions(targets['onlysoversion']), ('5.0.0', '5.0.0'))
self.assertEqual(self._get_darwin_versions(targets['intver']), ('2.0.0', '2.0.0'))
self.assertEqual(self._get_darwin_versions(targets['stringver']), ('2.3.0', '2.3.0'))
self.assertEqual(self._get_darwin_versions(targets['stringlistver']), ('2.4.0', '2.4.0'))
self.assertEqual(self._get_darwin_versions(targets['intstringver']), ('1111.0.0', '2.5.0'))
self.assertEqual(self._get_darwin_versions(targets['stringlistvers']), ('2.6.0', '2.6.1'))
class LinuxlikeTests(BasePlatformTests):
'''
Tests that should run on Linux and *BSD
Tests that should run on Linux, macOS, and *BSD
'''
def test_basic_soname(self):
'''
@ -3763,53 +3841,6 @@ endian = 'little'
deps.append(b'-lintl')
self.assertEqual(set(deps), set(stdo.split()))
def test_apple_bitcode(self):
'''
Test that -fembed-bitcode is correctly added while compiling and
-bitcode_bundle is added while linking when b_bitcode is true and not
when it is false. This can't be an ordinary test case because we need
to inspect the compiler database.
'''
if not is_osx():
raise unittest.SkipTest('Apple bitcode only works on macOS')
testdir = os.path.join(self.common_test_dir, '4 shared')
# Try with bitcode enabled
out = self.init(testdir, extra_args='-Db_bitcode=true')
# Warning was printed
self.assertRegex(out, 'WARNING:.*b_bitcode')
# Compiler options were added
compdb = self.get_compdb()
self.assertIn('-fembed-bitcode', compdb[0]['command'])
build_ninja = os.path.join(self.builddir, 'build.ninja')
# Linker options were added
with open(build_ninja, 'r', encoding='utf-8') as f:
contents = f.read()
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
self.assertIsNotNone(m, msg=contents)
# Try with bitcode disabled
self.setconf('-Db_bitcode=false')
# Regenerate build
self.build()
compdb = self.get_compdb()
self.assertNotIn('-fembed-bitcode', compdb[0]['command'])
build_ninja = os.path.join(self.builddir, 'build.ninja')
with open(build_ninja, 'r', encoding='utf-8') as f:
contents = f.read()
m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
self.assertIsNone(m, msg=contents)
def test_apple_bitcode_modules(self):
'''
Same as above, just for shared_module()
'''
if not is_osx():
raise unittest.SkipTest('Apple bitcode not relevant')
testdir = os.path.join(self.common_test_dir, '153 shared module resolving symbol in executable')
# Ensure that it builds even with bitcode enabled
self.init(testdir, extra_args='-Db_bitcode=true')
self.build()
self.run_tests()
def test_deterministic_dep_order(self):
'''
Test that the dependencies are always listed in a deterministic order.
@ -4159,5 +4190,7 @@ if __name__ == '__main__':
cases += ['LinuxCrossMingwTests']
if is_windows() or is_cygwin():
cases += ['WindowsTests']
if is_osx():
cases += ['DarwinTests']
unittest.main(defaultTest=cases, buffer=True)

@ -1,5 +1,5 @@
usr/lib/libsome.dylib
usr/lib/libsome.0.dylib
usr/lib/libsome.7.dylib
usr/lib/libnoversion.dylib
usr/lib/libonlyversion.dylib
usr/lib/libonlyversion.1.dylib

@ -8,7 +8,7 @@ some = shared_library('some', 'lib.c',
build_rpath : zlib_dep.get_pkgconfig_variable('libdir'),
dependencies : zlib_dep,
version : '1.2.3',
soversion : '0',
soversion : '7',
install : true)
noversion = shared_library('noversion', 'lib.c',
@ -23,6 +23,21 @@ onlysoversion = shared_library('onlysoversion', 'lib.c',
soversion : 5,
install : true)
shared_library('intver', 'lib.c',
darwin_versions : 2)
shared_library('stringver', 'lib.c',
darwin_versions : '2.3')
shared_library('stringlistver', 'lib.c',
darwin_versions : ['2.4'])
shared_library('intstringver', 'lib.c',
darwin_versions : [1111, '2.5'])
shared_library('stringlistvers', 'lib.c',
darwin_versions : ['2.6', '2.6.1'])
# Hack to make the executables below depend on the shared libraries above
# without actually adding them as `link_with` dependencies since we want to try
# linking to them with -lfoo linker arguments.

Loading…
Cancel
Save