1# Copyright 2013-2021 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 15from .base import ExternalDependency, DependencyException, DependencyTypeName 16from ..mesonlib import is_windows, MesonException, OptionKey, PerMachine, stringlistify, extract_as_list 17from ..mesondata import mesondata 18from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args, CMakeTarget 19from .. import mlog 20from pathlib import Path 21import functools 22import re 23import os 24import shutil 25import textwrap 26import typing as T 27 28if T.TYPE_CHECKING: 29 from ..environment import Environment 30 from ..envconfig import MachineInfo 31 32class CMakeInfo(T.NamedTuple): 33 module_paths: T.List[str] 34 cmake_root: str 35 archs: T.List[str] 36 common_paths: T.List[str] 37 38class CMakeDependency(ExternalDependency): 39 # The class's copy of the CMake path. Avoids having to search for it 40 # multiple times in the same Meson invocation. 41 class_cmakeinfo: PerMachine[T.Optional[CMakeInfo]] = PerMachine(None, None) 42 # Version string for the minimum CMake version 43 class_cmake_version = '>=3.4' 44 # CMake generators to try (empty for no generator) 45 class_cmake_generators = ['', 'Ninja', 'Unix Makefiles', 'Visual Studio 10 2010'] 46 class_working_generator: T.Optional[str] = None 47 48 def _gen_exception(self, msg: str) -> DependencyException: 49 return DependencyException(f'Dependency {self.name} not found: {msg}') 50 51 def _main_cmake_file(self) -> str: 52 return 'CMakeLists.txt' 53 54 def _extra_cmake_opts(self) -> T.List[str]: 55 return [] 56 57 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]]: 58 # Map the input module list to something else 59 # This function will only be executed AFTER the initial CMake 60 # interpreter pass has completed. Thus variables defined in the 61 # CMakeLists.txt can be accessed here. 62 # 63 # Both the modules and components inputs contain the original lists. 64 return modules 65 66 def _map_component_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: 67 # Map the input components list to something else. This 68 # function will be executed BEFORE the initial CMake interpreter 69 # pass. Thus variables from the CMakeLists.txt can NOT be accessed. 70 # 71 # Both the modules and components inputs contain the original lists. 72 return components 73 74 def _original_module_name(self, module: str) -> str: 75 # Reverse the module mapping done by _map_module_list for 76 # one module 77 return module 78 79 def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: 80 # Gather a list of all languages to support 81 self.language_list = [] # type: T.List[str] 82 if language is None: 83 compilers = None 84 if kwargs.get('native', False): 85 compilers = environment.coredata.compilers.build 86 else: 87 compilers = environment.coredata.compilers.host 88 89 candidates = ['c', 'cpp', 'fortran', 'objc', 'objcxx'] 90 self.language_list += [x for x in candidates if x in compilers] 91 else: 92 self.language_list += [language] 93 94 # Add additional languages if required 95 if 'fortran' in self.language_list: 96 self.language_list += ['c'] 97 98 # Ensure that the list is unique 99 self.language_list = list(set(self.language_list)) 100 101 super().__init__(DependencyTypeName('cmake'), environment, kwargs, language=language) 102 self.name = name 103 self.is_libtool = False 104 # Store a copy of the CMake path on the object itself so it is 105 # stored in the pickled coredata and recovered. 106 self.cmakebin: T.Optional[CMakeExecutor] = None 107 self.cmakeinfo: T.Optional[CMakeInfo] = None 108 109 # Where all CMake "build dirs" are located 110 self.cmake_root_dir = environment.scratch_dir 111 112 # T.List of successfully found modules 113 self.found_modules: T.List[str] = [] 114 115 # Initialize with None before the first return to avoid 116 # AttributeError exceptions in derived classes 117 self.traceparser: T.Optional[CMakeTraceParser] = None 118 119 # TODO further evaluate always using MachineChoice.BUILD 120 self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent) 121 if not self.cmakebin.found(): 122 self.cmakebin = None 123 msg = f'CMake binary for machine {self.for_machine} not found. Giving up.' 124 if self.required: 125 raise DependencyException(msg) 126 mlog.debug(msg) 127 return 128 129 # Setup the trace parser 130 self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) 131 132 cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) 133 cm_args = check_cmake_args(cm_args) 134 if CMakeDependency.class_cmakeinfo[self.for_machine] is None: 135 CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info(cm_args) 136 self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine] 137 if self.cmakeinfo is None: 138 raise self._gen_exception('Unable to obtain CMake system information') 139 140 package_version = kwargs.get('cmake_package_version', '') 141 if not isinstance(package_version, str): 142 raise DependencyException('Keyword "cmake_package_version" must be a string.') 143 components = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'components'))] 144 modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] 145 modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] 146 cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) 147 cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] 148 if cm_path: 149 cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) 150 if not self._preliminary_find_check(name, cm_path, self.cmakebin.get_cmake_prefix_paths(), environment.machines[self.for_machine]): 151 mlog.debug('Preliminary CMake check failed. Aborting.') 152 return 153 self._detect_dep(name, package_version, modules, components, cm_args) 154 155 def __repr__(self) -> str: 156 return f'<{self.__class__.__name__} {self.name}: {self.is_found} {self.version_reqs}>' 157 158 def _get_cmake_info(self, cm_args: T.List[str]) -> T.Optional[CMakeInfo]: 159 mlog.debug("Extracting basic cmake information") 160 161 # Try different CMake generators since specifying no generator may fail 162 # in cygwin for some reason 163 gen_list = [] 164 # First try the last working generator 165 if CMakeDependency.class_working_generator is not None: 166 gen_list += [CMakeDependency.class_working_generator] 167 gen_list += CMakeDependency.class_cmake_generators 168 169 temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) 170 toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) 171 toolchain.write() 172 173 for i in gen_list: 174 mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) 175 176 # Prepare options 177 cmake_opts = temp_parser.trace_args() + toolchain.get_cmake_args() + ['.'] 178 cmake_opts += cm_args 179 if len(i) > 0: 180 cmake_opts = ['-G', i] + cmake_opts 181 182 # Run CMake 183 ret1, out1, err1 = self._call_cmake(cmake_opts, 'CMakePathInfo.txt') 184 185 # Current generator was successful 186 if ret1 == 0: 187 CMakeDependency.class_working_generator = i 188 break 189 190 mlog.debug(f'CMake failed to gather system information for generator {i} with error code {ret1}') 191 mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') 192 193 # Check if any generator succeeded 194 if ret1 != 0: 195 return None 196 197 try: 198 temp_parser.parse(err1) 199 except MesonException: 200 return None 201 202 def process_paths(l: T.List[str]) -> T.Set[str]: 203 if is_windows(): 204 # Cannot split on ':' on Windows because its in the drive letter 205 tmp = [x.split(os.pathsep) for x in l] 206 else: 207 # https://github.com/mesonbuild/meson/issues/7294 208 tmp = [re.split(r':|;', x) for x in l] 209 flattened = [x for sublist in tmp for x in sublist] 210 return set(flattened) 211 212 # Extract the variables and sanity check them 213 root_paths_set = process_paths(temp_parser.get_cmake_var('MESON_FIND_ROOT_PATH')) 214 root_paths_set.update(process_paths(temp_parser.get_cmake_var('MESON_CMAKE_SYSROOT'))) 215 root_paths = sorted(root_paths_set) 216 root_paths = [x for x in root_paths if os.path.isdir(x)] 217 module_paths_set = process_paths(temp_parser.get_cmake_var('MESON_PATHS_LIST')) 218 rooted_paths: T.List[str] = [] 219 for j in [Path(x) for x in root_paths]: 220 for p in [Path(x) for x in module_paths_set]: 221 rooted_paths.append(str(j / p.relative_to(p.anchor))) 222 module_paths = sorted(module_paths_set.union(rooted_paths)) 223 module_paths = [x for x in module_paths if os.path.isdir(x)] 224 archs = temp_parser.get_cmake_var('MESON_ARCH_LIST') 225 226 common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share'] 227 for i in archs: 228 common_paths += [os.path.join('lib', i)] 229 230 res = CMakeInfo( 231 module_paths=module_paths, 232 cmake_root=temp_parser.get_cmake_var('MESON_CMAKE_ROOT')[0], 233 archs=archs, 234 common_paths=common_paths, 235 ) 236 237 mlog.debug(f' -- Module search paths: {res.module_paths}') 238 mlog.debug(f' -- CMake root: {res.cmake_root}') 239 mlog.debug(f' -- CMake architectures: {res.archs}') 240 mlog.debug(f' -- CMake lib search paths: {res.common_paths}') 241 242 return res 243 244 @staticmethod 245 @functools.lru_cache(maxsize=None) 246 def _cached_listdir(path: str) -> T.Tuple[T.Tuple[str, str], ...]: 247 try: 248 return tuple((x, str(x).lower()) for x in os.listdir(path)) 249 except OSError: 250 return tuple() 251 252 @staticmethod 253 @functools.lru_cache(maxsize=None) 254 def _cached_isdir(path: str) -> bool: 255 try: 256 return os.path.isdir(path) 257 except OSError: 258 return False 259 260 def _preliminary_find_check(self, name: str, module_path: T.List[str], prefix_path: T.List[str], machine: 'MachineInfo') -> bool: 261 lname = str(name).lower() 262 263 # Checks <path>, <path>/cmake, <path>/CMake 264 def find_module(path: str) -> bool: 265 for i in [path, os.path.join(path, 'cmake'), os.path.join(path, 'CMake')]: 266 if not self._cached_isdir(i): 267 continue 268 269 # Check the directory case insensitive 270 content = self._cached_listdir(i) 271 candidates = ['Find{}.cmake', '{}Config.cmake', '{}-config.cmake'] 272 candidates = [x.format(name).lower() for x in candidates] 273 if any([x[1] in candidates for x in content]): 274 return True 275 return False 276 277 # Search in <path>/(lib/<arch>|lib*|share) for cmake files 278 def search_lib_dirs(path: str) -> bool: 279 for i in [os.path.join(path, x) for x in self.cmakeinfo.common_paths]: 280 if not self._cached_isdir(i): 281 continue 282 283 # Check <path>/(lib/<arch>|lib*|share)/cmake/<name>*/ 284 cm_dir = os.path.join(i, 'cmake') 285 if self._cached_isdir(cm_dir): 286 content = self._cached_listdir(cm_dir) 287 content = tuple(x for x in content if x[1].startswith(lname)) 288 for k in content: 289 if find_module(os.path.join(cm_dir, k[0])): 290 return True 291 292 # <path>/(lib/<arch>|lib*|share)/<name>*/ 293 # <path>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ 294 content = self._cached_listdir(i) 295 content = tuple(x for x in content if x[1].startswith(lname)) 296 for k in content: 297 if find_module(os.path.join(i, k[0])): 298 return True 299 300 return False 301 302 # Check the user provided and system module paths 303 for i in module_path + [os.path.join(self.cmakeinfo.cmake_root, 'Modules')]: 304 if find_module(i): 305 return True 306 307 # Check the user provided prefix paths 308 for i in prefix_path: 309 if search_lib_dirs(i): 310 return True 311 312 # Check PATH 313 system_env = [] # type: T.List[str] 314 for i in os.environ.get('PATH', '').split(os.pathsep): 315 if i.endswith('/bin') or i.endswith('\\bin'): 316 i = i[:-4] 317 if i.endswith('/sbin') or i.endswith('\\sbin'): 318 i = i[:-5] 319 system_env += [i] 320 321 # Check the system paths 322 for i in self.cmakeinfo.module_paths + system_env: 323 if find_module(i): 324 return True 325 326 if search_lib_dirs(i): 327 return True 328 329 content = self._cached_listdir(i) 330 content = tuple(x for x in content if x[1].startswith(lname)) 331 for k in content: 332 if search_lib_dirs(os.path.join(i, k[0])): 333 return True 334 335 # Mac framework support 336 if machine.is_darwin(): 337 for j in [f'{lname}.framework', f'{lname}.app']: 338 for k in content: 339 if k[1] != j: 340 continue 341 if find_module(os.path.join(i, k[0], 'Resources')) or find_module(os.path.join(i, k[0], 'Version')): 342 return True 343 344 # Check the environment path 345 env_path = os.environ.get(f'{name}_DIR') 346 if env_path and find_module(env_path): 347 return True 348 349 # Check the Linux CMake registry 350 linux_reg = Path.home() / '.cmake' / 'packages' 351 for p in [linux_reg / name, linux_reg / lname]: 352 if p.exists(): 353 return True 354 355 return False 356 357 def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]], args: T.List[str]) -> None: 358 # Detect a dependency with CMake using the '--find-package' mode 359 # and the trace output (stderr) 360 # 361 # When the trace output is enabled CMake prints all functions with 362 # parameters to stderr as they are executed. Since CMake 3.4.0 363 # variables ("${VAR}") are also replaced in the trace output. 364 mlog.debug('\nDetermining dependency {!r} with CMake executable ' 365 '{!r}'.format(name, self.cmakebin.executable_path())) 366 367 # Try different CMake generators since specifying no generator may fail 368 # in cygwin for some reason 369 gen_list = [] 370 # First try the last working generator 371 if CMakeDependency.class_working_generator is not None: 372 gen_list += [CMakeDependency.class_working_generator] 373 gen_list += CMakeDependency.class_cmake_generators 374 375 # Map the components 376 comp_mapped = self._map_component_list(modules, components) 377 toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) 378 toolchain.write() 379 380 for i in gen_list: 381 mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) 382 383 # Prepare options 384 cmake_opts = [] 385 cmake_opts += [f'-DNAME={name}'] 386 cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo.archs))] 387 cmake_opts += [f'-DVERSION={package_version}'] 388 cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] 389 cmake_opts += args 390 cmake_opts += self.traceparser.trace_args() 391 cmake_opts += toolchain.get_cmake_args() 392 cmake_opts += self._extra_cmake_opts() 393 cmake_opts += ['.'] 394 if len(i) > 0: 395 cmake_opts = ['-G', i] + cmake_opts 396 397 # Run CMake 398 ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file()) 399 400 # Current generator was successful 401 if ret1 == 0: 402 CMakeDependency.class_working_generator = i 403 break 404 405 mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}') 406 mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') 407 408 # Check if any generator succeeded 409 if ret1 != 0: 410 return 411 412 try: 413 self.traceparser.parse(err1) 414 except CMakeException as e: 415 e2 = self._gen_exception(str(e)) 416 if self.required: 417 raise 418 else: 419 self.compile_args = [] 420 self.link_args = [] 421 self.is_found = False 422 self.reason = e2 423 return 424 425 # Whether the package is found or not is always stored in PACKAGE_FOUND 426 self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND') 427 if not self.is_found: 428 return 429 430 # Try to detect the version 431 vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION') 432 433 if len(vers_raw) > 0: 434 self.version = vers_raw[0] 435 self.version.strip('"\' ') 436 437 # Post-process module list. Used in derived classes to modify the 438 # module list (append prepend a string, etc.). 439 modules = self._map_module_list(modules, components) 440 autodetected_module_list = False 441 442 # Check if we need a DEBUG or RELEASE CMake dependencies 443 is_debug = False 444 if OptionKey('b_vscrt') in self.env.coredata.options: 445 is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug' 446 if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: 447 is_debug = True 448 else: 449 # Don't directly assign to is_debug to make mypy happy 450 debug_opt = self.env.coredata.get_option(OptionKey('debug')) 451 assert isinstance(debug_opt, bool) 452 is_debug = debug_opt 453 454 # Try guessing a CMake target if none is provided 455 if len(modules) == 0: 456 for i in self.traceparser.targets: 457 tg = i.lower() 458 lname = name.lower() 459 if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''): 460 mlog.debug(f'Guessed CMake target \'{i}\'') 461 modules = [(i, True)] 462 autodetected_module_list = True 463 break 464 465 # Failed to guess a target --> try the old-style method 466 if len(modules) == 0: 467 # Warn when there might be matching imported targets but no automatic match was used 468 partial_modules: T.List[CMakeTarget] = [] 469 for k, v in self.traceparser.targets.items(): 470 tg = k.lower() 471 lname = name.lower() 472 if tg.startswith(f'{lname}::'): 473 partial_modules += [v] 474 if partial_modules: 475 mlog.warning(textwrap.dedent(f'''\ 476 Could not find and exact match for the CMake dependency {name}. 477 478 However, Meson found the following partial matches: 479 480 {[x.name for x in partial_modules]} 481 482 Using imported is recommended, since this approach is less error prone 483 and better supported by Meson. Consider explicitly specifying one of 484 these in the dependency call with: 485 486 dependency('{name}', modules: ['{name}::<name>', ...]) 487 488 Meson will now continue to use the old-style {name}_LIBRARIES CMake 489 variables to extract the dependency information since no explicit 490 target is currently specified. 491 492 ''')) 493 mlog.debug('More info for the partial match targets:') 494 for tgt in partial_modules: 495 mlog.debug(tgt) 496 497 498 incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x] 499 defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x] 500 libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] 501 502 # CMake has a "fun" API, where certain keywords describing 503 # configurations can be in the *_LIBRARIES vraiables. See: 504 # - https://github.com/mesonbuild/meson/issues/9197 505 # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140 506 # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview (the last point in the section) 507 libs: T.List[str] = [] 508 cfg_matches = True 509 cm_tag_map = {'debug': is_debug, 'optimized': not is_debug, 'general': True} 510 for i in libs_raw: 511 if i.lower() in cm_tag_map: 512 cfg_matches = cm_tag_map[i.lower()] 513 continue 514 if cfg_matches: 515 libs += [i] 516 # According to the CMake docs, a keyword only works for the 517 # directly the following item and all items without a keyword 518 # are implizitly `general` 519 cfg_matches = True 520 521 # Try to use old style variables if no module is specified 522 if len(libs) > 0: 523 self.compile_args = list(map(lambda x: f'-I{x}', incDirs)) + defs 524 self.link_args = libs 525 mlog.debug(f'using old-style CMake variables for dependency {name}') 526 mlog.debug(f'Include Dirs: {incDirs}') 527 mlog.debug(f'Compiler Definitions: {defs}') 528 mlog.debug(f'Libraries: {libs}') 529 return 530 531 # Even the old-style approach failed. Nothing else we can do here 532 self.is_found = False 533 raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n' 534 'Try to explicitly specify one or more targets with the "modules" property.\n' 535 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys()))) 536 537 # Set dependencies with CMake targets 538 # recognise arguments we should pass directly to the linker 539 reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-pthread|-delayload:[a-zA-Z0-9_\.]+|[a-zA-Z0-9_]+\.lib)$') 540 reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$') 541 processed_targets = [] 542 incDirs = [] 543 compileDefinitions = [] 544 compileOptions = [] 545 libraries = [] 546 for i, required in modules: 547 if i not in self.traceparser.targets: 548 if not required: 549 mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found') 550 continue 551 raise self._gen_exception('CMake: invalid module {} for {}.\n' 552 'Try to explicitly specify one or more targets with the "modules" property.\n' 553 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys()))) 554 555 targets = [i] 556 if not autodetected_module_list: 557 self.found_modules += [i] 558 559 while len(targets) > 0: 560 curr = targets.pop(0) 561 562 # Skip already processed targets 563 if curr in processed_targets: 564 continue 565 566 tgt = self.traceparser.targets[curr] 567 cfgs = [] 568 cfg = '' 569 otherDeps = [] 570 mlog.debug(tgt) 571 572 if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties: 573 incDirs += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x] 574 575 if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties: 576 compileDefinitions += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x] 577 578 if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties: 579 compileOptions += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x] 580 581 if 'IMPORTED_CONFIGURATIONS' in tgt.properties: 582 cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x] 583 cfg = cfgs[0] 584 585 if is_debug: 586 if 'DEBUG' in cfgs: 587 cfg = 'DEBUG' 588 elif 'RELEASE' in cfgs: 589 cfg = 'RELEASE' 590 else: 591 if 'RELEASE' in cfgs: 592 cfg = 'RELEASE' 593 594 if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties: 595 libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x] 596 elif 'IMPORTED_IMPLIB' in tgt.properties: 597 libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x] 598 elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties: 599 libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x] 600 elif 'IMPORTED_LOCATION' in tgt.properties: 601 libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x] 602 603 if 'INTERFACE_LINK_LIBRARIES' in tgt.properties: 604 otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x] 605 606 if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties: 607 otherDeps += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x] 608 elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties: 609 otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x] 610 611 for j in otherDeps: 612 if j in self.traceparser.targets: 613 targets += [j] 614 elif reg_is_lib.match(j): 615 libraries += [j] 616 elif os.path.isabs(j) and os.path.exists(j): 617 libraries += [j] 618 elif self.env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(j): 619 # On Windows, CMake library dependencies can be passed as bare library names, 620 # CMake brute-forces a combination of prefix/suffix combinations to find the 621 # right library. Assume any bare argument passed which is not also a CMake 622 # target must be a system library we should try to link against. 623 libraries += self.clib_compiler.find_library(j, self.env, []) 624 else: 625 mlog.warning('CMake: Dependency', mlog.bold(j), 'for', mlog.bold(name), 'target', mlog.bold(self._original_module_name(curr)), 'was not found') 626 627 processed_targets += [curr] 628 629 # Make sure all elements in the lists are unique and sorted 630 incDirs = sorted(set(incDirs)) 631 compileDefinitions = sorted(set(compileDefinitions)) 632 compileOptions = sorted(set(compileOptions)) 633 libraries = sorted(set(libraries)) 634 635 mlog.debug(f'Include Dirs: {incDirs}') 636 mlog.debug(f'Compiler Definitions: {compileDefinitions}') 637 mlog.debug(f'Compiler Options: {compileOptions}') 638 mlog.debug(f'Libraries: {libraries}') 639 640 self.compile_args = compileOptions + compileDefinitions + [f'-I{x}' for x in incDirs] 641 self.link_args = libraries 642 643 def _get_build_dir(self) -> Path: 644 build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}' 645 build_dir.mkdir(parents=True, exist_ok=True) 646 return build_dir 647 648 def _setup_cmake_dir(self, cmake_file: str) -> Path: 649 # Setup the CMake build environment and return the "build" directory 650 build_dir = self._get_build_dir() 651 652 # Remove old CMake cache so we can try out multiple generators 653 cmake_cache = build_dir / 'CMakeCache.txt' 654 cmake_files = build_dir / 'CMakeFiles' 655 if cmake_cache.exists(): 656 cmake_cache.unlink() 657 shutil.rmtree(cmake_files.as_posix(), ignore_errors=True) 658 659 # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt 660 cmake_txt = mesondata['dependencies/data/' + cmake_file].data 661 662 # In general, some Fortran CMake find_package() also require C language enabled, 663 # even if nothing from C is directly used. An easy Fortran example that fails 664 # without C language is 665 # find_package(Threads) 666 # To make this general to 667 # any other language that might need this, we use a list for all 668 # languages and expand in the cmake Project(... LANGUAGES ...) statement. 669 from ..cmake import language_map 670 cmake_language = [language_map[x] for x in self.language_list if x in language_map] 671 if not cmake_language: 672 cmake_language += ['NONE'] 673 674 cmake_txt = textwrap.dedent(""" 675 cmake_minimum_required(VERSION ${{CMAKE_VERSION}}) 676 project(MesonTemp LANGUAGES {}) 677 """).format(' '.join(cmake_language)) + cmake_txt 678 679 cm_file = build_dir / 'CMakeLists.txt' 680 cm_file.write_text(cmake_txt, encoding='utf-8') 681 mlog.cmd_ci_include(cm_file.absolute().as_posix()) 682 683 return build_dir 684 685 def _call_cmake(self, 686 args: T.List[str], 687 cmake_file: str, 688 env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, T.Optional[str], T.Optional[str]]: 689 build_dir = self._setup_cmake_dir(cmake_file) 690 return self.cmakebin.call(args, build_dir, env=env) 691 692 def log_tried(self) -> str: 693 return self.type_name 694 695 def log_details(self) -> str: 696 modules = [self._original_module_name(x) for x in self.found_modules] 697 modules = sorted(set(modules)) 698 if modules: 699 return 'modules: ' + ', '.join(modules) 700 return '' 701 702 def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, 703 configtool: T.Optional[str] = None, internal: T.Optional[str] = None, 704 default_value: T.Optional[str] = None, 705 pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: 706 if cmake and self.traceparser is not None: 707 try: 708 v = self.traceparser.vars[cmake] 709 except KeyError: 710 pass 711 else: 712 if len(v) == 1: 713 return v[0] 714 elif v: 715 return v 716 if default_value is not None: 717 return default_value 718 raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') 719