1# Copyright 2013-2019 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 useful for
16# development purposes, such as testing, debugging, etc..
17
18import glob
19import os
20import re
21import typing as T
22
23from .. import mesonlib, mlog
24from ..mesonlib import version_compare, stringlistify, extract_as_list, MachineChoice
25from ..environment import get_llvm_tool_names
26from .base import (
27    DependencyException, DependencyMethods, ExternalDependency, PkgConfigDependency,
28    strip_system_libdirs, ConfigToolDependency, CMakeDependency, DependencyFactory,
29)
30from .misc import threads_factory
31from ..compilers.c import AppleClangCCompiler
32from ..compilers.cpp import AppleClangCPPCompiler
33
34if T.TYPE_CHECKING:
35    from .. environment import Environment
36
37
38def get_shared_library_suffix(environment, for_machine: MachineChoice):
39    """This is only guaranteed to work for languages that compile to machine
40    code, not for languages like C# that use a bytecode and always end in .dll
41    """
42    m = environment.machines[for_machine]
43    if m.is_windows():
44        return '.dll'
45    elif m.is_darwin():
46        return '.dylib'
47    return '.so'
48
49
50class GTestDependencySystem(ExternalDependency):
51    def __init__(self, name: str, environment, kwargs):
52        super().__init__(name, environment, kwargs, language='cpp')
53        self.main = kwargs.get('main', False)
54        self.src_dirs = ['/usr/src/gtest/src', '/usr/src/googletest/googletest/src']
55        if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
56            self.is_found = False
57            return
58        self.detect()
59
60    def detect(self):
61        gtest_detect = self.clib_compiler.find_library("gtest", self.env, [])
62        gtest_main_detect = self.clib_compiler.find_library("gtest_main", self.env, [])
63        if gtest_detect and (not self.main or gtest_main_detect):
64            self.is_found = True
65            self.compile_args = []
66            self.link_args = gtest_detect
67            if self.main:
68                self.link_args += gtest_main_detect
69            self.sources = []
70            self.prebuilt = True
71        elif self.detect_srcdir():
72            self.is_found = True
73            self.compile_args = ['-I' + d for d in self.src_include_dirs]
74            self.link_args = []
75            if self.main:
76                self.sources = [self.all_src, self.main_src]
77            else:
78                self.sources = [self.all_src]
79            self.prebuilt = False
80        else:
81            self.is_found = False
82
83    def detect_srcdir(self):
84        for s in self.src_dirs:
85            if os.path.exists(s):
86                self.src_dir = s
87                self.all_src = mesonlib.File.from_absolute_file(
88                    os.path.join(self.src_dir, 'gtest-all.cc'))
89                self.main_src = mesonlib.File.from_absolute_file(
90                    os.path.join(self.src_dir, 'gtest_main.cc'))
91                self.src_include_dirs = [os.path.normpath(os.path.join(self.src_dir, '..')),
92                                         os.path.normpath(os.path.join(self.src_dir, '../include')),
93                                         ]
94                return True
95        return False
96
97    def log_info(self):
98        if self.prebuilt:
99            return 'prebuilt'
100        else:
101            return 'building self'
102
103    def log_tried(self):
104        return 'system'
105
106    @staticmethod
107    def get_methods():
108        return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
109
110
111class GTestDependencyPC(PkgConfigDependency):
112
113    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
114        assert name == 'gtest'
115        if kwargs.get('main'):
116            name = 'gtest_main'
117        super().__init__(name, environment, kwargs)
118
119
120class GMockDependencySystem(ExternalDependency):
121    def __init__(self, name: str, environment, kwargs):
122        super().__init__(name, environment, kwargs, language='cpp')
123        self.main = kwargs.get('main', False)
124        if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
125            self.is_found = False
126            return
127
128        # If we are getting main() from GMock, we definitely
129        # want to avoid linking in main() from GTest
130        gtest_kwargs = kwargs.copy()
131        if self.main:
132            gtest_kwargs['main'] = False
133
134        # GMock without GTest is pretty much useless
135        # this also mimics the structure given in WrapDB,
136        # where GMock always pulls in GTest
137        found = self._add_sub_dependency(gtest_factory(environment, self.for_machine, gtest_kwargs))
138        if not found:
139            self.is_found = False
140            return
141
142        # GMock may be a library or just source.
143        # Work with both.
144        gmock_detect = self.clib_compiler.find_library("gmock", self.env, [])
145        gmock_main_detect = self.clib_compiler.find_library("gmock_main", self.env, [])
146        if gmock_detect and (not self.main or gmock_main_detect):
147            self.is_found = True
148            self.link_args += gmock_detect
149            if self.main:
150                self.link_args += gmock_main_detect
151            self.prebuilt = True
152            return
153
154        for d in ['/usr/src/googletest/googlemock/src', '/usr/src/gmock/src', '/usr/src/gmock']:
155            if os.path.exists(d):
156                self.is_found = True
157                # Yes, we need both because there are multiple
158                # versions of gmock that do different things.
159                d2 = os.path.normpath(os.path.join(d, '..'))
160                self.compile_args += ['-I' + d, '-I' + d2, '-I' + os.path.join(d2, 'include')]
161                all_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock-all.cc'))
162                main_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock_main.cc'))
163                if self.main:
164                    self.sources += [all_src, main_src]
165                else:
166                    self.sources += [all_src]
167                self.prebuilt = False
168                return
169
170        self.is_found = False
171
172    def log_info(self):
173        if self.prebuilt:
174            return 'prebuilt'
175        else:
176            return 'building self'
177
178    def log_tried(self):
179        return 'system'
180
181    @staticmethod
182    def get_methods():
183        return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
184
185
186class GMockDependencyPC(PkgConfigDependency):
187
188    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
189        assert name == 'gmock'
190        if kwargs.get('main'):
191            name = 'gmock_main'
192        super().__init__(name, environment, kwargs)
193
194
195class LLVMDependencyConfigTool(ConfigToolDependency):
196    """
197    LLVM uses a special tool, llvm-config, which has arguments for getting
198    c args, cxx args, and ldargs as well as version.
199    """
200    tool_name = 'llvm-config'
201    __cpp_blacklist = {'-DNDEBUG'}
202
203    def __init__(self, name: str, environment, kwargs):
204        self.tools = get_llvm_tool_names('llvm-config')
205
206        # Fedora starting with Fedora 30 adds a suffix of the number
207        # of bits in the isa that llvm targets, for example, on x86_64
208        # and aarch64 the name will be llvm-config-64, on x86 and arm
209        # it will be llvm-config-32.
210        if environment.machines[self.get_for_machine_from_kwargs(kwargs)].is_64_bit:
211            self.tools.append('llvm-config-64')
212        else:
213            self.tools.append('llvm-config-32')
214
215        # It's necessary for LLVM <= 3.8 to use the C++ linker. For 3.9 and 4.0
216        # the C linker works fine if only using the C API.
217        super().__init__(name, environment, kwargs, language='cpp')
218        self.provided_modules = []
219        self.required_modules = set()
220        self.module_details = []
221        if not self.is_found:
222            return
223
224        self.provided_modules = self.get_config_value(['--components'], 'modules')
225        modules = stringlistify(extract_as_list(kwargs, 'modules'))
226        self.check_components(modules)
227        opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules'))
228        self.check_components(opt_modules, required=False)
229
230        cargs = set(self.get_config_value(['--cppflags'], 'compile_args'))
231        self.compile_args = list(cargs.difference(self.__cpp_blacklist))
232
233        if version_compare(self.version, '>= 3.9'):
234            self._set_new_link_args(environment)
235        else:
236            self._set_old_link_args()
237        self.link_args = strip_system_libdirs(environment, self.for_machine, self.link_args)
238        self.link_args = self.__fix_bogus_link_args(self.link_args)
239        if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})):
240            self.is_found = False
241            return
242
243    def __fix_bogus_link_args(self, args):
244        """This function attempts to fix bogus link arguments that llvm-config
245        generates.
246
247        Currently it works around the following:
248            - FreeBSD: when statically linking -l/usr/lib/libexecinfo.so will
249              be generated, strip the -l in cases like this.
250            - Windows: We may get -LIBPATH:... which is later interpreted as
251              "-L IBPATH:...", if we're using an msvc like compilers convert
252              that to "/LIBPATH", otherwise to "-L ..."
253        """
254        cpp = self.env.coredata.compilers[self.for_machine]['cpp']
255
256        new_args = []
257        for arg in args:
258            if arg.startswith('-l') and arg.endswith('.so'):
259                new_args.append(arg.lstrip('-l'))
260            elif arg.startswith('-LIBPATH:'):
261                new_args.extend(cpp.get_linker_search_args(arg.lstrip('-LIBPATH:')))
262            else:
263                new_args.append(arg)
264        return new_args
265
266    def __check_libfiles(self, shared):
267        """Use llvm-config's --libfiles to check if libraries exist."""
268        mode = '--link-shared' if shared else '--link-static'
269
270        # Set self.required to true to force an exception in get_config_value
271        # if the returncode != 0
272        restore = self.required
273        self.required = True
274
275        try:
276            # It doesn't matter what the stage is, the caller needs to catch
277            # the exception anyway.
278            self.link_args = self.get_config_value(['--libfiles', mode], '')
279        finally:
280            self.required = restore
281
282    def _set_new_link_args(self, environment):
283        """How to set linker args for LLVM versions >= 3.9"""
284        mode = self.get_config_value(['--shared-mode'], 'link_args')[0]
285        if not self.static and mode == 'static':
286            # If llvm is configured with LLVM_BUILD_LLVM_DYLIB but not with
287            # LLVM_LINK_LLVM_DYLIB and not LLVM_BUILD_SHARED_LIBS (which
288            # upstream doesn't recommend using), then llvm-config will lie to
289            # you about how to do shared-linking. It wants to link to a a bunch
290            # of individual shared libs (which don't exist because llvm wasn't
291            # built with LLVM_BUILD_SHARED_LIBS.
292            #
293            # Therefore, we'll try to get the libfiles, if the return code is 0
294            # or we get an empty list, then we'll try to build a working
295            # configuration by hand.
296            try:
297                self.__check_libfiles(True)
298            except DependencyException:
299                lib_ext = get_shared_library_suffix(environment, self.for_machine)
300                libdir = self.get_config_value(['--libdir'], 'link_args')[0]
301                # Sort for reproducibility
302                matches = sorted(glob.iglob(os.path.join(libdir, 'libLLVM*{}'.format(lib_ext))))
303                if not matches:
304                    if self.required:
305                        raise
306                    self.is_found = False
307                    return
308
309                self.link_args = self.get_config_value(['--ldflags'], 'link_args')
310                libname = os.path.basename(matches[0]).rstrip(lib_ext).lstrip('lib')
311                self.link_args.append('-l{}'.format(libname))
312                return
313        elif self.static and mode == 'shared':
314            # If, however LLVM_BUILD_SHARED_LIBS is true # (*cough* gentoo *cough*)
315            # then this is correct. Building with LLVM_BUILD_SHARED_LIBS has a side
316            # effect, it stops the generation of static archives. Therefore we need
317            # to check for that and error out on static if this is the case
318            try:
319                self.__check_libfiles(False)
320            except DependencyException:
321                if self.required:
322                    raise
323                self.is_found = False
324                return
325
326        link_args = ['--link-static', '--system-libs'] if self.static else ['--link-shared']
327        self.link_args = self.get_config_value(
328            ['--libs', '--ldflags'] + link_args + list(self.required_modules),
329            'link_args')
330
331    def _set_old_link_args(self):
332        """Setting linker args for older versions of llvm.
333
334        Old versions of LLVM bring an extra level of insanity with them.
335        llvm-config will provide the correct arguments for static linking, but
336        not for shared-linnking, we have to figure those out ourselves, because
337        of course we do.
338        """
339        if self.static:
340            self.link_args = self.get_config_value(
341                ['--libs', '--ldflags', '--system-libs'] + list(self.required_modules),
342                'link_args')
343        else:
344            # llvm-config will provide arguments for static linking, so we get
345            # to figure out for ourselves what to link with. We'll do that by
346            # checking in the directory provided by --libdir for a library
347            # called libLLVM-<ver>.(so|dylib|dll)
348            libdir = self.get_config_value(['--libdir'], 'link_args')[0]
349
350            expected_name = 'libLLVM-{}'.format(self.version)
351            re_name = re.compile(r'{}.(so|dll|dylib)$'.format(expected_name))
352
353            for file_ in os.listdir(libdir):
354                if re_name.match(file_):
355                    self.link_args = ['-L{}'.format(libdir),
356                                      '-l{}'.format(os.path.splitext(file_.lstrip('lib'))[0])]
357                    break
358            else:
359                raise DependencyException(
360                    'Could not find a dynamically linkable library for LLVM.')
361
362    def check_components(self, modules, required=True):
363        """Check for llvm components (modules in meson terms).
364
365        The required option is whether the module is required, not whether LLVM
366        is required.
367        """
368        for mod in sorted(set(modules)):
369            status = ''
370
371            if mod not in self.provided_modules:
372                if required:
373                    self.is_found = False
374                    if self.required:
375                        raise DependencyException(
376                            'Could not find required LLVM Component: {}'.format(mod))
377                    status = '(missing)'
378                else:
379                    status = '(missing but optional)'
380            else:
381                self.required_modules.add(mod)
382
383            self.module_details.append(mod + status)
384
385    def log_details(self):
386        if self.module_details:
387            return 'modules: ' + ', '.join(self.module_details)
388        return ''
389
390class LLVMDependencyCMake(CMakeDependency):
391    def __init__(self, name: str, env, kwargs):
392        self.llvm_modules = stringlistify(extract_as_list(kwargs, 'modules'))
393        self.llvm_opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules'))
394        super().__init__(name, env, kwargs, language='cpp')
395
396        # Cmake will always create a statically linked binary, so don't use
397        # cmake if dynamic is required
398        if not self.static:
399            self.is_found = False
400            mlog.warning('Ignoring LLVM CMake dependency because dynamic was requested')
401            return
402
403        if self.traceparser is None:
404            return
405
406        # Extract extra include directories and definitions
407        inc_dirs = self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS')
408        defs = self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS')
409        # LLVM explicitly uses space-separated variables rather than semicolon lists
410        if len(defs) == 1:
411            defs = defs[0].split(' ')
412        temp = ['-I' + x for x in inc_dirs] + defs
413        self.compile_args += [x for x in temp if x not in self.compile_args]
414        if not self._add_sub_dependency(threads_factory(env, self.for_machine, {})):
415            self.is_found = False
416            return
417
418    def _main_cmake_file(self) -> str:
419        # Use a custom CMakeLists.txt for LLVM
420        return 'CMakeListsLLVM.txt'
421
422    def _extra_cmake_opts(self) -> T.List[str]:
423        return ['-DLLVM_MESON_MODULES={}'.format(';'.join(self.llvm_modules + self.llvm_opt_modules))]
424
425    def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]:
426        res = []
427        for mod, required in modules:
428            cm_targets = self.traceparser.get_cmake_var('MESON_LLVM_TARGETS_{}'.format(mod))
429            if not cm_targets:
430                if required:
431                    raise self._gen_exception('LLVM module {} was not found'.format(mod))
432                else:
433                    mlog.warning('Optional LLVM module', mlog.bold(mod), 'was not found')
434                    continue
435            for i in cm_targets:
436                res += [(i, required)]
437        return res
438
439    def _original_module_name(self, module: str) -> str:
440        orig_name = self.traceparser.get_cmake_var('MESON_TARGET_TO_LLVM_{}'.format(module))
441        if orig_name:
442            return orig_name[0]
443        return module
444
445
446class ValgrindDependency(PkgConfigDependency):
447    '''
448    Consumers of Valgrind usually only need the compile args and do not want to
449    link to its (static) libraries.
450    '''
451    def __init__(self, env, kwargs):
452        super().__init__('valgrind', env, kwargs)
453
454    def get_link_args(self, **kwargs):
455        return []
456
457
458class ZlibSystemDependency(ExternalDependency):
459
460    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
461        super().__init__(name, environment, kwargs)
462
463        m = self.env.machines[self.for_machine]
464
465        # I'm not sure this is entirely correct. What if we're cross compiling
466        # from something to macOS?
467        if ((m.is_darwin() and isinstance(self.clib_compiler, (AppleClangCCompiler, AppleClangCPPCompiler))) or
468                m.is_freebsd() or m.is_dragonflybsd()):
469            self.is_found = True
470            self.link_args = ['-lz']
471
472            # No need to set includes,
473            # on macos xcode/clang will do that for us.
474            # on freebsd zlib.h is in /usr/include
475        elif m.is_windows():
476            if self.clib_compiler.get_argument_syntax() == 'msvc':
477                libs = ['zlib1' 'zlib']
478            else:
479                libs = ['z']
480            for lib in libs:
481                l = self.clib_compiler.find_library(lib, environment, [])
482                h = self.clib_compiler.has_header('zlib.h', '', environment, dependencies=[self])
483                if l and h:
484                    self.is_found = True
485                    self.link_args = l
486                    break
487            else:
488                return
489        else:
490            mlog.debug('Unsupported OS {}'.format(m.system))
491            return
492
493        v, _ = self.clib_compiler.get_define('ZLIB_VERSION', '#include <zlib.h>', self.env, [], [self])
494        self.version = v.strip('"')
495
496
497    @staticmethod
498    def get_methods():
499        return [DependencyMethods.SYSTEM]
500
501
502llvm_factory = DependencyFactory(
503    'LLVM',
504    [DependencyMethods.CMAKE, DependencyMethods.CONFIG_TOOL],
505    cmake_class=LLVMDependencyCMake,
506    configtool_class=LLVMDependencyConfigTool,
507)
508
509gtest_factory = DependencyFactory(
510    'gtest',
511    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
512    pkgconfig_class=GTestDependencyPC,
513    system_class=GTestDependencySystem,
514)
515
516gmock_factory = DependencyFactory(
517    'gmock',
518    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM],
519    pkgconfig_class=GMockDependencyPC,
520    system_class=GMockDependencySystem,
521)
522
523zlib_factory = DependencyFactory(
524    'zlib',
525    [DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM],
526    cmake_name='ZLIB',
527    system_class=ZlibSystemDependency,
528)
529