Repository: conan-io/conan
Pull request: Consider adding cmakeExecutable to presets
Changelog: Feature: Add cmakeExecutable to configure preset.
Docs: https://github.com/conan-io/docs/pull/3548

Closes: https://github.com/conan-io/conan/issues/15427


Problem statement:
[feature] configurePresets.cmakeExecutable in CMakePresets.json should be filled in
### What is your suggestion?

In `CMakePresets.json`, you can set `configurePresets.cmakeExecutable` to set a specific CMake binary. 
While this doesn't work at alll with Visual Studio (stupid IDE insists on using built-in, outdated CMake version), it does work for IDEs like Visual Studio Code and similar ones.

This avoids a nasty issue when working a "hybrid" model between IDE with CMake integration mixed with conan commands in the same workspace.

Why you would want to do that?... E.g. because the *initial* configuration of the CMake cache is something which still behaves somewhat different between doing it within the IDE vs doing it in Conan. Stuff like environment variables required for compiler detection not being correctly set in the `CMakePresets.json`, and loads of other similar (environment related) issues.

When you have such a workflow, and you are working in a setup where CMake is distributed as a Conan dependency (and not installed system wide like in Conan Center), then you end up with Conan and the IDE trying to use different CMake versions on the same cache with guaranteed conflicts.

Specifying the CMake version `conan` would use in the presets ensures that configuration of the workspace with Conan, but subsequent work *without* Conan is possible in the first place.

While at it, also have a look at `configurePreset.environment` please, that's yet another thing Conan isn't filling in, leaving you in an odd situation where IDEs invoking CMake will do something ***entirely*** different from what  Conan had prepared as an environment.

Worst case they pick up an entirely different compiler toolchain. E.g. trying to use Ninja together with MSVC and VSCode is something which should work out of the box, but absolutely doesn't when Conan is in the mix so you have to start re-wire commands just to make sure at least VCVars is eventually called before CMake.

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide

Hints from issue discussion:
Hi @Ext3h 

Thanks for your suggestion.
This looks interesting, we knew that there was a gap when ``tool_requires("cmake/...")`` for certain cases when we recently added the ``env`` environment variables to the ``CMakePresets``. But we didn't think about ``cmakeExecutable``. If this can help for some setups/IDEs, it might be worth checking it. cc @czoido 
Never mind the rambling about `env` - looks like something about #11666 doesn't work for me in 1.61 when I inject the generator via profile, and *not* specify in the recipe itself.
Okay, figured that out as well. `conf::tools.cmake.cmaketoolchain:toolset_arch` is used as the *only* source of `configurePresets.toolset.value`.

But that's wrong... `settings::compiler.toolset` also needs to be included in `configurePresets.toolset.value`. That's why #13136 popped up / isn't handled by https://github.com/conan-io/conan/pull/11666 as is.

It's wrong in both Conan 1.x and 2.x, as far as I can tell.
> Hi @Ext3h
> 
> Thanks for your suggestion. This looks interesting, we knew that there was a gap when `tool_requires("cmake/...")` for certain cases when we recently added the `env` environment variables to the `CMakePresets`. But we didn't think about `cmakeExecutable`. If this can help for some setups/IDEs, it might be worth checking it. cc @czoido

Yes, I think it would make sense to add that variable to our presets, I'll make some tests with that.

Commit message: minor changes

Patch to review:
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 27792af73b9..c6fd71bd851 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -14,9 +14,11 @@
 
 
 def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables,
-                        user_presets_path=None, preset_prefix=None, buildenv=None, runenv=None):
+                        user_presets_path=None, preset_prefix=None, buildenv=None, runenv=None,
+                        cmake_executable=None):
     preset_path, preset_data = _CMakePresets.generate(conanfile, toolchain_file, generator,
-                                                      cache_variables, preset_prefix, buildenv, runenv)
+                                                      cache_variables, preset_prefix, buildenv, runenv,
+                                                      cmake_executable)
     _IncludingPresets.generate(conanfile, preset_path, user_presets_path, preset_prefix, preset_data)
 
 
@@ -24,7 +26,8 @@ class _CMakePresets:
     """ Conan generated main CMakePresets.json inside the generators_folder
     """
     @staticmethod
-    def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix, buildenv, runenv):
+    def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefix, buildenv, runenv,
+                 cmake_executable):
         cache_variables = cache_variables or {}
         if platform.system() == "Windows" and generator == "MinGW Makefiles":
             if "CMAKE_SH" not in cache_variables:
@@ -61,12 +64,13 @@ def generate(conanfile, toolchain_file, generator, cache_variables, preset_prefi
             _CMakePresets._insert_preset(data, "testPresets", test_preset)
             configure_preset = _CMakePresets._configure_preset(conanfile, generator, cache_variables,
                                                                toolchain_file, multiconfig,
-                                                               preset_prefix, buildenv)
+                                                               preset_prefix, buildenv,
+                                                               cmake_executable)
             # Conan generated presets should have only 1 configurePreset, no more, overwrite it
             data["configurePresets"] = [configure_preset]
         else:
             data = _CMakePresets._contents(conanfile, toolchain_file, cache_variables, generator,
-                                           preset_prefix, buildenv, runenv)
+                                           preset_prefix, buildenv, runenv, cmake_executable)
 
         preset_content = json.dumps(data, indent=4)
         save(preset_path, preset_content)
@@ -85,14 +89,14 @@ def _insert_preset(data, preset_type, preset):
 
     @staticmethod
     def _contents(conanfile, toolchain_file, cache_variables, generator, preset_prefix, buildenv,
-                  runenv):
+                  runenv, cmake_executable):
         """
         Contents for the CMakePresets.json
         It uses schema version 3 unless it is forced to 2
         """
         multiconfig = is_multi_configuration(generator)
         conf = _CMakePresets._configure_preset(conanfile, generator, cache_variables, toolchain_file,
-                                               multiconfig, preset_prefix, buildenv)
+                                               multiconfig, preset_prefix, buildenv, cmake_executable)
         build = _CMakePresets._build_preset_fields(conanfile, multiconfig, preset_prefix)
         test = _CMakePresets._test_preset_fields(conanfile, multiconfig, preset_prefix, runenv)
         ret = {"version": 3,
@@ -106,7 +110,7 @@ def _contents(conanfile, toolchain_file, cache_variables, generator, preset_pref
 
     @staticmethod
     def _configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig,
-                          preset_prefix, buildenv):
+                          preset_prefix, buildenv, cmake_executable):
         build_type = conanfile.settings.get_safe("build_type")
         name = _CMakePresets._configure_preset_name(conanfile, multiconfig)
         if preset_prefix:
@@ -124,6 +128,9 @@ def _configure_preset(conanfile, generator, cache_variables, toolchain_file, mul
         if buildenv:
             ret["environment"] = buildenv
 
+        if cmake_executable:
+            ret["cmakeExecutable"] = cmake_executable
+
         if is_msvc(conanfile):
             # We can force the generator Visual even if it is Ninja, to define the toolset
             toolset = GenericSystemBlock.get_toolset("Visual", conanfile)
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index f60259c4525..0b06fe4ac5c 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -184,6 +184,18 @@ def content(self):
         content = relativize_generated_file(content, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}")
         return content
 
+    def _find_cmake_exe(self):
+        for req in self._conanfile.dependencies.build.values():
+            if req.ref.name == "cmake":
+                for bindir in req.cpp_info.bindirs:
+                    cmake_path = os.path.join(bindir, "cmake")
+                    cmake_exe_path = os.path.join(bindir, "cmake.exe")
+
+                    if os.path.exists(cmake_path):
+                        return cmake_path
+                    elif os.path.exists(cmake_exe_path):
+                        return cmake_exe_path
+
     def generate(self):
         """
           This method will save the generated files to the conanfile.generators_folder
@@ -215,7 +227,7 @@ def generate(self):
             else:
                 cache_variables[name] = value
 
-        buildenv, runenv = None, None
+        buildenv, runenv, cmake_executable = None, None, None
 
         if self._conanfile.conf.get("tools.cmake.cmaketoolchain:presets_environment", default="",
                                     check_type=str, choices=("disabled", "")) != "disabled":
@@ -228,8 +240,11 @@ def generate(self):
             runenv = {name: value for name, value in
                       run_env.items(variable_reference="$penv{{{name}}}")}
 
+            cmake_executable = self._find_cmake_exe()
+
         write_cmake_presets(self._conanfile, toolchain, self.generator, cache_variables,
-                            self.user_presets_path, self.presets_prefix, buildenv, runenv)
+                            self.user_presets_path, self.presets_prefix, buildenv, runenv,
+                            cmake_executable)
 
     def _get_generator(self, recipe_generator):
         # Returns the name of the generator to be used by CMake
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 604aaf01d75..ae0566e6b32 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -1222,3 +1222,40 @@ def test_presets_njobs():
     c.run('install . -g CMakeToolchain -c tools.build:jobs=42')
     presets = json.loads(c.load("CMakePresets.json"))
     assert presets["buildPresets"][0]["jobs"] == 42
+
+
+def test_add_cmakeexe_to_presets():
+    c = TestClient()
+
+    tool = textwrap.dedent(r"""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import chdir, save
+        class Tool(ConanFile):
+            name = "cmake"
+            version = "3.27"
+            settings = "os", "compiler", "arch", "build_type"
+            def package(self):
+                with chdir(self, self.package_folder):
+                    save(self, "bin/{}", "")
+        """)
+
+    consumer = textwrap.dedent("""
+        [tool_requires]
+        cmake/3.27
+        [layout]
+        cmake_layout
+    """)
+
+    cmake_exe = "cmake.exe" if platform.system() == "Windows" else "cmake"
+
+    c.save({"tool.py": tool.format(cmake_exe),
+            "conanfile.txt": consumer})
+    c.run("create tool.py")
+    c.run("install . -g CMakeToolchain -g CMakeDeps")
+
+    presets_path = os.path.join("build", "Release", "generators", "CMakePresets.json") \
+        if platform.system() != "Windows" else os.path.join("build", "generators", "CMakePresets.json")
+    presets = json.loads(c.load(presets_path))
+
+    assert cmake_exe == os.path.basename(presets["configurePresets"][0].get("cmakeExecutable"))


Code review comments:
diff_hunk: @@ -184,6 +184,18 @@ def content(self):
         content = relativize_generated_file(content, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}")
         return content
 
+    def _find_cmake_exe(self):
+        for req in self._conanfile.dependencies.build.values():
path: conan/tools/cmake/toolchain/toolchain.py
text: @user1:
Better ``direct_build`` only?
```suggestion
        for req in self._conanfile.dependencies.direct_build.values():
```