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