# 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 self.modversion = 'none' # 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)) 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 not os.path.isdir(path) 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 = self.get_pkgconfig_host_bins(core) 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) def get_pkgconfig_host_bins(self, core): return core.get_pkgconfig_variable('host_bins') class Qt4Dependency(QtBaseDependency): def __init__(self, env, kwargs): QtBaseDependency.__init__(self, 'qt4', env, kwargs) def get_pkgconfig_host_bins(self, core): # Only return one bins dir, because the tools are generally all in one # directory for Qt4, in Qt5, they must all be in one directory. Return # the first one found among the bin variables, in case one tool is not # configured to be built. applications = ['moc', 'uic', 'rcc', 'lupdate', 'lrelease'] for application in applications: try: return os.path.dirname(core.get_pkgconfig_variable('%s_location' % application)) except MesonException: pass 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 class ValgrindDependency(PkgConfigDependency): def __init__(self, environment, kwargs): PkgConfigDependency.__init__(self, 'valgrind', environment, kwargs) def get_link_args(self): return [] 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, 'valgrind': ValgrindDependency, }