# Copyright 2013-2017 The Meson development team

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This file contains the detection logic for external
# dependencies. Mostly just uses pkg-config but also contains
# custom logic for packages that don't provide them.

# Currently one file, should probably be split into a
# package before this gets too big.

import re
import sys
import os, stat, glob, shutil
import subprocess
import sysconfig
from collections import OrderedDict
from . mesonlib import MesonException, version_compare, version_compare_many, Popen_safe
from . import mlog
from . import mesonlib
from .environment import detect_cpu_family, for_windows

class DependencyException(MesonException):
    '''Exceptions raised while trying to find dependencies'''

class Dependency:
    def __init__(self, type_name='unknown'):
        self.name = "null"
        self.is_found = False
        self.type_name = type_name

    def __repr__(self):
        s = '<{0} {1}: {2}>'
        return s.format(self.__class__.__name__, self.name, self.is_found)

    def get_compile_args(self):
        return []

    def get_link_args(self):
        return []

    def found(self):
        return self.is_found

    def get_sources(self):
        """Source files that need to be added to the target.
        As an example, gtest-all.cc when using GTest."""
        return []

    def get_name(self):
        return self.name

    def get_exe_args(self, compiler):
        return []

    def need_threads(self):
        return False

    def get_pkgconfig_variable(self, variable_name):
        raise MesonException('Tried to get a pkg-config variable from a non-pkgconfig dependency.')

class InternalDependency(Dependency):
    def __init__(self, version, incdirs, compile_args, link_args, libraries, sources, ext_deps):
        super().__init__('internal')
        self.version = version
        self.include_directories = incdirs
        self.compile_args = compile_args
        self.link_args = link_args
        self.libraries = libraries
        self.sources = sources
        self.ext_deps = ext_deps

    def get_compile_args(self):
        return self.compile_args

    def get_link_args(self):
        return self.link_args

    def get_version(self):
        return self.version

class PkgConfigDependency(Dependency):
    # The class's copy of the pkg-config path. Avoids having to search for it
    # multiple times in the same Meson invocation.
    class_pkgbin = None

    def __init__(self, name, environment, kwargs):
        Dependency.__init__(self, 'pkgconfig')
        self.is_libtool = False
        self.required = kwargs.get('required', True)
        self.static = kwargs.get('static', False)
        self.silent = kwargs.get('silent', False)
        if not isinstance(self.static, bool):
            raise DependencyException('Static keyword must be boolean')
        # Store a copy of the pkg-config path on the object itself so it is
        # stored in the pickled coredata and recovered.
        self.pkgbin = None
        self.cargs = []
        self.libs = []
        if 'native' in kwargs and environment.is_cross_build():
            self.want_cross = not kwargs['native']
        else:
            self.want_cross = environment.is_cross_build()
        self.name = name

        # When finding dependencies for cross-compiling, we don't care about
        # the 'native' pkg-config
        if self.want_cross:
            if 'pkgconfig' not in environment.cross_info.config['binaries']:
                if self.required:
                    raise DependencyException('Pkg-config binary missing from cross file')
            else:
                pkgname = environment.cross_info.config['binaries']['pkgconfig']
                potential_pkgbin = ExternalProgram(pkgname, silent=True)
                if potential_pkgbin.found():
                    # FIXME, we should store all pkg-configs in ExternalPrograms.
                    # However that is too destabilizing a change to do just before release.
                    self.pkgbin = potential_pkgbin.get_command()[0]
                    PkgConfigDependency.class_pkgbin = self.pkgbin
                else:
                    mlog.debug('Cross pkg-config %s not found.' % potential_pkgbin.name)
        # Only search for the native pkg-config the first time and
        # store the result in the class definition
        elif PkgConfigDependency.class_pkgbin is None:
            self.pkgbin = self.check_pkgconfig()
            PkgConfigDependency.class_pkgbin = self.pkgbin
        else:
            self.pkgbin = PkgConfigDependency.class_pkgbin

        self.is_found = False
        if not self.pkgbin:
            if self.required:
                raise DependencyException('Pkg-config not found.')
            return
        if self.want_cross:
            self.type_string = 'Cross'
        else:
            self.type_string = 'Native'

        mlog.debug('Determining dependency {!r} with pkg-config executable '
                   '{!r}'.format(name, self.pkgbin))
        ret, self.modversion = self._call_pkgbin(['--modversion', name])
        if ret != 0:
            if self.required:
                raise DependencyException('{} dependency {!r} not found'
                                          ''.format(self.type_string, name))
            self.modversion = 'none'
            return
        found_msg = [self.type_string + ' dependency', mlog.bold(name), 'found:']
        self.version_reqs = kwargs.get('version', None)
        if self.version_reqs is None:
            self.is_found = True
        else:
            if not isinstance(self.version_reqs, (str, list)):
                raise DependencyException('Version argument must be string or list.')
            if isinstance(self.version_reqs, str):
                self.version_reqs = [self.version_reqs]
            (self.is_found, not_found, found) = \
                version_compare_many(self.modversion, self.version_reqs)
            if not self.is_found:
                found_msg += [mlog.red('NO'),
                              'found {!r} but need:'.format(self.modversion),
                              ', '.join(["'{}'".format(e) for e in not_found])]
                if found:
                    found_msg += ['; matched:',
                                  ', '.join(["'{}'".format(e) for e in found])]
                if not self.silent:
                    mlog.log(*found_msg)
                if self.required:
                    m = 'Invalid version of dependency, need {!r} {!r} found {!r}.'
                    raise DependencyException(m.format(name, not_found, self.modversion))
                return
        found_msg += [mlog.green('YES'), self.modversion]
        # Fetch cargs to be used while using this dependency
        self._set_cargs()
        # Fetch the libraries and library paths needed for using this
        self._set_libs()
        # Print the found message only at the very end because fetching cflags
        # and libs can also fail if other needed pkg-config files aren't found.
        if not self.silent:
            mlog.log(*found_msg)

    def __repr__(self):
        s = '<{0} {1}: {2} {3}>'
        return s.format(self.__class__.__name__, self.name, self.is_found,
                        self.version_reqs)

    def _call_pkgbin(self, args):
        p, out = Popen_safe([self.pkgbin] + args, env=os.environ)[0:2]
        return p.returncode, out.strip()

    def _set_cargs(self):
        ret, out = self._call_pkgbin(['--cflags', self.name])
        if ret != 0:
            raise DependencyException('Could not generate cargs for %s:\n\n%s' %
                                      (self.name, out))
        self.cargs = out.split()

    def _set_libs(self):
        libcmd = [self.name, '--libs']
        if self.static:
            libcmd.append('--static')
        ret, out = self._call_pkgbin(libcmd)
        if ret != 0:
            raise DependencyException('Could not generate libs for %s:\n\n%s' %
                                      (self.name, out))
        self.libs = []
        for lib in out.split():
            if lib.endswith(".la"):
                shared_libname = self.extract_libtool_shlib(lib)
                shared_lib = os.path.join(os.path.dirname(lib), shared_libname)
                if not os.path.exists(shared_lib):
                    shared_lib = os.path.join(os.path.dirname(lib), ".libs", shared_libname)

                if not os.path.exists(shared_lib):
                    raise DependencyException('Got a libtools specific "%s" dependencies'
                                              'but we could not compute the actual shared'
                                              'library path' % lib)
                lib = shared_lib
                self.is_libtool = True
            self.libs.append(lib)

    def get_pkgconfig_variable(self, variable_name):
        ret, out = self._call_pkgbin(['--variable=' + variable_name, self.name])
        variable = ''
        if ret != 0:
            if self.required:
                raise DependencyException('%s dependency %s not found.' %
                                          (self.type_string, self.name))
        else:
            variable = out.strip()
        mlog.debug('Got pkgconfig variable %s : %s' % (variable_name, variable))
        return variable

    def get_modversion(self):
        return self.modversion

    def get_version(self):
        return self.modversion

    def get_compile_args(self):
        return self.cargs

    def get_link_args(self):
        return self.libs

    def check_pkgconfig(self):
        evar = 'PKG_CONFIG'
        if evar in os.environ:
            pkgbin = os.environ[evar].strip()
        else:
            pkgbin = 'pkg-config'
        try:
            p, out = Popen_safe([pkgbin, '--version'])[0:2]
            if p.returncode != 0:
                # Set to False instead of None to signify that we've already
                # searched for it and not found it
                pkgbin = False
        except (FileNotFoundError, PermissionError):
            pkgbin = False
        if pkgbin and not os.path.isabs(pkgbin) and shutil.which(pkgbin):
            # Sometimes shutil.which fails where Popen succeeds, so
            # only find the abs path if it can be found by shutil.which
            pkgbin = shutil.which(pkgbin)
        if not self.silent:
            if pkgbin:
                mlog.log('Found pkg-config:', mlog.bold(pkgbin),
                         '(%s)' % out.strip())
            else:
                mlog.log('Found Pkg-config:', mlog.red('NO'))
        return pkgbin

    def found(self):
        return self.is_found

    def extract_field(self, la_file, fieldname):
        with open(la_file) as f:
            for line in f:
                arr = line.strip().split('=')
                if arr[0] == fieldname:
                    return arr[1][1:-1]
        return None

    def extract_dlname_field(self, la_file):
        return self.extract_field(la_file, 'dlname')

    def extract_libdir_field(self, la_file):
        return self.extract_field(la_file, 'libdir')

    def extract_libtool_shlib(self, la_file):
        '''
        Returns the path to the shared library
        corresponding to this .la file
        '''
        dlname = self.extract_dlname_field(la_file)
        if dlname is None:
            return None

        # Darwin uses absolute paths where possible; since the libtool files never
        # contain absolute paths, use the libdir field
        if mesonlib.is_osx():
            dlbasename = os.path.basename(dlname)
            libdir = self.extract_libdir_field(la_file)
            if libdir is None:
                return dlbasename
            return os.path.join(libdir, dlbasename)
        # From the comments in extract_libtool(), older libtools had
        # a path rather than the raw dlname
        return os.path.basename(dlname)

class WxDependency(Dependency):
    wx_found = None

    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'wx')
        self.is_found = False
        self.modversion = 'none'
        if WxDependency.wx_found is None:
            self.check_wxconfig()
        if not WxDependency.wx_found:
            mlog.log("Neither wx-config-3.0 nor wx-config found; can't detect dependency")
            return

        p, out = Popen_safe([self.wxc, '--version'])[0:2]
        if p.returncode != 0:
            mlog.log('Dependency wxwidgets found:', mlog.red('NO'))
            self.cargs = []
            self.libs = []
        else:
            self.modversion = out.strip()
            version_req = kwargs.get('version', None)
            if version_req is not None:
                if not version_compare(self.modversion, version_req, strict=True):
                    mlog.log('Wxwidgets version %s does not fullfill requirement %s' %
                             (self.modversion, version_req))
                    return
            mlog.log('Dependency wxwidgets found:', mlog.green('YES'))
            self.is_found = True
            self.requested_modules = self.get_requested(kwargs)
            # wx-config seems to have a cflags as well but since it requires C++,
            # this should be good, at least for now.
            p, out = Popen_safe([self.wxc, '--cxxflags'])[0:2]
            if p.returncode != 0:
                raise DependencyException('Could not generate cargs for wxwidgets.')
            self.cargs = out.split()

            p, out = Popen_safe([self.wxc, '--libs'] + self.requested_modules)[0:2]
            if p.returncode != 0:
                raise DependencyException('Could not generate libs for wxwidgets.')
            self.libs = out.split()

    def get_requested(self, kwargs):
        modules = 'modules'
        if modules not in kwargs:
            return []
        candidates = kwargs[modules]
        if isinstance(candidates, str):
            return [candidates]
        for c in candidates:
            if not isinstance(c, str):
                raise DependencyException('wxwidgets module argument is not a string.')
        return candidates

    def get_modversion(self):
        return self.modversion

    def get_version(self):
        return self.modversion

    def get_compile_args(self):
        return self.cargs

    def get_link_args(self):
        return self.libs

    def check_wxconfig(self):
        for wxc in ['wx-config-3.0', 'wx-config']:
            try:
                p, out = Popen_safe([wxc, '--version'])[0:2]
                if p.returncode == 0:
                    mlog.log('Found wx-config:', mlog.bold(shutil.which(wxc)),
                             '(%s)' % out.strip())
                    self.wxc = wxc
                    WxDependency.wx_found = True
                    return
            except (FileNotFoundError, PermissionError):
                pass
        WxDependency.wxconfig_found = False
        mlog.log('Found wx-config:', mlog.red('NO'))

    def found(self):
        return self.is_found

class ExternalProgram:
    windows_exts = ('exe', 'msc', 'com', 'bat')

    def __init__(self, name, command=None, silent=False, search_dir=None):
        self.name = name
        if command is not None:
            if not isinstance(command, list):
                self.command = [command]
            else:
                self.command = command
        else:
            self.command = self._search(name, search_dir)
        if not silent:
            if self.found():
                mlog.log('Program', mlog.bold(name), 'found:', mlog.green('YES'),
                         '(%s)' % ' '.join(self.command))
            else:
                mlog.log('Program', mlog.bold(name), 'found:', mlog.red('NO'))

    def __repr__(self):
        r = '<{} {!r} -> {!r}>'
        return r.format(self.__class__.__name__, self.name, self.command)

    @staticmethod
    def _shebang_to_cmd(script):
        """
        Windows does not understand shebangs, so we check if the file has a
        shebang and manually parse it to figure out the interpreter to use
        """
        try:
            with open(script) as f:
                first_line = f.readline().strip()
            if first_line.startswith('#!'):
                commands = first_line[2:].split('#')[0].strip().split()
                if mesonlib.is_windows():
                    # Windows does not have UNIX paths so remove them,
                    # but don't remove Windows paths
                    if commands[0].startswith('/'):
                        commands[0] = commands[0].split('/')[-1]
                    if len(commands) > 0 and commands[0] == 'env':
                        commands = commands[1:]
                    # Windows does not ship python3.exe, but we know the path to it
                    if len(commands) > 0 and commands[0] == 'python3':
                        commands[0] = sys.executable
                return commands + [script]
        except Exception:
            pass
        return False

    def _is_executable(self, path):
        suffix = os.path.splitext(path)[-1].lower()[1:]
        if mesonlib.is_windows():
            if suffix in self.windows_exts:
                return True
        elif os.access(path, os.X_OK):
            return True
        return False

    def _search_dir(self, name, search_dir):
        if search_dir is None:
            return False
        trial = os.path.join(search_dir, name)
        if os.path.exists(trial):
            if self._is_executable(trial):
                return [trial]
        else:
            for ext in self.windows_exts:
                trial_ext = '{}.{}'.format(trial, ext)
                if os.path.exists(trial_ext):
                    return [trial_ext]
            return False
        # Now getting desperate. Maybe it is a script file that is a) not chmodded
        # executable or b) we are on windows so they can't be directly executed.
        return self._shebang_to_cmd(trial)

    def _search(self, name, search_dir):
        '''
        Search in the specified dir for the specified executable by name
        and if not found search in PATH
        '''
        commands = self._search_dir(name, search_dir)
        if commands:
            return commands
        # Do a standard search in PATH
        command = shutil.which(name)
        if not mesonlib.is_windows():
            # On UNIX-like platforms, the standard PATH search is enough
            return [command]
        # HERE BEGINS THE TERROR OF WINDOWS
        if command:
            # On Windows, even if the PATH search returned a full path, we can't be
            # sure that it can be run directly if it's not a native executable.
            # For instance, interpreted scripts sometimes need to be run explicitly
            # with an interpreter if the file association is not done properly.
            name_ext = os.path.splitext(command)[1]
            if name_ext[1:].lower() in self.windows_exts:
                # Good, it can be directly executed
                return [command]
            # Try to extract the interpreter from the shebang
            commands = self._shebang_to_cmd(command)
            if commands:
                return commands
        else:
            # Maybe the name is an absolute path to a native Windows
            # executable, but without the extension. This is technically wrong,
            # but many people do it because it works in the MinGW shell.
            if os.path.isabs(name):
                for ext in self.windows_exts:
                    command = '{}.{}'.format(name, ext)
                    if os.path.exists(command):
                        return [command]
            # On Windows, interpreted scripts must have an extension otherwise they
            # cannot be found by a standard PATH search. So we do a custom search
            # where we manually search for a script with a shebang in PATH.
            search_dirs = os.environ.get('PATH', '').split(';')
            for search_dir in search_dirs:
                commands = self._search_dir(name, search_dir)
                if commands:
                    return commands
        return [None]

    def found(self):
        return self.command[0] is not None

    def get_command(self):
        return self.command[:]

    def get_path(self):
        # Assume that the last element is the full path to the script
        # If it's not a script, this will be an array of length 1
        if self.found():
            return self.command[-1]
        return None

    def get_name(self):
        return self.name

class ExternalLibrary(Dependency):
    # TODO: Add `lang` to the parent Dependency object so that dependencies can
    # be expressed for languages other than C-like
    def __init__(self, name, link_args=None, language=None, silent=False):
        super().__init__('external')
        self.name = name
        self.is_found = False
        self.link_args = []
        self.lang_args = []
        if link_args:
            self.is_found = True
            if not isinstance(link_args, list):
                link_args = [link_args]
            if language:
                self.lang_args = {language: link_args}
            else:
                self.link_args = link_args
        if not silent:
            if self.is_found:
                mlog.log('Library', mlog.bold(name), 'found:', mlog.green('YES'))
            else:
                mlog.log('Library', mlog.bold(name), 'found:', mlog.red('NO'))

    def found(self):
        return self.is_found

    def get_name(self):
        return self.name

    def get_link_args(self):
        return self.link_args

    def get_lang_args(self, lang):
        if lang in self.lang_args:
            return self.lang_args[lang]
        return []

class BoostDependency(Dependency):
    # Some boost libraries have different names for
    # their sources and libraries. This dict maps
    # between the two.
    name2lib = {'test': 'unit_test_framework'}

    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'boost')
        self.name = 'boost'
        self.environment = environment
        self.libdir = ''
        if 'native' in kwargs and environment.is_cross_build():
            self.want_cross = not kwargs['native']
        else:
            self.want_cross = environment.is_cross_build()
        try:
            self.boost_root = os.environ['BOOST_ROOT']
            if not os.path.isabs(self.boost_root):
                raise DependencyException('BOOST_ROOT must be an absolute path.')
        except KeyError:
            self.boost_root = None
        if self.boost_root is None:
            if self.want_cross:
                raise DependencyException('BOOST_ROOT is needed while cross-compiling')
            if mesonlib.is_windows():
                self.boost_root = self.detect_win_root()
                self.incdir = self.boost_root
            else:
                self.incdir = '/usr/include'
        else:
            self.incdir = os.path.join(self.boost_root, 'include')
        self.boost_inc_subdir = os.path.join(self.incdir, 'boost')
        mlog.debug('Boost library root dir is', self.boost_root)
        self.src_modules = {}
        self.lib_modules = {}
        self.lib_modules_mt = {}
        self.detect_version()
        self.requested_modules = self.get_requested(kwargs)
        module_str = ', '.join(self.requested_modules)
        if self.version is not None:
            self.detect_src_modules()
            self.detect_lib_modules()
            self.validate_requested()
            if self.boost_root is not None:
                info = self.version + ', ' + self.boost_root
            else:
                info = self.version
            mlog.log('Dependency Boost (%s) found:' % module_str, mlog.green('YES'), info)
        else:
            mlog.log("Dependency Boost (%s) found:" % module_str, mlog.red('NO'))
        if 'cpp' not in self.environment.coredata.compilers:
            raise DependencyException('Tried to use Boost but a C++ compiler is not defined.')
        self.cpp_compiler = self.environment.coredata.compilers['cpp']

    def detect_win_root(self):
        globtext = 'c:\\local\\boost_*'
        files = glob.glob(globtext)
        if len(files) > 0:
            return files[0]
        return 'C:\\'

    def get_compile_args(self):
        args = []
        if self.boost_root is not None:
            if mesonlib.is_windows():
                args.append('-I' + self.boost_root)
            else:
                args.append('-I' + os.path.join(self.boost_root, 'include'))
        else:
            args.append('-I' + self.incdir)
        return args

    def get_requested(self, kwargs):
        candidates = kwargs.get('modules', [])
        if isinstance(candidates, str):
            return [candidates]
        for c in candidates:
            if not isinstance(c, str):
                raise DependencyException('Boost module argument is not a string.')
        return candidates

    def validate_requested(self):
        for m in self.requested_modules:
            if m not in self.src_modules:
                raise DependencyException('Requested Boost module "%s" not found.' % m)

    def found(self):
        return self.version is not None

    def get_version(self):
        return self.version

    def detect_version(self):
        try:
            ifile = open(os.path.join(self.boost_inc_subdir, 'version.hpp'))
        except FileNotFoundError:
            self.version = None
            return
        with ifile:
            for line in ifile:
                if line.startswith("#define") and 'BOOST_LIB_VERSION' in line:
                    ver = line.split()[-1]
                    ver = ver[1:-1]
                    self.version = ver.replace('_', '.')
                    return
        self.version = None

    def detect_src_modules(self):
        for entry in os.listdir(self.boost_inc_subdir):
            entry = os.path.join(self.boost_inc_subdir, entry)
            if stat.S_ISDIR(os.stat(entry).st_mode):
                self.src_modules[os.path.split(entry)[-1]] = True

    def detect_lib_modules(self):
        if mesonlib.is_windows():
            return self.detect_lib_modules_win()
        return self.detect_lib_modules_nix()

    def detect_lib_modules_win(self):
        arch = detect_cpu_family(self.environment.coredata.compilers)
        # Guess the libdir
        if arch == 'x86':
            gl = 'lib32*'
        elif arch == 'x86_64':
            gl = 'lib64*'
        else:
            # Does anyone do Boost cross-compiling to other archs on Windows?
            gl = None
        # See if the libdir is valid
        if gl:
            libdir = glob.glob(os.path.join(self.boost_root, gl))
        else:
            libdir = []
        # Can't find libdir, bail
        if len(libdir) == 0:
            return
        libdir = libdir[0]
        self.libdir = libdir
        globber = 'boost_*-gd-*.lib' # FIXME
        for entry in glob.glob(os.path.join(libdir, globber)):
            (_, fname) = os.path.split(entry)
            base = fname.split('_', 1)[1]
            modname = base.split('-', 1)[0]
            self.lib_modules_mt[modname] = fname

    def detect_lib_modules_nix(self):
        if mesonlib.is_osx():
            libsuffix = 'dylib'
        else:
            libsuffix = 'so'

        globber = 'libboost_*.{}'.format(libsuffix)
        if self.boost_root is None:
            libdirs = mesonlib.get_library_dirs()
        else:
            libdirs = [os.path.join(self.boost_root, 'lib')]
        for libdir in libdirs:
            for entry in glob.glob(os.path.join(libdir, globber)):
                lib = os.path.basename(entry)
                name = lib.split('.')[0].split('_', 1)[-1]
                # I'm not 100% sure what to do here. Some distros
                # have modules such as thread only as -mt versions.
                if entry.endswith('-mt.so'):
                    self.lib_modules_mt[name] = True
                else:
                    self.lib_modules[name] = True

    def get_win_link_args(self):
        args = []
        if self.boost_root:
            args.append('-L' + self.libdir)
        for module in self.requested_modules:
            module = BoostDependency.name2lib.get(module, module)
            if module in self.lib_modules_mt:
                args.append(self.lib_modules_mt[module])
        return args

    def get_link_args(self):
        if mesonlib.is_windows():
            return self.get_win_link_args()
        args = []
        if self.boost_root:
            args.append('-L' + os.path.join(self.boost_root, 'lib'))
        for module in self.requested_modules:
            module = BoostDependency.name2lib.get(module, module)
            libname = 'boost_' + module
            # The compiler's library detector is the most reliable so use that first.
            default_detect = self.cpp_compiler.find_library(libname, self.environment, [])
            if default_detect is not None:
                if module == 'unit_testing_framework':
                    emon_args = self.cpp_compiler.find_library('boost_test_exec_monitor')
                else:
                    emon_args = None
                args += default_detect
                if emon_args is not None:
                    args += emon_args
            elif module in self.lib_modules or module in self.lib_modules_mt:
                linkcmd = '-l' + libname
                args.append(linkcmd)
                # FIXME a hack, but Boost's testing framework has a lot of
                # different options and it's hard to determine what to do
                # without feedback from actual users. Update this
                # as we get more bug reports.
                if module == 'unit_testing_framework':
                    args.append('-lboost_test_exec_monitor')
            elif module + '-mt' in self.lib_modules_mt:
                linkcmd = '-lboost_' + module + '-mt'
                args.append(linkcmd)
                if module == 'unit_testing_framework':
                    args.append('-lboost_test_exec_monitor-mt')
        return args

    def get_sources(self):
        return []

    def need_threads(self):
        return 'thread' in self.requested_modules

class GTestDependency(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'gtest')
        self.main = kwargs.get('main', False)
        self.name = 'gtest'
        self.libname = 'libgtest.so'
        self.libmain_name = 'libgtest_main.so'
        self.include_dir = '/usr/include'
        self.src_dirs = ['/usr/src/gtest/src', '/usr/src/googletest/googletest/src']
        self.detect()

    def found(self):
        return self.is_found

    def detect(self):
        trial_dirs = mesonlib.get_library_dirs()
        glib_found = False
        gmain_found = False
        for d in trial_dirs:
            if os.path.isfile(os.path.join(d, self.libname)):
                glib_found = True
            if os.path.isfile(os.path.join(d, self.libmain_name)):
                gmain_found = True
        if glib_found and gmain_found:
            self.is_found = True
            self.compile_args = []
            self.link_args = ['-lgtest']
            if self.main:
                self.link_args.append('-lgtest_main')
            self.sources = []
            mlog.log('Dependency GTest found:', mlog.green('YES'), '(prebuilt)')
        elif self.detect_srcdir():
            self.is_found = True
            self.compile_args = ['-I' + self.src_include_dir]
            self.link_args = []
            if self.main:
                self.sources = [self.all_src, self.main_src]
            else:
                self.sources = [self.all_src]
            mlog.log('Dependency GTest found:', mlog.green('YES'), '(building self)')
        else:
            mlog.log('Dependency GTest found:', mlog.red('NO'))
            self.is_found = False
        return self.is_found

    def detect_srcdir(self):
        for s in self.src_dirs:
            if os.path.exists(s):
                self.src_dir = s
                self.all_src = mesonlib.File.from_absolute_file(
                    os.path.join(self.src_dir, 'gtest-all.cc'))
                self.main_src = mesonlib.File.from_absolute_file(
                    os.path.join(self.src_dir, 'gtest_main.cc'))
                self.src_include_dir = os.path.normpath(os.path.join(self.src_dir, '..'))
                return True
        return False

    def get_compile_args(self):
        arr = []
        if self.include_dir != '/usr/include':
            arr.append('-I' + self.include_dir)
        if hasattr(self, 'src_include_dir'):
            arr.append('-I' + self.src_include_dir)
        return arr

    def get_link_args(self):
        return self.link_args

    def get_version(self):
        return '1.something_maybe'

    def get_sources(self):
        return self.sources

    def need_threads(self):
        return True

class GMockDependency(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'gmock')
        # GMock may be a library or just source.
        # Work with both.
        self.name = 'gmock'
        self.libname = 'libgmock.so'
        trial_dirs = mesonlib.get_library_dirs()
        gmock_found = False
        for d in trial_dirs:
            if os.path.isfile(os.path.join(d, self.libname)):
                gmock_found = True
        if gmock_found:
            self.is_found = True
            self.compile_args = []
            self.link_args = ['-lgmock']
            self.sources = []
            mlog.log('Dependency GMock found:', mlog.green('YES'), '(prebuilt)')
            return

        for d in ['/usr/src/googletest/googlemock/src', '/usr/src/gmock/src', '/usr/src/gmock']:
            if os.path.exists(d):
                self.is_found = True
                # Yes, we need both because there are multiple
                # versions of gmock that do different things.
                d2 = os.path.normpath(os.path.join(d, '..'))
                self.compile_args = ['-I' + d, '-I' + d2]
                self.link_args = []
                all_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock-all.cc'))
                main_src = mesonlib.File.from_absolute_file(os.path.join(d, 'gmock_main.cc'))
                if kwargs.get('main', False):
                    self.sources = [all_src, main_src]
                else:
                    self.sources = [all_src]
                mlog.log('Dependency GMock found:', mlog.green('YES'), '(building self)')
                return

        mlog.log('Dependency GMock found:', mlog.red('NO'))
        self.is_found = False

    def get_version(self):
        return '1.something_maybe'

    def get_compile_args(self):
        return self.compile_args

    def get_sources(self):
        return self.sources

    def get_link_args(self):
        return self.link_args

    def found(self):
        return self.is_found

class QtBaseDependency(Dependency):
    def __init__(self, name, env, kwargs):
        Dependency.__init__(self, name)
        self.name = name
        self.qtname = name.capitalize()
        self.qtver = name[-1]
        if self.qtver == "4":
            self.qtpkgname = 'Qt'
        else:
            self.qtpkgname = self.qtname
        self.root = '/usr'
        self.bindir = None
        self.silent = kwargs.get('silent', False)
        # We store the value of required here instead of passing it on to
        # PkgConfigDependency etc because we want to try the qmake-based
        # fallback as well.
        self.required = kwargs.pop('required', True)
        kwargs['required'] = False
        mods = kwargs.get('modules', [])
        self.cargs = []
        self.largs = []
        self.is_found = False
        if isinstance(mods, str):
            mods = [mods]
        if len(mods) == 0:
            raise DependencyException('No ' + self.qtname + '  modules specified.')
        type_text = 'cross' if env.is_cross_build() else 'native'
        found_msg = '{} {} {{}} dependency (modules: {}) found:' \
                    ''.format(self.qtname, type_text, ', '.join(mods))
        from_text = '`pkg-config`'
        # Prefer pkg-config, then fallback to `qmake -query`
        self._pkgconfig_detect(mods, env, kwargs)
        if not self.is_found:
            from_text = self._qmake_detect(mods, env, kwargs)
            if not self.is_found:
                # Reset compile args and link args
                self.cargs = []
                self.largs = []
                from_text = '(checked pkg-config, qmake-{}, and qmake)' \
                            ''.format(self.name)
                self.version = 'none'
                if self.required:
                    err_msg = '{} {} dependency not found {}' \
                              ''.format(self.qtname, type_text, from_text)
                    raise DependencyException(err_msg)
                if not self.silent:
                    mlog.log(found_msg.format(from_text), mlog.red('NO'))
                return
            from_text = '`{}`'.format(from_text)
        if not self.silent:
            mlog.log(found_msg.format(from_text), mlog.green('YES'))

    def compilers_detect(self):
        "Detect Qt (4 or 5) moc, uic, rcc in the specified bindir or in PATH"
        if self.bindir:
            moc = ExternalProgram(os.path.join(self.bindir, 'moc'), silent=True)
            uic = ExternalProgram(os.path.join(self.bindir, 'uic'), silent=True)
            rcc = ExternalProgram(os.path.join(self.bindir, 'rcc'), silent=True)
        else:
            # We don't accept unsuffixed 'moc', 'uic', and 'rcc' because they
            # are sometimes older, or newer versions.
            moc = ExternalProgram('moc-' + self.name, silent=True)
            uic = ExternalProgram('uic-' + self.name, silent=True)
            rcc = ExternalProgram('rcc-' + self.name, silent=True)
        return moc, uic, rcc

    def _pkgconfig_detect(self, mods, env, kwargs):
        modules = OrderedDict()
        for module in mods:
            modules[module] = PkgConfigDependency(self.qtpkgname + module, env, kwargs)
        self.is_found = True
        for m in modules.values():
            if not m.found():
                self.is_found = False
                return
            self.cargs += m.get_compile_args()
            self.largs += m.get_link_args()
        self.version = m.modversion
        # Try to detect moc, uic, rcc
        if 'Core' in modules:
            core = modules['Core']
        else:
            corekwargs = {'required': 'false', 'silent': 'true'}
            core = PkgConfigDependency(self.qtpkgname + 'Core', env, corekwargs)
        # Used by self.compilers_detect()
        self.bindir = core.get_pkgconfig_variable('host_bins')
        if not self.bindir:
            # If exec_prefix is not defined, the pkg-config file is broken
            prefix = core.get_pkgconfig_variable('exec_prefix')
            if prefix:
                self.bindir = os.path.join(prefix, 'bin')

    def _find_qmake(self, qmake, env):
        # Even when cross-compiling, if we don't get a cross-info qmake, we
        # fallback to using the qmake in PATH because that's what we used to do
        if env.is_cross_build():
            qmake = env.cross_info.config['binaries'].get('qmake', qmake)
        return ExternalProgram(qmake, silent=True)

    def _qmake_detect(self, mods, env, kwargs):
        for qmake in ('qmake-' + self.name, 'qmake'):
            self.qmake = self._find_qmake(qmake, env)
            if not self.qmake.found():
                continue
            # Check that the qmake is for qt5
            pc, stdo = Popen_safe(self.qmake.get_command() + ['-v'])[0:2]
            if pc.returncode != 0:
                continue
            if not 'Qt version ' + self.qtver in stdo:
                mlog.log('QMake is not for ' + self.qtname)
                continue
            # Found qmake for Qt5!
            break
        else:
            # Didn't find qmake :(
            return
        self.version = re.search(self.qtver + '(\.\d+)+', stdo).group(0)
        # Query library path, header path, and binary path
        stdo = Popen_safe(self.qmake.get_command() + ['-query'])[1]
        qvars = {}
        for line in stdo.split('\n'):
            line = line.strip()
            if line == '':
                continue
            (k, v) = tuple(line.split(':', 1))
            qvars[k] = v
        if mesonlib.is_osx():
            return self._framework_detect(qvars, mods, kwargs)
        incdir = qvars['QT_INSTALL_HEADERS']
        self.cargs.append('-I' + incdir)
        libdir = qvars['QT_INSTALL_LIBS']
        # Used by self.compilers_detect()
        self.bindir = qvars['QT_INSTALL_BINS']
        self.is_found = True
        for module in mods:
            mincdir = os.path.join(incdir, 'Qt' + module)
            self.cargs.append('-I' + mincdir)
            if for_windows(env.is_cross_build(), env):
                libfile = os.path.join(libdir, self.qtpkgname + module + '.lib')
                if not os.path.isfile(libfile):
                    # MinGW can link directly to .dll
                    libfile = os.path.join(self.bindir, self.qtpkgname + module + '.dll')
                    if not os.path.isfile(libfile):
                        self.is_found = False
                        break
            else:
                libfile = os.path.join(libdir, 'lib{}{}.so'.format(self.qtpkgname, module))
                if not os.path.isfile(libfile):
                    self.is_found = False
                    break
            self.largs.append(libfile)
        return qmake

    def _framework_detect(self, qvars, modules, kwargs):
        libdir = qvars['QT_INSTALL_LIBS']
        for m in modules:
            fname = 'Qt' + m
            fwdep = ExtraFrameworkDependency(fname, kwargs.get('required', True), libdir)
            self.cargs.append('-F' + libdir)
            if fwdep.found():
                self.is_found = True
                self.cargs += fwdep.get_compile_args()
                self.largs += fwdep.get_link_args()
        # Used by self.compilers_detect()
        self.bindir = qvars['QT_INSTALL_BINS']

    def get_version(self):
        return self.version

    def get_compile_args(self):
        return self.cargs

    def get_sources(self):
        return []

    def get_link_args(self):
        return self.largs

    def found(self):
        return self.is_found

    def get_exe_args(self, compiler):
        # Originally this was -fPIE but nowadays the default
        # for upstream and distros seems to be -reduce-relocations
        # which requires -fPIC. This may cause a performance
        # penalty when using self-built Qt or on platforms
        # where -fPIC is not required. If this is an issue
        # for you, patches are welcome.
        return compiler.get_pic_args()

class Qt5Dependency(QtBaseDependency):
    def __init__(self, env, kwargs):
        QtBaseDependency.__init__(self, 'qt5', env, kwargs)

class Qt4Dependency(QtBaseDependency):
    def __init__(self, env, kwargs):
        QtBaseDependency.__init__(self, 'qt4', env, kwargs)

class GnuStepDependency(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'gnustep')
        self.required = kwargs.get('required', True)
        self.modules = kwargs.get('modules', [])
        self.detect()

    def detect(self):
        self.confprog = 'gnustep-config'
        try:
            gp = Popen_safe([self.confprog, '--help'])[0]
        except (FileNotFoundError, PermissionError):
            self.args = None
            mlog.log('Dependency GnuStep found:', mlog.red('NO'), '(no gnustep-config)')
            return
        if gp.returncode != 0:
            self.args = None
            mlog.log('Dependency GnuStep found:', mlog.red('NO'))
            return
        if 'gui' in self.modules:
            arg = '--gui-libs'
        else:
            arg = '--base-libs'
        fp, flagtxt, flagerr = Popen_safe([self.confprog, '--objc-flags'])
        if fp.returncode != 0:
            raise DependencyException('Error getting objc-args: %s %s' % (flagtxt, flagerr))
        args = flagtxt.split()
        self.args = self.filter_arsg(args)
        fp, libtxt, liberr = Popen_safe([self.confprog, arg])
        if fp.returncode != 0:
            raise DependencyException('Error getting objc-lib args: %s %s' % (libtxt, liberr))
        self.libs = self.weird_filter(libtxt.split())
        self.version = self.detect_version()
        mlog.log('Dependency', mlog.bold('GnuStep'), 'found:',
                 mlog.green('YES'), self.version)

    def weird_filter(self, elems):
        """When building packages, the output of the enclosing Make
is sometimes mixed among the subprocess output. I have no idea
why. As a hack filter out everything that is not a flag."""
        return [e for e in elems if e.startswith('-')]

    def filter_arsg(self, args):
        """gnustep-config returns a bunch of garbage args such
        as -O2 and so on. Drop everything that is not needed."""
        result = []
        for f in args:
            if f.startswith('-D') \
                    or f.startswith('-f') \
                    or f.startswith('-I') \
                    or f == '-pthread' \
                    or (f.startswith('-W') and not f == '-Wall'):
                result.append(f)
        return result

    def detect_version(self):
        gmake = self.get_variable('GNUMAKE')
        makefile_dir = self.get_variable('GNUSTEP_MAKEFILES')
        # This Makefile has the GNUStep version set
        base_make = os.path.join(makefile_dir, 'Additional', 'base.make')
        # Print the Makefile variable passed as the argument. For instance, if
        # you run the make target `print-SOME_VARIABLE`, this will print the
        # value of the variable `SOME_VARIABLE`.
        printver = "print-%:\n\t@echo '$($*)'"
        env = os.environ.copy()
        # See base.make to understand why this is set
        env['FOUNDATION_LIB'] = 'gnu'
        p, o, e = Popen_safe([gmake, '-f', '-', '-f', base_make,
                              'print-GNUSTEP_BASE_VERSION'],
                             env=env, write=printver, stdin=subprocess.PIPE)
        version = o.strip()
        if not version:
            mlog.debug("Couldn't detect GNUStep version, falling back to '1'")
            # Fallback to setting some 1.x version
            version = '1'
        return version

    def get_variable(self, var):
        p, o, e = Popen_safe([self.confprog, '--variable=' + var])
        if p.returncode != 0 and self.required:
            raise DependencyException('{!r} for variable {!r} failed to run'
                                      ''.format(self.confprog, var))
        return o.strip()

    def found(self):
        return self.args is not None

    def get_version(self):
        return self.version

    def get_compile_args(self):
        if self.args is None:
            return []
        return self.args

    def get_link_args(self):
        return self.libs

class AppleFrameworks(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'appleframeworks')
        modules = kwargs.get('modules', [])
        if isinstance(modules, str):
            modules = [modules]
        if len(modules) == 0:
            raise DependencyException("AppleFrameworks dependency requires at least one module.")
        self.frameworks = modules

    def get_link_args(self):
        args = []
        for f in self.frameworks:
            args.append('-framework')
            args.append(f)
        return args

    def found(self):
        return mesonlib.is_osx()

    def get_version(self):
        return 'unknown'

class GLDependency(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'gl')
        self.is_found = False
        self.cargs = []
        self.linkargs = []
        try:
            pcdep = PkgConfigDependency('gl', environment, kwargs)
            if pcdep.found():
                self.type_name = 'pkgconfig'
                self.is_found = True
                self.cargs = pcdep.get_compile_args()
                self.linkargs = pcdep.get_link_args()
                self.version = pcdep.get_version()
                return
        except Exception:
            pass
        if mesonlib.is_osx():
            self.is_found = True
            self.linkargs = ['-framework', 'OpenGL']
            self.version = '1' # FIXME
            return
        if mesonlib.is_windows():
            self.is_found = True
            self.linkargs = ['-lopengl32']
            self.version = '1' # FIXME: unfixable?
            return

    def get_link_args(self):
        return self.linkargs

    def get_version(self):
        return self.version

# There are three different ways of depending on SDL2:
# sdl2-config, pkg-config and OSX framework
class SDL2Dependency(Dependency):
    def __init__(self, environment, kwargs):
        Dependency.__init__(self, 'sdl2')
        self.is_found = False
        self.cargs = []
        self.linkargs = []
        try:
            pcdep = PkgConfigDependency('sdl2', environment, kwargs)
            if pcdep.found():
                self.type_name = 'pkgconfig'
                self.is_found = True
                self.cargs = pcdep.get_compile_args()
                self.linkargs = pcdep.get_link_args()
                self.version = pcdep.get_version()
                return
        except Exception as e:
            mlog.debug('SDL 2 not found via pkgconfig. Trying next, error was:', str(e))
            pass
        sdlconf = shutil.which('sdl2-config')
        if sdlconf:
            stdo = Popen_safe(['sdl2-config', '--cflags'])[1]
            self.cargs = stdo.strip().split()
            stdo = Popen_safe(['sdl2-config', '--libs'])[1]
            self.linkargs = stdo.strip().split()
            stdo = Popen_safe(['sdl2-config', '--version'])[1]
            self.version = stdo.strip()
            self.is_found = True
            mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.green('YES'),
                     self.version, '(%s)' % sdlconf)
            return
        mlog.debug('Could not find sdl2-config binary, trying next.')
        if mesonlib.is_osx():
            fwdep = ExtraFrameworkDependency('sdl2', kwargs.get('required', True))
            if fwdep.found():
                self.is_found = True
                self.cargs = fwdep.get_compile_args()
                self.linkargs = fwdep.get_link_args()
                self.version = '2' # FIXME
                return
        mlog.log('Dependency', mlog.bold('sdl2'), 'found:', mlog.red('NO'))

    def get_compile_args(self):
        return self.cargs

    def get_link_args(self):
        return self.linkargs

    def found(self):
        return self.is_found

    def get_version(self):
        return self.version

class ExtraFrameworkDependency(Dependency):
    def __init__(self, name, required, path=None):
        Dependency.__init__(self, 'extraframeworks')
        self.name = None
        self.detect(name, path)
        if self.found():
            mlog.log('Dependency', mlog.bold(name), 'found:', mlog.green('YES'),
                     os.path.join(self.path, self.name))
        else:
            mlog.log('Dependency', name, 'found:', mlog.red('NO'))

    def detect(self, name, path):
        lname = name.lower()
        if path is None:
            paths = ['/Library/Frameworks']
        else:
            paths = [path]
        for p in paths:
            for d in os.listdir(p):
                fullpath = os.path.join(p, d)
                if lname != d.split('.')[0].lower():
                    continue
                if not stat.S_ISDIR(os.stat(fullpath).st_mode):
                    continue
                self.path = p
                self.name = d
                return

    def get_compile_args(self):
        if self.found():
            return ['-I' + os.path.join(self.path, self.name, 'Headers')]
        return []

    def get_link_args(self):
        if self.found():
            return ['-F' + self.path, '-framework', self.name.split('.')[0]]
        return []

    def found(self):
        return self.name is not None

    def get_version(self):
        return 'unknown'

class ThreadDependency(Dependency):
    def __init__(self, environment, kwargs):
        super().__init__('threads')
        self.name = 'threads'
        self.is_found = True
        mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.green('YES'))

    def need_threads(self):
        return True

    def get_version(self):
        return 'unknown'

class Python3Dependency(Dependency):
    def __init__(self, environment, kwargs):
        super().__init__('python3')
        self.name = 'python3'
        self.is_found = False
        # We can only be sure that it is Python 3 at this point
        self.version = '3'
        try:
            pkgdep = PkgConfigDependency('python3', environment, kwargs)
            if pkgdep.found():
                self.cargs = pkgdep.cargs
                self.libs = pkgdep.libs
                self.version = pkgdep.get_version()
                self.is_found = True
                return
        except Exception:
            pass
        if not self.is_found:
            if mesonlib.is_windows():
                self._find_libpy3_windows(environment)
            elif mesonlib.is_osx():
                # In OSX the Python 3 framework does not have a version
                # number in its name.
                fw = ExtraFrameworkDependency('python', False)
                if fw.found():
                    self.cargs = fw.get_compile_args()
                    self.libs = fw.get_link_args()
                    self.is_found = True
        if self.is_found:
            mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.green('YES'))
        else:
            mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.red('NO'))

    def _find_libpy3_windows(self, env):
        '''
        Find python3 libraries on Windows and also verify that the arch matches
        what we are building for.
        '''
        pyarch = sysconfig.get_platform()
        arch = detect_cpu_family(env.coredata.compilers)
        if arch == 'x86':
            arch = '32'
        elif arch == 'x86_64':
            arch = '64'
        else:
            # We can't cross-compile Python 3 dependencies on Windows yet
            mlog.log('Unknown architecture {!r} for'.format(arch),
                     mlog.bold(self.name))
            self.is_found = False
            return
        # Pyarch ends in '32' or '64'
        if arch != pyarch[-2:]:
            mlog.log('Need', mlog.bold(self.name),
                     'for {}-bit, but found {}-bit'.format(arch, pyarch[-2:]))
            self.is_found = False
            return
        inc = sysconfig.get_path('include')
        platinc = sysconfig.get_path('platinclude')
        self.cargs = ['-I' + inc]
        if inc != platinc:
            self.cargs.append('-I' + platinc)
        # Nothing exposes this directly that I coulf find
        basedir = sysconfig.get_config_var('base')
        vernum = sysconfig.get_config_var('py_version_nodot')
        self.libs = ['-L{}/libs'.format(basedir),
                     '-lpython{}'.format(vernum)]
        self.version = sysconfig.get_config_var('py_version_short')
        self.is_found = True

    def get_compile_args(self):
        return self.cargs

    def get_link_args(self):
        return self.libs

    def get_version(self):
        return self.version

def get_dep_identifier(name, kwargs):
    elements = [name]
    modlist = kwargs.get('modules', [])
    if isinstance(modlist, str):
        modlist = [modlist]
    for module in modlist:
        elements.append(module)
    # We use a tuple because we need a non-mutable structure to use as the key
    # of a dictionary and a string has potential for name collisions
    identifier = tuple(elements)
    identifier += ('main', kwargs.get('main', False))
    identifier += ('static', kwargs.get('static', False))
    if 'fallback' in kwargs:
        f = kwargs.get('fallback')
        identifier += ('fallback', f[0], f[1])
    return identifier

def find_external_dependency(name, environment, kwargs):
    required = kwargs.get('required', True)
    if not isinstance(required, bool):
        raise DependencyException('Keyword "required" must be a boolean.')
    lname = name.lower()
    if lname in packages:
        dep = packages[lname](environment, kwargs)
        if required and not dep.found():
            raise DependencyException('Dependency "%s" not found' % name)
        return dep
    pkg_exc = None
    pkgdep = None
    try:
        pkgdep = PkgConfigDependency(name, environment, kwargs)
        if pkgdep.found():
            return pkgdep
    except Exception as e:
        pkg_exc = e
    if mesonlib.is_osx():
        fwdep = ExtraFrameworkDependency(name, required)
        if required and not fwdep.found():
            m = 'Dependency {!r} not found, tried Extra Frameworks ' \
                'and Pkg-Config:\n\n' + str(pkg_exc)
            raise DependencyException(m.format(name))
        return fwdep
    if pkg_exc is not None:
        raise pkg_exc
    mlog.log('Dependency', mlog.bold(name), 'found:', mlog.red('NO'))
    return pkgdep

# This has to be at the end so the classes it references
# are defined.
packages = {'boost': BoostDependency,
            'gtest': GTestDependency,
            'gmock': GMockDependency,
            'qt5': Qt5Dependency,
            'qt4': Qt4Dependency,
            'gnustep': GnuStepDependency,
            'appleframeworks': AppleFrameworks,
            'wxwidgets': WxDependency,
            'sdl2': SDL2Dependency,
            'gl': GLDependency,
            'threads': ThreadDependency,
            'python3': Python3Dependency,
            }
