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, DependencyMethods, 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 return False 350 351 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: 352 # Detect a dependency with CMake using the '--find-package' mode 353 # and the trace output (stderr) 354 # 355 # When the trace output is enabled CMake prints all functions with 356 # parameters to stderr as they are executed. Since CMake 3.4.0 357 # variables ("${VAR}") are also replaced in the trace output. 358 mlog.debug('\nDetermining dependency {!r} with CMake executable ' 359 '{!r}'.format(name, self.cmakebin.executable_path())) 360 361 # Try different CMake generators since specifying no generator may fail 362 # in cygwin for some reason 363 gen_list = [] 364 # First try the last working generator 365 if CMakeDependency.class_working_generator is not None: 366 gen_list += [CMakeDependency.class_working_generator] 367 gen_list += CMakeDependency.class_cmake_generators 368 369 # Map the components 370 comp_mapped = self._map_component_list(modules, components) 371 toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) 372 toolchain.write() 373 374 for i in gen_list: 375 mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) 376 377 # Prepare options 378 cmake_opts = [] 379 cmake_opts += [f'-DNAME={name}'] 380 cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo.archs))] 381 cmake_opts += [f'-DVERSION={package_version}'] 382 cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] 383 cmake_opts += args 384 cmake_opts += self.traceparser.trace_args() 385 cmake_opts += toolchain.get_cmake_args() 386 cmake_opts += self._extra_cmake_opts() 387 cmake_opts += ['.'] 388 if len(i) > 0: 389 cmake_opts = ['-G', i] + cmake_opts 390 391 # Run CMake 392 ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file()) 393 394 # Current generator was successful 395 if ret1 == 0: 396 CMakeDependency.class_working_generator = i 397 break 398 399 mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}') 400 mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') 401 402 # Check if any generator succeeded 403 if ret1 != 0: 404 return 405 406 try: 407 self.traceparser.parse(err1) 408 except CMakeException as e: 409 e2 = self._gen_exception(str(e)) 410 if self.required: 411 raise 412 else: 413 self.compile_args = [] 414 self.link_args = [] 415 self.is_found = False 416 self.reason = e2 417 return 418 419 # Whether the package is found or not is always stored in PACKAGE_FOUND 420 self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND') 421 if not self.is_found: 422 return 423 424 # Try to detect the version 425 vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION') 426 427 if len(vers_raw) > 0: 428 self.version = vers_raw[0] 429 self.version.strip('"\' ') 430 431 # Post-process module list. Used in derived classes to modify the 432 # module list (append prepend a string, etc.). 433 modules = self._map_module_list(modules, components) 434 autodetected_module_list = False 435 436 # Check if we need a DEBUG or RELEASE CMake dependencies 437 is_debug = False 438 if OptionKey('b_vscrt') in self.env.coredata.options: 439 is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug' 440 if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: 441 is_debug = True 442 else: 443 # Don't directly assign to is_debug to make mypy happy 444 debug_opt = self.env.coredata.get_option(OptionKey('debug')) 445 assert isinstance(debug_opt, bool) 446 is_debug = debug_opt 447 448 # Try guessing a CMake target if none is provided 449 if len(modules) == 0: 450 for i in self.traceparser.targets: 451 tg = i.lower() 452 lname = name.lower() 453 if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''): 454 mlog.debug(f'Guessed CMake target \'{i}\'') 455 modules = [(i, True)] 456 autodetected_module_list = True 457 break 458 459 # Failed to guess a target --> try the old-style method 460 if len(modules) == 0: 461 # Warn when there might be matching imported targets but no automatic match was used 462 partial_modules: T.List[CMakeTarget] = [] 463 for k, v in self.traceparser.targets.items(): 464 tg = k.lower() 465 lname = name.lower() 466 if tg.startswith(f'{lname}::'): 467 partial_modules += [v] 468 if partial_modules: 469 mlog.warning(textwrap.dedent(f'''\ 470 Could not find and exact match for the CMake dependency {name}. 471 472 However, Meson found the following partial matches: 473 474 {[x.name for x in partial_modules]} 475 476 Using imported is recommended, since this approach is less error prone 477 and better supported by Meson. Consider explicitly specifying one of 478 these in the dependency call with: 479 480 dependency('{name}', modules: ['{name}::<name>', ...]) 481 482 Meson will now continue to use the old-style {name}_LIBRARIES CMake 483 variables to extract the dependency information since no explicit 484 target is currently specified. 485 486 ''')) 487 mlog.debug('More info for the partial match targets:') 488 for tgt in partial_modules: 489 mlog.debug(tgt) 490 491 492 incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x] 493 defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x] 494 libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] 495 496 # CMake has a "fun" API, where certain keywords describing 497 # configurations can be in the *_LIBRARIES vraiables. See: 498 # - https://github.com/mesonbuild/meson/issues/9197 499 # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140 500 # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview (the last point in the section) 501 libs: T.List[str] = [] 502 cfg_matches = True 503 cm_tag_map = {'debug': is_debug, 'optimized': not is_debug, 'general': True} 504 for i in libs_raw: 505 if i.lower() in cm_tag_map: 506 cfg_matches = cm_tag_map[i.lower()] 507 continue 508 if cfg_matches: 509 libs += [i] 510 # According to the CMake docs, a keyword only works for the 511 # directly the following item and all items without a keyword 512 # are implizitly `general` 513 cfg_matches = True 514 515 # Try to use old style variables if no module is specified 516 if len(libs) > 0: 517 self.compile_args = list(map(lambda x: f'-I{x}', incDirs)) + defs 518 self.link_args = libs 519 mlog.debug(f'using old-style CMake variables for dependency {name}') 520 mlog.debug(f'Include Dirs: {incDirs}') 521 mlog.debug(f'Compiler Definitions: {defs}') 522 mlog.debug(f'Libraries: {libs}') 523 return 524 525 # Even the old-style approach failed. Nothing else we can do here 526 self.is_found = False 527 raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n' 528 'Try to explicitly specify one or more targets with the "modules" property.\n' 529 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys()))) 530 531 # Set dependencies with CMake targets 532 # recognise arguments we should pass directly to the linker 533 reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-pthread|-delayload:[a-zA-Z0-9_\.]+|[a-zA-Z0-9_]+\.lib)$') 534 reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$') 535 processed_targets = [] 536 incDirs = [] 537 compileDefinitions = [] 538 compileOptions = [] 539 libraries = [] 540 for i, required in modules: 541 if i not in self.traceparser.targets: 542 if not required: 543 mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found') 544 continue 545 raise self._gen_exception('CMake: invalid module {} for {}.\n' 546 'Try to explicitly specify one or more targets with the "modules" property.\n' 547 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys()))) 548 549 targets = [i] 550 if not autodetected_module_list: 551 self.found_modules += [i] 552 553 while len(targets) > 0: 554 curr = targets.pop(0) 555 556 # Skip already processed targets 557 if curr in processed_targets: 558 continue 559 560 tgt = self.traceparser.targets[curr] 561 cfgs = [] 562 cfg = '' 563 otherDeps = [] 564 mlog.debug(tgt) 565 566 if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties: 567 incDirs += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x] 568 569 if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties: 570 compileDefinitions += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x] 571 572 if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties: 573 compileOptions += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x] 574 575 if 'IMPORTED_CONFIGURATIONS' in tgt.properties: 576 cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x] 577 cfg = cfgs[0] 578 579 if is_debug: 580 if 'DEBUG' in cfgs: 581 cfg = 'DEBUG' 582 elif 'RELEASE' in cfgs: 583 cfg = 'RELEASE' 584 else: 585 if 'RELEASE' in cfgs: 586 cfg = 'RELEASE' 587 588 if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties: 589 libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x] 590 elif 'IMPORTED_IMPLIB' in tgt.properties: 591 libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x] 592 elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties: 593 libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x] 594 elif 'IMPORTED_LOCATION' in tgt.properties: 595 libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x] 596 597 if 'INTERFACE_LINK_LIBRARIES' in tgt.properties: 598 otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x] 599 600 if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties: 601 otherDeps += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x] 602 elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties: 603 otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x] 604 605 for j in otherDeps: 606 if j in self.traceparser.targets: 607 targets += [j] 608 elif reg_is_lib.match(j): 609 libraries += [j] 610 elif os.path.isabs(j) and os.path.exists(j): 611 libraries += [j] 612 elif self.env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(j): 613 # On Windows, CMake library dependencies can be passed as bare library names, 614 # e.g. 'version' should translate into 'version.lib'. CMake brute-forces a 615 # combination of prefix/suffix combinations to find the right library, however 616 # as we do not have a compiler environment available to us, we cannot do the 617 # same, but must assume any bare argument passed which is not also a CMake 618 # target must be a system library we should try to link against 619 libraries += [f"{j}.lib"] 620 else: 621 mlog.warning('CMake: Dependency', mlog.bold(j), 'for', mlog.bold(name), 'target', mlog.bold(self._original_module_name(curr)), 'was not found') 622 623 processed_targets += [curr] 624 625 # Make sure all elements in the lists are unique and sorted 626 incDirs = sorted(set(incDirs)) 627 compileDefinitions = sorted(set(compileDefinitions)) 628 compileOptions = sorted(set(compileOptions)) 629 libraries = sorted(set(libraries)) 630 631 mlog.debug(f'Include Dirs: {incDirs}') 632 mlog.debug(f'Compiler Definitions: {compileDefinitions}') 633 mlog.debug(f'Compiler Options: {compileOptions}') 634 mlog.debug(f'Libraries: {libraries}') 635 636 self.compile_args = compileOptions + compileDefinitions + [f'-I{x}' for x in incDirs] 637 self.link_args = libraries 638 639 def _get_build_dir(self) -> Path: 640 build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}' 641 build_dir.mkdir(parents=True, exist_ok=True) 642 return build_dir 643 644 def _setup_cmake_dir(self, cmake_file: str) -> Path: 645 # Setup the CMake build environment and return the "build" directory 646 build_dir = self._get_build_dir() 647 648 # Remove old CMake cache so we can try out multiple generators 649 cmake_cache = build_dir / 'CMakeCache.txt' 650 cmake_files = build_dir / 'CMakeFiles' 651 if cmake_cache.exists(): 652 cmake_cache.unlink() 653 shutil.rmtree(cmake_files.as_posix(), ignore_errors=True) 654 655 # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt 656 cmake_txt = mesondata['dependencies/data/' + cmake_file].data 657 658 # In general, some Fortran CMake find_package() also require C language enabled, 659 # even if nothing from C is directly used. An easy Fortran example that fails 660 # without C language is 661 # find_package(Threads) 662 # To make this general to 663 # any other language that might need this, we use a list for all 664 # languages and expand in the cmake Project(... LANGUAGES ...) statement. 665 from ..cmake import language_map 666 cmake_language = [language_map[x] for x in self.language_list if x in language_map] 667 if not cmake_language: 668 cmake_language += ['NONE'] 669 670 cmake_txt = textwrap.dedent(""" 671 cmake_minimum_required(VERSION ${{CMAKE_VERSION}}) 672 project(MesonTemp LANGUAGES {}) 673 """).format(' '.join(cmake_language)) + cmake_txt 674 675 cm_file = build_dir / 'CMakeLists.txt' 676 cm_file.write_text(cmake_txt, encoding='utf-8') 677 mlog.cmd_ci_include(cm_file.absolute().as_posix()) 678 679 return build_dir 680 681 def _call_cmake(self, 682 args: T.List[str], 683 cmake_file: str, 684 env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, T.Optional[str], T.Optional[str]]: 685 build_dir = self._setup_cmake_dir(cmake_file) 686 return self.cmakebin.call(args, build_dir, env=env) 687 688 @staticmethod 689 def get_methods() -> T.List[DependencyMethods]: 690 return [DependencyMethods.CMAKE] 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