diff --git a/conan/tools/cmake/cmakedeps/cmakedeps.py b/conan/tools/cmake/cmakedeps/cmakedeps.py index 9ab2816c060..04964f18ffd 100644 --- a/conan/tools/cmake/cmakedeps/cmakedeps.py +++ b/conan/tools/cmake/cmakedeps/cmakedeps.py @@ -1,5 +1,10 @@ import os +import textwrap +import jinja2 +from jinja2 import Template + +from conan.api.output import Color from conan.internal import check_duplicated_generator from conan.tools.cmake.cmakedeps import FIND_MODE_CONFIG, FIND_MODE_NONE, FIND_MODE_BOTH, \ FIND_MODE_MODULE @@ -43,6 +48,7 @@ def generate(self): generator_files = self.content for generator_file, content in generator_files.items(): save(self._conanfile, generator_file, content) + self.generate_aggregator() @property def content(self): @@ -66,6 +72,7 @@ def content(self): "generator.".format(common_name)) # Iterate all the transitive requires + direct_configs = [] for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): # Require is not used at the moment, but its information could be used, # and will be used in Conan 2.0 @@ -87,6 +94,23 @@ def content(self): if cmake_find_mode in (FIND_MODE_MODULE, FIND_MODE_BOTH): self._generate_files(require, dep, ret, find_module_mode=True) + if require.direct: # aggregate config information for user convenience + find_module_mode = True if cmake_find_mode == FIND_MODE_MODULE else False + config = ConfigTemplate(self, require, dep, find_module_mode) + direct_configs.append(config) + + if direct_configs: + # Some helpful verbose messages about generated files + msg = ["CMakeDeps necessary find_package() and targets for your CMakeLists.txt"] + for config in direct_configs: + msg.append(f" find_package({config.file_name})") + targets = ' '.join(c.root_target_name for c in direct_configs) + msg.append(f" target_link_libraries(... {targets})") + if self._conanfile._conan_is_consumer: + self._conanfile.output.info("\n".join(msg), fg=Color.CYAN) + else: + self._conanfile.output.verbose("\n".join(msg)) + return ret def _generate_files(self, require, dep, ret, find_module_mode): @@ -162,3 +186,37 @@ def get_find_mode(self, dep): if tmp is None: return "config" return tmp.lower() + + def generate_aggregator(self): + host = self._conanfile.dependencies.host + build_req = self._conanfile.dependencies.direct_build + test_req = self._conanfile.dependencies.test + + configs = [] + for require, dep in list(host.items()) + list(build_req.items()) + list(test_req.items()): + if not require.direct: + continue + if require.build and dep.ref.name not in self.build_context_activated: + continue + cmake_find_mode = self.get_property("cmake_find_mode", dep) + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG + cmake_find_mode = cmake_find_mode.lower() + find_module_mode = True if cmake_find_mode == FIND_MODE_MODULE else False + config = ConfigTemplate(self, require, dep, find_module_mode) + configs.append(config) + + template = textwrap.dedent("""\ + message(STATUS "Conan: Using CMakeDeps conandeps_legacy.cmake aggregator via include()") + message(STATUS "Conan: It is recommended to use explicit find_package() per dependency instead") + + {% for config in configs %} + find_package({{config.file_name}}) + {% endfor %} + + set(CONANDEPS_LEGACY {% for t in configs %} {{t.root_target_name}} {% endfor %}) + """) + + template = Template(template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + conandeps = template.render({"configs": configs}) + save(self._conanfile, "conandeps_legacy.cmake", conandeps) diff --git a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_aggregator.py b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_aggregator.py new file mode 100644 index 00000000000..1d087965a31 --- /dev/null +++ b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_aggregator.py @@ -0,0 +1,44 @@ +import textwrap + +from conans.test.assets.sources import gen_function_cpp + + +def test_aggregator(transitive_libraries): + c = transitive_libraries + + conanfile = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Pkg(ConanFile): + settings = "os", "arch", "compiler", "build_type" + requires = "engine/1.0" + generators = "CMakeDeps", "CMakeToolchain" + exports_sources = "*" + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run(os.path.join(self.cpp.build.bindir, "app")) + """) + + cmakelists = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + project(app CXX) + include(${CMAKE_BINARY_DIR}/generators/conandeps_legacy.cmake) + add_executable(app main.cpp) + target_link_libraries(app ${CONANDEPS_LEGACY}) + """) + + c.save({ + "conanfile.py": conanfile, + "main.cpp": gen_function_cpp(name="main", includes=["engine"], calls=["engine"]), + "CMakeLists.txt": cmakelists + }, clean_first=True) + c.run("build .") + assert "matrix/1.0: Hello World Release!" in c.out + assert "engine/1.0: Hello World Release!" in c.out