| # Copyright 2013-2018 The Meson development team |
| |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # This file contains the detection logic for external dependencies. |
| # Custom logic for several other packages are in separate files. |
| import copy |
| import os |
| import itertools |
| import typing as T |
| from enum import Enum |
| |
| from .. import mlog |
| from ..compilers import clib_langs |
| from ..mesonlib import LibType, MachineChoice, MesonException, HoldableObject |
| from ..mesonlib import version_compare_many |
| from ..interpreterbase import FeatureDeprecated |
| |
| if T.TYPE_CHECKING: |
| from ..compilers.compilers import Compiler |
| from ..environment import Environment |
| from ..build import BuildTarget |
| from ..mesonlib import FileOrString |
| |
| |
| class DependencyException(MesonException): |
| '''Exceptions raised while trying to find dependencies''' |
| |
| |
| class DependencyMethods(Enum): |
| # Auto means to use whatever dependency checking mechanisms in whatever order meson thinks is best. |
| AUTO = 'auto' |
| PKGCONFIG = 'pkg-config' |
| CMAKE = 'cmake' |
| # The dependency is provided by the standard library and does not need to be linked |
| BUILTIN = 'builtin' |
| # Just specify the standard link arguments, assuming the operating system provides the library. |
| SYSTEM = 'system' |
| # This is only supported on OSX - search the frameworks directory by name. |
| EXTRAFRAMEWORK = 'extraframework' |
| # Detect using the sysconfig module. |
| SYSCONFIG = 'sysconfig' |
| # Specify using a "program"-config style tool |
| CONFIG_TOOL = 'config-tool' |
| # For backwards compatibility |
| SDLCONFIG = 'sdlconfig' |
| CUPSCONFIG = 'cups-config' |
| PCAPCONFIG = 'pcap-config' |
| LIBWMFCONFIG = 'libwmf-config' |
| QMAKE = 'qmake' |
| # Misc |
| DUB = 'dub' |
| |
| |
| DependencyTypeName = T.NewType('DependencyTypeName', str) |
| |
| |
| class Dependency(HoldableObject): |
| |
| @classmethod |
| def _process_include_type_kw(cls, kwargs: T.Dict[str, T.Any]) -> str: |
| if 'include_type' not in kwargs: |
| return 'preserve' |
| if not isinstance(kwargs['include_type'], str): |
| raise DependencyException('The include_type kwarg must be a string type') |
| if kwargs['include_type'] not in ['preserve', 'system', 'non-system']: |
| raise DependencyException("include_type may only be one of ['preserve', 'system', 'non-system']") |
| return kwargs['include_type'] |
| |
| def __init__(self, type_name: DependencyTypeName, kwargs: T.Dict[str, T.Any]) -> None: |
| self.name = "null" |
| self.version: T.Optional[str] = None |
| self.language: T.Optional[str] = None # None means C-like |
| self.is_found = False |
| self.type_name = type_name |
| self.compile_args: T.List[str] = [] |
| self.link_args: T.List[str] = [] |
| # Raw -L and -l arguments without manual library searching |
| # If None, self.link_args will be used |
| self.raw_link_args: T.Optional[T.List[str]] = None |
| self.sources: T.List['FileOrString'] = [] |
| self.include_type = self._process_include_type_kw(kwargs) |
| self.ext_deps: T.List[Dependency] = [] |
| |
| def __repr__(self) -> str: |
| return f'<{self.__class__.__name__} {self.name}: {self.is_found}>' |
| |
| def is_built(self) -> bool: |
| return False |
| |
| def summary_value(self) -> T.Union[str, mlog.AnsiDecorator, mlog.AnsiText]: |
| if not self.found(): |
| return mlog.red('NO') |
| if not self.version: |
| return mlog.green('YES') |
| return mlog.AnsiText([mlog.green('YES'), ' ', mlog.cyan(self.version)]) |
| |
| def get_compile_args(self) -> T.List[str]: |
| if self.include_type == 'system': |
| converted = [] |
| for i in self.compile_args: |
| if i.startswith('-I') or i.startswith('/I'): |
| converted += ['-isystem' + i[2:]] |
| else: |
| converted += [i] |
| return converted |
| if self.include_type == 'non-system': |
| converted = [] |
| for i in self.compile_args: |
| if i.startswith('-isystem'): |
| converted += ['-I' + i[8:]] |
| else: |
| converted += [i] |
| return converted |
| return self.compile_args |
| |
| def get_all_compile_args(self) -> T.List[str]: |
| """Get the compile arguments from this dependency and it's sub dependencies.""" |
| return list(itertools.chain(self.get_compile_args(), |
| *[d.get_all_compile_args() for d in self.ext_deps])) |
| |
| def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: |
| if raw and self.raw_link_args is not None: |
| return self.raw_link_args |
| return self.link_args |
| |
| def get_all_link_args(self) -> T.List[str]: |
| """Get the link arguments from this dependency and it's sub dependencies.""" |
| return list(itertools.chain(self.get_link_args(), |
| *[d.get_all_link_args() for d in self.ext_deps])) |
| |
| def found(self) -> bool: |
| return self.is_found |
| |
| def get_sources(self) -> T.List['FileOrString']: |
| """Source files that need to be added to the target. |
| As an example, gtest-all.cc when using GTest.""" |
| return self.sources |
| |
| def get_name(self) -> str: |
| return self.name |
| |
| def get_version(self) -> str: |
| if self.version: |
| return self.version |
| else: |
| return 'unknown' |
| |
| def get_include_type(self) -> str: |
| return self.include_type |
| |
| def get_exe_args(self, compiler: 'Compiler') -> T.List[str]: |
| return [] |
| |
| def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: |
| raise DependencyException(f'{self.name!r} is not a pkgconfig dependency') |
| |
| def get_configtool_variable(self, variable_name: str) -> str: |
| raise DependencyException(f'{self.name!r} is not a config-tool dependency') |
| |
| def get_partial_dependency(self, *, compile_args: bool = False, |
| link_args: bool = False, links: bool = False, |
| includes: bool = False, sources: bool = False) -> 'Dependency': |
| """Create a new dependency that contains part of the parent dependency. |
| |
| The following options can be inherited: |
| links -- all link_with arguments |
| includes -- all include_directory and -I/-isystem calls |
| sources -- any source, header, or generated sources |
| compile_args -- any compile args |
| link_args -- any link args |
| |
| Additionally the new dependency will have the version parameter of it's |
| parent (if any) and the requested values of any dependencies will be |
| added as well. |
| """ |
| raise RuntimeError('Unreachable code in partial_dependency called') |
| |
| def _add_sub_dependency(self, deplist: T.Iterable[T.Callable[[], 'Dependency']]) -> bool: |
| """Add an internal depdency from a list of possible dependencies. |
| |
| This method is intended to make it easier to add additional |
| dependencies to another dependency internally. |
| |
| Returns true if the dependency was successfully added, false |
| otherwise. |
| """ |
| for d in deplist: |
| dep = d() |
| if dep.is_found: |
| self.ext_deps.append(dep) |
| return True |
| return False |
| |
| def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, |
| configtool: T.Optional[str] = None, internal: T.Optional[str] = None, |
| default_value: T.Optional[str] = None, |
| pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: |
| if default_value is not None: |
| return default_value |
| raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.') |
| |
| def generate_system_dependency(self, include_type: str) -> 'Dependency': |
| new_dep = copy.deepcopy(self) |
| new_dep.include_type = self._process_include_type_kw({'include_type': include_type}) |
| return new_dep |
| |
| class InternalDependency(Dependency): |
| def __init__(self, version: str, incdirs: T.List[str], compile_args: T.List[str], |
| link_args: T.List[str], libraries: T.List['BuildTarget'], |
| whole_libraries: T.List['BuildTarget'], sources: T.List['FileOrString'], |
| ext_deps: T.List[Dependency], variables: T.Dict[str, T.Any]): |
| super().__init__(DependencyTypeName('internal'), {}) |
| self.version = version |
| self.is_found = True |
| self.include_directories = incdirs |
| self.compile_args = compile_args |
| self.link_args = link_args |
| self.libraries = libraries |
| self.whole_libraries = whole_libraries |
| self.sources = sources |
| self.ext_deps = ext_deps |
| self.variables = variables |
| |
| def __deepcopy__(self, memo: T.Dict[int, 'InternalDependency']) -> 'InternalDependency': |
| result = self.__class__.__new__(self.__class__) |
| assert isinstance(result, InternalDependency) |
| memo[id(self)] = result |
| for k, v in self.__dict__.items(): |
| if k in ['libraries', 'whole_libraries']: |
| setattr(result, k, copy.copy(v)) |
| else: |
| setattr(result, k, copy.deepcopy(v, memo)) |
| return result |
| |
| def summary_value(self) -> mlog.AnsiDecorator: |
| # Omit the version. Most of the time it will be just the project |
| # version, which is uninteresting in the summary. |
| return mlog.green('YES') |
| |
| def is_built(self) -> bool: |
| if self.sources or self.libraries or self.whole_libraries: |
| return True |
| return any(d.is_built() for d in self.ext_deps) |
| |
| def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: |
| raise DependencyException('Method "get_pkgconfig_variable()" is ' |
| 'invalid for an internal dependency') |
| |
| def get_configtool_variable(self, variable_name: str) -> str: |
| raise DependencyException('Method "get_configtool_variable()" is ' |
| 'invalid for an internal dependency') |
| |
| def get_partial_dependency(self, *, compile_args: bool = False, |
| link_args: bool = False, links: bool = False, |
| includes: bool = False, sources: bool = False) -> 'InternalDependency': |
| final_compile_args = self.compile_args.copy() if compile_args else [] |
| final_link_args = self.link_args.copy() if link_args else [] |
| final_libraries = self.libraries.copy() if links else [] |
| final_whole_libraries = self.whole_libraries.copy() if links else [] |
| final_sources = self.sources.copy() if sources else [] |
| final_includes = self.include_directories.copy() if includes else [] |
| final_deps = [d.get_partial_dependency( |
| compile_args=compile_args, link_args=link_args, links=links, |
| includes=includes, sources=sources) for d in self.ext_deps] |
| return InternalDependency( |
| self.version, final_includes, final_compile_args, |
| final_link_args, final_libraries, final_whole_libraries, |
| final_sources, final_deps, self.variables) |
| |
| def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, |
| configtool: T.Optional[str] = None, internal: T.Optional[str] = None, |
| default_value: T.Optional[str] = None, |
| pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: |
| val = self.variables.get(internal, default_value) |
| if val is not None: |
| # TODO: Try removing this assert by better typing self.variables |
| if isinstance(val, str): |
| return val |
| if isinstance(val, list): |
| for i in val: |
| assert isinstance(i, str) |
| return val |
| raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}') |
| |
| def generate_link_whole_dependency(self) -> Dependency: |
| new_dep = copy.deepcopy(self) |
| new_dep.whole_libraries += new_dep.libraries |
| new_dep.libraries = [] |
| return new_dep |
| |
| class HasNativeKwarg: |
| def __init__(self, kwargs: T.Dict[str, T.Any]): |
| self.for_machine = self.get_for_machine_from_kwargs(kwargs) |
| |
| def get_for_machine_from_kwargs(self, kwargs: T.Dict[str, T.Any]) -> MachineChoice: |
| return MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST |
| |
| class ExternalDependency(Dependency, HasNativeKwarg): |
| def __init__(self, type_name: DependencyTypeName, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None): |
| Dependency.__init__(self, type_name, kwargs) |
| self.env = environment |
| self.name = type_name # default |
| self.is_found = False |
| self.language = language |
| self.version_reqs = kwargs.get('version', None) |
| if isinstance(self.version_reqs, str): |
| self.version_reqs = [self.version_reqs] |
| self.required = kwargs.get('required', True) |
| self.silent = kwargs.get('silent', False) |
| self.static = kwargs.get('static', False) |
| self.libtype = LibType.STATIC if self.static else LibType.PREFER_SHARED |
| if not isinstance(self.static, bool): |
| raise DependencyException('Static keyword must be boolean') |
| # Is this dependency to be run on the build platform? |
| HasNativeKwarg.__init__(self, kwargs) |
| self.clib_compiler = detect_compiler(self.name, environment, self.for_machine, self.language) |
| |
| def get_compiler(self) -> 'Compiler': |
| return self.clib_compiler |
| |
| def get_partial_dependency(self, *, compile_args: bool = False, |
| link_args: bool = False, links: bool = False, |
| includes: bool = False, sources: bool = False) -> Dependency: |
| new = copy.copy(self) |
| if not compile_args: |
| new.compile_args = [] |
| if not link_args: |
| new.link_args = [] |
| if not sources: |
| new.sources = [] |
| if not includes: |
| pass # TODO maybe filter compile_args? |
| if not sources: |
| new.sources = [] |
| |
| return new |
| |
| def log_details(self) -> str: |
| return '' |
| |
| def log_info(self) -> str: |
| return '' |
| |
| def log_tried(self) -> str: |
| return '' |
| |
| # Check if dependency version meets the requirements |
| def _check_version(self) -> None: |
| if not self.is_found: |
| return |
| |
| if self.version_reqs: |
| # an unknown version can never satisfy any requirement |
| if not self.version: |
| self.is_found = False |
| found_msg: mlog.TV_LoggableList = [] |
| found_msg += ['Dependency', mlog.bold(self.name), 'found:'] |
| found_msg += [mlog.red('NO'), 'unknown version, but need:', self.version_reqs] |
| mlog.log(*found_msg) |
| |
| if self.required: |
| m = f'Unknown version of dependency {self.name!r}, but need {self.version_reqs!r}.' |
| raise DependencyException(m) |
| |
| else: |
| (self.is_found, not_found, found) = \ |
| version_compare_many(self.version, self.version_reqs) |
| if not self.is_found: |
| found_msg = ['Dependency', mlog.bold(self.name), 'found:'] |
| found_msg += [mlog.red('NO'), |
| 'found', mlog.normal_cyan(self.version), 'but need:', |
| mlog.bold(', '.join([f"'{e}'" for e in not_found]))] |
| if found: |
| found_msg += ['; matched:', |
| ', '.join([f"'{e}'" for e in found])] |
| mlog.log(*found_msg) |
| |
| if self.required: |
| m = 'Invalid version of dependency, need {!r} {!r} found {!r}.' |
| raise DependencyException(m.format(self.name, not_found, self.version)) |
| return |
| |
| |
| class NotFoundDependency(Dependency): |
| def __init__(self, environment: 'Environment') -> None: |
| super().__init__(DependencyTypeName('not-found'), {}) |
| self.env = environment |
| self.name = 'not-found' |
| self.is_found = False |
| |
| def get_partial_dependency(self, *, compile_args: bool = False, |
| link_args: bool = False, links: bool = False, |
| includes: bool = False, sources: bool = False) -> 'NotFoundDependency': |
| return copy.copy(self) |
| |
| |
| class ExternalLibrary(ExternalDependency): |
| def __init__(self, name: str, link_args: T.List[str], environment: 'Environment', |
| language: str, silent: bool = False) -> None: |
| super().__init__(DependencyTypeName('library'), environment, {}, language=language) |
| self.name = name |
| self.language = language |
| self.is_found = False |
| if link_args: |
| self.is_found = True |
| self.link_args = link_args |
| if not silent: |
| if self.is_found: |
| mlog.log('Library', mlog.bold(name), 'found:', mlog.green('YES')) |
| else: |
| mlog.log('Library', mlog.bold(name), 'found:', mlog.red('NO')) |
| |
| def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: |
| ''' |
| External libraries detected using a compiler must only be used with |
| compatible code. For instance, Vala libraries (.vapi files) cannot be |
| used with C code, and not all Rust library types can be linked with |
| C-like code. Note that C++ libraries *can* be linked with C code with |
| a C++ linker (and vice-versa). |
| ''' |
| # Using a vala library in a non-vala target, or a non-vala library in a vala target |
| # XXX: This should be extended to other non-C linkers such as Rust |
| if (self.language == 'vala' and language != 'vala') or \ |
| (language == 'vala' and self.language != 'vala'): |
| return [] |
| return super().get_link_args(language=language, raw=raw) |
| |
| def get_partial_dependency(self, *, compile_args: bool = False, |
| link_args: bool = False, links: bool = False, |
| includes: bool = False, sources: bool = False) -> 'ExternalLibrary': |
| # External library only has link_args, so ignore the rest of the |
| # interface. |
| new = copy.copy(self) |
| if not link_args: |
| new.link_args = [] |
| return new |
| |
| |
| def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]: |
| """Sort <libpaths> according to <refpaths> |
| |
| It is intended to be used to sort -L flags returned by pkg-config. |
| Pkg-config returns flags in random order which cannot be relied on. |
| """ |
| if len(refpaths) == 0: |
| return list(libpaths) |
| |
| def key_func(libpath: str) -> T.Tuple[int, int]: |
| common_lengths: T.List[int] = [] |
| for refpath in refpaths: |
| try: |
| common_path: str = os.path.commonpath([libpath, refpath]) |
| except ValueError: |
| common_path = '' |
| common_lengths.append(len(common_path)) |
| max_length = max(common_lengths) |
| max_index = common_lengths.index(max_length) |
| reversed_max_length = len(refpaths[max_index]) - max_length |
| return (max_index, reversed_max_length) |
| return sorted(libpaths, key=key_func) |
| |
| def strip_system_libdirs(environment: 'Environment', for_machine: MachineChoice, link_args: T.List[str]) -> T.List[str]: |
| """Remove -L<system path> arguments. |
| |
| leaving these in will break builds where a user has a version of a library |
| in the system path, and a different version not in the system path if they |
| want to link against the non-system path version. |
| """ |
| exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)} |
| return [l for l in link_args if l not in exclude] |
| |
| def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs: T.Dict[str, T.Any]) -> T.List[DependencyMethods]: |
| method = kwargs.get('method', 'auto') # type: T.Union[DependencyMethods, str] |
| if isinstance(method, DependencyMethods): |
| return [method] |
| # TODO: try/except? |
| if method not in [e.value for e in DependencyMethods]: |
| raise DependencyException(f'method {method!r} is invalid') |
| method = DependencyMethods(method) |
| |
| # This sets per-tool config methods which are deprecated to to the new |
| # generic CONFIG_TOOL value. |
| if method in [DependencyMethods.SDLCONFIG, DependencyMethods.CUPSCONFIG, |
| DependencyMethods.PCAPCONFIG, DependencyMethods.LIBWMFCONFIG]: |
| FeatureDeprecated.single_use(f'Configuration method {method.value}', '0.44', 'Use "config-tool" instead.') |
| method = DependencyMethods.CONFIG_TOOL |
| if method is DependencyMethods.QMAKE: |
| FeatureDeprecated.single_use(f'Configuration method "qmake"', '0.58', 'Use "config-tool" instead.') |
| method = DependencyMethods.CONFIG_TOOL |
| |
| # Set the detection method. If the method is set to auto, use any available method. |
| # If method is set to a specific string, allow only that detection method. |
| if method == DependencyMethods.AUTO: |
| methods = list(possible) |
| elif method in possible: |
| methods = [method] |
| else: |
| raise DependencyException( |
| 'Unsupported detection method: {}, allowed methods are {}'.format( |
| method.value, |
| mlog.format_list([x.value for x in [DependencyMethods.AUTO] + list(possible)]))) |
| |
| return methods |
| |
| def detect_compiler(name: str, env: 'Environment', for_machine: MachineChoice, |
| language: T.Optional[str]) -> T.Optional['Compiler']: |
| """Given a language and environment find the compiler used.""" |
| compilers = env.coredata.compilers[for_machine] |
| |
| # Set the compiler for this dependency if a language is specified, |
| # else try to pick something that looks usable. |
| if language: |
| if language not in compilers: |
| m = name.capitalize() + ' requires a {0} compiler, but ' \ |
| '{0} is not in the list of project languages' |
| raise DependencyException(m.format(language.capitalize())) |
| return compilers[language] |
| else: |
| for lang in clib_langs: |
| try: |
| return compilers[lang] |
| except KeyError: |
| continue |
| return None |
| |
| |
| class SystemDependency(ExternalDependency): |
| |
| """Dependency base for System type dependencies.""" |
| |
| def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], |
| language: T.Optional[str] = None) -> None: |
| super().__init__(DependencyTypeName('system'), env, kwargs, language=language) |
| self.name = name |
| |
| def log_tried(self) -> str: |
| return 'system' |
| |
| |
| class BuiltinDependency(ExternalDependency): |
| |
| """Dependency base for Builtin type dependencies.""" |
| |
| def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], |
| language: T.Optional[str] = None) -> None: |
| super().__init__(DependencyTypeName('builtin'), env, kwargs, language=language) |
| self.name = name |
| |
| def log_tried(self) -> str: |
| return 'builtin' |