Repository: mesonbuild/meson

Issue:
clang-cl does not recognize /ZI flag (and so executables can't be debugged)
**Describe the bug**
The build flags for a clang-cl native build include `/ZI` to generate PDB debug info. This does not seem to be recognized by clang-cl, which warns:
```
clang-cl: warning: argument unused during compilation: '/ZI' [-Wunused-command-line-argument]
```

PDBs are generated, but presumably without much information in them.
I have tried debugging the programs and can't get them to break inside my code. 
If I manually replace the `/ZI` in `build.ninja` with either `/Z7` or `/Zi`, the executables can then be debugged.

Debugging tested with:
* Microsoft Visual Studio Community 2017 v15.9.4
* [remedybg v0.2.9.2](https://remedybg.itch.io/remedybg)

MSDN/cl debug info: 
https://docs.microsoft.com/en-us/cpp/build/reference/z7-zi-zi-debug-information-format?view=vs-2017

**To Reproduce**
```
// main.c
int main() { int x = 0; return x; }
```
```
# native.meson
[binaries]
c='clang-cl'
```
```
# meson.build
project('demo', 'c')
executable('demo', 'main.c')
```
```
meson build --native-file=native.meson
ninja -C build
```
**Expected behavior**
* Default to /Z7 or /Zi (lowercase i) for clang-cl (both do the same thing in clang-cl but not cl).
* Some sort of override for debug info type (note that I don't think you can have unity builds and /Z7 on cl)?
Unfortunately this is made more difficult by clang-cl not quite copying all of cl's behaviour...

**system parameters**
* Native build
* Windows 7
* Python 3.8.1
* `meson --version` => `0.52.1`
* `ninja --version`  => `1.8.2`


Discussion:

Resolving patch:
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -21,7 +21,7 @@
 from .mixins.clike import CLikeCompiler
 from .mixins.ccrx import CcrxCompiler
 from .mixins.arm import ArmCompiler, ArmclangCompiler
-from .mixins.visualstudio import VisualStudioLikeCompiler
+from .mixins.visualstudio import MSVCCompiler, ClangClCompiler
 from .mixins.gnu import GnuCompiler
 from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
 from .mixins.clang import ClangCompiler
@@ -303,24 +303,22 @@ def get_option_link_args(self, options):
         return options['c_winlibs'].value[:]
 
 
-class VisualStudioCCompiler(VisualStudioLikeCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
+class VisualStudioCCompiler(MSVCCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
 
     def __init__(self, exelist, version, for_machine: MachineChoice,
                  is_cross, info: 'MachineInfo', exe_wrap, target: str,
                  **kwargs):
         CCompiler.__init__(self, exelist, version, for_machine, is_cross,
                            info, exe_wrap, **kwargs)
-        VisualStudioLikeCompiler.__init__(self, target)
-        self.id = 'msvc'
+        MSVCCompiler.__init__(self, target)
 
 
-class ClangClCCompiler(VisualStudioLikeCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
+class ClangClCCompiler(ClangClCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
     def __init__(self, exelist, version, for_machine: MachineChoice,
                  is_cross, info: 'MachineInfo', exe_wrap, target, **kwargs):
         CCompiler.__init__(self, exelist, version, for_machine, is_cross,
                            info, exe_wrap, **kwargs)
-        VisualStudioLikeCompiler.__init__(self, target)
-        self.id = 'clang-cl'
+        ClangClCompiler.__init__(self, target)
 
 
 class IntelClCCompiler(IntelVisualStudioLikeCompiler, VisualStudioLikeCCompilerMixin, CCompiler):
diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py
--- a/mesonbuild/compilers/cpp.py
+++ b/mesonbuild/compilers/cpp.py
@@ -30,7 +30,7 @@
 from .mixins.clike import CLikeCompiler
 from .mixins.ccrx import CcrxCompiler
 from .mixins.arm import ArmCompiler, ArmclangCompiler
-from .mixins.visualstudio import VisualStudioLikeCompiler
+from .mixins.visualstudio import MSVCCompiler, ClangClCompiler
 from .mixins.gnu import GnuCompiler
 from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
 from .mixins.clang import ClangCompiler
@@ -498,11 +498,11 @@ def get_option_compile_args(self, options):
         return super().get_option_compile_args(options)
 
 
-class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, VisualStudioLikeCompiler, CPPCompiler):
+class VisualStudioCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, MSVCCompiler, CPPCompiler):
     def __init__(self, exelist, version, for_machine: MachineChoice,
                  is_cross: bool, info: 'MachineInfo', exe_wrap, target, **kwargs):
         CPPCompiler.__init__(self, exelist, version, for_machine, is_cross, info, exe_wrap, **kwargs)
-        VisualStudioLikeCompiler.__init__(self, target)
+        MSVCCompiler.__init__(self, target)
         self.base_options = ['b_pch', 'b_vscrt'] # FIXME add lto, pgo and the like
         self.id = 'msvc'
 
@@ -532,12 +532,12 @@ def get_option_compile_args(self, options):
             del args[i]
         return args
 
-class ClangClCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, VisualStudioLikeCompiler, CPPCompiler):
+class ClangClCPPCompiler(CPP11AsCPP14Mixin, VisualStudioLikeCPPCompilerMixin, ClangClCompiler, CPPCompiler):
     def __init__(self, exelist, version, for_machine: MachineChoice,
                  is_cross, info: 'MachineInfo', exe_wrap, target, **kwargs):
         CPPCompiler.__init__(self, exelist, version, for_machine, is_cross,
                              info, exe_wrap, **kwargs)
-        VisualStudioLikeCompiler.__init__(self, target)
+        ClangClCompiler.__init__(self, target)
         self.id = 'clang-cl'
 
     def get_options(self):
diff --git a/mesonbuild/compilers/mixins/visualstudio.py b/mesonbuild/compilers/mixins/visualstudio.py
--- a/mesonbuild/compilers/mixins/visualstudio.py
+++ b/mesonbuild/compilers/mixins/visualstudio.py
@@ -61,6 +61,15 @@
     'custom': [],
 }  # type: T.Dict[str, T.List[str]]
 
+# Clang-cl doesn't have /ZI, and /Zi and /Z7 do the same thing
+# quoting the docs (https://clang.llvm.org/docs/MSVCCompatibility.html):
+#
+# Clang emits relatively complete CodeView debug information if /Z7 or /Zi is
+# passed. Microsoft’s link.exe will transform the CodeView debug information
+# into a PDB
+clangcl_buildtype_args = msvc_buildtype_args.copy()
+clangcl_buildtype_args['debug'] = ['/Zi', '/Ob0', '/Od', '/RTC1']
+
 msvc_optimization_args = {
     '0': [],
     'g': ['/O0'],
@@ -130,12 +139,6 @@ def __init__(self, target: str):
     def get_always_args(self) -> T.List[str]:
         return self.always_args
 
-    def get_buildtype_args(self, buildtype: str) -> T.List[str]:
-        args = msvc_buildtype_args[buildtype]
-        if self.id == 'msvc' and mesonlib.version_compare(self.version, '<18.0'):
-            args = [arg for arg in args if arg != '/Gw']
-        return args
-
     def get_pch_suffix(self) -> str:
         return 'pch'
 
@@ -145,10 +148,12 @@ def get_pch_name(self, header: str) -> str:
         pchname = '.'.join(chopped)
         return pchname
 
+    def get_pch_base_name(self, header: str) -> str:
+        # This needs to be implemented by inherting classes
+        raise NotImplementedError
+
     def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
-        base = os.path.basename(header)
-        if self.id == 'clang-cl':
-            base = header
+        base = self.get_pch_base_name(header)
         pchname = self.get_pch_name(header)
         return ['/FI' + base, '/Yu' + base, '/Fp' + os.path.join(pch_dir, pchname)]
 
@@ -285,8 +290,6 @@ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], bu
     # http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t
     def has_arguments(self, args: T.List[str], env: 'Environment', code, mode: str) -> T.Tuple[bool, bool]:
         warning_text = '4044' if mode == 'link' else '9002'
-        if self.id == 'clang-cl' and mode != 'link':
-            args = args + ['-Werror=unknown-argument']
         with self._build_wrapper(code, env, extra_args=args, mode=mode) as p:
             if p.returncode != 0:
                 return False, p.cached
@@ -296,24 +299,11 @@ def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[
         pdbarr = rel_obj.split('.')[:-1]
         pdbarr += ['pdb']
         args = ['/Fd' + '.'.join(pdbarr)]
-        # When generating a PDB file with PCH, all compile commands write
-        # to the same PDB file. Hence, we need to serialize the PDB
-        # writes using /FS since we do parallel builds. This slows down the
-        # build obviously, which is why we only do this when PCH is on.
-        # This was added in Visual Studio 2013 (MSVC 18.0). Before that it was
-        # always on: https://msdn.microsoft.com/en-us/library/dn502518.aspx
-        if pch and self.id == 'msvc' and mesonlib.version_compare(self.version, '>=18.0'):
-            args = ['/FS'] + args
         return args
 
     def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
         if self.is_64:
             return vs64_instruction_set_args.get(instruction_set, None)
-        if self.id == 'msvc' and self.version.split('.')[0] == '16' and instruction_set == 'avx':
-            # VS documentation says that this exists and should work, but
-            # it does not. The headers do not contain AVX intrinsics
-            # and the can not be called.
-            return None
         return vs32_instruction_set_args.get(instruction_set, None)
 
     def _calculate_toolset_version(self, version: int) -> T.Optional[str]:
@@ -341,10 +331,6 @@ def _calculate_toolset_version(self, version: int) -> T.Optional[str]:
         return None
 
     def get_toolset_version(self) -> T.Optional[str]:
-        if self.id == 'clang-cl':
-            # I have no idea
-            return '14.1'
-
         # See boost/config/compiler/visualc.cpp for up to date mapping
         try:
             version = int(''.join(self.version.split('.')[0:2]))
@@ -387,3 +373,65 @@ def get_argument_syntax(self) -> str:
     @classmethod
     def use_linker_args(cls, linker: str) -> T.List[str]:
         return []
+
+
+class MSVCCompiler(VisualStudioLikeCompiler):
+
+    """Spcific to the Microsoft Compilers."""
+
+    def __init__(self, target: str):
+        super().__init__(target)
+        self.id = 'msvc'
+
+    def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
+        args = super().get_compile_debugfile_args(rel_obj, pch)
+        # When generating a PDB file with PCH, all compile commands write
+        # to the same PDB file. Hence, we need to serialize the PDB
+        # writes using /FS since we do parallel builds. This slows down the
+        # build obviously, which is why we only do this when PCH is on.
+        # This was added in Visual Studio 2013 (MSVC 18.0). Before that it was
+        # always on: https://msdn.microsoft.com/en-us/library/dn502518.aspx
+        if pch and mesonlib.version_compare(self.version, '>=18.0'):
+            args = ['/FS'] + args
+        return args
+
+    def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
+        if self.version.split('.')[0] == '16' and instruction_set == 'avx':
+            # VS documentation says that this exists and should work, but
+            # it does not. The headers do not contain AVX intrinsics
+            # and they can not be called.
+            return None
+        return super().get_instruction_set_args(instruction_set)
+
+    def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+        args = msvc_buildtype_args[buildtype]
+        if mesonlib.version_compare(self.version, '<18.0'):
+            args = [arg for arg in args if arg != '/Gw']
+        return args
+
+    def get_pch_base_name(self, header: str) -> str:
+        return os.path.basename(header)
+
+
+class ClangClCompiler(VisualStudioLikeCompiler):
+
+    """Spcific to Clang-CL."""
+
+    def __init__(self, target: str):
+        super().__init__(target)
+        self.id = 'clang-cl'
+
+    def has_arguments(self, args: T.List[str], env: 'Environment', code, mode: str) -> T.Tuple[bool, bool]:
+        if mode != 'link':
+            args = args + ['-Werror=unknown-argument']
+        return super().has_arguments(args, env, code, mode)
+
+    def get_toolset_version(self) -> T.Optional[str]:
+        # XXX: what is the right thing to do here?
+        return '14.1'
+
+    def get_pch_base_name(self, header: str) -> str:
+        return header
+
+    def get_buildtype_args(self, buildtype: str) -> T.List[str]:
+        return clangcl_buildtype_args[buildtype]


Test patch: