Repository: conan-io/conan
Pull request #12829 (2023-01-03 02:26:08)

Fixing cmake_layout single config
Changelog: Bugfix: Make ``cmake_layout`` to use single-config folder for generated files
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/12827


Patch:
diff --git a/conan/tools/cmake/layout.py b/conan/tools/cmake/layout.py
index f39ef6974..abddc047f 100644
--- a/conan/tools/cmake/layout.py
+++ b/conan/tools/cmake/layout.py
@@ -22,20 +22,21 @@ def cmake_layout(conanfile, generator=None, src_folder=".", build_folder="build"
         raise ConanException("'build_type' setting not defined, it is necessary for cmake_layout()")
 
     build_folder = build_folder if not subproject else os.path.join(subproject, build_folder)
-    custom_conf = get_build_folder_custom_vars(conanfile)
-    if custom_conf:
-        build_folder = os.path.join(build_folder, custom_conf)
+    config_build_folder, user_defined_build = get_build_folder_custom_vars(conanfile)
 
-    if multi:
-        conanfile.folders.build = build_folder
+    if config_build_folder:
+        conanfile.folders.build = os.path.join(build_folder, config_build_folder)
     else:
-        conanfile.folders.build = os.path.join(build_folder, build_type)
+        if not multi and not user_defined_build:
+            conanfile.folders.build = os.path.join(build_folder, build_type)
+        else:
+            conanfile.folders.build = build_folder
 
-    conanfile.folders.generators = os.path.join(build_folder, "generators")
+    conanfile.folders.generators = os.path.join(conanfile.folders.build, "generators")
 
     conanfile.cpp.source.includedirs = ["include"]
 
-    if multi:
+    if multi and not user_defined_build:
         conanfile.cpp.build.libdirs = ["{}".format(build_type)]
         conanfile.cpp.build.bindirs = ["{}".format(build_type)]
     else:
@@ -49,17 +50,11 @@ def get_build_folder_custom_vars(conanfile):
                                     default=[], check_type=list)
     ret = []
     for s in build_vars:
+        group, var = s.split(".", 1)
         tmp = None
-        if s.startswith("settings."):
-            _, var = s.split("settings.", 1)
-            if var == "build_type":
-                raise ConanException("Error, don't include 'settings.build_type' in the "
-                                     "'tools.cmake.cmake_layout:build_folder_vars' conf. It is "
-                                     "managed by default because 'CMakeToolchain' and 'CMakeDeps' "
-                                     "are multi-config generators.`")
+        if group == "settings":
             tmp = conanfile.settings.get_safe(var)
-        elif s.startswith("options."):
-            _, var = s.split("options.", 1)
+        elif group == "options":
             value = conanfile.options.get_safe(var)
             if value is not None:
                 tmp = "{}_{}".format(var, value)
@@ -69,4 +64,5 @@ def get_build_folder_custom_vars(conanfile):
         if tmp:
             ret.append(tmp.lower())
 
-    return "-".join(ret)
+    user_defined_build = "settings.build_type" in build_vars
+    return "-".join(ret), user_defined_build
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 6d4a9a735..dacf021ef 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -21,7 +21,10 @@ def _build_preset(conanfile, multiconfig):
 
 def _build_preset_name(conanfile):
     build_type = conanfile.settings.get_safe("build_type")
-    custom_conf = get_build_folder_custom_vars(conanfile)
+    custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile)
+    if user_defined_build:
+        return custom_conf
+
     if custom_conf:
         if build_type:
             return "{}-{}".format(custom_conf, build_type.lower())
@@ -32,7 +35,10 @@ def _build_preset_name(conanfile):
 
 def _configure_preset_name(conanfile, multiconfig):
     build_type = conanfile.settings.get_safe("build_type")
-    custom_conf = get_build_folder_custom_vars(conanfile)
+    custom_conf, user_defined_build = get_build_folder_custom_vars(conanfile)
+
+    if user_defined_build:
+        return custom_conf
 
     if multiconfig or not build_type:
         return "default" if not custom_conf else custom_conf


Test patch:
diff --git a/conans/test/functional/layout/test_build_system_layout_helpers.py b/conans/test/functional/layout/test_build_system_layout_helpers.py
index 4bd7c8cd4..c683114b8 100644
--- a/conans/test/functional/layout/test_build_system_layout_helpers.py
+++ b/conans/test/functional/layout/test_build_system_layout_helpers.py
@@ -168,7 +168,8 @@ def test_cmake_layout_external_sources():
 
     # Local flow
     client.run("install . foo/1.0 -s os=Linux")
-    assert os.path.exists(os.path.join(client.current_folder, "build", "generators", "generate.txt"))
+    assert os.path.exists(os.path.join(client.current_folder,
+                                       "build", "Release", "generators", "generate.txt"))
     client.run("source .")
     assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt"))
     client.run("build .")
@@ -294,4 +295,4 @@ def test_cmake_layout_custom_build_folder():
     client.save({"conanfile.py": conanfile})
     client.run("install .")
     assert os.path.exists(os.path.join(client.current_folder,
-                                       "mybuild/generators/conan_toolchain.cmake"))
+                                       "mybuild/Release/generators/conan_toolchain.cmake"))
diff --git a/conans/test/functional/layout/test_local_commands.py b/conans/test/functional/layout/test_local_commands.py
index bd036d617..0c07d5cdd 100644
--- a/conans/test/functional/layout/test_local_commands.py
+++ b/conans/test/functional/layout/test_local_commands.py
@@ -372,7 +372,11 @@ def test_start_dir_failure():
     c = TestClient()
     c.save(pkg_cmake("dep", "0.1"))
     c.run("install .")
-    expected_path = os.path.join(c.current_folder, "build", "generators", "conan_toolchain.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(c.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(c.current_folder, "build", "generators")
+    expected_path = os.path.join(gen_folder, "conan_toolchain.cmake")
     assert os.path.exists(expected_path)
     os.unlink(expected_path)
     with c.chdir("build"):
diff --git a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
index 45b5d1d97..c1ceb5d3a 100644
--- a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -497,7 +497,7 @@ def test_system_dep():
     files = pkg_cmake("mylib", "0.1", requires=["zlib/0.1"])
     files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("find_package(zlib)",
                                                               "find_package(ZLIB)")
-    files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("zlib::zlib","ZLIB::ZLIB")
+    files["CMakeLists.txt"] = files["CMakeLists.txt"].replace("zlib::zlib", "ZLIB::ZLIB")
     client.save({os.path.join("mylib", name): content for name, content in files.items()})
     files = pkg_cmake("consumer", "0.1", requires=["mylib/0.1"])
     client.save({os.path.join("consumer", name): content for name, content in files.items()})
@@ -509,7 +509,7 @@ def test_system_dep():
     client.run("install consumer")
     if platform.system() != "Windows":
         host_arch = client.get_default_host_profile().settings['arch']
-        data = os.path.join(f"consumer/build/generators/mylib-release-{host_arch}-data.cmake")
+        data = f"consumer/build/Release/generators/mylib-release-{host_arch}-data.cmake"
         contents = client.load(data)
         assert 'set(ZLIB_FIND_MODE "")' in contents
 
diff --git a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
index e03007054..06c6d1943 100644
--- a/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
+++ b/conans/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_and_linker_flags.py
@@ -52,7 +52,8 @@ def test_shared_link_flags():
     client.run("new hello/1.0 -m cmake_lib")
     client.save({"conanfile.py": conanfile})
     client.run("create .")
-    t = os.path.join("test_package", "build", "generators", "hello-release-x86_64-data.cmake")
+    t = os.path.join("test_package", "build", "Release",
+                     "generators", "hello-release-x86_64-data.cmake")
     target_data_cmake_content = client.load(t)
     assert 'set(hello_SHARED_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content
     assert 'set(hello_EXE_LINK_FLAGS_RELEASE "-z now;-z relro")' in target_data_cmake_content
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 49f73cf5a..dbd70681e 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -297,8 +297,14 @@ def test_install_output_directories():
     p_folder = client.cache.package_layout(pref.ref).package(pref)
     assert os.path.exists(os.path.join(p_folder, "mylibs"))
     assert not os.path.exists(os.path.join(p_folder, "lib"))
+
     b_folder = client.cache.package_layout(pref.ref).build(pref)
-    toolchain = client.load(os.path.join(b_folder, "build", "generators", "conan_toolchain.cmake"))
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(b_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(b_folder, "build", "generators")
+
+    toolchain = client.load(os.path.join(gen_folder, "conan_toolchain.cmake"))
     assert 'set(CMAKE_INSTALL_LIBDIR "mylibs")' in toolchain
 
 
@@ -962,15 +968,15 @@ def test_cmake_presets_with_conanfile_txt():
     assert "Hello World Release!" in c.out
 
 
-def test_cmake_presets_forbidden_build_type():
+def test_cmake_presets_not_forbidden_build_type():
     client = TestClient(path_with_spaces=False)
     client.run("new hello/0.1 --template cmake_exe")
     # client.run("new cmake_exe -d name=hello -d version=0.1")
     settings_layout = '-c tools.cmake.cmake_layout:build_folder_vars=' \
                       '\'["options.missing", "settings.build_type"]\''
-    client.run("install . {}".format(settings_layout), assert_error=True)
-    assert "Error, don't include 'settings.build_type' in the " \
-           "'tools.cmake.cmake_layout:build_folder_vars' conf" in client.out
+    client.run("install . {}".format(settings_layout))
+    assert os.path.exists(os.path.join(client.current_folder,
+                                       "build/release/generators/conan_toolchain.cmake"))
 
 
 def test_resdirs_cmake_install():
@@ -1116,7 +1122,8 @@ def test_cmake_toolchain_vars_when_option_declared():
     # the CMakeLists
     fpic_option = "-o mylib:fPIC=False" if platform.system() != "Windows" else ""
     t.run(f"install . -o mylib:shared=False {fpic_option}")
-    t.run_command("cmake -S . -B build/ -DCMAKE_TOOLCHAIN_FILE=build/generators/conan_toolchain.cmake")
+    folder = "build/generators" if platform.system() == "Windows" else "build/Release/generators"
+    t.run_command(f"cmake -S . -B build/ -DCMAKE_TOOLCHAIN_FILE={folder}/conan_toolchain.cmake")
     assert "mylib target type: STATIC_LIBRARY" in t.out
     assert f"mylib position independent code: OFF" in t.out
 
@@ -1185,8 +1192,6 @@ def test_find_program_for_tool_requires():
                 "build_profile": build_profile
                 })
 
-    xxx = client.get_default_build_profile()
-
     client.run("create . -pr:b build_profile -pr:h build_profile")
     client.run("create . -pr:b build_profile -pr:h host_profile")
 
@@ -1226,7 +1231,7 @@ def test_find_program_for_tool_requires():
     host_context_package_id = "bf544cd3bc20b82121fd76b82eacbb36d75fa167"
 
     with client.chdir("build"):
-        client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release")
+        client.run_command("cmake .. -DCMAKE_TOOLCHAIN_FILE=Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release")
         # Verify binary executable is found from build context package,
         # and library comes from host context package
         assert f"package/{build_context_package_id}/bin/foobin" in client.out
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
index 30b5622b2..f108945ef 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain_xcode_flags.py
@@ -38,7 +38,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_enabled(op_system, os_vers
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "YES")' in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' in toolchain
@@ -121,7 +121,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_disabled(op_system, os_ver
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain
@@ -163,7 +163,7 @@ def test_cmake_apple_bitcode_arc_and_visibility_flags_are_none(op_system, os_ver
     client.run("new hello/0.1 --template=cmake_lib")
     _add_message_status_flags(client)
     client.run("install . --profile:build=default --profile:host=host")
-    toolchain = client.load(os.path.join("build", "generators", "conan_toolchain.cmake"))
+    toolchain = client.load(os.path.join("build", "Release", "generators", "conan_toolchain.cmake"))
     # bitcode
     assert 'set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO")' not in toolchain
     assert 'set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode")' not in toolchain
diff --git a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
index 6d3e9b57d..b3da541f9 100644
--- a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -1,4 +1,5 @@
 import os
+import platform
 import textwrap
 
 from conans.test.assets.genconanfile import GenConanfile
@@ -225,9 +226,14 @@ def test_dependency_props_from_consumer():
 
     client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True)
 
-    module_file = os.path.join(client.current_folder, "build", "generators", "module-custom_bar_module_file_nameTargets.cmake")
-    components_module = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_name-Target-release.cmake")
-    config_file = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_nameTargets.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(client.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(client.current_folder, "build", "generators")
+
+    module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake")
+    components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake")
+    config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake")
 
     # uses cmake_find_mode set in bar: both
     client.run("create bar.py bar/1.0@")
@@ -311,19 +317,20 @@ def test_props_from_consumer_build_context_activated():
 
     client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True)
 
-    module_file = os.path.join(client.current_folder, "build", "generators",
-                               "module-custom_bar_module_file_nameTargets.cmake")
-    components_module = os.path.join(client.current_folder, "build", "generators",
-                                     "custom_bar_file_name-Target-release.cmake")
-    config_file = os.path.join(client.current_folder, "build", "generators",
-                               "custom_bar_file_nameTargets.cmake")
+    if platform.system() != "Windows":
+        gen_folder = os.path.join(client.current_folder, "build", "Release", "generators")
+    else:
+        gen_folder = os.path.join(client.current_folder, "build", "generators")
+
+    module_file = os.path.join(gen_folder, "module-custom_bar_module_file_nameTargets.cmake")
+    components_module = os.path.join(gen_folder, "custom_bar_file_name-Target-release.cmake")
+    config_file = os.path.join(gen_folder, "custom_bar_file_nameTargets.cmake")
 
-    module_file_build = os.path.join(client.current_folder, "build", "generators",
+    module_file_build = os.path.join(gen_folder,
                                      "module-custom_bar_build_module_file_name_BUILDTargets.cmake")
-    components_module_build = os.path.join(client.current_folder, "build", "generators",
+    components_module_build = os.path.join(gen_folder,
                                            "custom_bar_build_file_name_BUILD-Target-release.cmake")
-    config_file_build = os.path.join(client.current_folder, "build", "generators",
-                                     "custom_bar_build_file_name_BUILDTargets.cmake")
+    config_file_build = os.path.join(gen_folder, "custom_bar_build_file_name_BUILDTargets.cmake")
 
     # uses cmake_find_mode set in bar: both
     client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default")
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index e219bcc5a..38a793f42 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -8,7 +8,6 @@ from mock import mock
 
 from conan.tools.cmake.presets import load_cmake_presets
 from conans.test.assets.genconanfile import GenConanfile
-from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.util.files import rmdir
 
@@ -379,7 +378,7 @@ def test_cmake_presets_binary_dir_available():
     else:
         build_dir = os.path.join(client.current_folder, "build")
 
-    presets = load_cmake_presets(os.path.join(client.current_folder, "build", "generators"))
+    presets = load_cmake_presets(os.path.join(build_dir, "generators"))
     assert presets["configurePresets"][0]["binaryDir"] == build_dir
 
 
@@ -422,6 +421,7 @@ def test_cmake_presets_multiconfig():
     # Repeat one
     client.run("install mylib/1.0@ -g CMakeToolchain -s build_type=Debug --profile:h=profile")
     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["buildPresets"]) == 4
     assert presets["buildPresets"][0]["configuration"] == "Release"
     assert presets["buildPresets"][1]["configuration"] == "Debug"
@@ -517,6 +517,7 @@ def test_toolchain_cache_variables():
     assert "-DCMAKE_TOOLCHAIN_FILE=" in client.out
     assert f"-G {_format_val('MinGW Makefiles')}" in client.out
 
+
 def test_android_c_library():
     client = TestClient()
     conanfile = textwrap.dedent("""
@@ -845,3 +846,36 @@ def test_set_cmake_lang_compilers_and_launchers():
     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
+
+
+def test_cmake_layout_toolchain_folder():
+    """ in single-config generators, the toolchain is a different file per configuration
+    https://github.com/conan-io/conan/issues/12827
+    """
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+
+        class Conan(ConanFile):
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+
+            def layout(self):
+                cmake_layout(self)
+            """)
+    c.save({"conanfile.py": conanfile})
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Release "
+          "-s compiler.libcxx=libstdc++11")
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/Release/generators/conan_toolchain.cmake"))
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug "
+          "-s compiler.libcxx=libstdc++11")
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/Debug/generators/conan_toolchain.cmake"))
+    c.run("install . -s os=Linux -s compiler=gcc -s compiler.version=7 -s build_type=Debug "
+          "-s compiler.libcxx=libstdc++11 -s arch=x86 "
+          "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.arch\", \"settings.build_type\"]'")
+    print(c.out)
+    assert os.path.exists(os.path.join(c.current_folder,
+                                       "build/x86-debug/generators/conan_toolchain.cmake"))