Repository: conan-io/conan
Pull request #17367 (2024-11-23 23:04:24)

handle CMAKE_FIND_PROGRAM in new CMakeDeps
Changelog: Omit
Docs: Omit



Patch:
diff --git a/conan/cli/printers/graph.py b/conan/cli/printers/graph.py
index ddbd95326..931848037 100644
--- a/conan/cli/printers/graph.py
+++ b/conan/cli/printers/graph.py
@@ -91,6 +91,7 @@ def print_graph_basic(graph):
                     output.info(f"        {src_ref}->{option}={conflict_value}", Color.BRIGHT_CYAN)
         output.info("    It is recommended to define options values in profiles, not in recipes",
                     Color.BRIGHT_CYAN)
+        output.warning("There are options conflicts in the dependency graph", warn_tag="risk")
 
 
 def print_graph_packages(graph):
diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py
index b152569fd..0847d1848 100644
--- a/conan/tools/cmake/cmakedeps2/cmakedeps.py
+++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py
@@ -6,6 +6,7 @@ from jinja2 import Template
 
 from conan.api.output import Color
 from conan.internal import check_duplicated_generator
+from conan.internal.api.install.generators import relativize_path
 from conan.tools.cmake.cmakedeps2.config import ConfigTemplate2
 from conan.tools.cmake.cmakedeps2.config_version import ConfigVersionTemplate2
 from conan.tools.cmake.cmakedeps2.target_configuration import TargetConfigurationTemplate2
@@ -150,6 +151,13 @@ class CMakeDeps2:
         return get_transitive_requires(self._conanfile, conanfile)
 
 
+# TODO: Repeated from CMakeToolchain blocks
+def _join_paths(conanfile, paths):
+    paths = [p.replace('\\', '/').replace('$', '\\$').replace('"', '\\"') for p in paths]
+    paths = [relativize_path(p, conanfile, "${CMAKE_CURRENT_LIST_DIR}") for p in paths]
+    return " ".join([f'"{p}"' for p in paths])
+
+
 class _PathGenerator:
     _conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake"
 
@@ -165,6 +173,9 @@ class _PathGenerator:
             {% if host_runtime_dirs %}
             set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} )
             {% endif %}
+            {% if cmake_program_path %}
+            list(PREPEND CMAKE_PROGRAM_PATH {{ cmake_program_path }})
+            {% endif %}
             """)
 
         host_req = self._conanfile.dependencies.host
@@ -201,8 +212,26 @@ 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]
+
         context = {"host_runtime_dirs": self._get_host_runtime_dirs(),
-                   "pkg_paths": pkg_paths}
+                   "pkg_paths": pkg_paths,
+                   "cmake_program_path": _join_paths(self._conanfile, cmake_program_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 945ea9c18..862444722 100644
--- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py
+++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py
@@ -97,3 +97,54 @@ class TestRuntimeDirs:
         runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1)
         assert "<CONFIG:Release>" in runtime_lib_dirs
         assert "<CONFIG:Debug>" in runtime_lib_dirs
+
+
+@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
diff --git a/test/integration/options/options_test.py b/test/integration/options/options_test.py
index 8aedbbfb1..aac9dbc9c 100644
--- a/test/integration/options/options_test.py
+++ b/test/integration/options/options_test.py
@@ -744,11 +744,16 @@ class TestImportantOptions:
 
 class TestConflictOptionsWarnings:
 
-    def test_options_warnings(self):
+    @pytest.mark.parametrize("important", [True, False])
+    def test_options_warnings(self, important):
         c = TestClient()
         liba = GenConanfile("liba", "0.1").with_option("myoption", [1, 2, 3], default=1)
         libb = GenConanfile("libb", "0.1").with_requires("liba/0.1")
-        libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1", options={"myoption": 2})
+        if important:
+            libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1") \
+                                              .with_default_option("liba*:myoption!", 2)
+        else:
+            libc = GenConanfile("libc", "0.1").with_requirement("liba/0.1", options={"myoption": 2})
         app = GenConanfile().with_requires("libb/0.1", "libc/0.1")
 
         c.save({"liba/conanfile.py": liba,
@@ -767,3 +772,4 @@ class TestConflictOptionsWarnings:
                 It is recommended to define options values in profiles, not in recipes
             """)
         assert expected in c.out
+        assert "WARN: risk: There are options conflicts in the dependency graph" in c.out