diff options
author | Angelos Mouzakitis <a.mouzakitis@virtualopensystems.com> | 2023-10-10 14:33:42 +0000 |
---|---|---|
committer | Angelos Mouzakitis <a.mouzakitis@virtualopensystems.com> | 2023-10-10 14:33:42 +0000 |
commit | af1a266670d040d2f4083ff309d732d648afba2a (patch) | |
tree | 2fc46203448ddcc6f81546d379abfaeb323575e9 /meson/mesonbuild/dependencies/cmake.py | |
parent | e02cda008591317b1625707ff8e115a4841aa889 (diff) |
Change-Id: Iaf8d18082d3991dec7c0ebbea540f092188eb4ec
Diffstat (limited to 'meson/mesonbuild/dependencies/cmake.py')
-rw-r--r-- | meson/mesonbuild/dependencies/cmake.py | 718 |
1 files changed, 718 insertions, 0 deletions
diff --git a/meson/mesonbuild/dependencies/cmake.py b/meson/mesonbuild/dependencies/cmake.py new file mode 100644 index 000000000..047950da5 --- /dev/null +++ b/meson/mesonbuild/dependencies/cmake.py @@ -0,0 +1,718 @@ +# Copyright 2013-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .base import ExternalDependency, DependencyException, DependencyMethods, DependencyTypeName +from ..mesonlib import is_windows, MesonException, OptionKey, PerMachine, stringlistify, extract_as_list +from ..mesondata import mesondata +from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args, CMakeTarget +from .. import mlog +from pathlib import Path +import functools +import re +import os +import shutil +import textwrap +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + from ..envconfig import MachineInfo + +class CMakeInfo(T.NamedTuple): + module_paths: T.List[str] + cmake_root: str + archs: T.List[str] + common_paths: T.List[str] + +class CMakeDependency(ExternalDependency): + # The class's copy of the CMake path. Avoids having to search for it + # multiple times in the same Meson invocation. + class_cmakeinfo: PerMachine[T.Optional[CMakeInfo]] = PerMachine(None, None) + # Version string for the minimum CMake version + class_cmake_version = '>=3.4' + # CMake generators to try (empty for no generator) + class_cmake_generators = ['', 'Ninja', 'Unix Makefiles', 'Visual Studio 10 2010'] + class_working_generator: T.Optional[str] = None + + def _gen_exception(self, msg: str) -> DependencyException: + return DependencyException(f'Dependency {self.name} not found: {msg}') + + def _main_cmake_file(self) -> str: + return 'CMakeLists.txt' + + def _extra_cmake_opts(self) -> T.List[str]: + return [] + + 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]]: + # Map the input module list to something else + # This function will only be executed AFTER the initial CMake + # interpreter pass has completed. Thus variables defined in the + # CMakeLists.txt can be accessed here. + # + # Both the modules and components inputs contain the original lists. + return modules + + 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]]: + # Map the input components list to something else. This + # function will be executed BEFORE the initial CMake interpreter + # pass. Thus variables from the CMakeLists.txt can NOT be accessed. + # + # Both the modules and components inputs contain the original lists. + return components + + def _original_module_name(self, module: str) -> str: + # Reverse the module mapping done by _map_module_list for + # one module + return module + + def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: + # Gather a list of all languages to support + self.language_list = [] # type: T.List[str] + if language is None: + compilers = None + if kwargs.get('native', False): + compilers = environment.coredata.compilers.build + else: + compilers = environment.coredata.compilers.host + + candidates = ['c', 'cpp', 'fortran', 'objc', 'objcxx'] + self.language_list += [x for x in candidates if x in compilers] + else: + self.language_list += [language] + + # Add additional languages if required + if 'fortran' in self.language_list: + self.language_list += ['c'] + + # Ensure that the list is unique + self.language_list = list(set(self.language_list)) + + super().__init__(DependencyTypeName('cmake'), environment, kwargs, language=language) + self.name = name + self.is_libtool = False + # Store a copy of the CMake path on the object itself so it is + # stored in the pickled coredata and recovered. + self.cmakebin: T.Optional[CMakeExecutor] = None + self.cmakeinfo: T.Optional[CMakeInfo] = None + + # Where all CMake "build dirs" are located + self.cmake_root_dir = environment.scratch_dir + + # T.List of successfully found modules + self.found_modules: T.List[str] = [] + + # Initialize with None before the first return to avoid + # AttributeError exceptions in derived classes + self.traceparser: T.Optional[CMakeTraceParser] = None + + # TODO further evaluate always using MachineChoice.BUILD + self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent) + if not self.cmakebin.found(): + self.cmakebin = None + msg = f'CMake binary for machine {self.for_machine} not found. Giving up.' + if self.required: + raise DependencyException(msg) + mlog.debug(msg) + return + + # Setup the trace parser + self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) + + cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) + cm_args = check_cmake_args(cm_args) + if CMakeDependency.class_cmakeinfo[self.for_machine] is None: + CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info(cm_args) + self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine] + if self.cmakeinfo is None: + raise self._gen_exception('Unable to obtain CMake system information') + + package_version = kwargs.get('cmake_package_version', '') + if not isinstance(package_version, str): + raise DependencyException('Keyword "cmake_package_version" must be a string.') + components = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'components'))] + modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] + modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] + cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) + cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] + if cm_path: + cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) + if not self._preliminary_find_check(name, cm_path, self.cmakebin.get_cmake_prefix_paths(), environment.machines[self.for_machine]): + mlog.debug('Preliminary CMake check failed. Aborting.') + return + self._detect_dep(name, package_version, modules, components, cm_args) + + def __repr__(self) -> str: + return f'<{self.__class__.__name__} {self.name}: {self.is_found} {self.version_reqs}>' + + def _get_cmake_info(self, cm_args: T.List[str]) -> T.Optional[CMakeInfo]: + mlog.debug("Extracting basic cmake information") + + # Try different CMake generators since specifying no generator may fail + # in cygwin for some reason + gen_list = [] + # First try the last working generator + if CMakeDependency.class_working_generator is not None: + gen_list += [CMakeDependency.class_working_generator] + gen_list += CMakeDependency.class_cmake_generators + + temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) + toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) + toolchain.write() + + for i in gen_list: + mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) + + # Prepare options + cmake_opts = temp_parser.trace_args() + toolchain.get_cmake_args() + ['.'] + cmake_opts += cm_args + if len(i) > 0: + cmake_opts = ['-G', i] + cmake_opts + + # Run CMake + ret1, out1, err1 = self._call_cmake(cmake_opts, 'CMakePathInfo.txt') + + # Current generator was successful + if ret1 == 0: + CMakeDependency.class_working_generator = i + break + + mlog.debug(f'CMake failed to gather system information for generator {i} with error code {ret1}') + mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') + + # Check if any generator succeeded + if ret1 != 0: + return None + + try: + temp_parser.parse(err1) + except MesonException: + return None + + def process_paths(l: T.List[str]) -> T.Set[str]: + if is_windows(): + # Cannot split on ':' on Windows because its in the drive letter + tmp = [x.split(os.pathsep) for x in l] + else: + # https://github.com/mesonbuild/meson/issues/7294 + tmp = [re.split(r':|;', x) for x in l] + flattened = [x for sublist in tmp for x in sublist] + return set(flattened) + + # Extract the variables and sanity check them + root_paths_set = process_paths(temp_parser.get_cmake_var('MESON_FIND_ROOT_PATH')) + root_paths_set.update(process_paths(temp_parser.get_cmake_var('MESON_CMAKE_SYSROOT'))) + root_paths = sorted(root_paths_set) + root_paths = [x for x in root_paths if os.path.isdir(x)] + module_paths_set = process_paths(temp_parser.get_cmake_var('MESON_PATHS_LIST')) + rooted_paths: T.List[str] = [] + for j in [Path(x) for x in root_paths]: + for p in [Path(x) for x in module_paths_set]: + rooted_paths.append(str(j / p.relative_to(p.anchor))) + module_paths = sorted(module_paths_set.union(rooted_paths)) + module_paths = [x for x in module_paths if os.path.isdir(x)] + archs = temp_parser.get_cmake_var('MESON_ARCH_LIST') + + common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share'] + for i in archs: + common_paths += [os.path.join('lib', i)] + + res = CMakeInfo( + module_paths=module_paths, + cmake_root=temp_parser.get_cmake_var('MESON_CMAKE_ROOT')[0], + archs=archs, + common_paths=common_paths, + ) + + mlog.debug(f' -- Module search paths: {res.module_paths}') + mlog.debug(f' -- CMake root: {res.cmake_root}') + mlog.debug(f' -- CMake architectures: {res.archs}') + mlog.debug(f' -- CMake lib search paths: {res.common_paths}') + + return res + + @staticmethod + @functools.lru_cache(maxsize=None) + def _cached_listdir(path: str) -> T.Tuple[T.Tuple[str, str], ...]: + try: + return tuple((x, str(x).lower()) for x in os.listdir(path)) + except OSError: + return tuple() + + @staticmethod + @functools.lru_cache(maxsize=None) + def _cached_isdir(path: str) -> bool: + try: + return os.path.isdir(path) + except OSError: + return False + + def _preliminary_find_check(self, name: str, module_path: T.List[str], prefix_path: T.List[str], machine: 'MachineInfo') -> bool: + lname = str(name).lower() + + # Checks <path>, <path>/cmake, <path>/CMake + def find_module(path: str) -> bool: + for i in [path, os.path.join(path, 'cmake'), os.path.join(path, 'CMake')]: + if not self._cached_isdir(i): + continue + + # Check the directory case insensitive + content = self._cached_listdir(i) + candidates = ['Find{}.cmake', '{}Config.cmake', '{}-config.cmake'] + candidates = [x.format(name).lower() for x in candidates] + if any([x[1] in candidates for x in content]): + return True + return False + + # Search in <path>/(lib/<arch>|lib*|share) for cmake files + def search_lib_dirs(path: str) -> bool: + for i in [os.path.join(path, x) for x in self.cmakeinfo.common_paths]: + if not self._cached_isdir(i): + continue + + # Check <path>/(lib/<arch>|lib*|share)/cmake/<name>*/ + cm_dir = os.path.join(i, 'cmake') + if self._cached_isdir(cm_dir): + content = self._cached_listdir(cm_dir) + content = tuple(x for x in content if x[1].startswith(lname)) + for k in content: + if find_module(os.path.join(cm_dir, k[0])): + return True + + # <path>/(lib/<arch>|lib*|share)/<name>*/ + # <path>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ + content = self._cached_listdir(i) + content = tuple(x for x in content if x[1].startswith(lname)) + for k in content: + if find_module(os.path.join(i, k[0])): + return True + + return False + + # Check the user provided and system module paths + for i in module_path + [os.path.join(self.cmakeinfo.cmake_root, 'Modules')]: + if find_module(i): + return True + + # Check the user provided prefix paths + for i in prefix_path: + if search_lib_dirs(i): + return True + + # Check PATH + system_env = [] # type: T.List[str] + for i in os.environ.get('PATH', '').split(os.pathsep): + if i.endswith('/bin') or i.endswith('\\bin'): + i = i[:-4] + if i.endswith('/sbin') or i.endswith('\\sbin'): + i = i[:-5] + system_env += [i] + + # Check the system paths + for i in self.cmakeinfo.module_paths + system_env: + if find_module(i): + return True + + if search_lib_dirs(i): + return True + + content = self._cached_listdir(i) + content = tuple(x for x in content if x[1].startswith(lname)) + for k in content: + if search_lib_dirs(os.path.join(i, k[0])): + return True + + # Mac framework support + if machine.is_darwin(): + for j in [f'{lname}.framework', f'{lname}.app']: + for k in content: + if k[1] != j: + continue + if find_module(os.path.join(i, k[0], 'Resources')) or find_module(os.path.join(i, k[0], 'Version')): + return True + + # Check the environment path + env_path = os.environ.get(f'{name}_DIR') + if env_path and find_module(env_path): + return True + + return False + + 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: + # Detect a dependency with CMake using the '--find-package' mode + # and the trace output (stderr) + # + # When the trace output is enabled CMake prints all functions with + # parameters to stderr as they are executed. Since CMake 3.4.0 + # variables ("${VAR}") are also replaced in the trace output. + mlog.debug('\nDetermining dependency {!r} with CMake executable ' + '{!r}'.format(name, self.cmakebin.executable_path())) + + # Try different CMake generators since specifying no generator may fail + # in cygwin for some reason + gen_list = [] + # First try the last working generator + if CMakeDependency.class_working_generator is not None: + gen_list += [CMakeDependency.class_working_generator] + gen_list += CMakeDependency.class_cmake_generators + + # Map the components + comp_mapped = self._map_component_list(modules, components) + toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) + toolchain.write() + + for i in gen_list: + mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) + + # Prepare options + cmake_opts = [] + cmake_opts += [f'-DNAME={name}'] + cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo.archs))] + cmake_opts += [f'-DVERSION={package_version}'] + cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] + cmake_opts += args + cmake_opts += self.traceparser.trace_args() + cmake_opts += toolchain.get_cmake_args() + cmake_opts += self._extra_cmake_opts() + cmake_opts += ['.'] + if len(i) > 0: + cmake_opts = ['-G', i] + cmake_opts + + # Run CMake + ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file()) + + # Current generator was successful + if ret1 == 0: + CMakeDependency.class_working_generator = i + break + + mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}') + mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') + + # Check if any generator succeeded + if ret1 != 0: + return + + try: + self.traceparser.parse(err1) + except CMakeException as e: + e2 = self._gen_exception(str(e)) + if self.required: + raise + else: + self.compile_args = [] + self.link_args = [] + self.is_found = False + self.reason = e2 + return + + # Whether the package is found or not is always stored in PACKAGE_FOUND + self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND') + if not self.is_found: + return + + # Try to detect the version + vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION') + + if len(vers_raw) > 0: + self.version = vers_raw[0] + self.version.strip('"\' ') + + # Post-process module list. Used in derived classes to modify the + # module list (append prepend a string, etc.). + modules = self._map_module_list(modules, components) + autodetected_module_list = False + + # Check if we need a DEBUG or RELEASE CMake dependencies + is_debug = False + if OptionKey('b_vscrt') in self.env.coredata.options: + is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug' + if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: + is_debug = True + else: + # Don't directly assign to is_debug to make mypy happy + debug_opt = self.env.coredata.get_option(OptionKey('debug')) + assert isinstance(debug_opt, bool) + is_debug = debug_opt + + # Try guessing a CMake target if none is provided + if len(modules) == 0: + for i in self.traceparser.targets: + tg = i.lower() + lname = name.lower() + if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''): + mlog.debug(f'Guessed CMake target \'{i}\'') + modules = [(i, True)] + autodetected_module_list = True + break + + # Failed to guess a target --> try the old-style method + if len(modules) == 0: + # Warn when there might be matching imported targets but no automatic match was used + partial_modules: T.List[CMakeTarget] = [] + for k, v in self.traceparser.targets.items(): + tg = k.lower() + lname = name.lower() + if tg.startswith(f'{lname}::'): + partial_modules += [v] + if partial_modules: + mlog.warning(textwrap.dedent(f'''\ + Could not find and exact match for the CMake dependency {name}. + + However, Meson found the following partial matches: + + {[x.name for x in partial_modules]} + + Using imported is recommended, since this approach is less error prone + and better supported by Meson. Consider explicitly specifying one of + these in the dependency call with: + + dependency('{name}', modules: ['{name}::<name>', ...]) + + Meson will now continue to use the old-style {name}_LIBRARIES CMake + variables to extract the dependency information since no explicit + target is currently specified. + + ''')) + mlog.debug('More info for the partial match targets:') + for tgt in partial_modules: + mlog.debug(tgt) + + + incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x] + defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x] + libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] + + # CMake has a "fun" API, where certain keywords describing + # configurations can be in the *_LIBRARIES vraiables. See: + # - https://github.com/mesonbuild/meson/issues/9197 + # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140 + # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview (the last point in the section) + libs: T.List[str] = [] + cfg_matches = True + cm_tag_map = {'debug': is_debug, 'optimized': not is_debug, 'general': True} + for i in libs_raw: + if i.lower() in cm_tag_map: + cfg_matches = cm_tag_map[i.lower()] + continue + if cfg_matches: + libs += [i] + # According to the CMake docs, a keyword only works for the + # directly the following item and all items without a keyword + # are implizitly `general` + cfg_matches = True + + # Try to use old style variables if no module is specified + if len(libs) > 0: + self.compile_args = list(map(lambda x: f'-I{x}', incDirs)) + defs + self.link_args = libs + mlog.debug(f'using old-style CMake variables for dependency {name}') + mlog.debug(f'Include Dirs: {incDirs}') + mlog.debug(f'Compiler Definitions: {defs}') + mlog.debug(f'Libraries: {libs}') + return + + # Even the old-style approach failed. Nothing else we can do here + self.is_found = False + raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n' + 'Try to explicitly specify one or more targets with the "modules" property.\n' + 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys()))) + + # Set dependencies with CMake targets + # recognise arguments we should pass directly to the linker + reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-pthread|-delayload:[a-zA-Z0-9_\.]+|[a-zA-Z0-9_]+\.lib)$') + reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$') + processed_targets = [] + incDirs = [] + compileDefinitions = [] + compileOptions = [] + libraries = [] + for i, required in modules: + if i not in self.traceparser.targets: + if not required: + mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found') + continue + raise self._gen_exception('CMake: invalid module {} for {}.\n' + 'Try to explicitly specify one or more targets with the "modules" property.\n' + 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys()))) + + targets = [i] + if not autodetected_module_list: + self.found_modules += [i] + + while len(targets) > 0: + curr = targets.pop(0) + + # Skip already processed targets + if curr in processed_targets: + continue + + tgt = self.traceparser.targets[curr] + cfgs = [] + cfg = '' + otherDeps = [] + mlog.debug(tgt) + + if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties: + incDirs += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x] + + if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties: + compileDefinitions += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x] + + if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties: + compileOptions += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x] + + if 'IMPORTED_CONFIGURATIONS' in tgt.properties: + cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x] + cfg = cfgs[0] + + if is_debug: + if 'DEBUG' in cfgs: + cfg = 'DEBUG' + elif 'RELEASE' in cfgs: + cfg = 'RELEASE' + else: + if 'RELEASE' in cfgs: + cfg = 'RELEASE' + + if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties: + libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x] + elif 'IMPORTED_IMPLIB' in tgt.properties: + libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x] + elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties: + libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x] + elif 'IMPORTED_LOCATION' in tgt.properties: + libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x] + + if 'INTERFACE_LINK_LIBRARIES' in tgt.properties: + otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x] + + if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties: + otherDeps += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x] + elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties: + otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x] + + for j in otherDeps: + if j in self.traceparser.targets: + targets += [j] + elif reg_is_lib.match(j): + libraries += [j] + elif os.path.isabs(j) and os.path.exists(j): + libraries += [j] + elif self.env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(j): + # On Windows, CMake library dependencies can be passed as bare library names, + # e.g. 'version' should translate into 'version.lib'. CMake brute-forces a + # combination of prefix/suffix combinations to find the right library, however + # as we do not have a compiler environment available to us, we cannot do the + # same, but must assume any bare argument passed which is not also a CMake + # target must be a system library we should try to link against + libraries += [f"{j}.lib"] + else: + mlog.warning('CMake: Dependency', mlog.bold(j), 'for', mlog.bold(name), 'target', mlog.bold(self._original_module_name(curr)), 'was not found') + + processed_targets += [curr] + + # Make sure all elements in the lists are unique and sorted + incDirs = sorted(set(incDirs)) + compileDefinitions = sorted(set(compileDefinitions)) + compileOptions = sorted(set(compileOptions)) + libraries = sorted(set(libraries)) + + mlog.debug(f'Include Dirs: {incDirs}') + mlog.debug(f'Compiler Definitions: {compileDefinitions}') + mlog.debug(f'Compiler Options: {compileOptions}') + mlog.debug(f'Libraries: {libraries}') + + self.compile_args = compileOptions + compileDefinitions + [f'-I{x}' for x in incDirs] + self.link_args = libraries + + def _get_build_dir(self) -> Path: + build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}' + build_dir.mkdir(parents=True, exist_ok=True) + return build_dir + + def _setup_cmake_dir(self, cmake_file: str) -> Path: + # Setup the CMake build environment and return the "build" directory + build_dir = self._get_build_dir() + + # Remove old CMake cache so we can try out multiple generators + cmake_cache = build_dir / 'CMakeCache.txt' + cmake_files = build_dir / 'CMakeFiles' + if cmake_cache.exists(): + cmake_cache.unlink() + shutil.rmtree(cmake_files.as_posix(), ignore_errors=True) + + # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt + cmake_txt = mesondata['dependencies/data/' + cmake_file].data + + # In general, some Fortran CMake find_package() also require C language enabled, + # even if nothing from C is directly used. An easy Fortran example that fails + # without C language is + # find_package(Threads) + # To make this general to + # any other language that might need this, we use a list for all + # languages and expand in the cmake Project(... LANGUAGES ...) statement. + from ..cmake import language_map + cmake_language = [language_map[x] for x in self.language_list if x in language_map] + if not cmake_language: + cmake_language += ['NONE'] + + cmake_txt = textwrap.dedent(""" + cmake_minimum_required(VERSION ${{CMAKE_VERSION}}) + project(MesonTemp LANGUAGES {}) + """).format(' '.join(cmake_language)) + cmake_txt + + cm_file = build_dir / 'CMakeLists.txt' + cm_file.write_text(cmake_txt, encoding='utf-8') + mlog.cmd_ci_include(cm_file.absolute().as_posix()) + + return build_dir + + def _call_cmake(self, + args: T.List[str], + cmake_file: str, + env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, T.Optional[str], T.Optional[str]]: + build_dir = self._setup_cmake_dir(cmake_file) + return self.cmakebin.call(args, build_dir, env=env) + + @staticmethod + def get_methods() -> T.List[DependencyMethods]: + return [DependencyMethods.CMAKE] + + def log_tried(self) -> str: + return self.type_name + + def log_details(self) -> str: + modules = [self._original_module_name(x) for x in self.found_modules] + modules = sorted(set(modules)) + if modules: + return 'modules: ' + ', '.join(modules) + return '' + + def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, + configtool: T.Optional[str] = None, internal: T.Optional[str] = None, + default_value: T.Optional[str] = None, + pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: + if cmake and self.traceparser is not None: + try: + v = self.traceparser.vars[cmake] + except KeyError: + pass + else: + if len(v) == 1: + return v[0] + elif v: + return v + if default_value is not None: + return default_value + raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') |