diff options
Diffstat (limited to 'meson/mesonbuild/dependencies/base.py')
-rw-r--r-- | meson/mesonbuild/dependencies/base.py | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/meson/mesonbuild/dependencies/base.py b/meson/mesonbuild/dependencies/base.py new file mode 100644 index 000000000..1882246bf --- /dev/null +++ b/meson/mesonbuild/dependencies/base.py @@ -0,0 +1,573 @@ +# 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 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.methods = process_method_kw(self.get_methods(), kwargs) + 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 + + @staticmethod + def get_methods() -> T.List[DependencyMethods]: + return [DependencyMethods.AUTO] + + 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) + 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 + + @staticmethod + def get_methods() -> T.List[DependencyMethods]: + return [DependencyMethods.SYSTEM] + + 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 + + @staticmethod + def get_methods() -> T.List[DependencyMethods]: + return [DependencyMethods.BUILTIN] + + def log_tried(self) -> str: + return 'builtin' |