Repository: mesonbuild/meson

Issue:
Combining default_options : ['cpp_std=c++2a', 'cuda_std=c++17'] breaks nvcc
I'm trying to compile a project where the host code (*.cpp) uses c++20, but the device code (*.cu) uses c++17, since that is the latest version currently supported by nvcc.

**Describe the bug**
In `meson.build`, I tried this:
```
project('C++ std', 'cpp', 'cuda', version : '1.0.0', default_options : ['cpp_std=c++2a', 'cuda_std=c++17'])
```
but `nvcc` fails because it was called with `--std=c++17 -std=c++2a`:

```
meson compile -v
ninja: Entering directory `.'
[1/2] nvcc -Xcompiler=-Wall -Xcompiler=-Winvalid-pch -Xcompiler=-Wnon-virtual-dtor --std=c++17 -std=c++2a -g -G -g -I.. -I. -Iprog.p -Iprog.p -o prog.p/main.cu.o -c ../main.cu
FAILED: prog.p/main.cu.o 
nvcc -Xcompiler=-Wall -Xcompiler=-Winvalid-pch -Xcompiler=-Wnon-virtual-dtor --std=c++17 -std=c++2a -g -G -g -I.. -I. -Iprog.p -Iprog.p -o prog.p/main.cu.o -c ../main.cu
nvcc fatal   : Value 'c++2a' is not defined for option 'std'
ninja: build stopped: subcommand failed.
```

**To Reproduce**
Start with cuda example [6 std](https://github.com/mesonbuild/meson/tree/master/test%20cases/cuda/6%20std), but use this:
```project('C++ std', 'cpp', 'cuda', version : '1.0.0', default_options : ['cpp_std=c++2a', 'cuda_std=c++17'])```

**Expected behavior**
I expected that `gcc` would be invoked with c++2a and `nvcc` would be invoked with c++17.

If I manually remove the spurious `-std=c++2a` then `nvcc` succeeds.

**system parameters**
This is a native build on 5.4.101-1-MANJARO with Meson 0.57.1.


Discussion:

Resolving patch:
diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py
--- a/mesonbuild/compilers/cuda.py
+++ b/mesonbuild/compilers/cuda.py
@@ -218,21 +218,6 @@ def _shield_nvcc_list_arg(cls, arg: str, listmode: bool=True) -> str:
                 # strings between them.
                 l = [cls._shield_nvcc_list_arg(s) for s in arg.split(SQ)]
                 l = sum([[s, DQSQ] for s in l][:-1], [])  # Interleave l with DQSQs
-
-                # The list l now has the structure of shielded strings interleaved
-                # with double-quoted single-quotes.
-                #
-                # Plain concatenation would result in the tripling of the length of
-                # a string made up only of single quotes. See if we can merge some
-                # DQSQs together first.
-                def isdqsq(x:str) -> bool:
-                    return x.startswith(SQ) and x.endswith(SQ) and x[1:-1].strip(SQ) == ''
-                for i in range(1, len(l)-2, 2):
-                    if isdqsq(l[i]) and l[i+1] == '' and isdqsq(l[i+2]):
-                        l[i+2] = l[i][:-1]+l[i+2][1:]
-                        l[i]   = ''
-
-                # With DQSQs merged, simply concatenate everything together and return.
                 return ''.join(l)
         else:
             # A comma is present, and list mode was active.
@@ -259,6 +244,52 @@ def isdqsq(x:str) -> bool:
             l = [cls._shield_nvcc_list_arg(s, listmode=False) for s in l]
             return r'\,'.join(l)
 
+    @classmethod
+    def _merge_flags(cls, flags: T.List[str]) -> T.List[str]:
+        r"""
+        The flags to NVCC gets exceedingly verbose and unreadable when too many of them
+        are shielded with -Xcompiler. Merge consecutive -Xcompiler-wrapped arguments
+        into one.
+        """
+        if len(flags) <= 1:
+            return flags
+        flagit = iter(flags)
+        xflags = []
+
+        def is_xcompiler_flag_isolated(flag: str) -> bool:
+            return flag == '-Xcompiler'
+        def is_xcompiler_flag_glued(flag: str) -> bool:
+            return flag.startswith('-Xcompiler=')
+        def is_xcompiler_flag(flag: str) -> bool:
+            return is_xcompiler_flag_isolated(flag) or is_xcompiler_flag_glued(flag)
+        def get_xcompiler_val(flag: str, flagit: T.Iterator[str]) -> str:
+            if is_xcompiler_flag_glued(flag):
+                return flag[len('-Xcompiler='):]
+            else:
+                try:
+                    return next(flagit)
+                except StopIteration:
+                    return ""
+
+        ingroup = False
+        for flag in flagit:
+            if not is_xcompiler_flag(flag):
+                ingroup = False
+                xflags.append(flag)
+            elif ingroup:
+                xflags[-1] += ','
+                xflags[-1] += get_xcompiler_val(flag, flagit)
+            elif is_xcompiler_flag_isolated(flag):
+                ingroup = True
+                xflags.append(flag)
+                xflags.append(get_xcompiler_val(flag, flagit))
+            elif is_xcompiler_flag_glued(flag):
+                ingroup = True
+                xflags.append(flag)
+            else:
+                raise ValueError("-Xcompiler flag merging failed, unknown argument form!")
+        return xflags
+
     @classmethod
     def _to_host_flags(cls, flags: T.List[str], phase: _Phase = _Phase.COMPILER) -> T.List[str]:
         """
@@ -432,7 +463,7 @@ def _to_host_flags(cls, flags: T.List[str], phase: _Phase = _Phase.COMPILER) ->
                 xflags.append(flag)
                 xflags.append(val)
 
-        return xflags
+        return cls._merge_flags(xflags)
 
     def needs_static_linker(self) -> bool:
         return False
@@ -586,8 +617,19 @@ def get_options(self) -> 'KeyedOptionDictType':
         return opts
 
     def _to_host_compiler_options(self, options: 'KeyedOptionDictType') -> 'KeyedOptionDictType':
+        """
+        Convert an NVCC Option set to a host compiler's option set.
+        """
+
+        # We must strip the -std option from the host compiler option set, as NVCC has
+        # its own -std flag that may not agree with the host compiler's.
         overrides = {name: opt.value for name, opt in options.items()}
-        return OptionOverrideProxy(overrides, self.host_compiler.get_options())
+        overrides.pop(OptionKey('std', machine=self.for_machine,
+                                       lang=self.host_compiler.language), None)
+        host_options = self.host_compiler.get_options().copy()
+        if 'std' in host_options:
+            del host_options['std'] # type: ignore
+        return OptionOverrideProxy(overrides, host_options)
 
     def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
         args = self.get_ccbin_args(options)


Test patch:
diff --git a/test cases/cuda/16 multistd/main.cu b/test cases/cuda/16 multistd/main.cu
new file mode 100644
--- /dev/null
+++ b/test cases/cuda/16 multistd/main.cu	
@@ -0,0 +1,20 @@
+#include <cuda_runtime.h>
+#include <iostream>
+
+auto cuda_devices(void) {
+    int result = 0;
+    cudaGetDeviceCount(&result);
+    return result;
+}
+
+
+int main(void) {
+    int n = cuda_devices();
+    if (n == 0) {
+        std::cout << "No Cuda hardware found. Exiting.\n";
+        return 0;
+    }
+
+    std::cout << "Found " << n << "Cuda devices.\n";
+    return 0;
+}
diff --git a/test cases/cuda/16 multistd/meson.build b/test cases/cuda/16 multistd/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/cuda/16 multistd/meson.build	
@@ -0,0 +1,4 @@
+project('C++-CUDA multi-std', 'cpp', 'cuda', version : '1.0.0', default_options : ['cpp_std=c++17', 'cuda_std=c++14'])
+
+exe = executable('prog', 'main.cu')
+test('cudatest', exe)