Repository: mesonbuild/meson

Issue:
dependency('gl') can return an object which claims to be a pkgconfig dependency, but isn't
```meson.build
project('gl dependency', 'c')

gl_dep = dependency('gl', method: 'pkg-config', required: false)
message(gl_dep.type_name())
if gl_dep.found() and gl_dep.type_name() == 'pkgconfig'
  gl_dep.get_pkgconfig_variable('prefix')
endif
```

confusingly fails with:

```
Message: pkgconfig

Meson encountered an error in file meson.build, line 6, column 0:
'gl' is not a pkgconfig dependency
```




Discussion:
https://github.com/mesonbuild/meson/blob/dfd368d4054bfe067545112717a5bd21bc37aca7/mesonbuild/dependencies/ui.py#L43-L51

Resolving patch:
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -61,21 +61,14 @@ class DependencyMethods(Enum):
 
 
 class Dependency:
-    def __init__(self, type_name, kwargs):
-        self.name = "null"
-        self.version = 'none'
-        self.language = None # None means C-like
-        self.is_found = False
-        self.type_name = type_name
-        self.compile_args = []
-        self.link_args = []
-        self.sources = []
+    @classmethod
+    def _process_method_kw(cls, kwargs):
         method = kwargs.get('method', 'auto')
         if method not in [e.value for e in DependencyMethods]:
             raise DependencyException('method {!r} is invalid'.format(method))
         method = DependencyMethods(method)
 
-        # This sets per-too config methods which are deprecated to to the new
+        # This sets per-tool config methods which are deprecated to to the new
         # generic CONFIG_TOOL value.
         if method in [DependencyMethods.SDLCONFIG, DependencyMethods.CUPSCONFIG,
                       DependencyMethods.PCAPCONFIG, DependencyMethods.LIBWMFCONFIG]:
@@ -88,14 +81,27 @@ def __init__(self, type_name, kwargs):
         # Set the detection method. If the method is set to auto, use any available method.
         # If method is set to a specific string, allow only that detection method.
         if method == DependencyMethods.AUTO:
-            self.methods = self.get_methods()
-        elif method in self.get_methods():
-            self.methods = [method]
+            methods = cls.get_methods()
+        elif method in cls.get_methods():
+            methods = [method]
         else:
             raise DependencyException(
                 'Unsupported detection method: {}, allowed methods are {}'.format(
                     method.value,
-                    mlog.format_list([x.value for x in [DependencyMethods.AUTO] + self.get_methods()])))
+                    mlog.format_list([x.value for x in [DependencyMethods.AUTO] + cls.get_methods()])))
+
+        return methods
+
+    def __init__(self, type_name, kwargs):
+        self.name = "null"
+        self.version = 'none'
+        self.language = None # None means C-like
+        self.is_found = False
+        self.type_name = type_name
+        self.compile_args = []
+        self.link_args = []
+        self.sources = []
+        self.methods = self._process_method_kw(kwargs)
 
     def __repr__(self):
         s = '<{0} {1}: {2}>'
@@ -115,7 +121,8 @@ def get_sources(self):
         As an example, gtest-all.cc when using GTest."""
         return self.sources
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         return [DependencyMethods.AUTO]
 
     def get_name(self):
@@ -235,14 +242,17 @@ def factory(cls, name, environment, language, kwargs, tools, tool_name):
         # instantiated and returned. The reduce function (method) is also
         # attached, since python's pickle module won't be able to do anything
         # with this dynamically generated class otherwise.
-        def reduce(_):
-            return (cls.factory,
-                    (name, environment, language, kwargs, tools, tool_name))
+        def reduce(self):
+            return (cls._unpickle, (), self.__dict__)
         sub = type('{}Dependency'.format(name.capitalize()), (cls, ),
                    {'tools': tools, 'tool_name': tool_name, '__reduce__': reduce})
 
         return sub(name, environment, language, kwargs)
 
+    @classmethod
+    def _unpickle(cls):
+        return cls.__new__(cls)
+
     def find_config(self, versions=None):
         """Helper method that searchs for config tool binaries in PATH and
         returns the one that best matches the given version requirements.
@@ -308,7 +318,8 @@ def get_config_value(self, args, stage):
             return []
         return shlex.split(out)
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         return [DependencyMethods.AUTO, DependencyMethods.CONFIG_TOOL]
 
     def get_configtool_variable(self, variable_name):
@@ -535,7 +546,8 @@ def get_pkgconfig_variable(self, variable_name, kwargs):
         mlog.debug('Got pkgconfig variable %s : %s' % (variable_name, variable))
         return variable
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         return [DependencyMethods.PKGCONFIG]
 
     def check_pkgconfig(self):
@@ -884,7 +896,12 @@ def find_external_dependency(name, env, kwargs):
     if lname in packages:
         if lname not in _packages_accept_language and 'language' in kwargs:
             raise DependencyException('%s dependency does not accept "language" keyword argument' % (lname, ))
-        dep = packages[lname](env, kwargs)
+        # Create the dependency object using a factory class method, if one
+        # exists, otherwise it is just constructed directly.
+        if getattr(packages[lname], '_factory', None):
+            dep = packages[lname]._factory(env, kwargs)
+        else:
+            dep = packages[lname](env, kwargs)
         if required and not dep.found():
             raise DependencyException('Dependency "%s" not found' % name)
         return dep
diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py
--- a/mesonbuild/dependencies/misc.py
+++ b/mesonbuild/dependencies/misc.py
@@ -792,7 +792,8 @@ def _find_libpy3_windows(self, env):
         self.version = sysconfig.get_config_var('py_version')
         self.is_found = True
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         if mesonlib.is_windows():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSCONFIG]
         elif mesonlib.is_osx():
@@ -810,90 +811,80 @@ def get_pkgconfig_variable(self, variable_name, kwargs):
 class PcapDependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('pcap', environment, None, kwargs)
-        kwargs['required'] = False
-        if DependencyMethods.PKGCONFIG in self.methods:
+
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        methods = cls._process_method_kw(kwargs)
+        if DependencyMethods.PKGCONFIG in methods:
             try:
                 pcdep = PkgConfigDependency('pcap', environment, kwargs)
                 if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
+                    return pcdep
             except Exception as e:
                 mlog.debug('Pcap not found via pkgconfig. Trying next, error was:', str(e))
-        if DependencyMethods.CONFIG_TOOL in self.methods:
+        if DependencyMethods.CONFIG_TOOL in methods:
             try:
                 ctdep = ConfigToolDependency.factory(
                     'pcap', environment, None, kwargs, ['pcap-config'], 'pcap-config')
                 if ctdep.found():
-                    self.config = ctdep.config
-                    self.type_name = 'config-tool'
-                    self.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
-                    self.link_args = ctdep.get_config_value(['--libs'], 'link_args')
-                    self.version = self.get_pcap_lib_version()
-                    self.is_found = True
-                    return
+                    ctdep.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
+                    ctdep.link_args = ctdep.get_config_value(['--libs'], 'link_args')
+                    ctdep.version = cls.get_pcap_lib_version(ctdep)
+                    return ctdep
             except Exception as e:
                 mlog.debug('Pcap not found via pcap-config. Trying next, error was:', str(e))
 
-    def get_methods(self):
+        return PcapDependency(environment, kwargs)
+
+    @staticmethod
+    def get_methods():
         if mesonlib.is_osx():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
         else:
             return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL]
 
-    def get_pcap_lib_version(self):
-        return self.compiler.get_return_value('pcap_lib_version', 'string',
-                                              '#include <pcap.h>', self.env, [], [self])
+    @staticmethod
+    def get_pcap_lib_version(ctdep):
+        return ctdep.compiler.get_return_value('pcap_lib_version', 'string',
+                                               '#include <pcap.h>', ctdep.env, [], [ctdep])
 
 
 class CupsDependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('cups', environment, None, kwargs)
-        kwargs['required'] = False
-        if DependencyMethods.PKGCONFIG in self.methods:
+
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        methods = cls._process_method_kw(kwargs)
+        if DependencyMethods.PKGCONFIG in methods:
             try:
                 pcdep = PkgConfigDependency('cups', environment, kwargs)
                 if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
+                    return pcdep
             except Exception as e:
                 mlog.debug('cups not found via pkgconfig. Trying next, error was:', str(e))
-        if DependencyMethods.CONFIG_TOOL in self.methods:
+        if DependencyMethods.CONFIG_TOOL in methods:
             try:
                 ctdep = ConfigToolDependency.factory(
                     'cups', environment, None, kwargs, ['cups-config'], 'cups-config')
                 if ctdep.found():
-                    self.config = ctdep.config
-                    self.type_name = 'config-tool'
-                    self.version = ctdep.version
-                    self.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
-                    self.link_args = ctdep.get_config_value(['--ldflags', '--libs'], 'link_args')
-                    self.is_found = True
-                    return
+                    ctdep.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
+                    ctdep.link_args = ctdep.get_config_value(['--ldflags', '--libs'], 'link_args')
+                    return ctdep
             except Exception as e:
                 mlog.debug('cups not found via cups-config. Trying next, error was:', str(e))
-        if DependencyMethods.EXTRAFRAMEWORK in self.methods:
+        if DependencyMethods.EXTRAFRAMEWORK in methods:
             if mesonlib.is_osx():
-                fwdep = ExtraFrameworkDependency('cups', False, None, self.env,
-                                                 self.language, kwargs)
+                fwdep = ExtraFrameworkDependency('cups', False, None, environment,
+                                                 kwargs.get('language', None), kwargs)
                 if fwdep.found():
-                    self.is_found = True
-                    self.compile_args = fwdep.get_compile_args()
-                    self.link_args = fwdep.get_link_args()
-                    self.version = fwdep.get_version()
-                    return
-            mlog.log('Dependency', mlog.bold('cups'), 'found:', mlog.red('NO'))
+                    return fwdep
+        mlog.log('Dependency', mlog.bold('cups'), 'found:', mlog.red('NO'))
+
+        return CupsDependency(environment, kwargs)
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         if mesonlib.is_osx():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
         else:
@@ -903,36 +894,33 @@ def get_methods(self):
 class LibWmfDependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('libwmf', environment, None, kwargs)
-        if DependencyMethods.PKGCONFIG in self.methods:
+
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        methods = cls._process_method_kw(kwargs)
+        if DependencyMethods.PKGCONFIG in methods:
             try:
                 kwargs['required'] = False
                 pcdep = PkgConfigDependency('libwmf', environment, kwargs)
                 if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
+                    return pcdep
             except Exception as e:
                 mlog.debug('LibWmf not found via pkgconfig. Trying next, error was:', str(e))
-        if DependencyMethods.CONFIG_TOOL in self.methods:
+        if DependencyMethods.CONFIG_TOOL in methods:
             try:
                 ctdep = ConfigToolDependency.factory(
                     'libwmf', environment, None, kwargs, ['libwmf-config'], 'libwmf-config')
                 if ctdep.found():
-                    self.config = ctdep.config
-                    self.type_name = 'config-too'
-                    self.version = ctdep.version
-                    self.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
-                    self.link_args = ctdep.get_config_value(['--libs'], 'link_args')
-                    self.is_found = True
-                    return
+                    ctdep.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
+                    ctdep.link_args = ctdep.get_config_value(['--libs'], 'link_args')
+                    return ctdep
             except Exception as e:
                 mlog.debug('cups not found via libwmf-config. Trying next, error was:', str(e))
 
-    def get_methods(self):
+        return LibWmfDependency(environment, kwargs)
+
+    @staticmethod
+    def get_methods():
         if mesonlib.is_osx():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
         else:
diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py
--- a/mesonbuild/dependencies/ui.py
+++ b/mesonbuild/dependencies/ui.py
@@ -38,19 +38,6 @@
 class GLDependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('gl', environment, None, kwargs)
-        if DependencyMethods.PKGCONFIG in self.methods:
-            try:
-                pcdep = PkgConfigDependency('gl', environment, kwargs)
-                if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
-            except Exception:
-                pass
         if DependencyMethods.SYSTEM in self.methods:
             if mesonlib.is_osx():
                 self.is_found = True
@@ -67,7 +54,19 @@ def __init__(self, environment, kwargs):
                 self.version = '1'
                 return
 
-    def get_methods(self):
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        if DependencyMethods.PKGCONFIG in cls._process_method_kw(kwargs):
+            try:
+                pcdep = PkgConfigDependency('gl', environment, kwargs)
+                if pcdep.found():
+                    return pcdep
+            except Exception:
+                pass
+        return GLDependency(environment, kwargs)
+
+    @staticmethod
+    def get_methods():
         if mesonlib.is_osx() or mesonlib.is_windows():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]
         else:
@@ -337,7 +336,8 @@ def get_qmake_host_bins(self, qvars):
         else:
             return qvars['QT_INSTALL_BINS']
 
-    def get_methods(self):
+    @staticmethod
+    def get_methods():
         return [DependencyMethods.PKGCONFIG, DependencyMethods.QMAKE]
 
     def get_exe_args(self, compiler):
@@ -380,47 +380,40 @@ def get_pkgconfig_host_bins(self, core):
 class SDL2Dependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('sdl2', environment, None, kwargs)
-        kwargs['required'] = False
-        if DependencyMethods.PKGCONFIG in self.methods:
+
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        methods = cls._process_method_kw(kwargs)
+        if DependencyMethods.PKGCONFIG in methods:
             try:
                 pcdep = PkgConfigDependency('sdl2', environment, kwargs)
                 if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
+                    return pcdep
             except Exception as e:
                 mlog.debug('SDL 2 not found via pkgconfig. Trying next, error was:', str(e))
-        if DependencyMethods.CONFIG_TOOL in self.methods:
+        if DependencyMethods.CONFIG_TOOL in methods:
             try:
                 ctdep = ConfigToolDependency.factory(
                     'sdl2', environment, None, kwargs, ['sdl2-config'], 'sdl2-config')
                 if ctdep.found():
-                    self.type_name = 'config-tool'
-                    self.config = ctdep.config
-                    self.version = ctdep.version
-                    self.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
-                    self.links_args = ctdep.get_config_value(['--libs'], 'link_args')
-                    self.is_found = True
-                    return
+                    ctdep.compile_args = ctdep.get_config_value(['--cflags'], 'compile_args')
+                    ctdep.links_args = ctdep.get_config_value(['--libs'], 'link_args')
+                    return ctdep
             except Exception as e:
                 mlog.debug('SDL 2 not found via sdl2-config. Trying next, error was:', str(e))
-        if DependencyMethods.EXTRAFRAMEWORK in self.methods:
+        if DependencyMethods.EXTRAFRAMEWORK in methods:
             if mesonlib.is_osx():
-                fwdep = ExtraFrameworkDependency('sdl2', False, None, self.env,
-                                                 self.language, kwargs)
+                fwdep = ExtraFrameworkDependency('sdl2', False, None, environment,
+                                                 kwargs.get('language', None), kwargs)
                 if fwdep.found():
-                    self.is_found = True
-                    self.compile_args = fwdep.get_compile_args()
-                    self.link_args = fwdep.get_link_args()
-                    self.version = '2'  # FIXME
-                    return
+                    fwdep.version = '2'  # FIXME
+                    return fwdep
             mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.red('NO'))
 
-    def get_methods(self):
+        return SDL2Dependency(environment, kwargs)
+
+    @staticmethod
+    def get_methods():
         if mesonlib.is_osx():
             return [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK]
         else:
@@ -456,20 +449,6 @@ class VulkanDependency(ExternalDependency):
     def __init__(self, environment, kwargs):
         super().__init__('vulkan', environment, None, kwargs)
 
-        if DependencyMethods.PKGCONFIG in self.methods:
-            try:
-                pcdep = PkgConfigDependency('vulkan', environment, kwargs)
-                if pcdep.found():
-                    self.type_name = 'pkgconfig'
-                    self.is_found = True
-                    self.compile_args = pcdep.get_compile_args()
-                    self.link_args = pcdep.get_link_args()
-                    self.version = pcdep.get_version()
-                    self.pcdep = pcdep
-                    return
-            except Exception:
-                pass
-
         if DependencyMethods.SYSTEM in self.methods:
             try:
                 self.vulkan_sdk = os.environ['VULKAN_SDK']
@@ -526,5 +505,18 @@ def __init__(self, environment, kwargs):
                         self.link_args.append(lib)
                     return
 
-    def get_methods(self):
+    @classmethod
+    def _factory(cls, environment, kwargs):
+        if DependencyMethods.PKGCONFIG in cls._process_method_kw(kwargs):
+            try:
+                pcdep = PkgConfigDependency('vulkan', environment, kwargs)
+                if pcdep.found():
+                    return pcdep
+            except Exception:
+                pass
+
+        return VulkanDependency(environment, kwargs)
+
+    @staticmethod
+    def get_methods():
         return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM]


Test patch:
diff --git a/test cases/common/174 dependency factory/meson.build b/test cases/common/174 dependency factory/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/174 dependency factory/meson.build	
@@ -0,0 +1,51 @@
+project('dependency factory')
+
+dep = dependency('gl', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('SDL2', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('SDL2', method: 'config-tool', required: false)
+if dep.found() and dep.type_name() == 'configtool'
+  dep.get_configtool_variable('prefix')
+endif
+
+dep = dependency('Vulkan', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('pcap', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('pcap', method: 'config-tool', required: false)
+if dep.found() and dep.type_name() == 'configtool'
+  dep.get_configtool_variable('prefix')
+endif
+
+dep = dependency('cups', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('cups', method: 'config-tool', required: false)
+if dep.found() and dep.type_name() == 'configtool'
+  dep.get_configtool_variable('prefix')
+endif
+
+dep = dependency('libwmf', method: 'pkg-config', required: false)
+if dep.found() and dep.type_name() == 'pkgconfig'
+  dep.get_pkgconfig_variable('prefix')
+endif
+
+dep = dependency('libwmf', method: 'config-tool', required: false)
+if dep.found() and dep.type_name() == 'configtool'
+  dep.get_configtool_variable('prefix')
+endif