# Copyright 2013-2019 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 useful for # development purposes, such as testing, debugging, etc.. from __future__ import annotations import glob import os import re import pathlib import shutil import subprocess import typing as T import functools from mesonbuild.interpreterbase.decorators import FeatureDeprecated from .. import mesonlib, mlog from ..environment import get_llvm_tool_names from ..mesonlib import version_compare, version_compare_many, search_version, stringlistify, extract_as_list from .base import DependencyException, DependencyMethods, detect_compiler, strip_system_libdirs, SystemDependency, ExternalDependency, DependencyTypeName from .cmake import CMakeDependency from .configtool import ConfigToolDependency from .factory import DependencyFactory from .misc import threads_factory from .pkgconfig import PkgConfigDependency if T.TYPE_CHECKING: from ..envconfig import MachineInfo from ..environment import Environment from ..mesonlib import MachineChoice from typing_extensions import TypedDict class JNISystemDependencyKW(TypedDict): modules: T.List[str] # FIXME: When dependency() moves to typed Kwargs, this should inherit # from its TypedDict type. version: T.Optional[str] def get_shared_library_suffix(environment: 'Environment', for_machine: MachineChoice) -> str: """This is only guaranteed to work for languages that compile to machine code, not for languages like C# that use a bytecode and always end in .dll """ m = environment.machines[for_machine] if m.is_windows(): return '.dll' elif m.is_darwin(): return '.dylib' return '.so' class GTestDependencySystem(SystemDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None: super().__init__(name, environment, kwargs, language='cpp') self.main = kwargs.get('main', False) self.src_dirs = ['/usr/src/gtest/src', '/usr/src/googletest/googletest/src'] if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})): self.is_found = False return self.detect() def detect(self) -> None: gtest_detect = self.clib_compiler.find_library("gtest", self.env, []) gtest_main_detect = self.clib_compiler.find_library("gtest_main", self.env, []) if gtest_detect and (not self.main or gtest_main_detect): self.is_found = True self.compile_args = [] self.link_args = gtest_detect if self.main: self.link_args += gtest_main_detect self.sources = [] self.prebuilt = True elif self.detect_srcdir(): self.is_found = True self.compile_args = ['-I' + d for d in self.src_include_dirs] self.link_args = [] if self.main: self.sources = [self.all_src, self.main_src] else: self.sources = [self.all_src] self.prebuilt = False else: self.is_found = False def detect_srcdir(self) -> bool: for s in self.src_dirs: if os.path.exists(s): self.src_dir = s self.all_src = mesonlib.File.from_absolute_file( os.path.join(self.src_dir, 'gtest-all.cc')) self.main_src = mesonlib.File.from_absolute_file( os.path.join(self.src_dir, 'gtest_main.cc')) self.src_include_dirs = [os.path.normpath(os.path.join(self.src_dir, '..')), os.path.normpath(os.path.join(self.src_dir, '../include')), ] return True return False def log_info(self) -> str: if self.prebuilt: return 'prebuilt' else: return 'building self' class GTestDependencyPC(PkgConfigDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): assert name == 'gtest' if kwargs.get('main'): name = 'gtest_main' super().__init__(name, environment, kwargs) class GMockDependencySystem(SystemDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None: super().__init__(name, environment, kwargs, language='cpp') self.main = kwargs.get('main', False) if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})): self.is_found = False return # If we are getting main() from GMock, we definitely # want to avoid linking in main() from GTest gtest_kwargs = kwargs.copy() if self.main: gtest_kwargs['main'] = False # GMock without GTest is pretty much useless # this also mimics the structure given in WrapDB, # where GMock always pulls in GTest found = self._add_sub_dependency(gtest_factory(environment, self.for_machine, gtest_kwargs)) if not found: self.is_found = False return # GMock may be a library or just source. # Work with both. gmock_detect = self.clib_compiler.find_library("gmock", self.env, []) gmock_main_detect = self.clib_compiler.find_library("gmock_main", self.env, []) if gmock_detect and (not self.main or gmock_main_detect): self.is_found = True self.link_args += gmock_detect if self.main: self.link_args += gmock_main_detect self.prebuilt = True return for d in ['/usr/src/googletest/googlemock/src', '/usr/src/gmock/src', '/usr/src/gmock']: if os.path.exists(d): self.is_found = True # Yes, we need both because there are multiple # versions of gmock that do different things. d2 = os.path.normpath(os.path.join(d, '..')) self.compile_args += ['-I' + d, '-I' + d2, '-I' + os.path.join(d2, 'include')] all_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock-all.cc')) main_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock_main.cc')) if self.main: self.sources += [all_src, main_src] else: self.sources += [all_src] self.prebuilt = False return self.is_found = False def log_info(self) -> str: if self.prebuilt: return 'prebuilt' else: return 'building self' class GMockDependencyPC(PkgConfigDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): assert name == 'gmock' if kwargs.get('main'): name = 'gmock_main' super().__init__(name, environment, kwargs) class LLVMDependencyConfigTool(ConfigToolDependency): """ LLVM uses a special tool, llvm-config, which has arguments for getting c args, cxx args, and ldargs as well as version. """ tool_name = 'llvm-config' __cpp_blacklist = {'-DNDEBUG'} def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): self.tools = get_llvm_tool_names('llvm-config') # Fedora starting with Fedora 30 adds a suffix of the number # of bits in the isa that llvm targets, for example, on x86_64 # and aarch64 the name will be llvm-config-64, on x86 and arm # it will be llvm-config-32. if environment.machines[self.get_for_machine_from_kwargs(kwargs)].is_64_bit: self.tools.append('llvm-config-64') else: self.tools.append('llvm-config-32') # It's necessary for LLVM <= 3.8 to use the C++ linker. For 3.9 and 4.0 # the C linker works fine if only using the C API. super().__init__(name, environment, kwargs, language='cpp') self.provided_modules: T.List[str] = [] self.required_modules: mesonlib.OrderedSet[str] = mesonlib.OrderedSet() self.module_details: T.List[str] = [] if not self.is_found: return self.provided_modules = self.get_config_value(['--components'], 'modules') modules = stringlistify(extract_as_list(kwargs, 'modules')) self.check_components(modules) opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules')) self.check_components(opt_modules, required=False) cargs = mesonlib.OrderedSet(self.get_config_value(['--cppflags'], 'compile_args')) self.compile_args = list(cargs.difference(self.__cpp_blacklist)) if version_compare(self.version, '>= 3.9'): self._set_new_link_args(environment) else: self._set_old_link_args() self.link_args = strip_system_libdirs(environment, self.for_machine, self.link_args) self.link_args = self.__fix_bogus_link_args(self.link_args) if not self._add_sub_dependency(threads_factory(environment, self.for_machine, {})): self.is_found = False return def __fix_bogus_link_args(self, args: T.List[str]) -> T.List[str]: """This function attempts to fix bogus link arguments that llvm-config generates. Currently it works around the following: - FreeBSD: when statically linking -l/usr/lib/libexecinfo.so will be generated, strip the -l in cases like this. - Windows: We may get -LIBPATH:... which is later interpreted as "-L IBPATH:...", if we're using an msvc like compilers convert that to "/LIBPATH", otherwise to "-L ..." """ new_args = [] for arg in args: if arg.startswith('-l') and arg.endswith('.so'): new_args.append(arg.lstrip('-l')) elif arg.startswith('-LIBPATH:'): cpp = self.env.coredata.compilers[self.for_machine]['cpp'] new_args.extend(cpp.get_linker_search_args(arg.lstrip('-LIBPATH:'))) else: new_args.append(arg) return new_args def __check_libfiles(self, shared: bool) -> None: """Use llvm-config's --libfiles to check if libraries exist.""" mode = '--link-shared' if shared else '--link-static' # Set self.required to true to force an exception in get_config_value # if the returncode != 0 restore = self.required self.required = True try: # It doesn't matter what the stage is, the caller needs to catch # the exception anyway. self.link_args = self.get_config_value(['--libfiles', mode], '') finally: self.required = restore def _set_new_link_args(self, environment: 'Environment') -> None: """How to set linker args for LLVM versions >= 3.9""" try: mode = self.get_config_value(['--shared-mode'], 'link_args')[0] except IndexError: mlog.debug('llvm-config --shared-mode returned an error') self.is_found = False return if not self.static and mode == 'static': # If llvm is configured with LLVM_BUILD_LLVM_DYLIB but not with # LLVM_LINK_LLVM_DYLIB and not LLVM_BUILD_SHARED_LIBS (which # upstream doesn't recommend using), then llvm-config will lie to # you about how to do shared-linking. It wants to link to a a bunch # of individual shared libs (which don't exist because llvm wasn't # built with LLVM_BUILD_SHARED_LIBS. # # Therefore, we'll try to get the libfiles, if the return code is 0 # or we get an empty list, then we'll try to build a working # configuration by hand. try: self.__check_libfiles(True) except DependencyException: lib_ext = get_shared_library_suffix(environment, self.for_machine) libdir = self.get_config_value(['--libdir'], 'link_args')[0] # Sort for reproducibility matches = sorted(glob.iglob(os.path.join(libdir, f'libLLVM*{lib_ext}'))) if not matches: if self.required: raise self.is_found = False return self.link_args = self.get_config_value(['--ldflags'], 'link_args') libname = os.path.basename(matches[0]).rstrip(lib_ext).lstrip('lib') self.link_args.append(f'-l{libname}') return elif self.static and mode == 'shared': # If, however LLVM_BUILD_SHARED_LIBS is true # (*cough* gentoo *cough*) # then this is correct. Building with LLVM_BUILD_SHARED_LIBS has a side # effect, it stops the generation of static archives. Therefore we need # to check for that and error out on static if this is the case try: self.__check_libfiles(False) except DependencyException: if self.required: raise self.is_found = False return link_args = ['--link-static', '--system-libs'] if self.static else ['--link-shared'] self.link_args = self.get_config_value( ['--libs', '--ldflags'] + link_args + list(self.required_modules), 'link_args') def _set_old_link_args(self) -> None: """Setting linker args for older versions of llvm. Old versions of LLVM bring an extra level of insanity with them. llvm-config will provide the correct arguments for static linking, but not for shared-linnking, we have to figure those out ourselves, because of course we do. """ if self.static: self.link_args = self.get_config_value( ['--libs', '--ldflags', '--system-libs'] + list(self.required_modules), 'link_args') else: # llvm-config will provide arguments for static linking, so we get # to figure out for ourselves what to link with. We'll do that by # checking in the directory provided by --libdir for a library # called libLLVM-.(so|dylib|dll) libdir = self.get_config_value(['--libdir'], 'link_args')[0] expected_name = f'libLLVM-{self.version}' re_name = re.compile(fr'{expected_name}.(so|dll|dylib)$') for file_ in os.listdir(libdir): if re_name.match(file_): self.link_args = [f'-L{libdir}', '-l{}'.format(os.path.splitext(file_.lstrip('lib'))[0])] break else: raise DependencyException( 'Could not find a dynamically linkable library for LLVM.') def check_components(self, modules: T.List[str], required: bool = True) -> None: """Check for llvm components (modules in meson terms). The required option is whether the module is required, not whether LLVM is required. """ for mod in sorted(set(modules)): status = '' if mod not in self.provided_modules: if required: self.is_found = False if self.required: raise DependencyException( f'Could not find required LLVM Component: {mod}') status = '(missing)' else: status = '(missing but optional)' else: self.required_modules.add(mod) self.module_details.append(mod + status) def log_details(self) -> str: if self.module_details: return 'modules: ' + ', '.join(self.module_details) return '' class LLVMDependencyCMake(CMakeDependency): def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]) -> None: self.llvm_modules = stringlistify(extract_as_list(kwargs, 'modules')) self.llvm_opt_modules = stringlistify(extract_as_list(kwargs, 'optional_modules')) compilers = None if kwargs.get('native', False): compilers = env.coredata.compilers.build else: compilers = env.coredata.compilers.host if not compilers or not all(x in compilers for x in ('c', 'cpp')): # Initialize basic variables ExternalDependency.__init__(self, DependencyTypeName('cmake'), env, kwargs) # Initialize CMake specific variables self.found_modules: T.List[str] = [] self.name = name # Warn and return mlog.warning('The LLVM dependency was not found via CMake since both a C and C++ compiler are required.') return super().__init__(name, env, kwargs, language='cpp', force_use_global_compilers=True) if self.traceparser is None: return if not self.is_found: return #CMake will return not found due to not defined LLVM_DYLIB_COMPONENTS if not self.static and version_compare(self.version, '< 7.0') and self.llvm_modules: mlog.warning('Before version 7.0 cmake does not export modules for dynamic linking, cannot check required modules') return # Extract extra include directories and definitions inc_dirs = self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') defs = self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') # LLVM explicitly uses space-separated variables rather than semicolon lists if len(defs) == 1: defs = defs[0].split(' ') temp = ['-I' + x for x in inc_dirs] + defs self.compile_args += [x for x in temp if x not in self.compile_args] if not self._add_sub_dependency(threads_factory(env, self.for_machine, {})): self.is_found = False return def _main_cmake_file(self) -> str: # Use a custom CMakeLists.txt for LLVM return 'CMakeListsLLVM.txt' # Check version in CMake to return exact version as config tool (latest allowed) # It is safe to add .0 to latest argument, it will discarded if we use search_version def llvm_cmake_versions(self) -> T.List[str]: def ver_from_suf(req: str) -> str: return search_version(req.strip('-')+'.0') def version_sorter(a: str, b: str) -> int: if version_compare(a, "="+b): return 0 if version_compare(a, "<"+b): return 1 return -1 llvm_requested_versions = [ver_from_suf(x) for x in get_llvm_tool_names('') if version_compare(ver_from_suf(x), '>=0')] if self.version_reqs: llvm_requested_versions = [ver_from_suf(x) for x in get_llvm_tool_names('') if version_compare_many(ver_from_suf(x), self.version_reqs)] # CMake sorting before 3.18 is incorrect, sort it here instead return sorted(llvm_requested_versions, key=functools.cmp_to_key(version_sorter)) # Split required and optional modules to distinguish it in CMake def _extra_cmake_opts(self) -> T.List[str]: return ['-DLLVM_MESON_REQUIRED_MODULES={}'.format(';'.join(self.llvm_modules)), '-DLLVM_MESON_OPTIONAL_MODULES={}'.format(';'.join(self.llvm_opt_modules)), '-DLLVM_MESON_PACKAGE_NAMES={}'.format(';'.join(get_llvm_tool_names(self.name))), '-DLLVM_MESON_VERSIONS={}'.format(';'.join(self.llvm_cmake_versions())), '-DLLVM_MESON_DYLIB={}'.format('OFF' if self.static else 'ON')] 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]]: res = [] for mod, required in modules: cm_targets = self.traceparser.get_cmake_var(f'MESON_LLVM_TARGETS_{mod}') if not cm_targets: if required: raise self._gen_exception(f'LLVM module {mod} was not found') else: mlog.warning('Optional LLVM module', mlog.bold(mod), 'was not found', fatal=False) continue for i in cm_targets: res += [(i, required)] return res def _original_module_name(self, module: str) -> str: orig_name = self.traceparser.get_cmake_var(f'MESON_TARGET_TO_LLVM_{module}') if orig_name: return orig_name[0] return module class ValgrindDependency(PkgConfigDependency): ''' Consumers of Valgrind usually only need the compile args and do not want to link to its (static) libraries. ''' def __init__(self, env: 'Environment', kwargs: T.Dict[str, T.Any]): super().__init__('valgrind', env, kwargs) def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: return [] class ZlibSystemDependency(SystemDependency): def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]): super().__init__(name, environment, kwargs) from ..compilers.c import AppleClangCCompiler from ..compilers.cpp import AppleClangCPPCompiler m = self.env.machines[self.for_machine] # I'm not sure this is entirely correct. What if we're cross compiling # from something to macOS? if ((m.is_darwin() and isinstance(self.clib_compiler, (AppleClangCCompiler, AppleClangCPPCompiler))) or m.is_freebsd() or m.is_dragonflybsd() or m.is_android()): # No need to set includes, # on macos xcode/clang will do that for us. # on freebsd zlib.h is in /usr/include self.is_found = True self.link_args = ['-lz'] else: if self.clib_compiler.get_argument_syntax() == 'msvc': libs = ['zlib1', 'zlib'] else: libs = ['z'] for lib in libs: l = self.clib_compiler.find_library(lib, environment, []) h = self.clib_compiler.has_header('zlib.h', '', environment, dependencies=[self]) if l and h[0]: self.is_found = True self.link_args = l break else: return v, _ = self.clib_compiler.get_define('ZLIB_VERSION', '#include ', self.env, [], [self]) self.version = v.strip('"') class JNISystemDependency(SystemDependency): def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW): super().__init__('jni', environment, T.cast('T.Dict[str, T.Any]', kwargs)) self.feature_since = ('0.62.0', '') m = self.env.machines[self.for_machine] if 'java' not in environment.coredata.compilers[self.for_machine]: detect_compiler(self.name, environment, self.for_machine, 'java') self.javac = environment.coredata.compilers[self.for_machine]['java'] self.version = self.javac.version modules: T.List[str] = mesonlib.listify(kwargs.get('modules', [])) for module in modules: if module not in {'jvm', 'awt'}: msg = f'Unknown JNI module ({module})' if self.required: mlog.error(msg) else: mlog.debug(msg) self.is_found = False return if 'version' in kwargs and not version_compare(self.version, kwargs['version']): mlog.error(f'Incorrect JDK version found ({self.version}), wanted {kwargs["version"]}') self.is_found = False return self.java_home = environment.properties[self.for_machine].get_java_home() if not self.java_home: self.java_home = pathlib.Path(shutil.which(self.javac.exelist[0])).resolve().parents[1] if m.is_darwin(): problem_java_prefix = pathlib.Path('/System/Library/Frameworks/JavaVM.framework/Versions') if problem_java_prefix in self.java_home.parents: res = subprocess.run(['/usr/libexec/java_home', '--failfast', '--arch', m.cpu_family], stdout=subprocess.PIPE) if res.returncode != 0: msg = 'JAVA_HOME could not be discovered on the system. Please set it explicitly.' if self.required: mlog.error(msg) else: mlog.debug(msg) self.is_found = False return self.java_home = pathlib.Path(res.stdout.decode().strip()) platform_include_dir = self.__machine_info_to_platform_include_dir(m) if platform_include_dir is None: mlog.error("Could not find a JDK platform include directory for your OS, please open an issue or provide a pull request.") self.is_found = False return java_home_include = self.java_home / 'include' self.compile_args.append(f'-I{java_home_include}') self.compile_args.append(f'-I{java_home_include / platform_include_dir}') if modules: if m.is_windows(): java_home_lib = self.java_home / 'lib' java_home_lib_server = java_home_lib else: if version_compare(self.version, '<= 1.8.0'): java_home_lib = self.java_home / 'jre' / 'lib' / self.__cpu_translate(m.cpu_family) else: java_home_lib = self.java_home / 'lib' java_home_lib_server = java_home_lib / 'server' if 'jvm' in modules: jvm = self.clib_compiler.find_library('jvm', environment, extra_dirs=[str(java_home_lib_server)]) if jvm is None: mlog.debug('jvm library not found.') self.is_found = False else: self.link_args.extend(jvm) if 'awt' in modules: jawt = self.clib_compiler.find_library('jawt', environment, extra_dirs=[str(java_home_lib)]) if jawt is None: mlog.debug('jawt library not found.') self.is_found = False else: self.link_args.extend(jawt) self.is_found = True @staticmethod def __cpu_translate(cpu: str) -> str: ''' The JDK and Meson have a disagreement here, so translate it over. In the event more translation needs to be done, add to following dict. ''' java_cpus = { 'x86_64': 'amd64', } return java_cpus.get(cpu, cpu) @staticmethod def __machine_info_to_platform_include_dir(m: 'MachineInfo') -> T.Optional[str]: '''Translates the machine information to the platform-dependent include directory When inspecting a JDK release tarball or $JAVA_HOME, inside the `include/` directory is a platform-dependent directory that must be on the target's include path in addition to the parent `include/` directory. ''' if m.is_linux(): return 'linux' elif m.is_windows(): return 'win32' elif m.is_darwin(): return 'darwin' elif m.is_sunos(): return 'solaris' elif m.is_freebsd(): return 'freebsd' elif m.is_netbsd(): return 'netbsd' elif m.is_openbsd(): return 'openbsd' elif m.is_dragonflybsd(): return 'dragonfly' return None class JDKSystemDependency(JNISystemDependency): def __init__(self, environment: 'Environment', kwargs: JNISystemDependencyKW): super().__init__(environment, kwargs) self.feature_since = ('0.59.0', '') self.featurechecks.append(FeatureDeprecated( 'jdk system dependency', '0.62.0', 'Use the jni system dependency instead' )) llvm_factory = DependencyFactory( 'LLVM', [DependencyMethods.CMAKE, DependencyMethods.CONFIG_TOOL], cmake_class=LLVMDependencyCMake, configtool_class=LLVMDependencyConfigTool, ) gtest_factory = DependencyFactory( 'gtest', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], pkgconfig_class=GTestDependencyPC, system_class=GTestDependencySystem, ) gmock_factory = DependencyFactory( 'gmock', [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM], pkgconfig_class=GMockDependencyPC, system_class=GMockDependencySystem, ) zlib_factory = DependencyFactory( 'zlib', [DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE, DependencyMethods.SYSTEM], cmake_name='ZLIB', system_class=ZlibSystemDependency, )