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