Repository: mesonbuild/meson

Issue:
pkgconfig: Include libdir in .pc files if referenced
Some packages define variables in their .pc files and those variables
may depend on the value of the libdir variable even if no public or
private libraries are referenced in the .pc file.

Walk the list of variables and check if any of them reference ${libdir}
and if so, make sure to include the libdir definition as well.


Discussion:
@Tachi107 here's a slightly cleaned up version. I couldn't find any projects that use both regular and unescaped variables, but hacking up gst-plugins-base to include a dummy unescaped variable seems to have worked. I'm not sure if it's worth adding a test to meson for this behavior.
LGTM, thanks.
Nice, thanks again for noticing the issue :)

Resolving patch:
diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py
--- a/mesonbuild/modules/pkgconfig.py
+++ b/mesonbuild/modules/pkgconfig.py
@@ -22,8 +22,9 @@
 from .. import dependencies
 from .. import mesonlib
 from .. import mlog
+from ..coredata import BUILTIN_DIR_OPTIONS
 from ..dependencies import ThreadDependency
-from ..interpreterbase import permittedKwargs, FeatureNew, FeatureNewKwargs
+from ..interpreterbase import permittedKwargs, FeatureNew, FeatureDeprecated, FeatureNewKwargs
 
 if T.TYPE_CHECKING:
     from . import ModuleState
@@ -329,6 +330,41 @@ def _generate_pkgconfig_file(self, state, deps, subdirs, name, description,
                                  url, version, pcfile, conflicts, variables,
                                  unescaped_variables, uninstalled=False, dataonly=False):
         coredata = state.environment.get_coredata()
+        referenced_vars = set()
+        optnames = [x.name for x in BUILTIN_DIR_OPTIONS.keys()]
+
+        if not dataonly:
+            # includedir is always implied, although libdir may not be
+            # needed for header-only libraries
+            referenced_vars |= {'prefix', 'includedir'}
+            if deps.pub_libs or deps.priv_libs:
+                referenced_vars |= {'libdir'}
+        # also automatically infer variables referenced in other variables
+        implicit_vars_warning = False
+        redundant_vars_warning = False
+        varnames = set()
+        varstrings = set()
+        for k, v in variables + unescaped_variables:
+            varnames |= {k}
+            varstrings |= {v}
+        for optname in optnames:
+            optvar = f'${{{optname}}}'
+            if any(x.startswith(optvar) for x in varstrings):
+                if optname in varnames:
+                    redundant_vars_warning = True
+                else:
+                    # these 3 vars were always "implicit"
+                    if dataonly or optname not in {'prefix', 'includedir', 'libdir'}:
+                        implicit_vars_warning = True
+                    referenced_vars |= {'prefix', optname}
+        if redundant_vars_warning:
+            FeatureDeprecated.single_use('pkgconfig.generate variable for builtin directories', '0.62.0',
+                                         state.subproject, 'They will be automatically included when referenced',
+                                         state.current_node)
+        if implicit_vars_warning:
+            FeatureNew.single_use('pkgconfig.generate implicit variable for builtin directories', '0.62.0',
+                                  state.subproject, location=state.current_node)
+
         if uninstalled:
             outdir = os.path.join(state.environment.build_dir, 'meson-uninstalled')
             if not os.path.exists(outdir):
@@ -338,18 +374,17 @@ def _generate_pkgconfig_file(self, state, deps, subdirs, name, description,
         else:
             outdir = state.environment.scratch_dir
             prefix = PurePath(coredata.get_option(mesonlib.OptionKey('prefix')))
-        # These always return paths relative to prefix
-        libdir = PurePath(coredata.get_option(mesonlib.OptionKey('libdir')))
-        incdir = PurePath(coredata.get_option(mesonlib.OptionKey('includedir')))
         fname = os.path.join(outdir, pcfile)
         with open(fname, 'w', encoding='utf-8') as ofile:
-            if not dataonly:
-                ofile.write('prefix={}\n'.format(self._escape(prefix)))
-                if uninstalled:
-                    ofile.write('srcdir={}\n'.format(self._escape(srcdir)))
-                if deps.pub_libs or deps.priv_libs:
-                    ofile.write('libdir={}\n'.format(self._escape('${prefix}' / libdir)))
-                ofile.write('includedir={}\n'.format(self._escape('${prefix}' / incdir)))
+            for optname in optnames:
+                if optname in referenced_vars - varnames:
+                    if optname == 'prefix':
+                        ofile.write('prefix={}\n'.format(self._escape(prefix)))
+                    else:
+                        dirpath = PurePath(coredata.get_option(mesonlib.OptionKey(optname)))
+                        ofile.write('{}={}\n'.format(optname, self._escape('${prefix}' / dirpath)))
+            if uninstalled and not dataonly:
+                ofile.write('srcdir={}\n'.format(self._escape(srcdir)))
             if variables or unescaped_variables:
                 ofile.write('\n')
             for k, v in variables:


Test patch:
diff --git a/test cases/common/44 pkgconfig-gen/meson.build b/test cases/common/44 pkgconfig-gen/meson.build
--- a/test cases/common/44 pkgconfig-gen/meson.build	
+++ b/test cases/common/44 pkgconfig-gen/meson.build	
@@ -1,4 +1,4 @@
-project('pkgconfig-gen', 'c')
+project('pkgconfig-gen', 'c', meson_version: '>=0.60.0')
 
 # Some CI runners does not have zlib, just skip them as we need some common
 # external dependency.
@@ -149,3 +149,17 @@ ct = custom_target('stat3',
 )
 simple6 = library('simple6', link_with: ct)
 pkgg.generate(simple6)
+
+# implicit variables
+pkgg.generate(
+  name : 'libvartest',
+  description : 'Check that implicit vars are created',
+  version : libver,
+  variables: ['datadir=${prefix}/data', 'foo=${datadir}/foo', 'bar=${bindir}/bar']
+)
+pkgg.generate(
+  name : 'libvartest2',
+  description : 'Check that libdir is not an implicit var',
+  version : libver,
+  variables: ['bar=${libdir}/bar']
+)
diff --git a/test cases/common/44 pkgconfig-gen/test.json b/test cases/common/44 pkgconfig-gen/test.json
--- a/test cases/common/44 pkgconfig-gen/test.json	
+++ b/test cases/common/44 pkgconfig-gen/test.json	
@@ -7,11 +7,26 @@
     {"type": "file", "file": "usr/lib/pkgconfig/libfoo.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/libhello.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/libhello_nolib.pc"},
+    {"type": "file", "file": "usr/lib/pkgconfig/libvartest.pc"},
+    {"type": "file", "file": "usr/lib/pkgconfig/libvartest2.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/simple2.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/simple3.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/simple5.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/simple6.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/ct.pc"},
     {"type": "file", "file": "usr/lib/pkgconfig/ct0.pc"}
+  ],
+  "stdout": [
+    {
+      "line": "test cases/common/44 pkgconfig-gen/meson.build:158: WARNING: Project targeting '>=0.60.0' but tried to use feature introduced in '0.62.0': pkgconfig.generate implicit variable for builtin directories."
+    },
+    {
+      "line": "test cases/common/44 pkgconfig-gen/meson.build:164: WARNING: Project targeting '>=0.60.0' but tried to use feature introduced in '0.62.0': pkgconfig.generate implicit variable for builtin directories.",
+      "count": 0
+    },
+    {
+      "comment": "This will either match in the future-deprecated notice summary, or match the warning summary",
+      "line": " * 0.62.0: {'pkgconfig.generate variable for builtin directories'}"
+    }
   ]
 }