Repository: conan-io/conan (Python)

Issue:
[bug] NMake toolchain works with CLang-cl incorrectly
### Describe the bug

Hi! Thank you for your great project!

I faced with the following issue.

Context:
- Conan supports `CLang-cl` and knows how to set debug parameters for it (for example, `gline-tables-only` and `fno-inline`);
- Conan has `NMakeToolchain` and supports `NMake`;

Issue:
- Conan sets the generated CLang `build_type_flags` to the `CL` environment variable (it's okay because `CLang-cl` supports it) and does it in the MSVC CL compiler format (Conan tranforms `-` characters to `/`) and that is the problem.

I see some possible solutions:
1. Disable transformations in `NMakeToolchain` if the compiler is `CLang-cl`;
2. Follow the previous point and to add the filling of the `CFLAGS` environment variable instead of the `CL` one.

Thank you!

### How to reproduce it

_No response_


Discussion:
Hi @molotok-sms 

> Hi! Thank you for your great project!

Thanks for your kind words!

Thanks for your report too.

I think this was addressed in https://github.com/conan-io/conan/pull/17387, which was released in latest Conan 2.10.
The way it works is it uses the ``tools.build:compiler_executables`` information to see if it is using ``clang-cl`` or not, and avoid using the wrong flags.

Could you please try that? Thanks for your feedback!
Thank you for the quick response!

I use the Conan version 2.10.0 from Pixi (Conda).
The mentioned changes are good but unfortunately it isn't enough.

In my case, "the right clang flags" pass through the NMakeToolchain "`_format_options`" function and become invalid.
It seems it would be good to improve the NMakeToolchain to support not only "msvc" compiler, but "clang-cl' too.
I attached the log of errors when NMakeToolchain transformed the format of debug parameters:

![image](https://github.com/user-attachments/assets/8b78041c-2e5e-486b-85b1-09995b55a7d1)

We have recently improved the support for clang-cl in Autotools, in Conan 2.17 (https://docs.conan.io/2/examples/tools/autotools/llvm_clang.html), lets try to have a look at this for next 2.18
I am doing a PR in https://github.com/conan-io/conan/pull/18422 that works with ``clang-cl`` for ``NMakeToolchain``
That PR will close this ticket.

If there are any further issues after merging that PR (intended for Conan 2.18), it would be necessary to re-open, or better, create a new ticket with full reproduction steps, that is a minimal reproducible example code that would fail. Many thanks for your feedback!
This has been closed by https://github.com/conan-io/conan/pull/18422, it will be in next Conan 2.18 release, feedback welcome.



Patch:
diff --git a/conan/tools/build/flags.py b/conan/tools/build/flags.py
index 98d068252ac..019eba51f63 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 785f3b05cba..f2e454474d9 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -174,7 +174,9 @@ def _get_msvc_runtime_flag(self):
             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 e3338283137..800cb35b495 100644
--- a/conan/tools/gnu/gnutoolchain.py
+++ b/conan/tools/gnu/gnutoolchain.py
@@ -239,7 +239,8 @@ def _get_msvc_runtime_flag(self):
             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 47ab66e17f8..b46840171a5 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 build(self):
 @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 build(self):
                  "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 9837d716bf4..36a4121e4dc 100644
--- a/test/functional/toolchains/test_nmake_toolchain.py
+++ b/test/functional/toolchains/test_nmake_toolchain.py
@@ -85,3 +85,78 @@ def build(self):
     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 fee6ff32c65..4c82661f58d 100644
--- a/test/unittests/client/build/compiler_flags_test.py
+++ b/test/unittests/client/build/compiler_flags_test.py
@@ -127,10 +127,10 @@ def test_build_type_flags(self, compiler, build_type, vs_toolset, flags):
         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,