1# Copyright 2013-2021 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from .base import DependencyTypeName, ExternalDependency, DependencyException
16from ..mesonlib import MesonException, Version, stringlistify
17from .. import mlog
18from pathlib import Path
19import typing as T
20
21if T.TYPE_CHECKING:
22    from ..environment import Environment
23
24class ExtraFrameworkDependency(ExternalDependency):
25    system_framework_paths: T.Optional[T.List[str]] = None
26
27    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
28        paths = stringlistify(kwargs.get('paths', []))
29        super().__init__(DependencyTypeName('extraframeworks'), env, kwargs, language=language)
30        self.name = name
31        # Full path to framework directory
32        self.framework_path: T.Optional[str] = None
33        if not self.clib_compiler:
34            raise DependencyException('No C-like compilers are available')
35        if self.system_framework_paths is None:
36            try:
37                self.system_framework_paths = self.clib_compiler.find_framework_paths(self.env)
38            except MesonException as e:
39                if 'non-clang' in str(e):
40                    # Apple frameworks can only be found (and used) with the
41                    # system compiler. It is not available so bail immediately.
42                    self.is_found = False
43                    return
44                raise
45        self.detect(name, paths)
46
47    def detect(self, name: str, paths: T.List[str]) -> None:
48        if not paths:
49            paths = self.system_framework_paths
50        for p in paths:
51            mlog.debug(f'Looking for framework {name} in {p}')
52            # We need to know the exact framework path because it's used by the
53            # Qt5 dependency class, and for setting the include path. We also
54            # want to avoid searching in an invalid framework path which wastes
55            # time and can cause a false positive.
56            framework_path = self._get_framework_path(p, name)
57            if framework_path is None:
58                continue
59            # We want to prefer the specified paths (in order) over the system
60            # paths since these are "extra" frameworks.
61            # For example, Python2's framework is in /System/Library/Frameworks and
62            # Python3's framework is in /Library/Frameworks, but both are called
63            # Python.framework. We need to know for sure that the framework was
64            # found in the path we expect.
65            allow_system = p in self.system_framework_paths
66            args = self.clib_compiler.find_framework(name, self.env, [p], allow_system)
67            if args is None:
68                continue
69            self.link_args = args
70            self.framework_path = framework_path.as_posix()
71            self.compile_args = ['-F' + self.framework_path]
72            # We need to also add -I includes to the framework because all
73            # cross-platform projects such as OpenGL, Python, Qt, GStreamer,
74            # etc do not use "framework includes":
75            # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html
76            incdir = self._get_framework_include_path(framework_path)
77            if incdir:
78                self.compile_args += ['-I' + incdir]
79            self.is_found = True
80            return
81
82    def _get_framework_path(self, path: str, name: str) -> T.Optional[Path]:
83        p = Path(path)
84        lname = name.lower()
85        for d in p.glob('*.framework/'):
86            if lname == d.name.rsplit('.', 1)[0].lower():
87                return d
88        return None
89
90    def _get_framework_latest_version(self, path: Path) -> str:
91        versions = []
92        for each in path.glob('Versions/*'):
93            # macOS filesystems are usually case-insensitive
94            if each.name.lower() == 'current':
95                continue
96            versions.append(Version(each.name))
97        if len(versions) == 0:
98            # most system frameworks do not have a 'Versions' directory
99            return 'Headers'
100        return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s)
101
102    def _get_framework_include_path(self, path: Path) -> T.Optional[str]:
103        # According to the spec, 'Headers' must always be a symlink to the
104        # Headers directory inside the currently-selected version of the
105        # framework, but sometimes frameworks are broken. Look in 'Versions'
106        # for the currently-selected version or pick the latest one.
107        trials = ('Headers', 'Versions/Current/Headers',
108                  self._get_framework_latest_version(path))
109        for each in trials:
110            trial = path / each
111            if trial.is_dir():
112                return trial.as_posix()
113        return None
114
115    def log_info(self) -> str:
116        return self.framework_path or ''
117
118    def log_tried(self) -> str:
119        return 'framework'
120