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