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