Repository: mesonbuild/meson

Issue:
Link against custom targets
For integration with custom toolchains and other such stuff. Basically if you put the output of custom_target to the `link_with` line of a build target it will:

 - add a dependency in the usual way
 - add the output file to the link line
 - assumes that this is enough to make it work, if not, then that is the build definition writer's developer's problem

This can probably only be done reasonably for static libraries. Dependencies of dependencies also don't really work, because to do so would mean having to replicate Meson's entire dependency resolution chain for custom targets.

I was going to do this today but am mostly been in bed suffering from the seasonal flu. Feel free to implement this if you wish.


Discussion:
Any chance for this to be implemented in 0.50? Since, AFAIK, there is currently no way to make a dependency relation for libs produced by `custom_target`, thus it's impossible to integrate subprojects based on non-meson build systems (i.e through `declare_dependency`).
Feature freeze for 0.50.0 was last Sunday, so sadly no.
That's a shame =(
Do you think this feature will be implemented in the trunk any time soon?
That is a shame :( 

(especially since this was opened before last Sunday)

Resolving patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -211,6 +211,10 @@ def get_target_filename_for_linking(self, target):
             return os.path.join(self.get_target_dir(target), link_lib)
         elif isinstance(target, build.StaticLibrary):
             return os.path.join(self.get_target_dir(target), target.get_filename())
+        elif isinstance(target, build.CustomTarget):
+            if not target.is_linkable_target():
+                raise MesonException('Tried to link against custom target "%s", which is not linkable.' % target.name)
+            return os.path.join(self.get_target_dir(target), target.get_filename())
         elif isinstance(target, build.Executable):
             if target.import_filename:
                 return os.path.join(self.get_target_dir(target), target.get_import_filename())
@@ -964,6 +968,12 @@ def eval_custom_target_command(self, target, absolute_outputs=False):
                     raise MesonException(msg)
                 dfilename = os.path.join(outdir, target.depfile)
                 i = i.replace('@DEPFILE@', dfilename)
+            elif '@PRIVATE_DIR@' in i:
+                if target.absolute_paths:
+                    pdir = self.get_target_private_dir_abs(target)
+                else:
+                    pdir = self.get_target_private_dir(target)
+                i = i.replace('@PRIVATE_DIR@', pdir)
             elif '@PRIVATE_OUTDIR_' in i:
                 match = re.search(r'@PRIVATE_OUTDIR_(ABS_)?([^/\s*]*)@', i)
                 if not match:
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -567,6 +567,8 @@ def process_compilers_late(self):
         if self.link_targets or self.link_whole_targets:
             extra = set()
             for t in itertools.chain(self.link_targets, self.link_whole_targets):
+                if isinstance(t, CustomTarget):
+                    continue # We can't know anything about these.
                 for name, compiler in t.compilers.items():
                     if name in clink_langs:
                         extra.add((name, compiler))
@@ -1062,19 +1064,24 @@ def link(self, target):
                 msg = "Can't link non-PIC static library {!r} into shared library {!r}. ".format(t.name, self.name)
                 msg += "Use the 'pic' option to static_library to build with PIC."
                 raise InvalidArguments(msg)
-            if self.is_cross != t.is_cross:
+            if not isinstance(t, CustomTarget) and self.is_cross != t.is_cross:
                 raise InvalidArguments('Tried to mix cross built and native libraries in target {!r}'.format(self.name))
             self.link_targets.append(t)
 
     def link_whole(self, target):
         for t in listify(target, unholder=True):
-            if not isinstance(t, StaticLibrary):
+            if isinstance(t, CustomTarget):
+                if not t.is_linkable_target():
+                    raise InvalidArguments('Custom target {!r} is not linkable.'.format(t))
+                if not t.get_filename().endswith('.a'):
+                    raise InvalidArguments('Can only link_whole custom targets that are .a archives.')
+            elif not isinstance(t, StaticLibrary):
                 raise InvalidArguments('{!r} is not a static library.'.format(t))
             if isinstance(self, SharedLibrary) and not t.pic:
                 msg = "Can't link non-PIC static library {!r} into shared library {!r}. ".format(t.name, self.name)
                 msg += "Use the 'pic' option to static_library to build with PIC."
                 raise InvalidArguments(msg)
-            if self.is_cross != t.is_cross:
+            if not isinstance(t, CustomTarget) and self.is_cross != t.is_cross:
                 raise InvalidArguments('Tried to mix cross built and native libraries in target {!r}'.format(self.name))
             self.link_whole_targets.append(t)
 
@@ -1149,6 +1156,8 @@ def get_langs_used_by_deps(self):
         # Check if any of the internal libraries this target links to were
         # written in this language
         for link_target in itertools.chain(self.link_targets, self.link_whole_targets):
+            if isinstance(link_target, CustomTarget):
+                continue
             for language in link_target.compilers:
                 if language not in langs:
                     langs.append(language)
@@ -2090,6 +2099,22 @@ def get_dep_outname(self, infilenames):
                 raise InvalidArguments('Substitution in depfile for custom_target that does not have an input file.')
             return self.depfile
 
+    def is_linkable_target(self):
+        if len(self.outputs) != 1:
+            return False
+        suf = os.path.splitext(self.outputs[0])[-1]
+        if suf == '.a' or suf == '.dll' or suf == '.lib' or suf == '.so':
+            return True
+
+    def get_link_deps_mapping(self, prefix, environment):
+        return {}
+
+    def get_link_dep_subdirs(self):
+        return OrderedSet()
+
+    def get_all_link_deps(self):
+        return []
+
     def type_suffix(self):
         return "@cus"
 


Test patch:
diff --git a/test cases/common/216 link custom/custom_stlib.py b/test cases/common/216 link custom/custom_stlib.py
new file mode 100755
--- /dev/null
+++ b/test cases/common/216 link custom/custom_stlib.py	
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+import shutil, sys, subprocess, argparse, pathlib
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument('--private-dir', required=True)
+parser.add_argument('-o', required=True)
+parser.add_argument('cmparr', nargs='+')
+
+contents = '''#include<stdio.h>
+
+void flob() {
+    printf("Now flobbing.\\n");
+}
+'''
+
+def generate_lib_gnulike(outfile, c_file, private_dir, compiler_array):
+    if shutil.which('ar'):
+        static_linker = 'ar'
+    elif shutil.which('llvm-ar'):
+        static_linker = 'llvm-ar'
+    elif shutil.which('gcc-ar'):
+        static_linker = 'gcc-ar'
+    else:
+        sys.exit('Could not detect a static linker.')
+    o_file = c_file.with_suffix('.o')
+    compile_cmd = compiler_array + ['-c', '-g', '-O2', '-o', str(o_file), str(c_file)]
+    subprocess.check_call(compile_cmd)
+    out_file = pathlib.Path(outfile)
+    if out_file.exists():
+        out_file.unlink()
+    link_cmd = [static_linker, 'csr', outfile, str(o_file)]
+    subprocess.check_call(link_cmd)
+    return 0
+
+
+def generate_lib_msvc(outfile, c_file, private_dir, compiler_array):
+    static_linker = 'lib'
+    o_file = c_file.with_suffix('.obj')
+    compile_cmd = compiler_array + ['/MDd',
+                                    '/nologo',
+                                    '/ZI',
+                                    '/Ob0',
+                                    '/Od',
+                                    '/c',
+                                    '/Fo' + str(o_file),
+                                    str(c_file)]
+    subprocess.check_call(compile_cmd)
+    out_file = pathlib.Path(outfile)
+    if out_file.exists():
+        out_file.unlink()
+    link_cmd = [static_linker,
+                '/nologo',
+                '/OUT:' + str(outfile),
+                str(o_file)]
+    subprocess.check_call(link_cmd)
+    return 0
+
+def generate_lib(outfile, private_dir, compiler_array):
+    private_dir = pathlib.Path(private_dir)
+    if not private_dir.exists():
+        private_dir.mkdir()
+    c_file = private_dir / 'flob.c'
+    c_file.write_text(contents)
+    for i in compiler_array:
+        if (i.endswith('cl') or i.endswith('cl.exe')) and 'clang-cl' not in i:
+            return generate_lib_msvc(outfile, c_file, private_dir, compiler_array)
+    return generate_lib_gnulike(outfile, c_file, private_dir, compiler_array)
+
+if __name__ == '__main__':
+    options = parser.parse_args()
+    sys.exit(generate_lib(options.o, options.private_dir, options.cmparr))
diff --git a/test cases/common/216 link custom/meson.build b/test cases/common/216 link custom/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/216 link custom/meson.build	
@@ -0,0 +1,35 @@
+project('linkcustom', 'c')
+
+# This would require passing the static linker to the build script or having
+# it detect it by itself. I'm too lazy to implement it now and it is not
+# really needed for testing that custom targets work. It is the responsibility
+# of the custom target to produce things in the correct format.
+assert(not meson.is_cross_build(),
+       'MESON_SKIP_TEST cross checking not implemented.')
+
+cc = meson.get_compiler('c')
+genprog = find_program('custom_stlib.py')
+
+clib = custom_target('linkcustom',
+  output: 'libflob.a',
+  command: [genprog,
+            '-o', '@OUTPUT@',
+            '--private-dir', '@PRIVATE_DIR@'] + cc.cmd_array())
+
+exe = executable('prog', 'prog.c', link_with: clib)
+test('linkcustom', exe)
+
+d = declare_dependency(link_with: clib)
+
+exe2 = executable('prog2', 'prog.c', dependencies: d)
+test('linkcustom2', exe2)
+
+# Link whole tests
+
+exe3 = executable('prog3', 'prog.c', link_whole: clib)
+test('linkwhole', exe)
+
+d2 = declare_dependency(link_whole: clib)
+
+exe4 = executable('prog4', 'prog.c', dependencies: d2)
+test('linkwhole2', exe2)
diff --git a/test cases/common/216 link custom/prog.c b/test cases/common/216 link custom/prog.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/216 link custom/prog.c	
@@ -0,0 +1,6 @@
+void flob();
+
+int main(int argc, char **argv) {
+    flob();
+    return 0;
+}
diff --git a/test cases/failing/89 link_with custom target/demo.c b/test cases/failing/89 link_with custom target/demo.c
deleted file mode 100644
--- a/test cases/failing/89 link_with custom target/demo.c	
+++ /dev/null
@@ -1,5 +0,0 @@
-int func_in_foo();
-
-int main(int argc, char **argv) {
-    return func_in_foo();
-}
diff --git a/test cases/failing/89 link_with custom target/foo.c b/test cases/failing/89 link_with custom target/foo.c
deleted file mode 100644
--- a/test cases/failing/89 link_with custom target/foo.c	
+++ /dev/null
@@ -1,3 +0,0 @@
-int func_in_foo() {
-    return 0;
-}
diff --git a/test cases/failing/89 link_with custom target/lib_generator.py b/test cases/failing/89 link_with custom target/lib_generator.py
deleted file mode 100755
--- a/test cases/failing/89 link_with custom target/lib_generator.py	
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python3
-
-# Mimic a binary that generates a static library
-
-import os
-import subprocess
-import sys
-
-if __name__ == '__main__':
-    if len(sys.argv) != 4:
-        print(sys.argv[0], 'compiler input_file output_file')
-        sys.exit(1)
-    compiler = sys.argv[1]
-    ifile = sys.argv[2]
-    ofile = sys.argv[3]
-    tmp = ifile + '.o'
-    if compiler.endswith('cl'):
-        subprocess.check_call([compiler, '/nologo', '/MDd', '/Fo' + tmp, '/c', ifile])
-        subprocess.check_call(['lib', '/nologo', '/OUT:' + ofile, tmp])
-    else:
-        subprocess.check_call([compiler, '-c', ifile, '-o', tmp])
-        subprocess.check_call(['ar', 'csr', ofile, tmp])
-
-os.unlink(tmp)
diff --git a/test cases/failing/89 link_with custom target/meson.build b/test cases/failing/89 link_with custom target/meson.build
deleted file mode 100644
--- a/test cases/failing/89 link_with custom target/meson.build	
+++ /dev/null
@@ -1,23 +0,0 @@
-project('link_with custom target', ['c'])
-
-#
-# libraries created by a custom_target currently can be used in sources: (see
-# common/100 manygen/ for an example of that), but not in link_with:
-#
-
-lib_generator = find_program('lib_generator.py')
-
-cc = meson.get_compiler('c').cmd_array().get(-1)
-
-libfoo_target = custom_target(
-    'libfoo',
-    input: ['foo.c'],
-    output: ['libfoo.a'],
-    command: [lib_generator, cc, '@INPUT@', '@OUTPUT@']
-)
-
-libfoo = declare_dependency(
-    link_with: libfoo_target,
-)
-
-executable('demo', ['demo.c'], dependencies: [libfoo])