Repository: mesonbuild/meson

Issue:
Failure when using CustomTarget with a Rust executable
**Describe the bug**

Using a CustomTarget as the only source of a Rust executable makes meson fail.


**To Reproduce**

The self-contained `meson.build` is:

```
project('proj', 'rust')

main_rs = custom_target(
  'main_rs',
  output: 'main.rs',
  capture: true,
  command: ['echo',
            'fn main() { println!("hi") }'
	   ],
)

executable('main', [main_rs])
```

Then fails at `meson build` with the following stack trace:

```
Found ninja-1.10.2 at /usr/bin/ninja
Traceback (most recent call last):                                                                  
  File "/usr/lib/python3.8/site-packages/mesonbuild/mesonmain.py", line 140, in run
    return options.run_func(options)
  File "/usr/lib/python3.8/site-packages/mesonbuild/msetup.py", line 253, in run
    app.generate()
  File "/usr/lib/python3.8/site-packages/mesonbuild/msetup.py", line 161, in generate
    self._generate(env)
  File "/usr/lib/python3.8/site-packages/mesonbuild/msetup.py", line 223, in _generate
    intr.backend.generate()
  File "/usr/lib/python3.8/site-packages/mesonbuild/backend/ninjabackend.py", line 519, in generate
    self.generate_target(t)
  File "/usr/lib/python3.8/site-packages/mesonbuild/backend/ninjabackend.py", line 842, in generate_target
    linker, stdlib_args = self.determine_linker_and_stdlib_args(target)
  File "/usr/lib/python3.8/site-packages/mesonbuild/backend/backends.py", line 465, in determine_linker_and_stdlib_args
    l, stdlib_args = target.get_clink_dynamic_linker_and_stdlibs()
  File "/usr/lib/python3.8/site-packages/mesonbuild/build.py", line 1325, in get_clink_dynamic_linker_and_stdlibs
    raise AssertionError(m.format(self.name))
AssertionError: Could not get a dynamic linker for build target 'main'
```


**Expected behavior**

Expected success, and when executing `ninja -C build` an executable called `main` would be created.


**Notes**

After digging a bit it seems that generated sources are not taken into account by neither `is_rust_target` nor `generate_rust_target` in the ninjabackend. Failing to identify this as a rust target seems to be the main problem here.


**System parameters**

* Linux
* Meson 0.56.0
* Ninja 1.10.2







Discussion:
Just be aware generating rust sources doesn't really work in general, because rust expects a unified build and source tree (because that's how cargo works). You need to copy all of your sources into the builddir by hand to make this work. Likely we'll eventually do that for you since upstream doesn't seem interested in fixing it.

Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -45,7 +45,7 @@
 )
 from ..mesonlib import get_compiler_for_source, has_path_sep, OptionKey
 from .backends import CleanTrees
-from ..build import InvalidArguments
+from ..build import GeneratedList, InvalidArguments
 from ..interpreter import Interpreter
 
 if T.TYPE_CHECKING:
@@ -691,13 +691,6 @@ def create_target_source_introspection(self, target: build.Target, comp: compile
         src_block['sources'] += sources
         src_block['generated_sources'] += generated_sources
 
-    def is_rust_target(self, target):
-        if len(target.sources) > 0:
-            first_file = target.sources[0]
-            if first_file.fname.endswith('.rs'):
-                return True
-        return False
-
     def generate_target(self, target):
         try:
             if isinstance(target, build.BuildTarget):
@@ -723,7 +716,7 @@ def generate_target(self, target):
         if isinstance(target, build.Jar):
             self.generate_jar_target(target)
             return
-        if self.is_rust_target(target):
+        if target.uses_rust():
             self.generate_rust_target(target)
             return
         if 'cs' in target.compilers:
@@ -1588,12 +1581,27 @@ def generate_rust_target(self, target: build.BuildTarget) -> None:
         args = rustc.compiler_args()
         # Compiler args for compiling this target
         args += compilers.get_base_compile_args(base_proxy, rustc)
+        self.generate_generator_list_rules(target)
+
+        orderdeps = [os.path.join(t.subdir, t.get_filename()) for t in target.link_targets]
+
         main_rust_file = None
         for i in target.get_sources():
             if not rustc.can_compile(i):
                 raise InvalidArguments('Rust target {} contains a non-rust source file.'.format(target.get_basename()))
             if main_rust_file is None:
                 main_rust_file = i.rel_to_builddir(self.build_to_src)
+        for g in target.get_generated_sources():
+            for i in g.get_outputs():
+                if not rustc.can_compile(i):
+                    raise InvalidArguments('Rust target {} contains a non-rust source file.'.format(target.get_basename()))
+                if isinstance(g, GeneratedList):
+                    fname = os.path.join(self.get_target_private_dir(target), i)
+                else:
+                    fname = i
+                if main_rust_file is None:
+                    main_rust_file = fname
+                orderdeps.append(fname)
         if main_rust_file is None:
             raise RuntimeError('A Rust target has no Rust sources. This is weird. Also a bug. Please report')
         target_name = os.path.join(target.subdir, target.get_filename())
@@ -1631,7 +1639,6 @@ def generate_rust_target(self, target: build.BuildTarget) -> None:
         args += target.get_extra_args('rust')
         args += rustc.get_output_args(os.path.join(target.subdir, target.get_filename()))
         args += self.environment.coredata.get_external_args(target.for_machine, rustc.language)
-        orderdeps = [os.path.join(t.subdir, t.get_filename()) for t in target.link_targets]
         linkdirs = OrderedDict()
         for d in target.link_targets:
             linkdirs[d.subdir] = True
@@ -1678,7 +1685,7 @@ def generate_rust_target(self, target: build.BuildTarget) -> None:
                 args += ['-C', 'link-arg=' + rpath_arg + ':' + os.path.join(rustc.get_sysroot(), 'lib')]
         compiler_name = self.get_compiler_rule_name('rust', target.for_machine)
         element = NinjaBuildElement(self.all_outputs, target_name, compiler_name, main_rust_file)
-        if len(orderdeps) > 0:
+        if orderdeps:
             element.add_orderdep(orderdeps)
         element.add_item('ARGS', args)
         element.add_item('targetdep', depfile)
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -569,7 +569,7 @@ def __init__(self, name: str, subdir: str, subproject: str, for_machine: Machine
         unity_opt = environment.coredata.get_option(OptionKey('unity'))
         self.is_unity = unity_opt == 'on' or (unity_opt == 'subprojects' and subproject != '')
         self.environment = environment
-        self.sources = []
+        self.sources: T.List[File] = []
         self.compilers = OrderedDict() # type: OrderedDict[str, Compiler]
         self.objects = []
         self.external_deps = []
@@ -588,7 +588,7 @@ def __init__(self, name: str, subdir: str, subproject: str, for_machine: Machine
         self.need_install = False
         self.pch = {}
         self.extra_args: T.Dict[str, T.List['FileOrString']] = {}
-        self.generated = []
+        self.generated: T.Sequence[T.Union[GeneratedList, CustomTarget, CustomTargetIndex]] = []
         self.d_features = {}
         self.pic = False
         self.pie = False
@@ -1412,9 +1412,16 @@ def get_clink_dynamic_linker_and_stdlibs(self):
         m = 'Could not get a dynamic linker for build target {!r}'
         raise AssertionError(m.format(self.name))
 
-    def get_using_rustc(self) -> bool:
+    def uses_rust(self) -> bool:
         """Is this target a rust target."""
-        return self.sources and self.sources[0].fname.endswith('.rs')
+        if self.sources:
+            first_file = self.sources[0]
+            if first_file.fname.endswith('.rs'):
+                return True
+        elif self.generated:
+            if self.generated[0].get_outputs()[0].endswith('.rs'):
+                return True
+        return False
 
     def get_using_msvc(self):
         '''
@@ -1434,11 +1441,13 @@ def get_using_msvc(self):
         2. If the target contains only objects, process_compilers guesses and
            picks the first compiler that smells right.
         '''
-        compiler, _ = self.get_clink_dynamic_linker_and_stdlibs()
+        # Rustc can use msvc style linkers
+        if self.uses_rust():
+            compiler = self.environment.coredata.compilers[self.for_machine]['rust']
+        else:
+            compiler, _ = self.get_clink_dynamic_linker_and_stdlibs()
         # Mixing many languages with MSVC is not supported yet so ignore stdlibs.
-        if compiler and compiler.get_linker_id() in {'link', 'lld-link', 'xilink', 'optlink'}:
-            return True
-        return False
+        return compiler and compiler.get_linker_id() in {'link', 'lld-link', 'xilink', 'optlink'}
 
     def check_module_linking(self):
         '''
@@ -1687,7 +1696,7 @@ def __init__(self, name: str, subdir: str, subproject: str, for_machine: Machine
                     self.import_filename = self.gcc_import_filename
 
         if m.is_windows() and ('cs' in self.compilers or
-                               self.get_using_rustc() or
+                               self.uses_rust() or
                                self.get_using_msvc()):
             self.debug_filename = self.name + '.pdb'
 
@@ -1877,7 +1886,7 @@ def determine_filenames(self, env):
             suffix = 'dll'
             self.vs_import_filename = '{0}{1}.lib'.format(self.prefix if self.prefix is not None else '', self.name)
             self.gcc_import_filename = '{0}{1}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name)
-            if self.get_using_rustc():
+            if self.uses_rust():
                 # Shared library is of the form foo.dll
                 prefix = ''
                 # Import library is called foo.dll.lib
diff --git a/mesonbuild/modules/unstable_rust.py b/mesonbuild/modules/unstable_rust.py
--- a/mesonbuild/modules/unstable_rust.py
+++ b/mesonbuild/modules/unstable_rust.py
@@ -85,7 +85,7 @@ def test(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any]) -
         base_target: BuildTarget = unholder(args[1])
         if not isinstance(base_target, BuildTarget):
             raise InterpreterException('Second positional argument to rustmod.test() must be a library or executable')
-        if not base_target.get_using_rustc():
+        if not base_target.uses_rust():
             raise InterpreterException('Second positional argument to rustmod.test() must be a rust based target')
         extra_args = stringlistify(kwargs.get('args', []))
 


Test patch:
diff --git a/test cases/rust/11 generated main/gen.py b/test cases/rust/11 generated main/gen.py
new file mode 100644
--- /dev/null
+++ b/test cases/rust/11 generated main/gen.py	
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+
+import argparse
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser()
+    parser.add_argument('out')
+    args = parser.parse_args()
+
+    with open(args.out, 'w') as f:
+        f.write('fn main() { println!("I prefer tarnish, actually.") }')
+
+
+if __name__ == "__main__":
+    main()
diff --git a/test cases/rust/11 generated main/meson.build b/test cases/rust/11 generated main/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/rust/11 generated main/meson.build	
@@ -0,0 +1,16 @@
+project('generated rust main', 'rust')
+
+gen = find_program('gen.py')
+
+c = custom_target(
+  'custom_target',
+  command : [gen, '@OUTPUT@'],
+  output : ['main.rs'],
+)
+
+executable('custom_target_main', c)
+executable('custom_target_index_main', c[0])
+
+gen = generator(gen, arguments : ['@OUTPUT@'], output : '@BASENAME@.rs')
+# Doesn't actually use gen.py as input, just a limitation of generators
+executable('generator_main', gen.process(['gen.py']))