|
| 1 | +import os |
| 2 | +import re |
| 3 | +import textwrap |
| 4 | + |
| 5 | +from jinja2 import Template |
| 6 | + |
| 7 | +from conan.internal import check_duplicated_generator |
| 8 | +from conan.tools.cmake.cmakedeps2.config import ConfigTemplate2 |
| 9 | +from conan.tools.cmake.cmakedeps2.config_version import ConfigVersionTemplate2 |
| 10 | +from conan.tools.cmake.cmakedeps2.target_configuration import TargetConfigurationTemplate2 |
| 11 | +from conan.tools.cmake.cmakedeps2.targets import TargetsTemplate2 |
| 12 | +from conan.tools.files import save |
| 13 | +from conan.errors import ConanException |
| 14 | +from conans.model.dependencies import get_transitive_requires |
| 15 | +from conans.util.files import load |
| 16 | + |
| 17 | +FIND_MODE_MODULE = "module" |
| 18 | +FIND_MODE_CONFIG = "config" |
| 19 | +FIND_MODE_NONE = "none" |
| 20 | +FIND_MODE_BOTH = "both" |
| 21 | + |
| 22 | + |
| 23 | +class CMakeDeps2: |
| 24 | + |
| 25 | + def __init__(self, conanfile): |
| 26 | + self._conanfile = conanfile |
| 27 | + self.configuration = str(self._conanfile.settings.build_type) |
| 28 | + |
| 29 | + # These are just for legacy compatibility, but not use at al |
| 30 | + self.build_context_activated = [] |
| 31 | + self.build_context_build_modules = [] |
| 32 | + self.build_context_suffix = {} |
| 33 | + # Enable/Disable checking if a component target exists or not |
| 34 | + self.check_components_exist = False |
| 35 | + |
| 36 | + self._properties = {} |
| 37 | + |
| 38 | + def generate(self): |
| 39 | + check_duplicated_generator(self, self._conanfile) |
| 40 | + # Current directory is the generators_folder |
| 41 | + generator_files = self._content() |
| 42 | + for generator_file, content in generator_files.items(): |
| 43 | + save(self._conanfile, generator_file, content) |
| 44 | + _PathGenerator(self, self._conanfile).generate() |
| 45 | + |
| 46 | + def _content(self): |
| 47 | + host_req = self._conanfile.dependencies.host |
| 48 | + build_req = self._conanfile.dependencies.direct_build |
| 49 | + test_req = self._conanfile.dependencies.test |
| 50 | + |
| 51 | + # Iterate all the transitive requires |
| 52 | + ret = {} |
| 53 | + for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): |
| 54 | + cmake_find_mode = self.get_property("cmake_find_mode", dep) |
| 55 | + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG |
| 56 | + cmake_find_mode = cmake_find_mode.lower() |
| 57 | + if cmake_find_mode == FIND_MODE_NONE: |
| 58 | + continue |
| 59 | + |
| 60 | + config = ConfigTemplate2(self, dep) |
| 61 | + ret[config.filename] = config.content() |
| 62 | + config_version = ConfigVersionTemplate2(self, dep) |
| 63 | + ret[config_version.filename] = config_version.content() |
| 64 | + |
| 65 | + targets = TargetsTemplate2(self, dep) |
| 66 | + ret[targets.filename] = targets.content() |
| 67 | + target_configuration = TargetConfigurationTemplate2(self, dep, require) |
| 68 | + ret[target_configuration.filename] = target_configuration.content() |
| 69 | + return ret |
| 70 | + |
| 71 | + def set_property(self, dep, prop, value, build_context=False): |
| 72 | + """ |
| 73 | + Using this method you can overwrite the :ref:`property<CMakeDeps Properties>` values set by |
| 74 | + the Conan recipes from the consumer. |
| 75 | +
|
| 76 | + :param dep: Name of the dependency to set the :ref:`property<CMakeDeps Properties>`. For |
| 77 | + components use the syntax: ``dep_name::component_name``. |
| 78 | + :param prop: Name of the :ref:`property<CMakeDeps Properties>`. |
| 79 | + :param value: Value of the property. Use ``None`` to invalidate any value set by the |
| 80 | + upstream recipe. |
| 81 | + :param build_context: Set to ``True`` if you want to set the property for a dependency that |
| 82 | + belongs to the build context (``False`` by default). |
| 83 | + """ |
| 84 | + build_suffix = "&build" if build_context else "" |
| 85 | + self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value}) |
| 86 | + |
| 87 | + def get_property(self, prop, dep, comp_name=None, check_type=None): |
| 88 | + dep_name = dep.ref.name |
| 89 | + build_suffix = "&build" if dep.context == "build" else "" |
| 90 | + dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" |
| 91 | + try: |
| 92 | + value = self._properties[f"{dep_comp}{build_suffix}"][prop] |
| 93 | + if check_type is not None and not isinstance(value, check_type): |
| 94 | + raise ConanException(f'The expected type for {prop} is "{check_type.__name__}", ' |
| 95 | + f'but "{type(value).__name__}" was found') |
| 96 | + return value |
| 97 | + except KeyError: |
| 98 | + # Here we are not using the cpp_info = deduce_cpp_info(dep) because it is not |
| 99 | + # necessary for the properties |
| 100 | + return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \ |
| 101 | + else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type) |
| 102 | + |
| 103 | + def get_cmake_filename(self, dep, module_mode=None): |
| 104 | + """Get the name of the file for the find_package(XXX)""" |
| 105 | + # This is used by CMakeDeps to determine: |
| 106 | + # - The filename to generate (XXX-config.cmake or FindXXX.cmake) |
| 107 | + # - The name of the defined XXX_DIR variables |
| 108 | + # - The name of transitive dependencies for calls to find_dependency |
| 109 | + if module_mode and self._get_find_mode(dep) in [FIND_MODE_MODULE, FIND_MODE_BOTH]: |
| 110 | + ret = self.get_property("cmake_module_file_name", dep) |
| 111 | + if ret: |
| 112 | + return ret |
| 113 | + ret = self.get_property("cmake_file_name", dep) |
| 114 | + return ret or dep.ref.name |
| 115 | + |
| 116 | + def _get_find_mode(self, dep): |
| 117 | + """ |
| 118 | + :param dep: requirement |
| 119 | + :return: "none" or "config" or "module" or "both" or "config" when not set |
| 120 | + """ |
| 121 | + tmp = self.get_property("cmake_find_mode", dep) |
| 122 | + if tmp is None: |
| 123 | + return "config" |
| 124 | + return tmp.lower() |
| 125 | + |
| 126 | + def get_transitive_requires(self, conanfile): |
| 127 | + # Prepared to filter transitive tool-requires with visible=True |
| 128 | + return get_transitive_requires(self._conanfile, conanfile) |
| 129 | + |
| 130 | + |
| 131 | +class _PathGenerator: |
| 132 | + _conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake" |
| 133 | + |
| 134 | + def __init__(self, cmakedeps, conanfile): |
| 135 | + self._conanfile = conanfile |
| 136 | + self._cmakedeps = cmakedeps |
| 137 | + |
| 138 | + def generate(self): |
| 139 | + template = textwrap.dedent("""\ |
| 140 | + {% for pkg_name, folder in pkg_paths.items() %} |
| 141 | + set({{pkg_name}}_DIR "{{folder}}") |
| 142 | + {% endfor %} |
| 143 | + {% if host_runtime_dirs %} |
| 144 | + set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) |
| 145 | + {% endif %} |
| 146 | + """) |
| 147 | + |
| 148 | + host_req = self._conanfile.dependencies.host |
| 149 | + build_req = self._conanfile.dependencies.direct_build |
| 150 | + test_req = self._conanfile.dependencies.test |
| 151 | + |
| 152 | + # gen_folder = self._conanfile.generators_folder.replace("\\", "/") |
| 153 | + # if not, test_cmake_add_subdirectory test fails |
| 154 | + # content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)') |
| 155 | + pkg_paths = {} |
| 156 | + for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): |
| 157 | + cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep) |
| 158 | + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG |
| 159 | + cmake_find_mode = cmake_find_mode.lower() |
| 160 | + |
| 161 | + pkg_name = self._cmakedeps.get_cmake_filename(dep) |
| 162 | + # https://cmake.org/cmake/help/v3.22/guide/using-dependencies/index.html |
| 163 | + if cmake_find_mode == FIND_MODE_NONE: |
| 164 | + try: |
| 165 | + # This is irrespective of the components, it should be in the root cpp_info |
| 166 | + # To define the location of the pkg-config.cmake file |
| 167 | + build_dir = dep.cpp_info.builddirs[0] |
| 168 | + except IndexError: |
| 169 | + build_dir = dep.package_folder |
| 170 | + pkg_folder = build_dir.replace("\\", "/") if build_dir else None |
| 171 | + if pkg_folder: |
| 172 | + config_file = ConfigTemplate2(self._cmakedeps, dep).filename |
| 173 | + if os.path.isfile(os.path.join(pkg_folder, config_file)): |
| 174 | + pkg_paths[pkg_name] = pkg_folder |
| 175 | + continue |
| 176 | + |
| 177 | + # If CMakeDeps generated, the folder is this one |
| 178 | + # content.append(f'set({pkg_name}_ROOT "{gen_folder}")') |
| 179 | + pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}" |
| 180 | + |
| 181 | + context = {"host_runtime_dirs": self._get_host_runtime_dirs(), |
| 182 | + "pkg_paths": pkg_paths} |
| 183 | + content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context) |
| 184 | + save(self._conanfile, self._conan_cmakedeps_paths, content) |
| 185 | + |
| 186 | + def _get_host_runtime_dirs(self): |
| 187 | + host_runtime_dirs = {} |
| 188 | + |
| 189 | + # Get the previous configuration |
| 190 | + if os.path.exists(self._conan_cmakedeps_paths): |
| 191 | + existing_toolchain = load(self._conan_cmakedeps_paths) |
| 192 | + pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" |
| 193 | + variable_match = re.search(pattern_lib_dirs, existing_toolchain) |
| 194 | + if variable_match: |
| 195 | + capture = variable_match.group(1) |
| 196 | + matches = re.findall(r'"\$<\$<CONFIG:([A-Za-z]*)>:([^>]*)>"', capture) |
| 197 | + for config, paths in matches: |
| 198 | + host_runtime_dirs.setdefault(config, []).append(paths) |
| 199 | + |
| 200 | + is_win = self._conanfile.settings.get_safe("os") == "Windows" |
| 201 | + for req in self._conanfile.dependencies.host.values(): |
| 202 | + config = req.settings.get_safe("build_type", self._cmakedeps.configuration) |
| 203 | + aggregated_cppinfo = req.cpp_info.aggregated_components() |
| 204 | + runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs |
| 205 | + for d in runtime_dirs: |
| 206 | + d = d.replace("\\", "/") |
| 207 | + existing = host_runtime_dirs.setdefault(config, []) |
| 208 | + if d not in existing: |
| 209 | + existing.append(d) |
| 210 | + |
| 211 | + return ' '.join(f'"$<$<CONFIG:{c}>:{i}>"' for c, v in host_runtime_dirs.items() for i in v) |
0 commit comments