From c0166355ceef5168b2f7b3c6cbace32e8dbafbb4 Mon Sep 17 00:00:00 2001
From: Nirbheek Chauhan <nirbheek@centricular.com>
Date: Wed, 30 Jan 2019 15:28:02 +0530
Subject: 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.
---
 mesonbuild/compilers/c.py           |  61 +++++++++++++++++++
 mesonbuild/dependencies/base.py     | 116 +++++++++++++++++++++++++++---------
 mesonbuild/dependencies/misc.py     |   2 +-
 mesonbuild/dependencies/platform.py |  16 +++--
 mesonbuild/dependencies/ui.py       |   8 +--
 mesonbuild/mesonlib.py              |   3 +
 6 files changed, 169 insertions(+), 37 deletions(-)

(limited to 'mesonbuild')

diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
index c0cd0bc..0a90c8c 100644
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -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 []
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
index d56b825..d881f0e 100644
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -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))
diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py
index e6f52a5..4a57952 100644
--- a/mesonbuild/dependencies/misc.py
+++ b/mesonbuild/dependencies/misc.py
@@ -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
diff --git a/mesonbuild/dependencies/platform.py b/mesonbuild/dependencies/platform.py
index c78ebed..20d3bd6 100644
--- a/mesonbuild/dependencies/platform.py
+++ b/mesonbuild/dependencies/platform.py
@@ -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'
diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py
index dbdba9b..0585be9 100644
--- a/mesonbuild/dependencies/ui.py
+++ b/mesonbuild/dependencies/ui.py
@@ -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():
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py
index 8454d79..5529ce4 100644
--- a/mesonbuild/mesonlib.py
+++ b/mesonbuild/mesonlib.py
@@ -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
 
-- 
cgit v1.1