Repository: conan-io/conan
Pull request #18422 (2025-06-06 07:00:38)

fix in autotools win-clang and test for NMake with clang-cl
Changelog: Fix: Fix AutotoolsToolchain/GnuToolchain with LLVM/Clang in Windows for dynamic runtime in Debug. Also test ``NMake`` integration with ``clang-cl``
Docs: Omit

Close https://github.com/conan-io/conan/issues/17491


Patch:
diff --git a/conan/tools/build/flags.py b/conan/tools/build/flags.py
index 98d068252..019eba51f 100644
--- a/conan/tools/build/flags.py
+++ b/conan/tools/build/flags.py
@@ -134,14 +134,15 @@ def build_type_flags(conanfile):
     if not compiler or not build_type:
         return []
 
-    # https://github.com/Kitware/CMake/blob/d7af8a34b67026feaee558433db3a835d6007e06/
-    # Modules/Platform/Windows-MSVC.cmake
     comp_exes = conanfile.conf.get("tools.build:compiler_executables", check_type=dict,
                                    default={})
     clangcl = "clang-cl" in (comp_exes.get("c") or comp_exes.get("cpp", ""))
 
     if compiler == "msvc" or clangcl:
-        if clangcl or (vs_toolset and "clang" in vs_toolset):
+        # https://github.com/Kitware/CMake/blob/d7af8a34b67026feaee558433db3a835d6007e06/
+        # Modules/Platform/Windows-MSVC.cmake
+        # FIXME: This condition seems legacy, as no more "clang" exists in Conan toolsets
+        if vs_toolset and "clang" in vs_toolset:
             flags = {"Debug": ["-gline-tables-only", "-fno-inline", "-O0"],
                      "Release": ["-O2"],
                      "RelWithDebInfo": ["-gline-tables-only", "-O2", "-fno-inline"],
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index 785f3b05c..f2e454474 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -174,7 +174,9 @@ class AutotoolsToolchain:
             if self._conanfile.settings.compiler.runtime == "dynamic":
                 runtime_type = self._conanfile.settings.get_safe("compiler.runtime_type")
                 library = "msvcrtd" if runtime_type == "Debug" else "msvcrt"
-                return f"-D_DLL -D_MT -Xclang --dependent-lib={library}"
+                # The -D_DEBUG is important to link with the Debug MSVCP140D.dll
+                debug = "-D_DEBUG " if runtime_type == "Debug" else ""
+                return f"{debug}-D_DLL -D_MT -Xclang --dependent-lib={library}"
             return ""  # By default it already link statically
 
         flag = msvc_runtime_flag(self._conanfile)
diff --git a/conan/tools/gnu/gnutoolchain.py b/conan/tools/gnu/gnutoolchain.py
index e33382831..800cb35b4 100644
--- a/conan/tools/gnu/gnutoolchain.py
+++ b/conan/tools/gnu/gnutoolchain.py
@@ -239,7 +239,8 @@ class GnuToolchain:
             if self._conanfile.settings.compiler.runtime == "dynamic":
                 runtime_type = self._conanfile.settings.get_safe("compiler.runtime_type")
                 library = "msvcrtd" if runtime_type == "Debug" else "msvcrt"
-                return f"-D_DLL -D_MT -Xclang --dependent-lib={library}"
+                debug = "-D_DEBUG " if runtime_type == "Debug" else ""
+                return f"{debug}-D_DLL -D_MT -Xclang --dependent-lib={library}"
             return ""  # By default it already link statically
 
         flag = msvc_runtime_flag(self._conanfile)


Test patch:
diff --git a/test/functional/toolchains/gnu/autotools/test_win_bash.py b/test/functional/toolchains/gnu/autotools/test_win_bash.py
index 47ab66e17..b46840171 100644
--- a/test/functional/toolchains/gnu/autotools/test_win_bash.py
+++ b/test/functional/toolchains/gnu/autotools/test_win_bash.py
@@ -70,7 +70,8 @@ def test_autotools_bash_complete():
 @pytest.mark.tool("msys2")
 @pytest.mark.parametrize("frontend", ("clang", "clang-cl"))
 @pytest.mark.parametrize("runtime", ("static", "dynamic"))
-def test_autotools_bash_complete_clang(frontend, runtime):
+@pytest.mark.parametrize("build_type", ("Debug", "Release"))
+def test_autotools_bash_complete_clang(frontend, runtime, build_type):
     client = TestClient(path_with_spaces=False)
     # Problem is that msys2 also has clang in the path, so we need to make it explicit
     clangpath = tools_locations["clang"]["18"]["path"]["Windows"]
@@ -81,7 +82,7 @@ def test_autotools_bash_complete_clang(frontend, runtime):
         [settings]
         os=Windows
         arch=x86_64
-        build_type=Release
+        build_type={build_type}
         compiler=clang
         compiler.version=18
         compiler.cppstd=14
@@ -92,6 +93,7 @@ def test_autotools_bash_complete_clang(frontend, runtime):
         tools.build:compiler_executables={comps}
         tools.microsoft.bash:subsystem=msys2
         tools.microsoft.bash:path=bash
+        tools.compilation:verbosity=verbose
         """)
 
     main = gen_function_cpp(name="main")
@@ -128,16 +130,17 @@ def test_autotools_bash_complete_clang(frontend, runtime):
                  "main.cpp": main,
                  "profile_win": profile_win})
     client.run("build . -pr=profile_win")
+    print(client.out)
     client.run_command("main.exe")
     assert "__GNUC__" not in client.out
     assert "main __clang_major__18" in client.out
-    check_exe_run(client.out, "main", "clang", None, "Release", "x86_64", None)
+    check_exe_run(client.out, "main", "clang", None, build_type, "x86_64", None)
 
     bat_contents = client.load("conanbuild.bat")
     assert "conanvcvars.bat" in bat_contents
 
     static_runtime = runtime == "static"
-    check_vs_runtime("main.exe", client, "17", build_type="Release", static_runtime=static_runtime)
+    check_vs_runtime("main.exe", client, "17", build_type=build_type, static_runtime=static_runtime)
 
 
 @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
diff --git a/test/functional/toolchains/test_nmake_toolchain.py b/test/functional/toolchains/test_nmake_toolchain.py
index 9837d716b..36a4121e4 100644
--- a/test/functional/toolchains/test_nmake_toolchain.py
+++ b/test/functional/toolchains/test_nmake_toolchain.py
@@ -85,3 +85,78 @@ def test_toolchain_nmake(compiler, version, runtime, cppstd, build_type,
     client.run_command("simple.exe")
     assert "dep/1.0" in client.out
     check_exe_run(client.out, "main", "msvc", version, build_type, "x86_64", cppstd, conf_preprocessors)
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
+@pytest.mark.tool("clang", "18")
+def test_toolchain_nmake_clang():
+    compiler = "clang"
+    version = "18"
+    runtime = "dynamic"
+    cppstd = "14"
+    build_type = "Debug"
+    defines = ["TEST_DEFINITION1", "TEST_DEFINITION2=0", "TEST_DEFINITION3=", "TEST_DEFINITION4=TestPpdValue4",
+          "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar"]
+    cflags = cxxflags = sharedlinkflags = exelinkflags = []
+    client = TestClient(path_with_spaces=False)
+    settings = {"compiler": compiler,
+                "compiler.version": version,
+                "compiler.cppstd": cppstd,
+                "compiler.runtime": runtime,
+                "build_type": build_type,
+                "compiler.runtime_version": "v144",
+                "arch": "x86_64"}
+
+    serialize_array = lambda arr: "[{}]".format(",".join([f"'{v}'" for v in arr]))
+    conf = {
+        "tools.build:defines": serialize_array(defines) if defines else "",
+        "tools.build:cflags": serialize_array(cflags) if cflags else "",
+        "tools.build:cxxflags": serialize_array(cxxflags) if cxxflags else "",
+        "tools.build:sharedlinkflags": serialize_array(sharedlinkflags) if sharedlinkflags else "",
+        "tools.build:exelinkflags": serialize_array(exelinkflags) if exelinkflags else "",
+        "tools.build:compiler_executables": r'{\"c\": \"clang-cl\", \"cpp\": \"clang-cl\"}',
+        "tools.cmake.cmaketoolchain:generator": "Visual Studio 17",
+    }
+
+    # Build the profile according to the settings provided
+    settings = " ".join('-s %s="%s"' % (k, v) for k, v in settings.items() if v)
+
+    client.run("new cmake_lib -d name=dep -d version=1.0")
+    conf = " ".join(f'-c {k}="{v}"' for k, v in conf.items() if v)
+    client.run(f'create . -tf=\"\" {settings} {conf}')
+
+    # Rearrange defines to macro / value dict
+    conf_preprocessors = {}
+    for define in defines:
+        if "=" in define:
+            key, value = define.split("=", 1)
+            # gen_function_cpp doesn't properly handle empty macros
+            if value:
+                conf_preprocessors[key] = value
+        else:
+            conf_preprocessors[define] = "1"
+
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            requires = "dep/1.0"
+            generators = "NMakeToolchain", "NMakeDeps"
+
+            def build(self):
+                self.run(f"nmake /f makefile")
+            """)
+    makefile = textwrap.dedent("""\
+        all: simple.exe
+
+        simple.exe: simple.cpp
+          $(CPP) simple.cpp -o simple.exe
+        """)
+    client.save({"conanfile.py": conanfile,
+                 "makefile": makefile,
+                 "simple.cpp": gen_function_cpp(name="main", includes=["dep"], calls=["dep"], preprocessor=conf_preprocessors.keys())},
+                clean_first=True)
+    client.run(f"build . {settings} {conf}")
+    client.run_command("simple.exe")
+    assert "dep/1.0" in client.out
+    check_exe_run(client.out, "main", "clang", "19.1", build_type, "x86_64", cppstd, conf_preprocessors)
diff --git a/test/unittests/client/build/compiler_flags_test.py b/test/unittests/client/build/compiler_flags_test.py
index fee6ff32c..4c82661f5 100644
--- a/test/unittests/client/build/compiler_flags_test.py
+++ b/test/unittests/client/build/compiler_flags_test.py
@@ -127,10 +127,10 @@ class CompilerFlagsTest(unittest.TestCase):
         conanfile.settings = settings
         self.assertEqual(' '.join(build_type_flags(conanfile)), flags)
 
-    @parameterized.expand([("clang", "Debug", "-gline-tables-only -fno-inline -O0"),
-                           ("clang", "Release", "-O2"),
-                           ("clang", "RelWithDebInfo", "-gline-tables-only -O2 -fno-inline"),
-                           ("clang", "MinSizeRel", ""),
+    @parameterized.expand([("clang", "Debug", "-Zi -Ob0 -Od"),
+                           ("clang", "Release", "-O2 -Ob2"),
+                           ("clang", "RelWithDebInfo", "-Zi -O2 -Ob1"),
+                           ("clang", "MinSizeRel", "-O1 -Ob1"),
                            ])
     def test_build_type_flags_clangcl(self, compiler, build_type, flags):
         settings = MockSettings({"compiler": compiler,