Repository: mesonbuild/meson

Issue:
Meson 0.60 missing RPATH for ExternalLibrary after static_library dependency propagation
**Describe the bug**
Build targets linking a `static_library` that itself depends on an external library found by `compiler.find_library(dirs: ...)` are missing the requisite RPATH arguments in the final link line. If the library provides an `SONAME` the resulting binaries will be unusable (without setting `LD_LIBRARY_PATH`).

This error is observable in 0.60.0 and the current master (5af556e2b85ce264bb7742261e68ef4347b316aa). It does not appear in 0.59.4.

**To Reproduce**
```meson
# meson.build
project('test', 'c')
foo_lib = meson.get_compiler('c').find_library('foo', dirs: meson.project_source_root())
main_lib = static_library('main', 'main.c', dependencies: foo_lib)
executable('main', link_with: main_lib)
```
```c
// foo.c
#include <stdio.h>
void foo() { printf("Foo\n"); }

// main.c
extern void foo();
int main() { foo(); return 0; }
```
```console
$ cc foo.c -o libfoo.so -shared -fPIC -Wl,-soname,libfoo.so
$ meson _builddir
The Meson build system
Version: 0.60.99
Source dir: /tmp/zzz
Build dir: /tmp/zzz/_builddir
Build type: native build
Project name: test
Project version: undefined
C compiler for the host machine: ccache cc (gcc 11.2.0 "cc (Debian 11.2.0-10) 11.2.0")
C linker for the host machine: cc ld.bfd 2.37
Host machine cpu family: x86_64
Host machine cpu: x86_64
Library foo found: YES
Build targets in project: 2

Found ninja-1.10.1 at /usr/bin/ninja
$ meson compile -C _builddir -v
ninja: Entering directory `/tmp/zzz/_builddir'
[1/3] ccache cc -Ilibmain.a.p -I. -I.. -fdiagnostics-color=always -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -g -fPIC -MD -MQ libmain.a.p/main.c.o -MF libmain.a.p/main.c.o.d -o libmain.a.p/main.c.o -c ../main.c
[2/3] rm -f libmain.a && gcc-ar csrDT libmain.a libmain.a.p/main.c.o
[3/3] cc  -o main  -Wl,--as-needed -Wl,--no-undefined -Wl,--start-group libmain.a /tmp/zzz/libfoo.so -Wl,--end-group
$ ./_builddir/main
./_builddir/main: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory
```

**Expected behavior**
RPATH arguments for the external library's directory would be present in the final link, as they are in 0.59.4:
```console
$ meson _builddir
The Meson build system
Version: 0.59.4
...
[3/3] cc  -o main  -Wl,--as-needed -Wl,--no-undefined '-Wl,-rpath,$ORIGIN/..' -Wl,-rpath-link,/tmp/zzz/_builddir/../ -Wl,--start-group libmain.a /tmp/zzz/libfoo.so -Wl,--end-group
$ ./_builddir/main
Foo
```

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)? Native build
* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.): Debian Unstable/Sid
* what Python version are you using e.g. 3.8.0: Python 3.9.7
* what `meson --version`: Meson 0.60.99
* what `ninja --version` if it's a Ninja build: Ninja 1.10.1


Discussion:
Bisected to commit f6f35aa906c8ebac21af07c17449df65d08aeaeb

Resolving patch:
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -395,15 +395,15 @@ def get_target_source_dir(self, target: build.Target) -> str:
             return os.path.join(self.build_to_src, target_dir)
         return self.build_to_src
 
-    def get_target_private_dir(self, target: build.Target) -> str:
+    def get_target_private_dir(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]) -> str:
         return os.path.join(self.get_target_filename(target, warn_multi_output=False) + '.p')
 
-    def get_target_private_dir_abs(self, target: build.Target) -> str:
+    def get_target_private_dir_abs(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]) -> str:
         return os.path.join(self.environment.get_build_dir(), self.get_target_private_dir(target))
 
     @lru_cache(maxsize=None)
     def get_target_generated_dir(
-            self, target: build.Target,
+            self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex],
             gensrc: T.Union[build.CustomTarget, build.CustomTargetIndex, build.GeneratedList],
             src: str) -> str:
         """
@@ -418,7 +418,8 @@ def get_target_generated_dir(
         # target that the GeneratedList is used in
         return os.path.join(self.get_target_private_dir(target), src)
 
-    def get_unity_source_file(self, target: build.Target, suffix: str, number: int) -> mesonlib.File:
+    def get_unity_source_file(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex],
+                              suffix: str, number: int) -> mesonlib.File:
         # There is a potential conflict here, but it is unlikely that
         # anyone both enables unity builds and has a file called foo-unity.cpp.
         osrc = f'{target.name}-unity{number}.{suffix}'
@@ -763,18 +764,24 @@ def rpaths_for_bundled_shared_libraries(self, target: build.BuildTarget, exclude
                 paths.append(os.path.join(self.build_to_src, rel_to_src))
             else:
                 paths.append(libdir)
+        for i in chain(target.link_targets, target.link_whole_targets):
+            if isinstance(i, build.BuildTarget):
+                paths.extend(self.rpaths_for_bundled_shared_libraries(i, exclude_system))
         return paths
 
-    def determine_rpath_dirs(self, target: build.BuildTarget) -> T.Tuple[str, ...]:
+    # This may take other types
+    def determine_rpath_dirs(self, target: T.Union[build.BuildTarget, build.CustomTarget, build.CustomTargetIndex]
+                             ) -> T.Tuple[str, ...]:
         result: OrderedSet[str]
         if self.environment.coredata.get_option(OptionKey('layout')) == 'mirror':
-            # NEed a copy here
+            # Need a copy here
             result = OrderedSet(target.get_link_dep_subdirs())
         else:
             result = OrderedSet()
             result.add('meson-out')
-        result.update(self.rpaths_for_bundled_shared_libraries(target))
-        target.rpath_dirs_to_remove.update([d.encode('utf-8') for d in result])
+        if isinstance(target, build.BuildTarget):
+            result.update(self.rpaths_for_bundled_shared_libraries(target))
+            target.rpath_dirs_to_remove.update([d.encode('utf-8') for d in result])
         return tuple(result)
 
     @staticmethod
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -695,7 +695,7 @@ def __init__(self, name: str, subdir: str, subproject: str, for_machine: Machine
         self.external_deps: T.List[dependencies.Dependency] = []
         self.include_dirs: T.List['IncludeDirs'] = []
         self.link_language = kwargs.get('link_language')
-        self.link_targets: T.List[BuildTarget] = []
+        self.link_targets: T.List[T.Union['BuildTarget', 'CustomTarget', 'CustomTargetIndex']] = []
         self.link_whole_targets = []
         self.link_depends = []
         self.added_deps = set()
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
--- a/unittests/allplatformstests.py
+++ b/unittests/allplatformstests.py
@@ -1499,19 +1499,45 @@ def test_prebuilt_shared_lib(self):
         else:
             shlibfile = os.path.join(tdir, 'libalexandria.' + shared_suffix)
         self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
+
+        if is_windows():
+            def cleanup() -> None:
+                """Clean up all the garbage MSVC writes in the source tree."""
+
+                for fname in glob(os.path.join(tdir, 'alexandria.*')):
+                    if os.path.splitext(fname)[1] not in {'.c', '.h'}:
+                        os.unlink(fname)
+            self.addCleanup(cleanup)
+        else:
+            self.addCleanup(os.unlink, shlibfile)
+
         # Run the test
-        try:
-            self.init(tdir)
+        self.init(tdir)
+        self.build()
+        self.run_tests()
+
+    def test_prebuilt_shared_lib_rpath(self) -> None:
+        (cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env()
+        tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared')
+        with tempfile.TemporaryDirectory() as d:
+            source = os.path.join(tdir, 'alexandria.c')
+            objectfile = os.path.join(d, 'alexandria.' + object_suffix)
+            impfile = os.path.join(d, 'alexandria.lib')
+            if cc.get_argument_syntax() == 'msvc':
+                shlibfile = os.path.join(d, 'alexandria.' + shared_suffix)
+            elif is_cygwin():
+                shlibfile = os.path.join(d, 'cygalexandria.' + shared_suffix)
+            else:
+                shlibfile = os.path.join(d, 'libalexandria.' + shared_suffix)
+            # Ensure MSVC extra files end up in the directory that gets deleted
+            # at the end
+            with chdir(d):
+                self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
+
+            # Run the test
+            self.init(tdir, extra_args=[f'-Dsearch_dir={d}'])
             self.build()
             self.run_tests()
-        finally:
-            os.unlink(shlibfile)
-            if is_windows():
-                # Clean up all the garbage MSVC writes in the
-                # source tree.
-                for fname in glob(os.path.join(tdir, 'alexandria.*')):
-                    if os.path.splitext(fname)[1] not in ['.c', '.h']:
-                        os.unlink(fname)
 
     @skipIfNoPkgconfig
     def test_pkgconfig_static(self):


Test patch:
diff --git a/test cases/unit/17 prebuilt shared/meson.build b/test cases/unit/17 prebuilt shared/meson.build
--- a/test cases/unit/17 prebuilt shared/meson.build	
+++ b/test cases/unit/17 prebuilt shared/meson.build	
@@ -1,7 +1,12 @@
 project('prebuilt shared library', 'c')
 
+search_dir = get_option('search_dir')
+if search_dir == 'auto'
+  search_dir = meson.current_source_dir()
+endif
+
 cc = meson.get_compiler('c')
-shlib = cc.find_library('alexandria', dirs : meson.current_source_dir())
+shlib = cc.find_library('alexandria', dirs : search_dir)
 
 exe = executable('patron', 'patron.c', dependencies : shlib)
 test('visitation', exe)
@@ -11,3 +16,23 @@ d = declare_dependency(dependencies : shlib)
 exe2 = executable('another_visitor', 'another_visitor.c',
   dependencies : d)
 test('another', exe2)
+
+stlib = static_library(
+  'rejected',
+  'rejected.c',
+  dependencies : shlib,
+)
+
+rejected = executable(
+  'rejected',
+  'rejected_main.c',
+  link_with : stlib,
+)
+test('rejected', rejected)
+
+rejected_whole = executable(
+  'rejected_whole',
+  'rejected_main.c',
+  link_whole : stlib,
+)
+test('rejected (whole archive)', rejected_whole)
diff --git a/test cases/unit/17 prebuilt shared/meson_options.txt b/test cases/unit/17 prebuilt shared/meson_options.txt
new file mode 100644
--- /dev/null
+++ b/test cases/unit/17 prebuilt shared/meson_options.txt	
@@ -0,0 +1 @@
+option('search_dir', type : 'string', value : 'auto')
diff --git a/test cases/unit/17 prebuilt shared/rejected.c b/test cases/unit/17 prebuilt shared/rejected.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/17 prebuilt shared/rejected.c	
@@ -0,0 +1,8 @@
+#include "rejected.h"
+
+void say(void) {
+    printf("You are standing outside the Great Library of Alexandria.\n");
+    printf("You decide to go inside.\n\n");
+    alexandria_visit();
+    printf("The librarian tells you it's time to leave\n");
+}
diff --git a/test cases/unit/17 prebuilt shared/rejected.h b/test cases/unit/17 prebuilt shared/rejected.h
new file mode 100644
--- /dev/null
+++ b/test cases/unit/17 prebuilt shared/rejected.h	
@@ -0,0 +1,6 @@
+#include <stdio.h>
+#include <alexandria.h>
+
+#pragma once
+
+void say(void);
diff --git a/test cases/unit/17 prebuilt shared/rejected_main.c b/test cases/unit/17 prebuilt shared/rejected_main.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/17 prebuilt shared/rejected_main.c	
@@ -0,0 +1,6 @@
+#include "rejected.h"
+
+int main(void) {
+    say();
+    return 0;
+}