Repository: conan-io/conan
Pull request #17668 (2025-01-30 06:20:42)

[incubating][CMakeDeps] Adding more CMake paths
Changelog: Feature: Added `CMAKE_LIBRARY_PATH` to `conan_cmakedeps_paths.cmake` (new CMakeDeps)
Changelog: Feature: Added `CMAKE_INCLUDE_PATH` to `conan_cmakedeps_paths.cmake` (new CMakeDeps)
Docs: omit



Patch:
diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py
index a68a9b53e..e190c268a 100644
--- a/conan/tools/cmake/cmakedeps2/cmakedeps.py
+++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py
@@ -165,30 +165,55 @@ class _PathGenerator:
         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 @@ class _PathGenerator:
             # 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)
 


Test patch:
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 8e73c2698..525b72c11 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 @@ class TestRuntimeDirs:
 
 
 @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 000000000..4b6001749
--- /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)