Repository: mesonbuild/meson

Issue:
Meson won't generate header dependency files for CUDA source files.
**Describe the bug**
When building CUDA executable header dependency files are not generated using nvcc, so updating headers won't trigger rebuild.

In the generated build.ninja file we can see that there is no compiler flags for nvcc to generate the dependency files:

```
# Rules for compiling.

rule cuda_COMPILER
 command = nvcc $ARGS -o $out -c $in
 deps = gcc
 depfile = $DEPFILE_UNQUOTED
 description = Compiling Cuda object $out
```

even if the targets specifies these non-generated dependency files: 

```
# Build rules for targets

build FOO.p/main.cu.o: cuda_COMPILER ../main.cu
 DEPFILE = FOO.p/main.cu.o.d
 DEPFILE_UNQUOTED = FOO.p/main.cu.o.d
 ARGS = -IFOO.p -Xcompiler=-Minform=inform -g -G -g -I../include -I.. -I. -IFOO.p
```

**To Reproduce**
Folder with following structure:

main.cu
include/Foo.hpp
meson.cpp

where main.cu is:

```
#include "Foo.hpp"

int main() {
	Foo();
}
```

include/Foo.hpp is:

```
#include <iostream>

void Foo() {
	std::cout << "Hello from Foo\n";
}
```

meson.build is:

```
project('example', 'cuda')

executable('FOO', 'main.cu', include_directories : ['include'])
```

To initialize build folder:
```
meson setup build
```

**Expected behavior**
Meson to specify compiler flags for nvcc to generate the dependency files so modifying headers would trigger recompilations.
 
https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#generate-dependencies-m

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)?
native

* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.)
Red Hat Enterprise Linux 8.6 (Ootpa)

* what Python version are you using e.g. 3.8.0
Python 3.10.12

* what `meson --version`
1.1.1

* what `ninja --version` if it's a Ninja build
1.12.0.git


Discussion:
Unfortunately, it does not work. See some prior attempts to fix this:
#4991
#10993
#11229

Discussion from the original cuda support issue: https://github.com/mesonbuild/meson/issues/1003#issuecomment-259103463


Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -2451,7 +2451,11 @@ def generate_compile_rule_for(self, langname, compiler):
             # See also: https://github.com/ninja-build/ninja/pull/2275
             options['extra'] = 'restat = 1'
         rule = self.compiler_to_rule_name(compiler)
-        depargs = NinjaCommandArg.list(compiler.get_dependency_gen_args('$out', '$DEPFILE'), Quoting.none)
+        if langname == 'cuda':
+            # for cuda, we manually escape target name ($out) as $CUDA_ESCAPED_TARGET because nvcc doesn't support `-MQ` flag
+            depargs = NinjaCommandArg.list(compiler.get_dependency_gen_args('$CUDA_ESCAPED_TARGET', '$DEPFILE'), Quoting.none)
+        else:
+            depargs = NinjaCommandArg.list(compiler.get_dependency_gen_args('$out', '$DEPFILE'), Quoting.none)
         command = compiler.get_exelist()
         args = ['$ARGS'] + depargs + NinjaCommandArg.list(compiler.get_output_args('$out'), Quoting.none) + compiler.get_compile_only_args() + ['$in']
         description = f'Compiling {compiler.get_display_language()} object $out'
@@ -3006,6 +3010,28 @@ def generate_single_compile(self, target: build.BuildTarget, src,
             element.add_orderdep(i)
         if dep_file:
             element.add_item('DEPFILE', dep_file)
+        if compiler.get_language() == 'cuda':
+            # for cuda, we manually escape target name ($out) as $CUDA_ESCAPED_TARGET because nvcc doesn't support `-MQ` flag
+            def quote_make_target(targetName: str) -> str:
+                # this escape implementation is taken from llvm
+                result = ''
+                for (i, c) in enumerate(targetName):
+                    if c in {' ', '\t'}:
+                        # Escape the preceding backslashes
+                        for j in range(i - 1, -1, -1):
+                            if targetName[j] == '\\':
+                                result += '\\'
+                            else:
+                                break
+                        # Escape the space/tab
+                        result += '\\'
+                    elif c == '$':
+                        result += '$'
+                    elif c == '#':
+                        result += '\\'
+                    result += c
+                return result
+            element.add_item('CUDA_ESCAPED_TARGET', quote_make_target(rel_obj))
         element.add_item('ARGS', commands)
 
         self.add_dependency_scanner_entries_to_element(target, compiler, element, src)
diff --git a/mesonbuild/compilers/cuda.py b/mesonbuild/compilers/cuda.py
--- a/mesonbuild/compilers/cuda.py
+++ b/mesonbuild/compilers/cuda.py
@@ -735,6 +735,15 @@ def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
     def get_output_args(self, target: str) -> T.List[str]:
         return ['-o', target]
 
+    def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
+        if version_compare(self.version, '>= 10.2'):
+            # According to nvcc Documentation, `-MD` option is added after 10.2
+            # Reference: [CUDA 10.1](https://docs.nvidia.com/cuda/archive/10.1/cuda-compiler-driver-nvcc/index.html#options-for-specifying-compilation-phase-generate-nonsystem-dependencies)
+            # Reference: [CUDA 10.2](https://docs.nvidia.com/cuda/archive/10.2/cuda-compiler-driver-nvcc/index.html#options-for-specifying-compilation-phase-generate-nonsystem-dependencies)
+            return ['-MD', '-MT', outtarget, '-MF', outfile]
+        else:
+            return []
+
     def get_std_exe_link_args(self) -> T.List[str]:
         return self._to_host_flags(self.host_compiler.get_std_exe_link_args(), _Phase.LINKER)
 


Test patch: