Repository: conan-io/conan
Pull request #11503 (2022-06-22 10:22:03)

Cache variables declaration in CMakeToolchain
Changelog: Feature: Added `CMakeToolchain(self).cache_variables` to declare `cache_variables`  in the `configurePresets` in the `CMakePresets.json` file. Conan reads them to call `cmake` with `-D` when using the `CMake(self)` build helper. These cache variables doesn't support multi-configuration generators.
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/11476


Patch:
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index b222091c9..918f6a831 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -42,7 +42,6 @@ def _configure_preset_name(conanfile, multiconfig):
         return str(build_type).lower()
 
 
-
 def _add_configure_preset(conanfile, generator, cache_variables, toolchain_file, multiconfig):
     build_type = conanfile.settings.get_safe("build_type")
     name = _configure_preset_name(conanfile, multiconfig)
@@ -78,15 +77,19 @@ def _contents(conanfile, toolchain_file, cache_variables, generator):
     return ret
 
 
-def write_cmake_presets(conanfile, toolchain_file, generator):
-    cache_variables = {}
+def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables):
+    cache_variables = cache_variables or {}
     if platform.system() == "Windows" and generator == "MinGW Makefiles":
-        cache_variables["CMAKE_SH"] = "CMAKE_SH-NOTFOUND"
-        cmake_make_program = conanfile.conf.get("tools.gnu:make_program", default=None)
+        if "CMAKE_SH" not in cache_variables:
+            cache_variables["CMAKE_SH"] = "CMAKE_SH-NOTFOUND"
+
+        cmake_make_program = conanfile.conf.get("tools.gnu:make_program", default=cache_variables.get("CMAKE_MAKE_PROGRAM"))
         if cmake_make_program:
             cmake_make_program = cmake_make_program.replace("\\", "/")
             cache_variables["CMAKE_MAKE_PROGRAM"] = cmake_make_program
-    cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] = "NEW"
+
+    if "CMAKE_POLICY_DEFAULT_CMP0091" not in cache_variables:
+        cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] = "NEW"
 
     preset_path = os.path.join(conanfile.generators_folder, "CMakePresets.json")
     multiconfig = is_multi_configuration(generator)
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index 1b5319d18..9b74a5b76 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -119,6 +119,8 @@ class CMakeToolchain(object):
         self._conanfile = conanfile
         self.generator = self._get_generator(generator)
         self.variables = Variables()
+        # This doesn't support multi-config, they go to the same configPreset common in multi-config
+        self.cache_variables = {}
         self.preprocessor_definitions = Variables()
 
         self.blocks = ToolchainBlocks(self._conanfile, self,
@@ -175,7 +177,14 @@ class CMakeToolchain(object):
         elif self.generator is not None and "Visual" not in self.generator:
             VCVars(self._conanfile).generate()
         toolchain = os.path.join(self._conanfile.generators_folder, toolchain_file or self.filename)
-        write_cmake_presets(self._conanfile, toolchain, self.generator)
+        cache_variables = {}
+        for name, value in self.cache_variables.items():
+            if isinstance(value, bool):
+                cache_variables[name] = "ON" if value else "OFF"
+            else:
+                cache_variables[name] = value
+
+        write_cmake_presets(self._conanfile, toolchain, self.generator, cache_variables)
 
     def _get_generator(self, recipe_generator):
         # Returns the name of the generator to be used by CMake


Test patch:
diff --git a/conans/test/conftest.py b/conans/test/conftest.py
index ea39c11b2..07905171f 100644
--- a/conans/test/conftest.py
+++ b/conans/test/conftest.py
@@ -169,65 +169,65 @@ def _get_tool(name, version):
     # True = tool not available, test error
     # (path, env) = tool available
     cached = _cached_tools.setdefault(name, {}).get(version)
-    if cached is None:
-        tool = tools_locations.get(name, {})
-        if tool.get("disabled"):
-            _cached_tools[name][version] = False
-            return False
+    if cached is not None:
+        return cached
+    result = _get_individual_tool(name, version)
+    _cached_tools[name][version] = result
+    return result
 
-        tool_platform = platform.system()
-        if tool.get("platform", tool_platform) != tool_platform:
-            _cached_tools[name][version] = None, None
-            return None, None
 
-        exe = tool.get("exe", name)
-        version = version or tool.get("default")
-        tool_version = tool.get(version)
-        if tool_version is not None:
-            assert isinstance(tool_version, dict)
-            if tool_version.get("disabled"):
-                _cached_tools[name][version] = False
-                return False
-            tool_path = tool_version.get("path", {}).get(tool_platform)
-            # To allow to skip for a platform, we can put the path to None
-            # "cmake": { "3.23": {
-            #               "path": {'Windows': 'C:/cmake/cmake-3.23.1-win64-x64/bin',
-            #                        'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin',
-            #                        'Linux': None}}
-            #          }
-            if tool_path is None:
-                _cached_tools[name][version] = False
-                return False
+def _get_individual_tool(name, version):
+    tool = tools_locations.get(name, {})
+    if tool.get("disabled"):
+        return False
 
-        else:
-            if version is not None:  # if the version is specified, it should be in the conf
-                _cached_tools[name][version] = True
-                return True
-            tool_path = None
+    tool_platform = platform.system()
+    if tool.get("platform", tool_platform) != tool_platform:
+        return None, None
 
-        try:
-            tool_env = tools_environments[name][tool_platform]
-        except KeyError:
-            tool_env = None
+    version = version or tool.get("default")
+    tool_version = tool.get(version)
+    if tool_version is not None:
+        assert isinstance(tool_version, dict)
+        if tool_version.get("disabled"):
+            return False
+        if name == "visual_studio":
+            if vswhere():  # TODO: Missing version detection
+                return None, None
+
+        tool_path = tool_version.get("path", {}).get(tool_platform)
+        # To allow to skip for a platform, we can put the path to None
+        # "cmake": { "3.23": {
+        #               "path": {'Windows': 'C:/cmake/cmake-3.23.1-win64-x64/bin',
+        #                        'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin',
+        #                        'Linux': None}}
+        #          }
+        if tool_path is None:
+            return False
+    else:
+        if version is not None:  # if the version is specified, it should be in the conf
+            return True
+        tool_path = None
 
-        cached = tool_path, tool_env
+    try:
+        tool_env = tools_environments[name][tool_platform]
+    except KeyError:
+        tool_env = None
 
-        # Check this particular tool is installed
-        if name == "visual_studio":
-            if not vswhere():  # TODO: Missing version detection
-                cached = True
-        else:  # which based detection
-            old_environ = None
-            if tool_path is not None:
-                old_environ = dict(os.environ)
-                os.environ["PATH"] = tool_path + os.pathsep + os.environ["PATH"]
-            if not which(exe):  # TODO: This which doesn't detect version either
-                cached = True
-            if old_environ is not None:
-                os.environ.clear()
-                os.environ.update(old_environ)
+    cached = tool_path, tool_env
 
-        _cached_tools[name][version] = cached
+    # Check this particular tool is installed
+    old_environ = None
+    if tool_path is not None:
+        old_environ = dict(os.environ)
+        os.environ["PATH"] = tool_path + os.pathsep + os.environ["PATH"]
+    exe = tool.get("exe", name)
+    exe_found = which(exe)  # TODO: This which doesn't detect version either
+    if not exe_found:
+        cached = True
+    if old_environ is not None:
+        os.environ.clear()
+        os.environ.update(old_environ)
 
     return cached
 
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 41a46681e..967329a5d 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -4,6 +4,7 @@ import platform
 import textwrap
 
 import pytest
+from mock import mock
 
 from conan.tools.cmake.presets import load_cmake_presets
 from conans.test.assets.genconanfile import GenConanfile
@@ -439,3 +440,36 @@ def test_cmake_presets_singleconfig():
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     presets = json.loads(client.load("CMakePresets.json"))
     assert len(presets["configurePresets"]) == 2
+
+
+def test_toolchain_cache_variables():
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        from conan.tools.cmake import CMakeToolchain, CMake
+
+        class Conan(ConanFile):
+            settings = "os", "arch", "compiler", "build_type"
+
+            def generate(self):
+                toolchain = CMakeToolchain(self)
+                toolchain.cache_variables["foo"] = True
+                toolchain.cache_variables["foo2"] = False
+                toolchain.cache_variables["var"] = "23"
+                toolchain.cache_variables["CMAKE_SH"] = "THIS VALUE HAS PRIORITY"
+                toolchain.cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] = "THIS VALUE HAS PRIORITY"
+                toolchain.cache_variables["CMAKE_MAKE_PROGRAM"] = "THIS VALUE HAS NO PRIORITY"
+                toolchain.generate()
+        """)
+    client.save({"conanfile.py": conanfile})
+    with mock.patch("platform.system", mock.MagicMock(return_value="Windows")):
+        client.run("install . mylib/1.0@ -c tools.cmake.cmaketoolchain:generator='MinGW Makefiles' "
+                   "-c tools.gnu:make_program='MyMake'")
+    presets = json.loads(client.load("CMakePresets.json"))
+    cache_variables = presets["configurePresets"][0]["cacheVariables"]
+    assert cache_variables["foo"] == 'ON'
+    assert cache_variables["foo2"] == 'OFF'
+    assert cache_variables["var"] == '23'
+    assert cache_variables["CMAKE_SH"] == "THIS VALUE HAS PRIORITY"
+    assert cache_variables["CMAKE_POLICY_DEFAULT_CMP0091"] == "THIS VALUE HAS PRIORITY"
+    assert cache_variables["CMAKE_MAKE_PROGRAM"] == "MyMake"
diff --git a/conans/test/unittests/tools/cmake/test_cmake_presets_definitions.py b/conans/test/unittests/tools/cmake/test_cmake_presets_definitions.py
index cb3f30709..d350c184b 100644
--- a/conans/test/unittests/tools/cmake/test_cmake_presets_definitions.py
+++ b/conans/test/unittests/tools/cmake/test_cmake_presets_definitions.py
@@ -40,7 +40,7 @@ def test_cmake_make_program(conanfile):
     conanfile.conf.define("tools.gnu:make_program", "C:\\mymake.exe")
 
     with mock.patch("platform.system", mock.MagicMock(return_value='Windows')):
-        write_cmake_presets(conanfile, "the_toolchain.cmake", "MinGW Makefiles")
+        write_cmake_presets(conanfile, "the_toolchain.cmake", "MinGW Makefiles", {})
 
     cmake = CMake(conanfile)
     cmake.configure()