From 31d320160a56c3fc0f49c11f471112e79720153b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ram=C3=ADrez?= Date: Mon, 3 Feb 2025 08:55:12 +0100 Subject: [PATCH] [incubating][CMakeDeps] Adding more CMake paths (#17668) * Refactored and added extra paths * Added tests * Added functional test. Fixed missing test requirements * Added dynamic libs too * fix test --------- Co-authored-by: memsharded --- conan/tools/cmake/cmakedeps2/cmakedeps.py | 80 ++++++---- .../cmakedeps/test_cmakedeps_new_paths.py | 148 ++++++++++++------ .../cmake/cmakedeps2/test_cmakedeps.py | 92 +++++++++++ 3 files changed, 240 insertions(+), 80 deletions(-) create mode 100644 test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py index a68a9b53e88..e190c268ac8 100644 --- a/conan/tools/cmake/cmakedeps2/cmakedeps.py +++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py @@ -165,30 +165,55 @@ def __init__(self, cmakedeps, conanfile): self._conanfile = conanfile self._cmakedeps = cmakedeps + def _get_cmake_paths(self, requirements, dirs_name): + paths = {} + cmake_vars = { + "bindirs": "CMAKE_PROGRAM_PATH", + "libdirs": "CMAKE_LIBRARY_PATH", + "includedirs": "CMAKE_INCLUDE_PATH", + } + for req, dep in requirements: + cppinfo = dep.cpp_info.aggregated_components() + cppinfo_dirs = getattr(cppinfo, dirs_name, []) + if not cppinfo_dirs: + continue + previous = paths.get(req.ref.name) + if previous: + self._conanfile.output.info(f"There is already a '{req.ref}' package contributing" + f" to {cmake_vars[dirs_name]}. Using the one" + f" defined by the context={dep.context}.") + paths[req.ref.name] = cppinfo_dirs + return [d for dirs in paths.values() for d in dirs] + def generate(self): template = textwrap.dedent("""\ - {% for pkg_name, folder in pkg_paths.items() %} - set({{pkg_name}}_DIR "{{folder}}") - {% endfor %} - {% if host_runtime_dirs %} - set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) - # Only for VS, needs CMake>=3.27 - set(CMAKE_VS_DEBUGGER_ENVIRONMENT "PATH=${CONAN_RUNTIME_LIB_DIRS};%PATH%") - {% endif %} - {% if cmake_program_path %} - list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }}) - {% endif %} - """) - + {% for pkg_name, folder in pkg_paths.items() %} + set({{pkg_name}}_DIR "{{folder}}") + {% endfor %} + {% if host_runtime_dirs %} + set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) + # Only for VS, needs CMake>=3.27 + set(CMAKE_VS_DEBUGGER_ENVIRONMENT "PATH=${CONAN_RUNTIME_LIB_DIRS};%PATH%") + {% endif %} + {% if cmake_program_path %} + list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }}) + {% endif %} + {% if cmake_library_path %} + list(PREPEND CMAKE_LIBRARY_PATH {{ cmake_library_path }}) + {% endif %} + {% if cmake_include_path %} + list(PREPEND CMAKE_INCLUDE_PATH {{ cmake_include_path }}) + {% endif %} + """) host_req = self._conanfile.dependencies.host build_req = self._conanfile.dependencies.direct_build test_req = self._conanfile.dependencies.test - + all_reqs = list(host_req.items()) + list(test_req.items()) + list(build_req.items()) # gen_folder = self._conanfile.generators_folder.replace("\\", "/") # if not, test_cmake_add_subdirectory test fails # content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)') pkg_paths = {} - for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): + for req, dep in all_reqs: cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep) cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG cmake_find_mode = cmake_find_mode.lower() @@ -214,26 +239,17 @@ def generate(self): # content.append(f'set({pkg_name}_ROOT "{gen_folder}")') pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}" - # CMAKE_PROGRAM_PATH - cmake_program_path = {} - for req, dep in list(host_req.items()) + list(test_req.items()) + list(build_req.items()): - if not req.direct: - continue - cppinfo = dep.cpp_info.aggregated_components() - if not cppinfo.bindirs: - continue - previous = cmake_program_path.get(req.ref.name) - if previous: - self._conanfile.output.info(f"There is already a '{req.ref}' package " - f"contributing to CMAKE_PROGRAM_PATH. The one with " - f"build={req.build} test={req.test} will be used") - - cmake_program_path[req.ref.name] = cppinfo.bindirs - cmake_program_path = [d for dirs in cmake_program_path.values() for d in dirs] + # CMAKE_PROGRAM_PATH | CMAKE_LIBRARY_PATH | CMAKE_INCLUDE_PATH + cmake_program_path = self._get_cmake_paths([(req, dep) for req, dep in all_reqs if req.direct], "bindirs") + cmake_library_path = self._get_cmake_paths(list(host_req.items()) + list(test_req.items()), "libdirs") + cmake_include_path = self._get_cmake_paths(list(host_req.items()) + list(test_req.items()), "includedirs") context = {"host_runtime_dirs": self._get_host_runtime_dirs(), "pkg_paths": pkg_paths, - "cmake_program_path": _join_paths(self._conanfile, cmake_program_path)} + "cmake_program_path": _join_paths(self._conanfile, cmake_program_path), + "cmake_library_path": _join_paths(self._conanfile, cmake_library_path), + "cmake_include_path": _join_paths(self._conanfile, cmake_include_path), + } content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context) save(self._conanfile, self._conan_cmakedeps_paths, content) diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py index 8e73c26982c..525b72c1181 100644 --- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py @@ -102,51 +102,103 @@ def test_runtime_lib_dirs_multiconf(self): @pytest.mark.tool("cmake") -@pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)]) -def test_cmaketoolchain_path_find_program(requires, tool_requires): - """Test that executables in bindirs of tool_requires can be found with - find_program() in consumer CMakeLists. - """ - c = TestClient() - - conanfile = textwrap.dedent(""" - import os - from conan.tools.files import copy - from conan import ConanFile - class TestConan(ConanFile): - name = "tool" - version = "1.0" - exports_sources = "*" - def package(self): - copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin")) - """) - c.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""}) - c.run("create .") - - requires = 'requires = "tool/1.0"' if requires else "" - tool_requires = 'tool_requires = "tool/1.0"' if tool_requires else "" - conanfile = textwrap.dedent(f""" - from conan import ConanFile - from conan.tools.cmake import CMake - class PkgConan(ConanFile): - {requires} - {tool_requires} - settings = "os", "arch", "compiler", "build_type" - generators = "CMakeToolchain", "CMakeDeps" - def build(self): - cmake = CMake(self) - cmake.configure() - """) - consumer = textwrap.dedent(""" - cmake_minimum_required(VERSION 3.15) - project(MyHello) - find_program(HELLOPROG hello) - if(HELLOPROG) - message(STATUS "Found hello prog: ${HELLOPROG}") - endif() - """) - c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") - assert "Found hello prog" in c.out - if requires and tool_requires: - assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out +class TestCMakeDepsPaths: + + @pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)]) + def test_find_program_path(self, requires, tool_requires): + """Test that executables in bindirs of tool_requires can be found with + find_program() in consumer CMakeLists. + """ + c = TestClient() + + conanfile = textwrap.dedent(""" + import os + from conan.tools.files import copy + from conan import ConanFile + class TestConan(ConanFile): + name = "tool" + version = "1.0" + exports_sources = "*" + def package(self): + copy(self, "*", self.source_folder, os.path.join(self.package_folder, "bin")) + """) + c.save({"conanfile.py": conanfile, "hello": "", "hello.exe": ""}) + c.run("create .") + + requires = 'requires = "tool/1.0"' if requires else "" + tool_requires = 'tool_requires = "tool/1.0"' if tool_requires else "" + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMake + class PkgConan(ConanFile): + {requires} + {tool_requires} + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain", "CMakeDeps" + def build(self): + cmake = CMake(self) + cmake.configure() + """) + consumer = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(MyHello) + find_program(HELLOPROG hello) + if(HELLOPROG) + message(STATUS "Found hello prog: ${HELLOPROG}") + endif() + """) + c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Found hello prog" in c.out + if requires and tool_requires: + assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out + + def test_find_include_and_lib_paths(self): + c = TestClient() + conanfile = textwrap.dedent(""" + import os + from conan.tools.files import copy + from conan import ConanFile + class TestConan(ConanFile): + name = "hello" + version = "1.0" + exports_sources = "*" + def package(self): + copy(self, "*.h", self.source_folder, os.path.join(self.package_folder, "include")) + copy(self, "*.lib", self.source_folder, os.path.join(self.package_folder, "lib")) + copy(self, "*.a", self.source_folder, os.path.join(self.package_folder, "lib")) + copy(self, "*.so", self.source_folder, os.path.join(self.package_folder, "lib")) + copy(self, "*.dll", self.source_folder, os.path.join(self.package_folder, "lib")) + """) + c.save({"conanfile.py": conanfile, + "hello.h": "", "hello.lib": "", "libhello.a": "", + "libhello.so": "", "libhello.dll": "" + }) + c.run("create .") + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMake + class PkgConan(ConanFile): + requires = "hello/1.0" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain", "CMakeDeps" + def build(self): + cmake = CMake(self) + cmake.configure() + """) + consumer = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(MyHello) + find_file(HELLOINC hello.h) + find_library(HELLOLIB hello) + if(HELLOINC) + message(STATUS "Found hello header: ${HELLOINC}") + endif() + if(HELLOLIB) + message(STATUS "Found hello lib: ${HELLOLIB}") + endif() + """) + c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Found hello header" in c.out + assert "Found hello lib" in c.out diff --git a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py new file mode 100644 index 00000000000..4b6001749d5 --- /dev/null +++ b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py @@ -0,0 +1,92 @@ +import re +import textwrap + +from conan.test.utils.tools import TestClient + +new_value = "will_break_next" + + +def test_cmakedeps_direct_deps_paths(): + c = TestClient() + conanfile = textwrap.dedent(""" + import os + from conan.tools.files import copy + from conan import ConanFile + class TestConan(ConanFile): + name = "lib" + version = "1.0" + def package_info(self): + self.cpp_info.includedirs = ["myincludes"] + self.cpp_info.libdirs = ["mylib"] + """) + c.save({"conanfile.py": conanfile}) + c.run("create .") + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMake + class PkgConan(ConanFile): + requires = "lib/1.0" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeDeps" + def build(self): + cmake = CMake(self) + cmake.configure() + """) + c.save({"conanfile.py": conanfile}, clean_first=True) + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + cmake_paths = c.load("conan_cmakedeps_paths.cmake") + assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/bin\"", cmake_paths) # default + assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/mylib\"", cmake_paths) + assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/myincludes\"", cmake_paths) + + +def test_cmakedeps_transitive_paths(): + c = TestClient() + conanfile = textwrap.dedent(""" + import os + from conan.tools.files import copy + from conan import ConanFile + class TestConan(ConanFile): + name = "liba" + version = "1.0" + def package_info(self): + self.cpp_info.includedirs = ["includea"] + self.cpp_info.libdirs = ["liba"] + self.cpp_info.bindirs = ["bina"] + """) + c.save({"conanfile.py": conanfile}) + c.run("create .") + conanfile = textwrap.dedent(""" + import os + from conan.tools.files import copy + from conan import ConanFile + class TestConan(ConanFile): + name = "libb" + version = "1.0" + requires = "liba/1.0" + def package_info(self): + self.cpp_info.includedirs = ["includeb"] + self.cpp_info.libdirs = ["libb"] + self.cpp_info.bindirs = ["binb"] + """) + c.save({"conanfile.py": conanfile}) + c.run("create .") + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMake + class PkgConan(ConanFile): + requires = "libb/1.0" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeDeps" + def build(self): + cmake = CMake(self) + cmake.configure() + """) + c.save({"conanfile.py": conanfile}, clean_first=True) + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + cmake_paths = c.load("conan_cmakedeps_paths.cmake") + cmake_paths.replace("\\", "/") + assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/libb.*/p/binb\"\)", cmake_paths) + assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths) + assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/libb.*/p/libb\" \".*/liba.*/p/liba\"\)", cmake_paths) + assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/libb.*/p/includeb\" \".*/liba.*/p/includea\"\)", cmake_paths)