From 50b863032ed3913b9737112b5250c2d35a7b9fb7 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Tue, 29 Jan 2019 16:12:35 +0530 Subject: [PATCH] find_library: Check arch of libraries on Darwin macOS provides the tool `lipo` to check the archs supported by an object (executable, static library, dylib, etc). This is especially useful for fat archives, but it also helps with thin archives. Without this, the linker will fail to link to the library we mistakenly 'found' like so: ld: warning: ignoring file /path/to/libfoo.a, missing required architecture armv7 in file /path/to/libfoo.a --- mesonbuild/backend/ninjabackend.py | 5 +-- mesonbuild/compilers/c.py | 28 +++++++++--- mesonbuild/mesonlib.py | 18 ++++++++ run_tests.py | 1 + run_unittests.py | 72 +++++++++++++++++------------- 5 files changed, 85 insertions(+), 39 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 493fc0ddf..9b215b2f1 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -2347,15 +2347,14 @@ rule FORTRAN_DEP_HACK%s target_args = self.build_target_link_arguments(linker, target.link_whole_targets) return linker.get_link_whole_for(target_args) if len(target_args) else [] - @staticmethod @lru_cache(maxsize=None) - def guess_library_absolute_path(linker, libname, search_dirs, patterns): + def guess_library_absolute_path(self, linker, libname, search_dirs, patterns): for d in search_dirs: for p in patterns: trial = CCompiler._get_trials_from_pattern(p, d, libname) if not trial: continue - trial = CCompiler._get_file_from_list(trial) + trial = CCompiler._get_file_from_list(self.environment, trial) if not trial: continue # Return the first result diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py index 0a90c8cc6..a59118388 100644 --- a/mesonbuild/compilers/c.py +++ b/mesonbuild/compilers/c.py @@ -27,6 +27,7 @@ from . import compilers from ..mesonlib import ( EnvironmentException, MesonException, version_compare, Popen_safe, listify, for_windows, for_darwin, for_cygwin, for_haiku, for_openbsd, + darwin_get_object_archs ) from .c_function_attributes import C_FUNC_ATTRIBUTES @@ -980,10 +981,28 @@ class CCompiler(Compiler): return [f.as_posix()] @staticmethod - def _get_file_from_list(files: List[str]) -> str: + def _get_file_from_list(env, files: List[str]) -> str: + ''' + We just check whether the library exists. We can't do a link check + because the library might have unresolved symbols that require other + libraries. On macOS we check if the library matches our target + architecture. + ''' + # If not building on macOS for Darwin, do a simple file check + if not env.machines.host.is_darwin() or not env.machines.build.is_darwin(): + for f in files: + if os.path.isfile(f): + return f + # Run `lipo` and check if the library supports the arch we want for f in files: - if os.path.isfile(f): + if not os.path.isfile(f): + continue + archs = darwin_get_object_archs(f) + if archs and env.machines.host.cpu_family in archs: return f + else: + mlog.debug('Rejected {}, supports {} but need {}' + .format(f, archs, env.machines.host.cpu_family)) return None @functools.lru_cache() @@ -1024,10 +1043,7 @@ class CCompiler(Compiler): trial = self._get_trials_from_pattern(p, d, libname) if not trial: continue - # We just check whether the library exists. We can't do a link - # check because the library might have unresolved symbols that - # require other libraries. - trial = self._get_file_from_list(trial) + trial = self._get_file_from_list(env, trial) if not trial: continue return [trial] diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index 5529ce48a..09228dca5 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -461,6 +461,24 @@ def exe_exists(arglist): pass return False +lru_cache(maxsize=None) +def darwin_get_object_archs(objpath): + ''' + For a specific object (executable, static library, dylib, etc), run `lipo` + to fetch the list of archs supported by it. Supports both thin objects and + 'fat' objects. + ''' + _, stdo, stderr = Popen_safe(['lipo', '-archs', objpath]) + if not stdo: + mlog.debug('lipo {}: {}'.format(objpath, stderr)) + return None + # Convert from lipo-style archs to meson-style CPUs + stdo = stdo.replace('i386', 'x86') + # Add generic name for armv7 and armv7s + if 'armv7' in stdo: + stdo += ' arm' + return stdo.split() + def detect_vcs(source_dir): vcs_systems = [ dict(name = 'git', cmd = 'git', repo_dir = '.git', get_rev = 'git describe --dirty=+', rev_regex = '(.*)', dep = '.git/logs/HEAD'), diff --git a/run_tests.py b/run_tests.py index 943e16f57..20cb4e21f 100755 --- a/run_tests.py +++ b/run_tests.py @@ -81,6 +81,7 @@ def get_fake_env(sdir='', bdir=None, prefix='', opts=None): opts = get_fake_options(prefix) env = Environment(sdir, bdir, opts) env.coredata.compiler_options['c_args'] = FakeCompilerOptions() + env.machines.host.cpu_family = 'x86_64' # Used on macOS inside find_library return env diff --git a/run_unittests.py b/run_unittests.py index b8b31928a..72ab062f1 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -45,7 +45,7 @@ from mesonbuild.interpreter import Interpreter, ObjectHolder from mesonbuild.mesonlib import ( is_windows, is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, windows_proof_rmtree, python_command, version_compare, - BuildDirLock, Version + BuildDirLock, Version, PerMachine ) from mesonbuild.environment import detect_ninja from mesonbuild.mesonlib import MesonException, EnvironmentException @@ -781,6 +781,17 @@ class InternalTests(unittest.TestCase): https://github.com/mesonbuild/meson/issues/3951 ''' + def create_static_lib(name): + if not is_osx(): + name.open('w').close() + return + src = name.with_suffix('.c') + out = name.with_suffix('.o') + with src.open('w') as f: + f.write('int meson_foobar (void) { return 0; }') + subprocess.check_call(['clang', '-c', str(src), '-o', str(out)]) + subprocess.check_call(['ar', 'csr', str(name), str(out)]) + with tempfile.TemporaryDirectory() as tmpdir: pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True) env = get_fake_env() @@ -792,16 +803,16 @@ class InternalTests(unittest.TestCase): p1.mkdir() p2.mkdir() # libfoo.a is in one prefix - (p1 / 'libfoo.a').open('w').close() + create_static_lib(p1 / 'libfoo.a') # libbar.a is in both prefixes - (p1 / 'libbar.a').open('w').close() - (p2 / 'libbar.a').open('w').close() + create_static_lib(p1 / 'libbar.a') + create_static_lib(p2 / 'libbar.a') # Ensure that we never statically link to these - (p1 / 'libpthread.a').open('w').close() - (p1 / 'libm.a').open('w').close() - (p1 / 'libc.a').open('w').close() - (p1 / 'libdl.a').open('w').close() - (p1 / 'librt.a').open('w').close() + create_static_lib(p1 / 'libpthread.a') + create_static_lib(p1 / 'libm.a') + create_static_lib(p1 / 'libc.a') + create_static_lib(p1 / 'libdl.a') + create_static_lib(p1 / 'librt.a') def fake_call_pkgbin(self, args, env=None): if '--libs' not in args: @@ -815,30 +826,31 @@ class InternalTests(unittest.TestCase): old_call = PkgConfigDependency._call_pkgbin old_check = PkgConfigDependency.check_pkgconfig - old_pkgbin = PkgConfigDependency.class_pkgbin PkgConfigDependency._call_pkgbin = fake_call_pkgbin PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin # Test begins - kwargs = {'required': True, 'silent': True} - foo_dep = PkgConfigDependency('foo', env, kwargs) - self.assertEqual(foo_dep.get_link_args(), - [(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()]) - bar_dep = PkgConfigDependency('bar', env, kwargs) - self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()]) - internal_dep = PkgConfigDependency('internal', env, kwargs) - if compiler.get_argument_syntax() == 'msvc': - self.assertEqual(internal_dep.get_link_args(), []) - else: - link_args = internal_dep.get_link_args() - for link_arg in link_args: - for lib in ('pthread', 'm', 'c', 'dl', 'rt'): - self.assertNotIn('lib{}.a'.format(lib), link_arg, msg=link_args) - # Test ends - PkgConfigDependency._call_pkgbin = old_call - PkgConfigDependency.check_pkgconfig = old_check - # Reset dependency class to ensure that in-process configure doesn't mess up - PkgConfigDependency.pkgbin_cache = {} - PkgConfigDependency.class_pkgbin = old_pkgbin + try: + kwargs = {'required': True, 'silent': True} + foo_dep = PkgConfigDependency('foo', env, kwargs) + self.assertEqual(foo_dep.get_link_args(), + [(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()]) + bar_dep = PkgConfigDependency('bar', env, kwargs) + self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()]) + internal_dep = PkgConfigDependency('internal', env, kwargs) + if compiler.get_argument_syntax() == 'msvc': + self.assertEqual(internal_dep.get_link_args(), []) + else: + link_args = internal_dep.get_link_args() + for link_arg in link_args: + for lib in ('pthread', 'm', 'c', 'dl', 'rt'): + self.assertNotIn('lib{}.a'.format(lib), link_arg, msg=link_args) + finally: + # Test ends + PkgConfigDependency._call_pkgbin = old_call + PkgConfigDependency.check_pkgconfig = old_check + # Reset dependency class to ensure that in-process configure doesn't mess up + PkgConfigDependency.pkgbin_cache = {} + PkgConfigDependency.class_pkgbin = PerMachine(None, None, None) def test_version_compare(self): comparefunc = mesonbuild.mesonlib.version_compare_many