1# Copyright 2013-2018 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
15# This file contains the detection logic for external dependencies.
16# Custom logic for several other packages are in separate files.
17import copy
18import os
19import itertools
20import typing as T
21from enum import Enum
22
23from .. import mlog
24from ..compilers import clib_langs
25from ..mesonlib import MachineChoice, MesonException, HoldableObject
26from ..mesonlib import version_compare_many
27from ..interpreterbase import FeatureDeprecated
28
29if T.TYPE_CHECKING:
30    from ..compilers.compilers import Compiler
31    from ..environment import Environment
32    from ..build import BuildTarget
33    from ..mesonlib import FileOrString
34
35
36class DependencyException(MesonException):
37    '''Exceptions raised while trying to find dependencies'''
38
39
40class DependencyMethods(Enum):
41    # Auto means to use whatever dependency checking mechanisms in whatever order meson thinks is best.
42    AUTO = 'auto'
43    PKGCONFIG = 'pkg-config'
44    CMAKE = 'cmake'
45    # The dependency is provided by the standard library and does not need to be linked
46    BUILTIN = 'builtin'
47    # Just specify the standard link arguments, assuming the operating system provides the library.
48    SYSTEM = 'system'
49    # This is only supported on OSX - search the frameworks directory by name.
50    EXTRAFRAMEWORK = 'extraframework'
51    # Detect using the sysconfig module.
52    SYSCONFIG = 'sysconfig'
53    # Specify using a "program"-config style tool
54    CONFIG_TOOL = 'config-tool'
55    # For backwards compatibility
56    SDLCONFIG = 'sdlconfig'
57    CUPSCONFIG = 'cups-config'
58    PCAPCONFIG = 'pcap-config'
59    LIBWMFCONFIG = 'libwmf-config'
60    QMAKE = 'qmake'
61    # Misc
62    DUB = 'dub'
63
64
65DependencyTypeName = T.NewType('DependencyTypeName', str)
66
67
68class Dependency(HoldableObject):
69
70    @classmethod
71    def _process_include_type_kw(cls, kwargs: T.Dict[str, T.Any]) -> str:
72        if 'include_type' not in kwargs:
73            return 'preserve'
74        if not isinstance(kwargs['include_type'], str):
75            raise DependencyException('The include_type kwarg must be a string type')
76        if kwargs['include_type'] not in ['preserve', 'system', 'non-system']:
77            raise DependencyException("include_type may only be one of ['preserve', 'system', 'non-system']")
78        return kwargs['include_type']
79
80    def __init__(self, type_name: DependencyTypeName, kwargs: T.Dict[str, T.Any]) -> None:
81        self.name = "null"
82        self.version:  T.Optional[str] = None
83        self.language: T.Optional[str] = None # None means C-like
84        self.is_found = False
85        self.type_name = type_name
86        self.compile_args: T.List[str] = []
87        self.link_args:    T.List[str] = []
88        # Raw -L and -l arguments without manual library searching
89        # If None, self.link_args will be used
90        self.raw_link_args: T.Optional[T.List[str]] = None
91        self.sources: T.List['FileOrString'] = []
92        self.methods = process_method_kw(self.get_methods(), kwargs)
93        self.include_type = self._process_include_type_kw(kwargs)
94        self.ext_deps: T.List[Dependency] = []
95
96    def __repr__(self) -> str:
97        return f'<{self.__class__.__name__} {self.name}: {self.is_found}>'
98
99    def is_built(self) -> bool:
100        return False
101
102    def summary_value(self) -> T.Union[str, mlog.AnsiDecorator, mlog.AnsiText]:
103        if not self.found():
104            return mlog.red('NO')
105        if not self.version:
106            return mlog.green('YES')
107        return mlog.AnsiText(mlog.green('YES'), ' ', mlog.cyan(self.version))
108
109    def get_compile_args(self) -> T.List[str]:
110        if self.include_type == 'system':
111            converted = []
112            for i in self.compile_args:
113                if i.startswith('-I') or i.startswith('/I'):
114                    converted += ['-isystem' + i[2:]]
115                else:
116                    converted += [i]
117            return converted
118        if self.include_type == 'non-system':
119            converted = []
120            for i in self.compile_args:
121                if i.startswith('-isystem'):
122                    converted += ['-I' + i[8:]]
123                else:
124                    converted += [i]
125            return converted
126        return self.compile_args
127
128    def get_all_compile_args(self) -> T.List[str]:
129        """Get the compile arguments from this dependency and it's sub dependencies."""
130        return list(itertools.chain(self.get_compile_args(),
131                                    *[d.get_all_compile_args() for d in self.ext_deps]))
132
133    def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
134        if raw and self.raw_link_args is not None:
135            return self.raw_link_args
136        return self.link_args
137
138    def get_all_link_args(self) -> T.List[str]:
139        """Get the link arguments from this dependency and it's sub dependencies."""
140        return list(itertools.chain(self.get_link_args(),
141                                    *[d.get_all_link_args() for d in self.ext_deps]))
142
143    def found(self) -> bool:
144        return self.is_found
145
146    def get_sources(self) -> T.List['FileOrString']:
147        """Source files that need to be added to the target.
148        As an example, gtest-all.cc when using GTest."""
149        return self.sources
150
151    @staticmethod
152    def get_methods() -> T.List[DependencyMethods]:
153        return [DependencyMethods.AUTO]
154
155    def get_name(self) -> str:
156        return self.name
157
158    def get_version(self) -> str:
159        if self.version:
160            return self.version
161        else:
162            return 'unknown'
163
164    def get_include_type(self) -> str:
165        return self.include_type
166
167    def get_exe_args(self, compiler: 'Compiler') -> T.List[str]:
168        return []
169
170    def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str:
171        raise DependencyException(f'{self.name!r} is not a pkgconfig dependency')
172
173    def get_configtool_variable(self, variable_name: str) -> str:
174        raise DependencyException(f'{self.name!r} is not a config-tool dependency')
175
176    def get_partial_dependency(self, *, compile_args: bool = False,
177                               link_args: bool = False, links: bool = False,
178                               includes: bool = False, sources: bool = False) -> 'Dependency':
179        """Create a new dependency that contains part of the parent dependency.
180
181        The following options can be inherited:
182            links -- all link_with arguments
183            includes -- all include_directory and -I/-isystem calls
184            sources -- any source, header, or generated sources
185            compile_args -- any compile args
186            link_args -- any link args
187
188        Additionally the new dependency will have the version parameter of it's
189        parent (if any) and the requested values of any dependencies will be
190        added as well.
191        """
192        raise RuntimeError('Unreachable code in partial_dependency called')
193
194    def _add_sub_dependency(self, deplist: T.Iterable[T.Callable[[], 'Dependency']]) -> bool:
195        """Add an internal depdency from a list of possible dependencies.
196
197        This method is intended to make it easier to add additional
198        dependencies to another dependency internally.
199
200        Returns true if the dependency was successfully added, false
201        otherwise.
202        """
203        for d in deplist:
204            dep = d()
205            if dep.is_found:
206                self.ext_deps.append(dep)
207                return True
208        return False
209
210    def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
211                     configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
212                     default_value: T.Optional[str] = None,
213                     pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]:
214        if default_value is not None:
215            return default_value
216        raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.')
217
218    def generate_system_dependency(self, include_type: str) -> 'Dependency':
219        new_dep = copy.deepcopy(self)
220        new_dep.include_type = self._process_include_type_kw({'include_type': include_type})
221        return new_dep
222
223class InternalDependency(Dependency):
224    def __init__(self, version: str, incdirs: T.List[str], compile_args: T.List[str],
225                 link_args: T.List[str], libraries: T.List['BuildTarget'],
226                 whole_libraries: T.List['BuildTarget'], sources: T.List['FileOrString'],
227                 ext_deps: T.List[Dependency], variables: T.Dict[str, T.Any]):
228        super().__init__(DependencyTypeName('internal'), {})
229        self.version = version
230        self.is_found = True
231        self.include_directories = incdirs
232        self.compile_args = compile_args
233        self.link_args = link_args
234        self.libraries = libraries
235        self.whole_libraries = whole_libraries
236        self.sources = sources
237        self.ext_deps = ext_deps
238        self.variables = variables
239
240    def __deepcopy__(self, memo: T.Dict[int, 'InternalDependency']) -> 'InternalDependency':
241        result = self.__class__.__new__(self.__class__)
242        assert isinstance(result, InternalDependency)
243        memo[id(self)] = result
244        for k, v in self.__dict__.items():
245            if k in ['libraries', 'whole_libraries']:
246                setattr(result, k, copy.copy(v))
247            else:
248                setattr(result, k, copy.deepcopy(v, memo))
249        return result
250
251    def summary_value(self) -> mlog.AnsiDecorator:
252        # Omit the version.  Most of the time it will be just the project
253        # version, which is uninteresting in the summary.
254        return mlog.green('YES')
255
256    def is_built(self) -> bool:
257        if self.sources or self.libraries or self.whole_libraries:
258            return True
259        return any(d.is_built() for d in self.ext_deps)
260
261    def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str:
262        raise DependencyException('Method "get_pkgconfig_variable()" is '
263                                  'invalid for an internal dependency')
264
265    def get_configtool_variable(self, variable_name: str) -> str:
266        raise DependencyException('Method "get_configtool_variable()" is '
267                                  'invalid for an internal dependency')
268
269    def get_partial_dependency(self, *, compile_args: bool = False,
270                               link_args: bool = False, links: bool = False,
271                               includes: bool = False, sources: bool = False) -> 'InternalDependency':
272        final_compile_args = self.compile_args.copy() if compile_args else []
273        final_link_args = self.link_args.copy() if link_args else []
274        final_libraries = self.libraries.copy() if links else []
275        final_whole_libraries = self.whole_libraries.copy() if links else []
276        final_sources = self.sources.copy() if sources else []
277        final_includes = self.include_directories.copy() if includes else []
278        final_deps = [d.get_partial_dependency(
279            compile_args=compile_args, link_args=link_args, links=links,
280            includes=includes, sources=sources) for d in self.ext_deps]
281        return InternalDependency(
282            self.version, final_includes, final_compile_args,
283            final_link_args, final_libraries, final_whole_libraries,
284            final_sources, final_deps, self.variables)
285
286    def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
287                     configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
288                     default_value: T.Optional[str] = None,
289                     pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]:
290        val = self.variables.get(internal, default_value)
291        if val is not None:
292            # TODO: Try removing this assert by better typing self.variables
293            if isinstance(val, str):
294                return val
295            if isinstance(val, list):
296                for i in val:
297                    assert isinstance(i, str)
298                return val
299        raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}')
300
301    def generate_link_whole_dependency(self) -> Dependency:
302        new_dep = copy.deepcopy(self)
303        new_dep.whole_libraries += new_dep.libraries
304        new_dep.libraries = []
305        return new_dep
306
307class HasNativeKwarg:
308    def __init__(self, kwargs: T.Dict[str, T.Any]):
309        self.for_machine = self.get_for_machine_from_kwargs(kwargs)
310
311    def get_for_machine_from_kwargs(self, kwargs: T.Dict[str, T.Any]) -> MachineChoice:
312        return MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST
313
314class ExternalDependency(Dependency, HasNativeKwarg):
315    def __init__(self, type_name: DependencyTypeName, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
316        Dependency.__init__(self, type_name, kwargs)
317        self.env = environment
318        self.name = type_name # default
319        self.is_found = False
320        self.language = language
321        self.version_reqs = kwargs.get('version', None)
322        if isinstance(self.version_reqs, str):
323            self.version_reqs = [self.version_reqs]
324        self.required = kwargs.get('required', True)
325        self.silent = kwargs.get('silent', False)
326        self.static = kwargs.get('static', False)
327        if not isinstance(self.static, bool):
328            raise DependencyException('Static keyword must be boolean')
329        # Is this dependency to be run on the build platform?
330        HasNativeKwarg.__init__(self, kwargs)
331        self.clib_compiler = detect_compiler(self.name, environment, self.for_machine, self.language)
332
333    def get_compiler(self) -> 'Compiler':
334        return self.clib_compiler
335
336    def get_partial_dependency(self, *, compile_args: bool = False,
337                               link_args: bool = False, links: bool = False,
338                               includes: bool = False, sources: bool = False) -> Dependency:
339        new = copy.copy(self)
340        if not compile_args:
341            new.compile_args = []
342        if not link_args:
343            new.link_args = []
344        if not sources:
345            new.sources = []
346        if not includes:
347            pass # TODO maybe filter compile_args?
348        if not sources:
349            new.sources = []
350
351        return new
352
353    def log_details(self) -> str:
354        return ''
355
356    def log_info(self) -> str:
357        return ''
358
359    def log_tried(self) -> str:
360        return ''
361
362    # Check if dependency version meets the requirements
363    def _check_version(self) -> None:
364        if not self.is_found:
365            return
366
367        if self.version_reqs:
368            # an unknown version can never satisfy any requirement
369            if not self.version:
370                self.is_found = False
371                found_msg: mlog.TV_LoggableList = []
372                found_msg += ['Dependency', mlog.bold(self.name), 'found:']
373                found_msg += [mlog.red('NO'), 'unknown version, but need:', self.version_reqs]
374                mlog.log(*found_msg)
375
376                if self.required:
377                    m = f'Unknown version of dependency {self.name!r}, but need {self.version_reqs!r}.'
378                    raise DependencyException(m)
379
380            else:
381                (self.is_found, not_found, found) = \
382                    version_compare_many(self.version, self.version_reqs)
383                if not self.is_found:
384                    found_msg = ['Dependency', mlog.bold(self.name), 'found:']
385                    found_msg += [mlog.red('NO'),
386                                  'found', mlog.normal_cyan(self.version), 'but need:',
387                                  mlog.bold(', '.join([f"'{e}'" for e in not_found]))]
388                    if found:
389                        found_msg += ['; matched:',
390                                      ', '.join([f"'{e}'" for e in found])]
391                    mlog.log(*found_msg)
392
393                    if self.required:
394                        m = 'Invalid version of dependency, need {!r} {!r} found {!r}.'
395                        raise DependencyException(m.format(self.name, not_found, self.version))
396                    return
397
398
399class NotFoundDependency(Dependency):
400    def __init__(self, environment: 'Environment') -> None:
401        super().__init__(DependencyTypeName('not-found'), {})
402        self.env = environment
403        self.name = 'not-found'
404        self.is_found = False
405
406    def get_partial_dependency(self, *, compile_args: bool = False,
407                               link_args: bool = False, links: bool = False,
408                               includes: bool = False, sources: bool = False) -> 'NotFoundDependency':
409        return copy.copy(self)
410
411
412class ExternalLibrary(ExternalDependency):
413    def __init__(self, name: str, link_args: T.List[str], environment: 'Environment',
414                 language: str, silent: bool = False) -> None:
415        super().__init__(DependencyTypeName('library'), environment, {}, language=language)
416        self.name = name
417        self.language = language
418        self.is_found = False
419        if link_args:
420            self.is_found = True
421            self.link_args = link_args
422        if not silent:
423            if self.is_found:
424                mlog.log('Library', mlog.bold(name), 'found:', mlog.green('YES'))
425            else:
426                mlog.log('Library', mlog.bold(name), 'found:', mlog.red('NO'))
427
428    def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]:
429        '''
430        External libraries detected using a compiler must only be used with
431        compatible code. For instance, Vala libraries (.vapi files) cannot be
432        used with C code, and not all Rust library types can be linked with
433        C-like code. Note that C++ libraries *can* be linked with C code with
434        a C++ linker (and vice-versa).
435        '''
436        # Using a vala library in a non-vala target, or a non-vala library in a vala target
437        # XXX: This should be extended to other non-C linkers such as Rust
438        if (self.language == 'vala' and language != 'vala') or \
439           (language == 'vala' and self.language != 'vala'):
440            return []
441        return super().get_link_args(language=language, raw=raw)
442
443    def get_partial_dependency(self, *, compile_args: bool = False,
444                               link_args: bool = False, links: bool = False,
445                               includes: bool = False, sources: bool = False) -> 'ExternalLibrary':
446        # External library only has link_args, so ignore the rest of the
447        # interface.
448        new = copy.copy(self)
449        if not link_args:
450            new.link_args = []
451        return new
452
453
454def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]:
455    """Sort <libpaths> according to <refpaths>
456
457    It is intended to be used to sort -L flags returned by pkg-config.
458    Pkg-config returns flags in random order which cannot be relied on.
459    """
460    if len(refpaths) == 0:
461        return list(libpaths)
462
463    def key_func(libpath: str) -> T.Tuple[int, int]:
464        common_lengths: T.List[int] = []
465        for refpath in refpaths:
466            try:
467                common_path: str = os.path.commonpath([libpath, refpath])
468            except ValueError:
469                common_path = ''
470            common_lengths.append(len(common_path))
471        max_length = max(common_lengths)
472        max_index = common_lengths.index(max_length)
473        reversed_max_length = len(refpaths[max_index]) - max_length
474        return (max_index, reversed_max_length)
475    return sorted(libpaths, key=key_func)
476
477def strip_system_libdirs(environment: 'Environment', for_machine: MachineChoice, link_args: T.List[str]) -> T.List[str]:
478    """Remove -L<system path> arguments.
479
480    leaving these in will break builds where a user has a version of a library
481    in the system path, and a different version not in the system path if they
482    want to link against the non-system path version.
483    """
484    exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)}
485    return [l for l in link_args if l not in exclude]
486
487def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs: T.Dict[str, T.Any]) -> T.List[DependencyMethods]:
488    method = kwargs.get('method', 'auto')  # type: T.Union[DependencyMethods, str]
489    if isinstance(method, DependencyMethods):
490        return [method]
491    # TODO: try/except?
492    if method not in [e.value for e in DependencyMethods]:
493        raise DependencyException(f'method {method!r} is invalid')
494    method = DependencyMethods(method)
495
496    # This sets per-tool config methods which are deprecated to to the new
497    # generic CONFIG_TOOL value.
498    if method in [DependencyMethods.SDLCONFIG, DependencyMethods.CUPSCONFIG,
499                  DependencyMethods.PCAPCONFIG, DependencyMethods.LIBWMFCONFIG]:
500        FeatureDeprecated.single_use(f'Configuration method {method.value}', '0.44', 'Use "config-tool" instead.')
501        method = DependencyMethods.CONFIG_TOOL
502    if method is DependencyMethods.QMAKE:
503        FeatureDeprecated.single_use(f'Configuration method "qmake"', '0.58', 'Use "config-tool" instead.')
504        method = DependencyMethods.CONFIG_TOOL
505
506    # Set the detection method. If the method is set to auto, use any available method.
507    # If method is set to a specific string, allow only that detection method.
508    if method == DependencyMethods.AUTO:
509        methods = list(possible)
510    elif method in possible:
511        methods = [method]
512    else:
513        raise DependencyException(
514            'Unsupported detection method: {}, allowed methods are {}'.format(
515                method.value,
516                mlog.format_list([x.value for x in [DependencyMethods.AUTO] + list(possible)])))
517
518    return methods
519
520def detect_compiler(name: str, env: 'Environment', for_machine: MachineChoice,
521                    language: T.Optional[str]) -> T.Optional['Compiler']:
522    """Given a language and environment find the compiler used."""
523    compilers = env.coredata.compilers[for_machine]
524
525    # Set the compiler for this dependency if a language is specified,
526    # else try to pick something that looks usable.
527    if language:
528        if language not in compilers:
529            m = name.capitalize() + ' requires a {0} compiler, but ' \
530                '{0} is not in the list of project languages'
531            raise DependencyException(m.format(language.capitalize()))
532        return compilers[language]
533    else:
534        for lang in clib_langs:
535            try:
536                return compilers[lang]
537            except KeyError:
538                continue
539    return None
540
541
542class SystemDependency(ExternalDependency):
543
544    """Dependency base for System type dependencies."""
545
546    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
547                 language: T.Optional[str] = None) -> None:
548        super().__init__(DependencyTypeName('system'), env, kwargs, language=language)
549        self.name = name
550
551    @staticmethod
552    def get_methods() -> T.List[DependencyMethods]:
553        return [DependencyMethods.SYSTEM]
554
555    def log_tried(self) -> str:
556        return 'system'
557
558
559class BuiltinDependency(ExternalDependency):
560
561    """Dependency base for Builtin type dependencies."""
562
563    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any],
564                 language: T.Optional[str] = None) -> None:
565        super().__init__(DependencyTypeName('builtin'), env, kwargs, language=language)
566        self.name = name
567
568    @staticmethod
569    def get_methods() -> T.List[DependencyMethods]:
570        return [DependencyMethods.BUILTIN]
571
572    def log_tried(self) -> str:
573        return 'builtin'
574