Repository: mesonbuild/meson

Issue:
RPATH removal breaks Xorg modules
Trying to build xorg (from github.com/anholt/xserver, branch Meson; I used `meson --prefix=~/prefix/xorg -Dxorg=yes -Dxephyr=true -Dxwayland=yes -Dglamor=yes -Dxnest=no -Ddmx=false -Dxkb_dir=/usr/share/X11/xkb -Dxkb_output_dir=/var/lib/xkb/compiled -Dhal=no -Dsystemd_logind=yes build` to build) works just fine. But the installation fails, specifically in how the RPATH removal mangles the modules.

With the default set of modules, dlopen() of modesetting_drv.so attempts to resolve a zero-length symbol:
```
[  8270.883] (II) LoadModule: "modesetting"
[  8270.883] (II) Loading /home/daniels/prefix/xorg/lib64/xorg/modules/drivers/modesetting_drv.so
[  8270.884] (EE) Failed to load /home/daniels/prefix/xorg/lib64/xorg/modules/drivers/modesetting_drv.so: /home/daniels/prefix/xorg/lib64/xorg/modules/drivers/modesetting_drv.so: undefined symbol: 
[  8270.884] (EE) Failed to load module "modesetting" (loader failed, 0)
```

Here is the file installed by `meson install`: https://static.fooishbar.org/tmp/modesetting_drv.so.broken
And here is the original: https://static.fooishbar.org/tmp/modesetting_drv.so.working
And, for completeness, the `chrpath -d` module, which also works fine: https://static.fooishbar.org/tmp/modesetting_drv.so.working.chrpath

@aaronp24 and ajax worked out that the difference was Meson NULLing out the rpath, whereas `chrpath` deletes it entirely. NULLing it out breaks entirely, because modesetting has a symbol named `modesetting`. Its rpath is also `.../hw/xorg/driver/modesetting`. Thanks to the magic of read-only data sharing, the symbol definition points to the end of the rpath string, which we've just obliterated. Oops.

Anyway, it looks like switching from `chrpath -r ''` to `chrpath -d` type semantics should fix it.


Discussion:

Resolving patch:
diff --git a/mesonbuild/scripts/depfixer.py b/mesonbuild/scripts/depfixer.py
--- a/mesonbuild/scripts/depfixer.py
+++ b/mesonbuild/scripts/depfixer.py
@@ -297,11 +297,20 @@ def fix_rpathtype_entry(self, new_rpath, entrynum):
         old_rpath = self.read_str()
         if len(old_rpath) < len(new_rpath):
             sys.exit("New rpath must not be longer than the old one.")
-        self.bf.seek(rp_off)
-        self.bf.write(new_rpath)
-        self.bf.write(b'\0' * (len(old_rpath) - len(new_rpath) + 1))
+        # The linker does read-only string deduplication. If there is a
+        # string that shares a suffix with the rpath, they might get
+        # dedupped. This means changing the rpath string might break something
+        # completely unrelated. This has already happened once with X.org.
+        # Thus we want to keep this change as small as possible to minimize
+        # the chance of obliterating other strings. It might still happen
+        # but our behaviour is identical to what chrpath does and it has
+        # been in use for ages so based on that this should be rare.
         if len(new_rpath) == 0:
             self.remove_rpath_entry(entrynum)
+        else:
+            self.bf.seek(rp_off)
+            self.bf.write(new_rpath)
+            self.bf.write(b'\0')
 
     def remove_rpath_entry(self, entrynum):
         sec = self.find_section(b'.dynamic')
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -1460,6 +1460,28 @@ def test_cpp_std_override(self):
         self.assertIn('-Werror', plain_comp)
         self.assertNotIn('-Werror', c03_comp)
 
+    def test_run_installed(self):
+        testdir = os.path.join(self.unit_test_dir, '7 run installed')
+        self.init(testdir)
+        self.build()
+        self.install()
+        installed_exe = os.path.join(self.installdir, 'usr/bin/prog')
+        installed_libdir = os.path.join(self.installdir, 'usr/foo')
+        installed_lib = os.path.join(installed_libdir, 'libfoo.so')
+        self.assertTrue(os.path.isfile(installed_exe))
+        self.assertTrue(os.path.isdir(installed_libdir))
+        self.assertTrue(os.path.isfile(installed_lib))
+        # Must fail when run without LD_LIBRARY_PATH to ensure that
+        # rpath has been properly stripped rather than pointing to the builddir.
+        self.assertNotEqual(subprocess.call(installed_exe, stderr=subprocess.DEVNULL), 0)
+        # When LD_LIBRARY_PATH is set it should start working.
+        # For some reason setting LD_LIBRARY_PATH in os.environ fails
+        # when all tests are run (but works when only this test is run),
+        # but doing this explicitly works.
+        env = os.environ.copy()
+        env['LD_LIBRARY_PATH'] = installed_libdir
+        self.assertEqual(subprocess.call(installed_exe, env=env), 0)
+
 class RewriterTests(unittest.TestCase):
 
     def setUp(self):


Test patch:
diff --git a/test cases/unit/7 run installed/foo/foo.c b/test cases/unit/7 run installed/foo/foo.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/7 run installed/foo/foo.c	
@@ -0,0 +1,3 @@
+int foo() {
+    return 0;
+}
diff --git a/test cases/unit/7 run installed/foo/meson.build b/test cases/unit/7 run installed/foo/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/7 run installed/foo/meson.build	
@@ -0,0 +1,7 @@
+# Try to invoke linker constant string deduplication,
+# to ensure we are not clobbering shared strings.
+# Name everything possible just as "foo".
+foolib = shared_library('foo', 'foo.c',
+  install_dir : 'foo',
+  install : true)
+
diff --git a/test cases/unit/7 run installed/meson.build b/test cases/unit/7 run installed/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/7 run installed/meson.build	
@@ -0,0 +1,9 @@
+project('foo', 'c',
+  default_options : 'libdir=lib')
+
+subdir('foo')
+
+executable('prog', 'prog.c',
+  link_with : foolib,
+  install : true)
+
diff --git a/test cases/unit/7 run installed/prog.c b/test cases/unit/7 run installed/prog.c
new file mode 100644
--- /dev/null
+++ b/test cases/unit/7 run installed/prog.c	
@@ -0,0 +1,5 @@
+int foo();
+
+int main(int argc, char **argv) {
+    return foo();
+}