Repository: mesonbuild/meson

Issue:
RFC: Add a generic variable getter method for dependencies
We now have 4 different kinds of dependencies, cmake, pkg-config, config-tool, and declare_dependency. pkg-config and config-tool have a method that allow getting arbitrary variables, cmake might but I haven't looked closely. Some dependencies can be found multiple ways (cmake or config-tool for example). Because of that it would be nice to have a generic getter method, such as

```meson
dep = dependency('dep')
use_thing = dep.get_variable(cmake : 'CMakeVariable', pkg-config : 'pkg-config-variable', config-tool : 'config-tool-argument')
```
Which would be implemented such that each kind of dependency would respect the proper keyword.

@jpakkane, opinions?


Discussion:
CMake also has a method for getting arbitrary variables, however, they might not be 100% accurate.
The main question I have with your proposal is what happens when you forget to specify a method in `dep.get_variable`.

For instance, what happens when you call `dep.get_variable(cmake : 'vcm', pkg-config : 'vpkg')` but `dep` is internally a config-tool dependency?
then you'd get an error, the same as calling one of the specific methods on the wrong dependency type.
Should it also have a `default` or something similar for a variable name to try if the dependency type is not one of the listed ones?
That might be a good idea it could have behavior such that if `default` is provided it returns that, otherwise error.

Resolving patch:
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -193,6 +193,13 @@ def _add_sub_dependency(self, dep_type: typing.Type['Dependency'], env: Environm
         kwargs['method'] = method
         self.ext_deps.append(dep_type(env, kwargs))
 
+    def get_variable(self, *, cmake: typing.Optional[str] = None, pkgconfig: typing.Optional[str] = None,
+                     configtool: typing.Optional[str] = None, default_value: typing.Optional[str] = None,
+                     pkgconfig_define: typing.Optional[typing.List[str]] = None) -> typing.Union[str, typing.List[str]]:
+        if default_value is not None:
+            return default_value
+        raise DependencyException('No default provided for dependency {!r}, which is not pkg-config, cmake, or config-tool based.'.format(self))
+
 
 class InternalDependency(Dependency):
     def __init__(self, version, incdirs, compile_args, link_args, libraries, whole_libraries, sources, ext_deps):
@@ -518,6 +525,26 @@ def get_configtool_variable(self, variable_name):
     def log_tried(self):
         return self.type_name
 
+    def get_variable(self, *, cmake: typing.Optional[str] = None, pkgconfig: typing.Optional[str] = None,
+                     configtool: typing.Optional[str] = None, default_value: typing.Optional[str] = None,
+                     pkgconfig_define: typing.Optional[typing.List[str]] = None) -> typing.Union[str, typing.List[str]]:
+        if configtool:
+            # In the not required case '' (empty string) will be returned if the
+            # variable is not found. Since '' is a valid value to return we
+            # set required to True here to force and error, and use the
+            # finally clause to ensure it's restored.
+            restore = self.required
+            self.required = True
+            try:
+                return self.get_configtool_variable(configtool)
+            except DependencyException:
+                pass
+            finally:
+                self.required = restore
+        if default_value is not None:
+            return default_value
+        raise DependencyException('Could not get config-tool variable and no default provided for {!r}'.format(self))
+
 
 class PkgConfigDependency(ExternalDependency):
     # The class's copy of the pkg-config path. Avoids having to search for it
@@ -928,6 +955,23 @@ def extract_libtool_shlib(self, la_file):
     def log_tried(self):
         return self.type_name
 
+    def get_variable(self, *, cmake: typing.Optional[str] = None, pkgconfig: typing.Optional[str] = None,
+                     configtool: typing.Optional[str] = None, default_value: typing.Optional[str] = None,
+                     pkgconfig_define: typing.Optional[typing.List[str]] = None) -> typing.Union[str, typing.List[str]]:
+        if pkgconfig:
+            kwargs = {}
+            if default_value is not None:
+                kwargs['default'] = default_value
+            if pkgconfig_define is not None:
+                kwargs['define_variable'] = pkgconfig_define
+            try:
+                return self.get_pkgconfig_variable(pkgconfig, kwargs)
+            except DependencyException:
+                pass
+        if default_value is not None:
+            return default_value
+        raise DependencyException('Could not get pkg-config variable and no default provided for {!r}'.format(self))
+
 class CMakeTraceLine:
     def __init__(self, file, line, func, args):
         self.file = file
@@ -1784,6 +1828,23 @@ def log_details(self) -> str:
             return 'modules: ' + ', '.join(modules)
         return ''
 
+    def get_variable(self, *, cmake: typing.Optional[str] = None, pkgconfig: typing.Optional[str] = None,
+                     configtool: typing.Optional[str] = None, default_value: typing.Optional[str] = None,
+                     pkgconfig_define: typing.Optional[typing.List[str]] = None) -> typing.Union[str, typing.List[str]]:
+        if cmake:
+            try:
+                v = self.vars[cmake]
+            except KeyError:
+                pass
+            else:
+                if len(v) == 1:
+                    return v[0]
+                elif v:
+                    return v
+        if default_value is not None:
+            return default_value
+        raise DependencyException('Could not get cmake variable and no default provided for {!r}'.format(self))
+
 class DubDependency(ExternalDependency):
     class_dubbin = None
 
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -383,6 +383,7 @@ def __init__(self, dep, pv):
                              'name': self.name_method,
                              'get_pkgconfig_variable': self.pkgconfig_method,
                              'get_configtool_variable': self.configtool_method,
+                             'get_variable': self.variable_method,
                              'partial_dependency': self.partial_dependency_method,
                              })
 
@@ -439,13 +440,21 @@ def partial_dependency_method(self, args, kwargs):
         pdep = self.held_object.get_partial_dependency(**kwargs)
         return DependencyHolder(pdep, self.subproject)
 
+    @FeatureNew('dep.get_variable', '0.51.0')
+    @noPosargs
+    @permittedKwargs({'cmake', 'pkgconfig', 'configtool', 'default', 'pkgconfig_define'})
+    def variable_method(self, args, kwargs):
+        return self.held_object.get_variable(**kwargs)
+
+
 class InternalDependencyHolder(InterpreterObject, ObjectHolder):
     def __init__(self, dep, pv):
         InterpreterObject.__init__(self)
         ObjectHolder.__init__(self, dep, pv)
         self.methods.update({'found': self.found_method,
-                             'version': self.version_method,
+                             'get_variable': self.variable_method,
                              'partial_dependency': self.partial_dependency_method,
+                             'version': self.version_method,
                              })
 
     @noPosargs
@@ -465,6 +474,13 @@ def partial_dependency_method(self, args, kwargs):
         pdep = self.held_object.get_partial_dependency(**kwargs)
         return DependencyHolder(pdep, self.subproject)
 
+    @FeatureNew('dep.get_variable', '0.51.0')
+    @noPosargs
+    @permittedKwargs({'cmake', 'pkgconfig', 'configtool', 'default_variable', 'pkgconfig_define'})
+    def variable_method(self, args, kwargs):
+        return self.held_object.get_variable(**kwargs)
+
+
 class ExternalProgramHolder(InterpreterObject, ObjectHolder):
     def __init__(self, ep):
         InterpreterObject.__init__(self)


Test patch:
diff --git a/test cases/common/219 dependency get_variable method/meson.build b/test cases/common/219 dependency get_variable method/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/219 dependency get_variable method/meson.build	
@@ -0,0 +1,52 @@
+project(
+  'dependency get_variable',
+  ['c', 'cpp'],
+)
+
+# Just some string that nothing should return
+default = 'asufoiqwjtl;adjfbpiuqwoehtl;ajdfl;ghal;sdjg'
+
+dep = dependency('zlib', method: 'pkg-config', required : false)
+if not dep.found()
+  warning('Skipping pkg-config tests as zlib is not avialable or is not pkg-config')
+else
+  # Test for regular pkg-config
+  # We don't know what the value will be, but we know it should be the same
+  dep = dependency('zlib', method : 'pkg-config')
+  assert(dep.get_pkgconfig_variable('prefix') == dep.get_variable(pkgconfig : 'prefix'),
+        'Got different values from get_pkgconfig_variable and get_variable(pkgconfig: )')
+  assert(dep.get_variable(pkgconfig : default, default_value : default) == default,
+        'pkg-config didnt get default when we should have.')
+  assert(dep.get_variable(pkgconfig : 'prefix', default_value : default) != default,
+        'pkg-config got default when we shouldnt have.')
+endif
+
+dep_ct = dependency('llvm', method : 'config-tool', required : false)
+if not dep_ct.found()
+  warning('Skipping config-tool tests as llvm is not available or llvm-config was not found.')
+else
+  assert(dep_ct.get_configtool_variable('has-rtti') == dep_ct.get_variable(configtool : 'has-rtti'),
+        'Got different values from get_configtool_variable and get_variable(configtool: )')
+  assert(dep_ct.get_variable(configtool : default, default_value : default) == default,
+        'config-tool didnt get default when we should have.')
+  assert(dep_ct.get_variable(configtool : 'has-rtti', default_value : default) != default,
+        'config-tool got default when we shouldnt have.')
+endif
+
+dep_cm = dependency('llvm', method : 'cmake', required : false)
+if not dep_cm.found()
+  warning('Skipping cmake tests as llvm is not available via the cmake finder.')
+else
+  if dep_ct.found()
+    assert((dep_cm.get_variable(cmake : 'LLVM_ENABLE_RTTI') == 'ON') == (dep_ct.get_variable(configtool : 'has-rtti') == 'YES'),
+          'RTTI information for cmake and config tools disagree')
+  endif
+  assert(dep_cm.get_variable(cmake : default, default_value : default) == default,
+        'cmake didnt get default when we should have.')
+  assert(dep_cm.get_variable(cmake : 'LLVM_ENABLE_RTTI', default_value : default) != default,
+        'cmake config-tool got default when we shouldnt have.')
+endif
+
+idep = declare_dependency()
+assert(idep.get_variable(pkgconfig : 'foo', cmake : 'foo', configtool : 'foo', default_value : default) == default,
+       'Got something other than default from an internal dependency')