Repository: mesonbuild/meson

Issue:
meson introspect --installed does not list symlinks
##  Problem

When enumerating installed artifacts with `meson introspect --installed` (with `ninja` on Linux, probably elsewhere as well), a `shared_library()` such as `libfoo.so.1.2.3` will appear, but auto-generated (and still installed) symlinks like `libfoo.so.1` and `libfoo.so` are not. Since they are nevertheless installed, I'd expect them to be enumerated.

## Reproduction

For instance,
```bash
cd "test cases/linuxlike/7 library versions"
meson build/
meson introspect --installed --indent build/
```
produces 
```json
{
    "/home/meson/test cases/linuxlike/7 library versions/build/libsome.so.1.2.3": "/usr/local/lib64/libsome.so.1.2.3",
    "/home/meson/test cases/linuxlike/7 library versions/build/libnoversion.so": "/usr/local/lib64/libnoversion.so",
    "/home/meson/test cases/linuxlike/7 library versions/build/libonlyversion.so.1.4.5": "/usr/local/lib64/libonlyversion.so.1.4.5",
    "/home/meson/test cases/linuxlike/7 library versions/build/libonlysoversion.so.5": "/usr/local/lib64/libonlysoversion.so.5",
    "/home/meson/test cases/linuxlike/7 library versions/build/libmodule.so": "/usr/local/lib64/libmodule.so"
}
```
yet installation with `cd build && ninja && ninja install` would place the symlinks `libsome.so` and `libsome.so.0` side-by-side with `libsome.so.1.2.3`.

## Expected

It would have been nice if there was a symmetry between `ninja install`'s actions and the accounting that `meson introspect --installed` gives. It's much easier to detect and remove symlinks from a list of installed targets that include them, than determining from a list of installed files which excluded symlinks transitively point into them.

Alternately, a new `--installed-extra` flag could be added, or a `--dry-run --verbose --json` flag set could be added to `meson install`, or some other machine-readable form to present that information.


Discussion:

Resolving patch:
diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py
--- a/mesonbuild/mintro.py
+++ b/mesonbuild/mintro.py
@@ -95,6 +95,9 @@ def list_installed(installdata):
         for t in installdata.targets:
             res[os.path.join(installdata.build_dir, t.fname)] = \
                 os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname))
+            for alias in t.aliases.keys():
+                res[os.path.join(installdata.build_dir, alias)] = \
+                    os.path.join(installdata.prefix, t.outdir, os.path.basename(alias))
         for path, installpath, _ in installdata.data:
             res[path] = os.path.join(installdata.prefix, installpath)
         for path, installdir, _ in installdata.headers:
@@ -154,8 +157,9 @@ def list_targets(builddata: build.Build, installdata, backend: backends.Backend)
     # Fast lookup table for installation files
     install_lookuptable = {}
     for i in installdata.targets:
-        outname = os.path.join(installdata.prefix, i.outdir, os.path.basename(i.fname))
-        install_lookuptable[os.path.basename(i.fname)] = str(PurePath(outname))
+        out = [os.path.join(installdata.prefix, i.outdir, os.path.basename(i.fname))]
+        out += [os.path.join(installdata.prefix, i.outdir, os.path.basename(x)) for x in i.aliases]
+        install_lookuptable[os.path.basename(i.fname)] = [str(PurePath(x)) for x in out]
 
     for (idname, target) in builddata.get_targets().items():
         if not isinstance(target, build.Target):
@@ -174,7 +178,8 @@ def list_targets(builddata: build.Build, installdata, backend: backends.Backend)
 
         if installdata and target.should_install():
             t['installed'] = True
-            t['install_filename'] = [install_lookuptable.get(x, None) for x in target.get_outputs()]
+            t['install_filename'] = [install_lookuptable.get(x, [None]) for x in target.get_outputs()]
+            t['install_filename'] = [x for sublist in t['install_filename'] for x in sublist]  # flatten the list
         else:
             t['installed'] = False
         tlist.append(t)
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -1466,6 +1466,7 @@ def setUp(self):
         self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks')
         self.unit_test_dir = os.path.join(src_root, 'test cases/unit')
         self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite')
+        self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike')
         # Misc stuff
         self.orig_env = os.environ.copy()
         if self.backend is Backend.ninja:
@@ -5753,6 +5754,31 @@ def test_introspect_dependencies(self):
         self.assertIsInstance(docbook_target, dict)
         self.assertEqual(os.path.basename(t['filename'][0]), 'generated-gdbus-doc-' + os.path.basename(t['target_sources'][0]['sources'][0]))
 
+    def test_introspect_installed(self):
+        testdir = os.path.join(self.linuxlike_test_dir, '7 library versions')
+        self.init(testdir)
+
+        install = self.introspect('--installed')
+        install = {os.path.basename(k): v for k, v in install.items()}
+        self.assertDictEqual(install, {
+            'libmodule.so': '/usr/lib/libmodule.so',
+            'libnoversion.so': '/usr/lib/libnoversion.so',
+            'libonlysoversion.so': '/usr/lib/libonlysoversion.so',
+            'libonlysoversion.so.5': '/usr/lib/libonlysoversion.so.5',
+            'libonlyversion.so': '/usr/lib/libonlyversion.so',
+            'libonlyversion.so.1': '/usr/lib/libonlyversion.so.1',
+            'libonlyversion.so.1.4.5': '/usr/lib/libonlyversion.so.1.4.5',
+            'libsome.so': '/usr/lib/libsome.so',
+            'libsome.so.0': '/usr/lib/libsome.so.0',
+            'libsome.so.1.2.3': '/usr/lib/libsome.so.1.2.3',
+        })
+
+        targets = self.introspect('--targets')
+        for t in targets:
+            if t['name'] != 'some':
+                continue
+            self.assertSetEqual({'/usr/lib/libsome.so', '/usr/lib/libsome.so.0', '/usr/lib/libsome.so.1.2.3'}, set(t['install_filename']))
+
     def test_build_rpath(self):
         if is_cygwin():
             raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH')


Test patch: