Repository: conan-io/conan
Pull request #12556 (2022-11-16 17:39:50)

[CMakeToolchain] Setting `CMAKE_<LANG>_COMPILER`
Changelog: Feature: `CMakeToolchain` setting `CMAKE_<LANG>_COMPILER` via `conf` variable (`tools.build:compiler_executables`).
Changelog: Fix: remove hardcoded definitions of CMAKE_CXX_COMPILER in `CMakeToolchain`.
Docs: https://github.com/conan-io/docs/pull/XXXX
Closes: https://github.com/conan-io/conan/issues/9962

 -- Coming from https://github.com/conan-io/conan/pull/12577 (CLOSED)

The hardcoded definition of CMAKE_XXX_COMPILER in CMakeToolchain can be removed:

* In Ninja it is good if VCVars activate the environment and put cl.exe there
* For Windows-Clang, it is necessary to add CC/CXX in [buildenv] in the profile. This is not great, because it forces users to have to activate the environment too. It would be better if there is a built-in mechanism like in https://github.com/conan-io/conan/pull/12556 that puts it directly in conan_toolchain.cmake


franramirez688: Pointing to your comments and the original purpose of this PR, I've proposed another point of view exclusively for CMake. Let's discuss this new approach.
czoido: Just a couple of comments. I think the last approach is much better, I really like it! On my side it's ready once they are solved

Patch:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index 698e050d2..f22d6f760 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -11,7 +11,7 @@ from conan.tools.build import build_jobs
 from conan.tools.build.cross_building import cross_building
 from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
 from conan.tools.intel import IntelCC
-from conan.tools.microsoft.visual import is_msvc, msvc_version_to_toolset_version
+from conan.tools.microsoft.visual import msvc_version_to_toolset_version
 from conans.client.subsystems import deduce_subsystem, WINDOWS
 from conans.errors import ConanException
 from conans.util.files import load
@@ -667,6 +667,30 @@ class TryCompileBlock(Block):
         """)
 
 
+class CompilersBlock(Block):
+    template = textwrap.dedent(r"""
+        {% for lang, compiler_path in compilers.items() %}
+        set(CMAKE_{{ lang }}_COMPILER "{{ compiler_path|replace('\\', '/') }}")
+        {% endfor %}
+    """)
+
+    def context(self):
+        # Reading configuration from "tools.build:compiler_executables" -> {"C": "/usr/bin/gcc"}
+        compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={},
+                                                     check_type=dict)
+        # Map the possible languages
+        compilers = {}
+        # Allowed <LANG> variables (and <LANG>_LAUNCHER)
+        compilers_mapping = {"c": "C", "cuda": "CUDA", "cpp": "CXX", "objc": "OBJC",
+                             "objcxx": "OBJCXX", "rc": "RC", 'fortran': "Fortran", 'asm': "ASM",
+                             "hip": "HIP", "ispc": "ISPC"}
+        for comp, lang in compilers_mapping.items():
+            # To set CMAKE_<LANG>_COMPILER
+            if comp in compilers_by_conf:
+                compilers[lang] = compilers_by_conf[comp]
+        return {"compilers": compilers}
+
+
 class GenericSystemBlock(Block):
     template = textwrap.dedent("""
         {% if cmake_sysroot %}
@@ -690,21 +714,6 @@ class GenericSystemBlock(Block):
         {% if toolset %}
         set(CMAKE_GENERATOR_TOOLSET "{{ toolset }}" CACHE STRING "" FORCE)
         {% endif %}
-        {% if compiler %}
-        if(NOT DEFINED ENV{CC})
-        set(CMAKE_C_COMPILER {{ compiler }})
-        endif()
-        {% endif %}
-        {% if compiler_cpp %}
-        if(NOT DEFINED ENV{CXX})
-        set(CMAKE_CXX_COMPILER {{ compiler_cpp }})
-        endif()
-        {% endif %}
-        {% if compiler_rc %}
-        if(NOT DEFINED ENV{RC})
-        set(CMAKE_RC_COMPILER {{ compiler_rc }})
-        endif()
-        {% endif %}
         """)
 
     def _get_toolset(self, generator):
@@ -766,23 +775,6 @@ class GenericSystemBlock(Block):
                     "armv8": "ARM64"}.get(arch)
         return None
 
-    def _get_compiler(self, generator):
-        compiler = self._conanfile.settings.get_safe("compiler")
-        os_ = self._conanfile.settings.get_safe("os")
-
-        compiler_c = compiler_cpp = compiler_rc = None
-
-        # TODO: Check if really necessary now that conanvcvars is used
-        if "Ninja" in str(generator) and is_msvc(self._conanfile):
-            compiler_c = compiler_cpp = "cl"
-        elif os_ == "Windows" and compiler == "clang" and "Visual" not in generator:
-            # definition of compiler only if not using the VS Clang toolset
-            compiler_rc = "clang"
-            compiler_c = "clang"
-            compiler_cpp = "clang++"
-
-        return compiler_c, compiler_cpp, compiler_rc
-
     def _get_generic_system_name(self):
         os_host = self._conanfile.settings.get_safe("os")
         os_build = self._conanfile.settings_build.get_safe("os")
@@ -843,19 +835,13 @@ class GenericSystemBlock(Block):
         generator = self._toolchain.generator
         generator_platform = self._get_generator_platform(generator)
         toolset = self._get_toolset(generator)
-
-        compiler, compiler_cpp, compiler_rc = self._get_compiler(generator)
-
         system_name, system_version, system_processor = self._get_cross_build()
 
         # This is handled by the tools.apple:sdk_path and CMAKE_OSX_SYSROOT in Apple
         cmake_sysroot = self._conanfile.conf.get("tools.build:sysroot")
         cmake_sysroot = cmake_sysroot.replace("\\", "/") if cmake_sysroot is not None else None
 
-        return {"compiler": compiler,
-                "compiler_rc": compiler_rc,
-                "compiler_cpp": compiler_cpp,
-                "toolset": toolset,
+        return {"toolset": toolset,
                 "generator_platform": generator_platform,
                 "cmake_system_name": system_name,
                 "cmake_system_version": system_version,
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index 599a9338c..f77849c51 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -12,7 +12,7 @@ from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
 from conan.tools.cmake.toolchain.blocks import ToolchainBlocks, UserToolchain, GenericSystemBlock, \
     AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, \
     CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, \
-    SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock
+    SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock
 from conan.tools.intel import IntelCC
 from conan.tools.microsoft import VCVars
 from conan.tools.microsoft.visual import vs_ide_version
@@ -127,6 +127,7 @@ class CMakeToolchain(object):
         self.blocks = ToolchainBlocks(self._conanfile, self,
                                       [("user_toolchain", UserToolchain),
                                        ("generic_system", GenericSystemBlock),
+                                       ("compilers", CompilersBlock),
                                        ("android_system", AndroidSystemBlock),
                                        ("apple_system", AppleSystemBlock),
                                        ("fpic", FPicBlock),
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index 7b0f735df..df84c6c2d 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -123,9 +123,12 @@ class AutotoolsToolchain:
 
     def environment(self):
         env = Environment()
-        if is_msvc(self._conanfile):
-            env.define("CXX", "cl")
-            env.define("CC", "cl")
+        compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={}, check_type=dict)
+        if compilers_by_conf:
+            compilers_mapping = {"c": "CC", "cpp": "CXX", "cuda": "NVCC", "fortran": "FC"}
+            for comp, env_var in compilers_mapping.items():
+                if comp in compilers_by_conf:
+                    env.define(env_var, compilers_by_conf[comp])
         env.append("CPPFLAGS", ["-D{}".format(d) for d in self.defines])
         env.append("CXXFLAGS", self.cxxflags)
         env.append("CFLAGS", self.cflags)
diff --git a/conan/tools/meson/toolchain.py b/conan/tools/meson/toolchain.py
index b75fec4bd..e5ca38954 100644
--- a/conan/tools/meson/toolchain.py
+++ b/conan/tools/meson/toolchain.py
@@ -163,10 +163,14 @@ class MesonToolchain(object):
                 default_comp = "gcc"
                 default_comp_cpp = "g++"
 
+        # Read configuration for compilers
+        compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={},
+                                                     check_type=dict)
         # Read the VirtualBuildEnv to update the variables
         build_env = VirtualBuildEnv(self._conanfile).vars()
-        self.c = build_env.get("CC") or default_comp
-        self.cpp = build_env.get("CXX") or default_comp_cpp
+        self.c = compilers_by_conf.get("c") or build_env.get("CC") or default_comp
+        self.cpp = compilers_by_conf.get("cpp") or build_env.get("CXX") or default_comp_cpp
+        # FIXME: Should we use the new tools.build:compiler_executables and avoid buildenv?
         self.c_ld = build_env.get("CC_LD") or build_env.get("LD")
         self.cpp_ld = build_env.get("CXX_LD") or build_env.get("LD")
         self.ar = build_env.get("AR")
@@ -190,7 +194,7 @@ class MesonToolchain(object):
         self.objcpp_args = []
         self.objcpp_link_args = []
 
-        self._resolve_apple_flags_and_variables(build_env)
+        self._resolve_apple_flags_and_variables(build_env, compilers_by_conf)
         self._resolve_android_cross_compilation()
 
     def _get_default_dirs(self):
@@ -232,7 +236,7 @@ class MesonToolchain(object):
             ret["libdir"] = libdir
         return ret
 
-    def _resolve_apple_flags_and_variables(self, build_env):
+    def _resolve_apple_flags_and_variables(self, build_env, compilers_by_conf):
         if not self._is_apple_system:
             return
         # SDK path is mandatory for cross-building
@@ -251,8 +255,8 @@ class MesonToolchain(object):
         self.apple_isysroot_flag = ["-isysroot", sdk_path] if sdk_path else []
         self.apple_min_version_flag = [apple_min_version_flag(self._conanfile)]
         # Objective C/C++ ones
-        self.objc = "clang"
-        self.objcpp = "clang++"
+        self.objc = compilers_by_conf.get("objc", "clang")
+        self.objcpp = compilers_by_conf.get("objcxx", "clang++")
         self.objc_args = self._get_env_list(build_env.get('OBJCFLAGS', []))
         self.objc_link_args = self._get_env_list(build_env.get('LDFLAGS', []))
         self.objcpp_args = self._get_env_list(build_env.get('OBJCXXFLAGS', []))
diff --git a/conans/model/conf.py b/conans/model/conf.py
index b7044625b..e19f04017 100644
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -52,6 +52,7 @@ BUILT_IN_CONFS = {
     "tools.build:defines": "List of extra definition flags used by different toolchains like CMakeToolchain and AutotoolsToolchain",
     "tools.build:sharedlinkflags": "List of extra flags used by CMakeToolchain for CMAKE_SHARED_LINKER_FLAGS_INIT variable",
     "tools.build:exelinkflags": "List of extra flags used by CMakeToolchain for CMAKE_EXE_LINKER_FLAGS_INIT variable",
+    "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcxx', 'rc', 'fortran', 'asm', 'hip', 'ispc'}",
     "tools.microsoft.bash:subsystem": "Set subsystem to use for Windows. Possible values: 'msys2', 'msys', 'cygwin', 'wsl' and 'sfu'",
     "tools.microsoft.bash:path": "Path to the shell executable. Default: 'bash'",
     "tools.apple:sdk_path": "Path for the sdk location. This value will be passed as SDKROOT or -isysroot depending on the generator used",


Test patch:
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
index 368645491..a6c30e1b7 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
@@ -27,6 +27,8 @@ def client():
         build_type=Release
         compiler=clang
         compiler.version=12
+        [conf]
+        tools.build:compiler_executables={"cpp": "clang++", "c": "clang", "rc": "clang"}
         """)
     conanfile = textwrap.dedent("""
         import os
diff --git a/conans/test/functional/toolchains/gnu/autotools/test_win_bash.py b/conans/test/functional/toolchains/gnu/autotools/test_win_bash.py
index 1fbb389a8..28d0972a2 100644
--- a/conans/test/functional/toolchains/gnu/autotools/test_win_bash.py
+++ b/conans/test/functional/toolchains/gnu/autotools/test_win_bash.py
@@ -20,6 +20,7 @@ def test_autotools_bash_complete():
     save(client.cache.new_config_path, textwrap.dedent("""
             tools.microsoft.bash:subsystem=msys2
             tools.microsoft.bash:path={}
+            tools.build:compiler_executables={{"c": "cl", "cpp": "cl"}}
             """.format(bash_path)))
 
     main = gen_function_cpp(name="main")
@@ -31,7 +32,6 @@ def test_autotools_bash_complete():
     conanfile = textwrap.dedent("""
         from conans import ConanFile
         from conan.tools.gnu import Autotools
-        from conan.tools.env import Environment
 
         class TestConan(ConanFile):
             settings = "os", "compiler", "arch", "build_type"
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 1b1a9b76c..786c06bf4 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -784,3 +784,28 @@ def test_pkg_config_block():
     pkg_config_path_set = 'set(ENV{PKG_CONFIG_PATH} "%s$ENV{PKG_CONFIG_PATH}")' % \
                           (client.current_folder.replace("\\", "/") + pathsep)
     assert pkg_config_path_set in toolchain
+
+
+def test_set_cmake_lang_compilers_and_launchers():
+    profile = textwrap.dedent(r"""
+    [settings]
+    os=Windows
+    arch=x86_64
+    compiler=clang
+    compiler.version=15
+    compiler.libcxx=libstdc++11
+    [conf]
+    tools.build:compiler_executables={"c": "/my/local/gcc", "cpp": "g++", "rc": "C:\\local\\rc.exe"}
+    """)
+    client = TestClient(path_with_spaces=False)
+    conanfile = GenConanfile().with_settings("os", "arch", "compiler")\
+        .with_generator("CMakeToolchain")
+    client.save({"conanfile.py": conanfile,
+                "profile": profile})
+    client.run("install . -pr:b profile -pr:h profile")
+    toolchain = client.load("conan_toolchain.cmake")
+    assert 'set(CMAKE_C_COMPILER clang)' not in toolchain
+    assert 'set(CMAKE_CXX_COMPILER clang++)' not in toolchain
+    assert 'set(CMAKE_C_COMPILER "/my/local/gcc")' in toolchain
+    assert 'set(CMAKE_CXX_COMPILER "g++")' in toolchain
+    assert 'set(CMAKE_RC_COMPILER "C:/local/rc.exe")' in toolchain
diff --git a/conans/test/unittests/tools/cmake/test_cmaketoolchain.py b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
index 2d30b9eb1..2b9670452 100644
--- a/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
+++ b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
@@ -29,6 +29,7 @@ def conanfile():
     c.settings.compiler.libcxx = "libstdc++"
     c.settings.os = "Windows"
     c.conf = Conf()
+    c.conf.define("tools.cmake.cmaketoolchain:system_name", "potato")
     c.folders.set_base_generators(".")
     c._conan_node = Mock()
     c._conan_node.dependencies = []
@@ -38,29 +39,29 @@ def conanfile():
 def test_cmake_toolchain(conanfile):
     toolchain = CMakeToolchain(conanfile)
     content = toolchain.content
-    assert 'set(CMAKE_C_COMPILER clang)' in content
+    assert 'set(CMAKE_SYSTEM_NAME potato)' in content
 
 
 def test_remove(conanfile):
     toolchain = CMakeToolchain(conanfile)
     toolchain.blocks.remove("generic_system")
     content = toolchain.content
-    assert 'CMAKE_C_COMPILER' not in content
+    assert 'CMAKE_SYSTEM_NAME' not in content
 
 
 def test_template_remove(conanfile):
     toolchain = CMakeToolchain(conanfile)
     toolchain.blocks["generic_system"].template = ""
     content = toolchain.content
-    assert 'CMAKE_C_COMPILER' not in content
+    assert 'CMAKE_SYSTEM_NAME' not in content
 
 
 def test_template_change(conanfile):
     toolchain = CMakeToolchain(conanfile)
     tmp = toolchain.blocks["generic_system"].template
-    toolchain.blocks["generic_system"].template = tmp.replace("CMAKE_C_COMPILER", "OTHER_THING")
+    toolchain.blocks["generic_system"].template = tmp.replace("CMAKE_SYSTEM_NAME", "OTHER_THING")
     content = toolchain.content
-    assert 'set(OTHER_THING clang)' in content
+    assert 'set(OTHER_THING potato)' in content
 
 
 def test_context_change(conanfile):
@@ -69,25 +70,26 @@ def test_context_change(conanfile):
 
     def context(self):
         assert self
-        return {"compiler": None}
+        return {"cmake_system_name": None}
+
     tmp.context = types.MethodType(context, tmp)
     content = toolchain.content
-    assert 'CMAKE_C_COMPILER' not in content
+    assert 'CMAKE_SYSTEM_NAME' not in content
 
 
 def test_context_update(conanfile):
     toolchain = CMakeToolchain(conanfile)
-    compiler = toolchain.blocks["generic_system"].values["compiler"]
-    toolchain.blocks["generic_system"].values["compiler"] = "Super" + compiler
+    cmake_system_name = toolchain.blocks["generic_system"].values["cmake_system_name"]
+    toolchain.blocks["generic_system"].values["cmake_system_name"] = "Super" + cmake_system_name
     content = toolchain.content
-    assert 'set(CMAKE_C_COMPILER Superclang)' in content
+    assert 'set(CMAKE_SYSTEM_NAME Superpotato)' in content
 
 
 def test_context_replace(conanfile):
     toolchain = CMakeToolchain(conanfile)
-    toolchain.blocks["generic_system"].values = {"compiler": "SuperClang"}
+    toolchain.blocks["generic_system"].values = {"cmake_system_name": "SuperPotato"}
     content = toolchain.content
-    assert 'set(CMAKE_C_COMPILER SuperClang)' in content
+    assert 'set(CMAKE_SYSTEM_NAME SuperPotato)' in content
 
 
 def test_replace_block(conanfile):
@@ -102,7 +104,7 @@ def test_replace_block(conanfile):
     toolchain.blocks["generic_system"] = MyBlock
     content = toolchain.content
     assert 'HelloWorld' in content
-    assert 'CMAKE_C_COMPILER' not in content
+    assert 'set(CMAKE_SYSTEM_NAME potato)' not in content
 
 
 def test_add_new_block(conanfile):
@@ -117,7 +119,7 @@ def test_add_new_block(conanfile):
     toolchain.blocks["mynewblock"] = MyBlock
     content = toolchain.content
     assert 'Hello World!!!' in content
-    assert 'CMAKE_C_COMPILER' in content
+    assert 'set(CMAKE_SYSTEM_NAME potato)' in content
 
 
 def test_user_toolchain(conanfile):