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 Dependency, ExternalDependency, DependencyException, DependencyMethods, NotFoundDependency
16from .cmake import CMakeDependency
17from .dub import DubDependency
18from .framework import ExtraFrameworkDependency
19from .pkgconfig import PkgConfigDependency
20
21from ..mesonlib import listify, MachineChoice, PerMachine
22from .. import mlog
23import functools
24import typing as T
25
26if T.TYPE_CHECKING:
27    from ..environment import Environment
28    from .factory import DependencyFactory, WrappedFactoryFunc, DependencyGenerator
29
30# These must be defined in this file to avoid cyclical references.
31packages: T.Dict[
32    str,
33    T.Union[T.Type[ExternalDependency], 'DependencyFactory', 'WrappedFactoryFunc']
34] = {}
35_packages_accept_language: T.Set[str] = set()
36
37if T.TYPE_CHECKING:
38    TV_DepIDEntry = T.Union[str, bool, int, T.Tuple[str, ...]]
39    TV_DepID = T.Tuple[T.Tuple[str, TV_DepIDEntry], ...]
40
41
42def get_dep_identifier(name: str, kwargs: T.Dict[str, T.Any]) -> 'TV_DepID':
43    identifier: 'TV_DepID' = (('name', name), )
44    from ..interpreter import permitted_dependency_kwargs
45    assert len(permitted_dependency_kwargs) == 19, \
46           'Extra kwargs have been added to dependency(), please review if it makes sense to handle it here'
47    for key, value in kwargs.items():
48        # 'version' is irrelevant for caching; the caller must check version matches
49        # 'native' is handled above with `for_machine`
50        # 'required' is irrelevant for caching; the caller handles it separately
51        # 'fallback' and 'allow_fallback' is not part of the cache because,
52        #     once a dependency has been found through a fallback, it should
53        #     be used for the rest of the Meson run.
54        # 'default_options' is only used in fallback case
55        # 'not_found_message' has no impact on the dependency lookup
56        # 'include_type' is handled after the dependency lookup
57        if key in ('version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options',
58                   'not_found_message', 'include_type'):
59            continue
60        # All keyword arguments are strings, ints, or lists (or lists of lists)
61        if isinstance(value, list):
62            value = frozenset(listify(value))
63            for i in value:
64                assert isinstance(i, str)
65        else:
66            assert isinstance(value, (str, bool, int))
67        identifier += (key, value)
68    return identifier
69
70display_name_map = {
71    'boost': 'Boost',
72    'cuda': 'CUDA',
73    'dub': 'DUB',
74    'gmock': 'GMock',
75    'gtest': 'GTest',
76    'hdf5': 'HDF5',
77    'llvm': 'LLVM',
78    'mpi': 'MPI',
79    'netcdf': 'NetCDF',
80    'openmp': 'OpenMP',
81    'wxwidgets': 'WxWidgets',
82}
83
84def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, object]) -> T.Union['ExternalDependency', NotFoundDependency]:
85    assert(name)
86    required = kwargs.get('required', True)
87    if not isinstance(required, bool):
88        raise DependencyException('Keyword "required" must be a boolean.')
89    if not isinstance(kwargs.get('method', ''), str):
90        raise DependencyException('Keyword "method" must be a string.')
91    lname = name.lower()
92    if lname not in _packages_accept_language and 'language' in kwargs:
93        raise DependencyException(f'{name} dependency does not accept "language" keyword argument')
94    if not isinstance(kwargs.get('version', ''), (str, list)):
95        raise DependencyException('Keyword "Version" must be string or list.')
96
97    # display the dependency name with correct casing
98    display_name = display_name_map.get(lname, lname)
99
100    for_machine = MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST
101
102    type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency'
103
104    # build a list of dependency methods to try
105    candidates = _build_external_dependency_list(name, env, for_machine, kwargs)
106
107    pkg_exc: T.List[DependencyException] = []
108    pkgdep:  T.List[ExternalDependency]  = []
109    details = ''
110
111    for c in candidates:
112        # try this dependency method
113        try:
114            d = c()
115            d._check_version()
116            pkgdep.append(d)
117        except DependencyException as e:
118            pkg_exc.append(e)
119            mlog.debug(str(e))
120        else:
121            pkg_exc.append(None)
122            details = d.log_details()
123            if details:
124                details = '(' + details + ') '
125            if 'language' in kwargs:
126                details += 'for ' + d.language + ' '
127
128            # if the dependency was found
129            if d.found():
130
131                info: mlog.TV_LoggableList = []
132                if d.version:
133                    info.append(mlog.normal_cyan(d.version))
134
135                log_info = d.log_info()
136                if log_info:
137                    info.append('(' + log_info + ')')
138
139                mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.green('YES'), *info)
140
141                return d
142
143    # otherwise, the dependency could not be found
144    tried_methods = [d.log_tried() for d in pkgdep if d.log_tried()]
145    if tried_methods:
146        tried = '{}'.format(mlog.format_list(tried_methods))
147    else:
148        tried = ''
149
150    mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.red('NO'),
151             f'(tried {tried})' if tried else '')
152
153    if required:
154        # if an exception occurred with the first detection method, re-raise it
155        # (on the grounds that it came from the preferred dependency detection
156        # method)
157        if pkg_exc and pkg_exc[0]:
158            raise pkg_exc[0]
159
160        # we have a list of failed ExternalDependency objects, so we can report
161        # the methods we tried to find the dependency
162        raise DependencyException('Dependency "%s" not found' % (name) +
163                                  (', tried %s' % (tried) if tried else ''))
164
165    return NotFoundDependency(env)
166
167
168def _build_external_dependency_list(name: str, env: 'Environment', for_machine: MachineChoice,
169                                    kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']:
170    # First check if the method is valid
171    if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]:
172        raise DependencyException('method {!r} is invalid'.format(kwargs['method']))
173
174    # Is there a specific dependency detector for this dependency?
175    lname = name.lower()
176    if lname in packages:
177        # Create the list of dependency object constructors using a factory
178        # class method, if one exists, otherwise the list just consists of the
179        # constructor
180        if isinstance(packages[lname], type):
181            entry1 = T.cast(T.Type[ExternalDependency], packages[lname])  # mypy doesn't understand isinstance(..., type)
182            if issubclass(entry1, ExternalDependency):
183                # TODO: somehow make mypy understand that entry1(env, kwargs) is OK...
184                func: T.Callable[[], 'ExternalDependency'] = lambda: entry1(env, kwargs)  # type: ignore
185                dep = [func]
186        else:
187            entry2 = T.cast(T.Union['DependencyFactory', 'WrappedFactoryFunc'], packages[lname])
188            dep = entry2(env, for_machine, kwargs)
189        return dep
190
191    candidates: T.List['DependencyGenerator'] = []
192
193    # If it's explicitly requested, use the dub detection method (only)
194    if 'dub' == kwargs.get('method', ''):
195        candidates.append(functools.partial(DubDependency, name, env, kwargs))
196        return candidates
197
198    # If it's explicitly requested, use the pkgconfig detection method (only)
199    if 'pkg-config' == kwargs.get('method', ''):
200        candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs))
201        return candidates
202
203    # If it's explicitly requested, use the CMake detection method (only)
204    if 'cmake' == kwargs.get('method', ''):
205        candidates.append(functools.partial(CMakeDependency, name, env, kwargs))
206        return candidates
207
208    # If it's explicitly requested, use the Extraframework detection method (only)
209    if 'extraframework' == kwargs.get('method', ''):
210        # On OSX, also try framework dependency detector
211        if env.machines[for_machine].is_darwin():
212            candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs))
213        return candidates
214
215    # Otherwise, just use the pkgconfig and cmake dependency detector
216    if 'auto' == kwargs.get('method', 'auto'):
217        candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs))
218
219        # On OSX, also try framework dependency detector
220        if env.machines[for_machine].is_darwin():
221            candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs))
222
223        # Only use CMake as a last resort, since it might not work 100% (see #6113)
224        candidates.append(functools.partial(CMakeDependency, name, env, kwargs))
225
226    return candidates
227