Rewrite appleframework and extraframework dependency classes

Instead of only doing a naive filesystem search, also run the linker
so that it can tell us whether the -F path specified actually contains
the framework we're looking for.

Unfortunately, `extraframework` searching is still not 100% correct in
the case when since we want to search in either /Library/Frameworks or
in /System/Library/Frameworks but not in both. The -Z flag disables
searching in those prefixes and would in theory allow this, but then
you cannot force the linker to look in those by manually adding -F
args, so that doesn't work.
pull/4846/head
Nirbheek Chauhan 6 years ago committed by Nirbheek Chauhan
parent 8481971ff2
commit c0166355ce
  1. 61
      mesonbuild/compilers/c.py
  2. 116
      mesonbuild/dependencies/base.py
  3. 2
      mesonbuild/dependencies/misc.py
  4. 16
      mesonbuild/dependencies/platform.py
  5. 8
      mesonbuild/dependencies/ui.py
  6. 3
      mesonbuild/mesonlib.py
  7. 1
      run_tests.py
  8. 8
      run_unittests.py
  9. 2
      test cases/osx/5 extra frameworks/installed_files.txt
  10. 13
      test cases/osx/5 extra frameworks/meson.build
  11. 3
      test cases/osx/5 extra frameworks/prog.c
  12. 1
      test cases/osx/5 extra frameworks/stat.c

@ -59,6 +59,7 @@ class CCompiler(Compiler):
library_dirs_cache = {}
program_dirs_cache = {}
find_library_cache = {}
find_framework_cache = {}
internal_libs = gnu_compiler_internal_libs
@staticmethod
@ -1052,6 +1053,66 @@ class CCompiler(Compiler):
code = 'int main(int argc, char **argv) { return 0; }'
return self.find_library_impl(libname, env, extra_dirs, code, libtype)
def find_framework_paths(self, env):
'''
These are usually /Library/Frameworks and /System/Library/Frameworks,
unless you select a particular macOS SDK with the -isysroot flag.
You can also add to this by setting -F in CFLAGS.
'''
if self.id != 'clang':
raise MesonException('Cannot find framework path with non-clang compiler')
# Construct the compiler command-line
commands = self.get_exelist() + ['-v', '-E', '-']
commands += self.get_always_args()
# Add CFLAGS/CXXFLAGS/OBJCFLAGS/OBJCXXFLAGS from the env
commands += env.coredata.get_external_args(self.language)
mlog.debug('Finding framework path by running: ', ' '.join(commands), '\n')
os_env = os.environ.copy()
os_env['LC_ALL'] = 'C'
_, _, stde = Popen_safe(commands, env=os_env, stdin=subprocess.PIPE)
paths = []
for line in stde.split('\n'):
if '(framework directory)' not in line:
continue
# line is of the form:
# ` /path/to/framework (framework directory)`
paths.append(line[:-21].strip())
return paths
def find_framework_real(self, name, env, extra_dirs, allow_system):
code = 'int main(int argc, char **argv) { return 0; }'
link_args = []
for d in extra_dirs:
link_args += ['-F' + d]
# We can pass -Z to disable searching in the system frameworks, but
# then we must also pass -L/usr/lib to pick up libSystem.dylib
extra_args = [] if allow_system else ['-Z', '-L/usr/lib']
link_args += ['-framework', name]
if self.links(code, env, extra_args=(extra_args + link_args)):
return link_args
def find_framework_impl(self, name, env, extra_dirs, allow_system):
if isinstance(extra_dirs, str):
extra_dirs = [extra_dirs]
key = (tuple(self.exelist), name, tuple(extra_dirs), allow_system)
if key in self.find_framework_cache:
value = self.find_framework_cache[key]
else:
value = self.find_framework_real(name, env, extra_dirs, allow_system)
self.find_framework_cache[key] = value
if value is None:
return None
return value[:]
def find_framework(self, name, env, extra_dirs, allow_system=True):
'''
Finds the framework with the specified name, and returns link args for
the same or returns None when the framework is not found.
'''
if self.id != 'clang':
raise MesonException('Cannot find frameworks with non-clang compiler')
return self.find_framework_impl(name, env, extra_dirs, allow_system)
def thread_flags(self, env):
if for_haiku(self.is_cross, env) or for_darwin(self.is_cross, env):
return []

@ -28,7 +28,7 @@ import platform
import itertools
import ctypes
from enum import Enum
from pathlib import PurePath
from pathlib import Path, PurePath
from .. import mlog
from .. import mesonlib
@ -36,6 +36,7 @@ from ..compilers import clib_langs
from ..environment import BinaryTable
from ..mesonlib import MachineChoice, MesonException, OrderedSet, PerMachine
from ..mesonlib import Popen_safe, version_compare_many, version_compare, listify
from ..mesonlib import Version
# These must be defined in this file to avoid cyclical references.
packages = {}
@ -1983,40 +1984,91 @@ class ExternalLibrary(ExternalDependency):
class ExtraFrameworkDependency(ExternalDependency):
def __init__(self, name, required, path, env, lang, kwargs):
system_framework_paths = None
def __init__(self, name, required, paths, env, lang, kwargs):
super().__init__('extraframeworks', env, lang, kwargs)
self.name = name
self.required = required
self.detect(name, path)
if self.found():
self.compile_args = ['-I' + os.path.join(self.path, self.name, 'Headers')]
self.link_args = ['-F' + self.path, '-framework', self.name.split('.')[0]]
def detect(self, name, path):
# should use the compiler to look for frameworks, rather than peering at
# the filesystem, so we can also find them when cross-compiling
if self.want_cross:
# Full path to framework directory
self.framework_path = None
if not self.clib_compiler:
raise DependencyException('No C-like compilers are available')
if self.system_framework_paths is None:
self.system_framework_paths = self.clib_compiler.find_framework_paths(self.env)
self.detect(name, paths)
def detect(self, name, paths):
if not paths:
paths = self.system_framework_paths
for p in paths:
mlog.debug('Looking for framework {} in {}'.format(name, p))
# We need to know the exact framework path because it's used by the
# Qt5 dependency class, and for setting the include path. We also
# want to avoid searching in an invalid framework path which wastes
# time and can cause a false positive.
framework_path = self._get_framework_path(p, name)
if framework_path is None:
continue
# We want to prefer the specified paths (in order) over the system
# paths since these are "extra" frameworks.
# For example, Python2's framework is in /System/Library/Frameworks and
# Python3's framework is in /Library/Frameworks, but both are called
# Python.framework. We need to know for sure that the framework was
# found in the path we expect.
allow_system = p in self.system_framework_paths
args = self.clib_compiler.find_framework(name, self.env, [p], allow_system)
if args is None:
continue
self.link_args = args
self.framework_path = framework_path.as_posix()
self.compile_args = ['-F' + self.framework_path]
# We need to also add -I includes to the framework because all
# cross-platform projects such as OpenGL, Python, Qt, GStreamer,
# etc do not use "framework includes":
# https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html
incdir = self._get_framework_include_path(framework_path)
if incdir:
self.compile_args += ['-I' + incdir]
self.is_found = True
return
def _get_framework_path(self, path, name):
p = Path(path)
lname = name.lower()
if path is None:
paths = ['/System/Library/Frameworks', '/Library/Frameworks']
else:
paths = [path]
for p in paths:
for d in os.listdir(p):
fullpath = os.path.join(p, d)
if lname != d.rsplit('.', 1)[0].lower():
continue
if not stat.S_ISDIR(os.stat(fullpath).st_mode):
continue
self.path = p
self.name = d
self.is_found = True
return
for d in p.glob('*.framework/'):
if lname == d.name.rsplit('.', 1)[0].lower():
return d
return None
def _get_framework_latest_version(self, path):
versions = []
for each in path.glob('Versions/*'):
# macOS filesystems are usually case-insensitive
if each.name.lower() == 'current':
continue
versions.append(Version(each.name))
return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s)
def _get_framework_include_path(self, path):
# According to the spec, 'Headers' must always be a symlink to the
# Headers directory inside the currently-selected version of the
# framework, but sometimes frameworks are broken. Look in 'Versions'
# for the currently-selected version or pick the latest one.
trials = ('Headers', 'Versions/Current/Headers',
self._get_framework_latest_version(path))
for each in trials:
trial = path / each
if trial.is_dir():
return trial.as_posix()
return None
@staticmethod
def get_methods():
return [DependencyMethods.EXTRAFRAMEWORK]
def log_info(self):
return os.path.join(self.path, self.name)
return self.framework_path
def log_tried(self):
return 'framework'
@ -2127,7 +2179,7 @@ def find_external_dependency(name, env, kwargs):
# if an exception occurred with the first detection method, re-raise it
# (on the grounds that it came from the preferred dependency detection
# method)
if pkg_exc[0]:
if pkg_exc and pkg_exc[0]:
raise pkg_exc[0]
# we have a list of failed ExternalDependency objects, so we can report
@ -2172,6 +2224,14 @@ def _build_external_dependency_list(name, env, kwargs):
candidates.append(functools.partial(CMakeDependency, name, env, kwargs))
return candidates
# If it's explicitly requested, use the Extraframework detection method (only)
if 'extraframework' == kwargs.get('method', ''):
# On OSX, also try framework dependency detector
if mesonlib.is_osx():
candidates.append(functools.partial(ExtraFrameworkDependency, name,
False, None, env, None, kwargs))
return candidates
# Otherwise, just use the pkgconfig and cmake dependency detector
if 'auto' == kwargs.get('method', 'auto'):
candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs))

@ -307,7 +307,7 @@ class Python3Dependency(ExternalDependency):
# There is a python in /System/Library/Frameworks, but that's
# python 2, Python 3 will always be in /Library
candidates.append(functools.partial(
ExtraFrameworkDependency, 'python', False, '/Library/Frameworks',
ExtraFrameworkDependency, 'Python', False, ['/Library/Frameworks'],
environment, kwargs.get('language', None), kwargs))
return candidates

@ -29,11 +29,19 @@ class AppleFrameworks(ExternalDependency):
if not modules:
raise DependencyException("AppleFrameworks dependency requires at least one module.")
self.frameworks = modules
# FIXME: Use self.clib_compiler to check if the frameworks are available
if not self.clib_compiler:
raise DependencyException('No C-like compilers are available, cannot find the framework')
self.is_found = True
for f in self.frameworks:
self.link_args += ['-framework', f]
self.is_found = mesonlib.for_darwin(self.want_cross, self.env)
args = self.clib_compiler.find_framework(f, env, [])
if args is not None:
# No compile args are needed for system frameworks
self.link_args = args
else:
self.is_found = False
def log_info(self):
return ', '.join(self.frameworks)
def log_tried(self):
return 'framework'

@ -177,13 +177,13 @@ def _qt_get_private_includes(mod_inc_dir, module, mod_version):
os.path.join(private_dir, 'Qt' + module))
class QtExtraFrameworkDependency(ExtraFrameworkDependency):
def __init__(self, name, required, path, env, lang, kwargs):
super().__init__(name, required, path, env, lang, kwargs)
def __init__(self, name, required, paths, env, lang, kwargs):
super().__init__(name, required, paths, env, lang, kwargs)
self.mod_name = name[2:]
def get_compile_args(self, with_private_headers=False, qt_version="0"):
if self.found():
mod_inc_dir = os.path.join(self.path, self.name, 'Headers')
mod_inc_dir = os.path.join(self.framework_path, 'Headers')
args = ['-I' + mod_inc_dir]
if with_private_headers:
args += ['-I' + dirname for dirname in _qt_get_private_includes(mod_inc_dir, self.mod_name, qt_version)]
@ -442,7 +442,7 @@ class QtBaseDependency(ExternalDependency):
for m in modules:
fname = 'Qt' + m
fwdep = QtExtraFrameworkDependency(fname, False, libdir, self.env,
fwdep = QtExtraFrameworkDependency(fname, False, [libdir], self.env,
self.language, fw_kwargs)
self.compile_args.append('-F' + libdir)
if fwdep.found():

@ -496,6 +496,9 @@ class Version:
def __str__(self):
return '%s (V=%s)' % (self._s, str(self._v))
def __repr__(self):
return '<Version: {}>'.format(self._s)
def __lt__(self, other):
return self.__cmp__(other) == -1

@ -217,6 +217,7 @@ def clear_meson_configure_class_caches():
mesonbuild.compilers.CCompiler.library_dirs_cache = {}
mesonbuild.compilers.CCompiler.program_dirs_cache = {}
mesonbuild.compilers.CCompiler.find_library_cache = {}
mesonbuild.compilers.CCompiler.find_framework_cache = {}
mesonbuild.dependencies.PkgConfigDependency.pkgbin_cache = {}
mesonbuild.dependencies.PkgConfigDependency.class_pkgbin = mesonlib.PerMachine(None, None, None)

@ -3519,6 +3519,14 @@ class FailureTests(BasePlatformTests):
self.assertMesonRaises("dependency('appleframeworks')",
"requires at least one module")
def test_extraframework_dependency_method(self):
code = "dependency('python', method : 'extraframework')"
if not is_osx():
self.assertMesonRaises(code, self.dnf)
else:
# Python2 framework is always available on macOS
self.assertMesonOutputs(code, '[Dd]ependency.*python.*found.*YES')
def test_sdl2_notfound_dependency(self):
# Want to test failure, so skip if available
if shutil.which('sdl2-config'):

@ -0,0 +1,2 @@
usr/bin/prog
usr/lib/libstat.a

@ -0,0 +1,13 @@
project('xcode extra framework test', 'c')
dep_libs = dependency('OpenGL', method : 'extraframework')
assert(dep_libs.type_name() == 'extraframeworks', 'type_name is ' + dep_libs.type_name())
dep_main = dependency('Foundation')
assert(dep_main.type_name() == 'extraframeworks', 'type_name is ' + dep_main.type_name())
dep_py = dependency('python', method : 'extraframework')
assert(dep_main.type_name() == 'extraframeworks', 'type_name is ' + dep_main.type_name())
stlib = static_library('stat', 'stat.c', install : true, dependencies: dep_libs)
exe = executable('prog', 'prog.c', install : true, dependencies: dep_main)

@ -0,0 +1,3 @@
int main(int argc, char **argv) {
return 0;
}

@ -0,0 +1 @@
int func() { return 933; }
Loading…
Cancel
Save