# SPDX-License-Identifier: Apache-2.0 # Copyright 2013-2021 The Meson development team from __future__ import annotations import collections, functools, importlib import typing as T from .base import ExternalDependency, DependencyException, DependencyMethods, NotFoundDependency from ..mesonlib import listify, MachineChoice, PerMachine from .. import mlog if T.TYPE_CHECKING: from ..environment import Environment from .factory import DependencyFactory, WrappedFactoryFunc, DependencyGenerator TV_DepIDEntry = T.Union[str, bool, int, T.Tuple[str, ...]] TV_DepID = T.Tuple[T.Tuple[str, TV_DepIDEntry], ...] PackageTypes = T.Union[T.Type[ExternalDependency], DependencyFactory, WrappedFactoryFunc] class DependencyPackages(collections.UserDict): data: T.Dict[str, PackageTypes] defaults: T.Dict[str, str] = {} def __missing__(self, key: str) -> PackageTypes: if key in self.defaults: modn = self.defaults[key] importlib.import_module(f'mesonbuild.dependencies.{modn}') return self.data[key] raise KeyError(key) def __contains__(self, key: object) -> bool: return key in self.defaults or key in self.data # These must be defined in this file to avoid cyclical references. packages = DependencyPackages() _packages_accept_language: T.Set[str] = set() def get_dep_identifier(name: str, kwargs: T.Dict[str, T.Any]) -> 'TV_DepID': identifier: 'TV_DepID' = (('name', name), ) from ..interpreter import permitted_dependency_kwargs assert len(permitted_dependency_kwargs) == 19, \ 'Extra kwargs have been added to dependency(), please review if it makes sense to handle it here' for key, value in kwargs.items(): # 'version' is irrelevant for caching; the caller must check version matches # 'native' is handled above with `for_machine` # 'required' is irrelevant for caching; the caller handles it separately # 'fallback' and 'allow_fallback' is not part of the cache because, # once a dependency has been found through a fallback, it should # be used for the rest of the Meson run. # 'default_options' is only used in fallback case # 'not_found_message' has no impact on the dependency lookup # 'include_type' is handled after the dependency lookup if key in {'version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options', 'not_found_message', 'include_type'}: continue # All keyword arguments are strings, ints, or lists (or lists of lists) if isinstance(value, list): for i in value: assert isinstance(i, str) value = tuple(frozenset(listify(value))) else: assert isinstance(value, (str, bool, int)) identifier = (*identifier, (key, value),) return identifier display_name_map = { 'boost': 'Boost', 'cuda': 'CUDA', 'dub': 'DUB', 'gmock': 'GMock', 'gtest': 'GTest', 'hdf5': 'HDF5', 'llvm': 'LLVM', 'mpi': 'MPI', 'netcdf': 'NetCDF', 'openmp': 'OpenMP', 'wxwidgets': 'WxWidgets', } def find_external_dependency(name: str, env: 'Environment', kwargs: T.Dict[str, object], candidates: T.Optional[T.List['DependencyGenerator']] = None) -> T.Union['ExternalDependency', NotFoundDependency]: assert name required = kwargs.get('required', True) if not isinstance(required, bool): raise DependencyException('Keyword "required" must be a boolean.') if not isinstance(kwargs.get('method', ''), str): raise DependencyException('Keyword "method" must be a string.') lname = name.lower() if lname not in _packages_accept_language and 'language' in kwargs: raise DependencyException(f'{name} dependency does not accept "language" keyword argument') if not isinstance(kwargs.get('version', ''), (str, list)): raise DependencyException('Keyword "Version" must be string or list.') # display the dependency name with correct casing display_name = display_name_map.get(lname, lname) for_machine = MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency' # build a list of dependency methods to try if candidates is None: candidates = _build_external_dependency_list(name, env, for_machine, kwargs) pkg_exc: T.List[DependencyException] = [] pkgdep: T.List[ExternalDependency] = [] details = '' for c in candidates: # try this dependency method try: d = c() d._check_version() pkgdep.append(d) except DependencyException as e: assert isinstance(c, functools.partial), 'for mypy' bettermsg = f'Dependency lookup for {name} with method {c.func.log_tried()!r} failed: {e}' mlog.debug(bettermsg) e.args = (bettermsg,) pkg_exc.append(e) else: pkg_exc.append(None) details = d.log_details() if details: details = '(' + details + ') ' if 'language' in kwargs: details += 'for ' + d.language + ' ' # if the dependency was found if d.found(): info: mlog.TV_LoggableList = [] if d.version: info.append(mlog.normal_cyan(d.version)) log_info = d.log_info() if log_info: info.append('(' + log_info + ')') mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.green('YES'), *info) return d # otherwise, the dependency could not be found tried_methods = [d.log_tried() for d in pkgdep if d.log_tried()] if tried_methods: tried = mlog.format_list(tried_methods) else: tried = '' mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.red('NO'), f'(tried {tried})' if tried else '') if required: # 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 and pkg_exc[0]: raise pkg_exc[0] # we have a list of failed ExternalDependency objects, so we can report # the methods we tried to find the dependency raise DependencyException(f'Dependency "{name}" not found' + (f', tried {tried}' if tried else '')) return NotFoundDependency(name, env) def _build_external_dependency_list(name: str, env: 'Environment', for_machine: MachineChoice, kwargs: T.Dict[str, T.Any]) -> T.List['DependencyGenerator']: # First check if the method is valid if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]: raise DependencyException('method {!r} is invalid'.format(kwargs['method'])) # Is there a specific dependency detector for this dependency? lname = name.lower() if lname in packages: # Create the list of dependency object constructors using a factory # class method, if one exists, otherwise the list just consists of the # constructor if isinstance(packages[lname], type): entry1 = T.cast('T.Type[ExternalDependency]', packages[lname]) # mypy doesn't understand isinstance(..., type) if issubclass(entry1, ExternalDependency): func: T.Callable[[], 'ExternalDependency'] = functools.partial(entry1, env, kwargs) dep = [func] else: entry2 = T.cast('T.Union[DependencyFactory, WrappedFactoryFunc]', packages[lname]) dep = entry2(env, for_machine, kwargs) return dep candidates: T.List['DependencyGenerator'] = [] if kwargs.get('method', 'auto') == 'auto': # Just use the standard detection methods. methods = ['pkg-config', 'extraframework', 'cmake'] else: # If it's explicitly requested, use that detection method (only). methods = [kwargs['method']] # Exclusive to when it is explicitly requested if 'dub' in methods: from .dub import DubDependency candidates.append(functools.partial(DubDependency, name, env, kwargs)) # Preferred first candidate for auto. if 'pkg-config' in methods: from .pkgconfig import PkgConfigDependency candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) # On OSX only, try framework dependency detector. if 'extraframework' in methods: if env.machines[for_machine].is_darwin(): from .framework import ExtraFrameworkDependency candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) # Only use CMake: # - if it's explicitly requested # - as a last resort, since it might not work 100% (see #6113) if 'cmake' in methods: from .cmake import CMakeDependency candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) return candidates