From 95b70bcb97afa9177645e4926afdf3792fb73eb5 Mon Sep 17 00:00:00 2001 From: Daniel Mensinger Date: Mon, 31 May 2021 19:46:09 +0200 Subject: deps: Split dependencies.base Split the Factory and dependency classes out of the base.py script to improve maintainability. --- mesonbuild/dependencies/__init__.py | 40 +- mesonbuild/dependencies/base.py | 1819 ----------------------- mesonbuild/dependencies/boost.py | 4 +- mesonbuild/dependencies/cmake.py | 655 ++++++++ mesonbuild/dependencies/coarrays.py | 5 +- mesonbuild/dependencies/configtool.py | 173 +++ mesonbuild/dependencies/detect.py | 209 +++ mesonbuild/dependencies/dev.py | 9 +- mesonbuild/dependencies/dub.py | 227 +++ mesonbuild/dependencies/factory.py | 125 ++ mesonbuild/dependencies/framework.py | 120 ++ mesonbuild/dependencies/hdf5.py | 8 +- mesonbuild/dependencies/misc.py | 10 +- mesonbuild/dependencies/mpi.py | 6 +- mesonbuild/dependencies/pkgconfig.py | 472 ++++++ mesonbuild/dependencies/qt.py | 10 +- mesonbuild/dependencies/scalapack.py | 7 +- mesonbuild/dependencies/ui.py | 3 +- mesonbuild/modules/dlang.py | 2 +- mesonbuild/modules/pkgconfig.py | 2 +- mesonbuild/modules/python.py | 2 +- mesonbuild/modules/qt.py | 2 +- mesonbuild/modules/unstable_external_project.py | 2 +- 23 files changed, 2055 insertions(+), 1857 deletions(-) create mode 100644 mesonbuild/dependencies/cmake.py create mode 100644 mesonbuild/dependencies/configtool.py create mode 100644 mesonbuild/dependencies/detect.py create mode 100644 mesonbuild/dependencies/dub.py create mode 100644 mesonbuild/dependencies/factory.py create mode 100644 mesonbuild/dependencies/framework.py create mode 100644 mesonbuild/dependencies/pkgconfig.py (limited to 'mesonbuild') diff --git a/mesonbuild/dependencies/__init__.py b/mesonbuild/dependencies/__init__.py index 722ede4..bfa2902 100644 --- a/mesonbuild/dependencies/__init__.py +++ b/mesonbuild/dependencies/__init__.py @@ -15,12 +15,15 @@ from .boost import BoostDependency from .cuda import CudaDependency from .hdf5 import hdf5_factory -from .base import ( # noqa: F401 - Dependency, DependencyException, DependencyMethods, ExternalDependency, - NotFoundDependency, ExternalLibrary, ExtraFrameworkDependency, - InternalDependency, PkgConfigDependency, CMakeDependency, - find_external_dependency, get_dep_identifier, packages, - _packages_accept_language, DependencyFactory) +from .base import Dependency, InternalDependency, ExternalDependency, NotFoundDependency +from .base import ExternalLibrary, DependencyException, DependencyMethods +from .cmake import CMakeDependency +from .configtool import ConfigToolDependency +from .dub import DubDependency +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency +from .factory import DependencyFactory +from .detect import find_external_dependency, get_dep_identifier, packages, _packages_accept_language from .dev import ( ValgrindDependency, JDKSystemDependency, gmock_factory, gtest_factory, llvm_factory, zlib_factory) @@ -30,12 +33,35 @@ from .scalapack import scalapack_factory from .misc import ( BlocksDependency, OpenMPDependency, cups_factory, curses_factory, gpgme_factory, libgcrypt_factory, libwmf_factory, netcdf_factory, pcap_factory, python3_factory, - shaderc_factory, threads_factory, + shaderc_factory, threads_factory, ThreadDependency, ) from .platform import AppleFrameworks from .qt import qt4_factory, qt5_factory, qt6_factory from .ui import GnuStepDependency, WxDependency, gl_factory, sdl2_factory, vulkan_factory +__all__ = [ + 'Dependency', + 'InternalDependency', + 'ExternalDependency', + 'NotFoundDependency', + 'ExternalLibrary', + 'DependencyException', + 'DependencyMethods', + + 'CMakeDependency', + 'ConfigToolDependency', + 'DubDependency', + 'ExtraFrameworkDependency', + 'PkgConfigDependency', + + 'DependencyFactory', + + 'ThreadDependency', + + 'find_external_dependency', + 'get_dep_identifier', +] + """Dependency representations and discovery logic. Meson attempts to largely abstract away dependency discovery information, and diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index d4b45fa..f18a7cc 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -23,7 +23,6 @@ import json import shlex import shutil import textwrap -import platform import typing as T from enum import Enum from pathlib import Path, PurePath @@ -32,11 +31,9 @@ from .. import mlog from .. import mesonlib from ..compilers import clib_langs from ..environment import Environment, MachineInfo -from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args from ..mesonlib import MachineChoice, MesonException, OrderedSet, PerMachine from ..mesonlib import Popen_safe, version_compare_many, version_compare, listify, stringlistify, extract_as_list, split_args from ..mesonlib import Version, LibType, OptionKey -from ..mesondata import mesondata from ..programs import ExternalProgram, find_external_program from ..interpreterbase import FeatureDeprecated @@ -44,10 +41,6 @@ if T.TYPE_CHECKING: from ..compilers.compilers import Compiler DependencyType = T.TypeVar('DependencyType', bound='Dependency') -# These must be defined in this file to avoid cyclical references. -packages = {} -_packages_accept_language = set() - class DependencyException(MesonException): '''Exceptions raised while trying to find dependencies''' @@ -411,1434 +404,6 @@ class NotFoundDependency(Dependency): return copy.copy(self) -class ConfigToolDependency(ExternalDependency): - - """Class representing dependencies found using a config tool. - - Takes the following extra keys in kwargs that it uses internally: - :tools List[str]: A list of tool names to use - :version_arg str: The argument to pass to the tool to get it's version - :returncode_value int: The value of the correct returncode - Because some tools are stupid and don't return 0 - """ - - tools = None - tool_name = None - version_arg = '--version' - __strip_version = re.compile(r'^[0-9][0-9.]+') - - def __init__(self, name, environment, kwargs, language: T.Optional[str] = None): - super().__init__('config-tool', environment, kwargs, language=language) - self.name = name - # You may want to overwrite the class version in some cases - self.tools = listify(kwargs.get('tools', self.tools)) - if not self.tool_name: - self.tool_name = self.tools[0] - if 'version_arg' in kwargs: - self.version_arg = kwargs['version_arg'] - - req_version = kwargs.get('version', None) - tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0)) - self.config = tool - self.is_found = self.report_config(version, req_version) - if not self.is_found: - self.config = None - return - self.version = version - - def _sanitize_version(self, version): - """Remove any non-numeric, non-point version suffixes.""" - m = self.__strip_version.match(version) - if m: - # Ensure that there isn't a trailing '.', such as an input like - # `1.2.3.git-1234` - return m.group(0).rstrip('.') - return version - - def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0) \ - -> T.Tuple[T.Optional[str], T.Optional[str]]: - """Helper method that searches for config tool binaries in PATH and - returns the one that best matches the given version requirements. - """ - if not isinstance(versions, list) and versions is not None: - versions = listify(versions) - best_match = (None, None) # type: T.Tuple[T.Optional[str], T.Optional[str]] - for potential_bin in find_external_program( - self.env, self.for_machine, self.tool_name, - self.tool_name, self.tools, allow_default_for_cross=False): - if not potential_bin.found(): - continue - tool = potential_bin.get_command() - try: - p, out = Popen_safe(tool + [self.version_arg])[:2] - except (FileNotFoundError, PermissionError): - continue - if p.returncode != returncode: - continue - - out = self._sanitize_version(out.strip()) - # Some tools, like pcap-config don't supply a version, but also - # don't fail with --version, in that case just assume that there is - # only one version and return it. - if not out: - return (tool, None) - if versions: - is_found = version_compare_many(out, versions)[0] - # This allows returning a found version without a config tool, - # which is useful to inform the user that you found version x, - # but y was required. - if not is_found: - tool = None - if best_match[1]: - if version_compare(out, '> {}'.format(best_match[1])): - best_match = (tool, out) - else: - best_match = (tool, out) - - return best_match - - def report_config(self, version, req_version): - """Helper method to print messages about the tool.""" - - found_msg = [mlog.bold(self.tool_name), 'found:'] - - if self.config is None: - found_msg.append(mlog.red('NO')) - if version is not None and req_version is not None: - found_msg.append(f'found {version!r} but need {req_version!r}') - elif req_version: - found_msg.append(f'need {req_version!r}') - else: - found_msg += [mlog.green('YES'), '({})'.format(' '.join(self.config)), version] - - mlog.log(*found_msg) - - return self.config is not None - - def get_config_value(self, args: T.List[str], stage: str) -> T.List[str]: - p, out, err = Popen_safe(self.config + args) - if p.returncode != 0: - if self.required: - raise DependencyException( - 'Could not generate {} for {}.\n{}'.format( - stage, self.name, err)) - return [] - return split_args(out) - - @staticmethod - def get_methods(): - return [DependencyMethods.AUTO, DependencyMethods.CONFIG_TOOL] - - def get_configtool_variable(self, variable_name): - p, out, _ = Popen_safe(self.config + [f'--{variable_name}']) - if p.returncode != 0: - if self.required: - raise DependencyException( - 'Could not get variable "{}" for dependency {}'.format( - variable_name, self.name)) - variable = out.strip() - mlog.debug(f'Got config-tool variable {variable_name} : {variable}') - return variable - - def log_tried(self): - return self.type_name - - def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, - configtool: T.Optional[str] = None, internal: T.Optional[str] = None, - default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: - if configtool: - # In the not required case '' (empty string) will be returned if the - # variable is not found. Since '' is a valid value to return we - # set required to True here to force and error, and use the - # finally clause to ensure it's restored. - restore = self.required - self.required = True - try: - return self.get_configtool_variable(configtool) - except DependencyException: - pass - finally: - self.required = restore - if default_value is not None: - return default_value - raise DependencyException(f'Could not get config-tool variable and no default provided for {self!r}') - - -class PkgConfigDependency(ExternalDependency): - # The class's copy of the pkg-config path. Avoids having to search for it - # multiple times in the same Meson invocation. - class_pkgbin = PerMachine(None, None) - # We cache all pkg-config subprocess invocations to avoid redundant calls - pkgbin_cache = {} - - def __init__(self, name, environment: 'Environment', kwargs, language: T.Optional[str] = None): - super().__init__('pkgconfig', environment, kwargs, language=language) - self.name = name - self.is_libtool = False - # 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 - - # Only search for pkg-config for each machine the first time and store - # the result in the class definition - if PkgConfigDependency.class_pkgbin[self.for_machine] is False: - mlog.debug('Pkg-config binary for %s is cached as not found.' % self.for_machine) - elif PkgConfigDependency.class_pkgbin[self.for_machine] is not None: - mlog.debug('Pkg-config binary for %s is cached.' % self.for_machine) - else: - assert PkgConfigDependency.class_pkgbin[self.for_machine] is None - mlog.debug('Pkg-config binary for %s is not cached.' % self.for_machine) - for potential_pkgbin in find_external_program( - self.env, self.for_machine, 'pkgconfig', 'Pkg-config', - environment.default_pkgconfig, allow_default_for_cross=False): - version_if_ok = self.check_pkgconfig(potential_pkgbin) - if not version_if_ok: - continue - if not self.silent: - mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()), - '(%s)' % version_if_ok) - PkgConfigDependency.class_pkgbin[self.for_machine] = potential_pkgbin - break - else: - if not self.silent: - mlog.log('Found Pkg-config:', mlog.red('NO')) - # Set to False instead of None to signify that we've already - # searched for it and not found it - PkgConfigDependency.class_pkgbin[self.for_machine] = False - - self.pkgbin = PkgConfigDependency.class_pkgbin[self.for_machine] - if self.pkgbin is False: - self.pkgbin = None - msg = 'Pkg-config binary for machine %s not found. Giving up.' % self.for_machine - if self.required: - raise DependencyException(msg) - else: - mlog.debug(msg) - return - - mlog.debug('Determining dependency {!r} with pkg-config executable ' - '{!r}'.format(name, self.pkgbin.get_path())) - ret, self.version, _ = self._call_pkgbin(['--modversion', name]) - if ret != 0: - return - - self.is_found = True - - try: - # 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() - except DependencyException as e: - mlog.debug(f"pkg-config error with '{name}': {e}") - if self.required: - raise - else: - self.compile_args = [] - self.link_args = [] - self.is_found = False - self.reason = e - - 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_real(self, args, env): - cmd = self.pkgbin.get_command() + args - p, out, err = Popen_safe(cmd, env=env) - rc, out, err = p.returncode, out.strip(), err.strip() - call = ' '.join(cmd) - mlog.debug(f"Called `{call}` -> {rc}\n{out}") - return rc, out, err - - @staticmethod - def setup_env(env: T.MutableMapping[str, str], environment: 'Environment', for_machine: MachineChoice, - extra_path: T.Optional[str] = None) -> None: - extra_paths: T.List[str] = environment.coredata.options[OptionKey('pkg_config_path', machine=for_machine)].value[:] - if extra_path and extra_path not in extra_paths: - extra_paths.append(extra_path) - sysroot = environment.properties[for_machine].get_sys_root() - if sysroot: - env['PKG_CONFIG_SYSROOT_DIR'] = sysroot - new_pkg_config_path = ':'.join([p for p in extra_paths]) - env['PKG_CONFIG_PATH'] = new_pkg_config_path - - pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir() - if pkg_config_libdir_prop: - new_pkg_config_libdir = ':'.join([p for p in pkg_config_libdir_prop]) - env['PKG_CONFIG_LIBDIR'] = new_pkg_config_libdir - # Dump all PKG_CONFIG environment variables - for key, value in env.items(): - if key.startswith('PKG_'): - mlog.debug(f'env[{key}]: {value}') - - def _call_pkgbin(self, args, env=None): - # Always copy the environment since we're going to modify it - # with pkg-config variables - if env is None: - env = os.environ.copy() - else: - env = env.copy() - - PkgConfigDependency.setup_env(env, self.env, self.for_machine) - - fenv = frozenset(env.items()) - targs = tuple(args) - cache = PkgConfigDependency.pkgbin_cache - if (self.pkgbin, targs, fenv) not in cache: - cache[(self.pkgbin, targs, fenv)] = self._call_pkgbin_real(args, env) - return cache[(self.pkgbin, targs, fenv)] - - def _convert_mingw_paths(self, args: T.List[str]) -> T.List[str]: - ''' - Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo - paths so convert them to C:/foo. We cannot resolve other paths starting - with / like /home/foo so leave them as-is so that the user gets an - error/warning from the compiler/linker. - ''' - if not self.env.machines.build.is_windows(): - return args - converted = [] - for arg in args: - pargs = [] - # Library search path - if arg.startswith('-L/'): - pargs = PurePath(arg[2:]).parts - tmpl = '-L{}:/{}' - elif arg.startswith('-I/'): - pargs = PurePath(arg[2:]).parts - tmpl = '-I{}:/{}' - # Full path to library or .la file - elif arg.startswith('/'): - pargs = PurePath(arg).parts - tmpl = '{}:/{}' - elif arg.startswith(('-L', '-I')) or (len(arg) > 2 and arg[1] == ':'): - # clean out improper '\\ ' as comes from some Windows pkg-config files - arg = arg.replace('\\ ', ' ') - if len(pargs) > 1 and len(pargs[1]) == 1: - arg = tmpl.format(pargs[1], '/'.join(pargs[2:])) - converted.append(arg) - return converted - - def _split_args(self, cmd): - # pkg-config paths follow Unix conventions, even on Windows; split the - # output using shlex.split rather than mesonlib.split_args - return shlex.split(cmd) - - def _set_cargs(self): - env = None - if self.language == 'fortran': - # gfortran doesn't appear to look in system paths for INCLUDE files, - # so don't allow pkg-config to suppress -I flags for system paths - env = os.environ.copy() - env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' - ret, out, err = self._call_pkgbin(['--cflags', self.name], env=env) - if ret != 0: - raise DependencyException('Could not generate cargs for %s:\n%s\n' % - (self.name, err)) - self.compile_args = self._convert_mingw_paths(self._split_args(out)) - - def _search_libs(self, out, out_raw): - ''' - @out: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs - @out_raw: pkg-config --libs - - We always look for the file ourselves instead of depending on the - compiler to find it with -lfoo or foo.lib (if possible) because: - 1. We want to be able to select static or shared - 2. We need the full path of the library to calculate RPATH values - 3. De-dup of libraries is easier when we have absolute paths - - Libraries that are provided by the toolchain or are not found by - find_library() will be added with -L -l pairs. - ''' - # Library paths should be safe to de-dup - # - # First, figure out what library paths to use. Originally, we were - # doing this as part of the loop, but due to differences in the order - # of -L values between pkg-config and pkgconf, we need to do that as - # a separate step. See: - # https://github.com/mesonbuild/meson/issues/3951 - # https://github.com/mesonbuild/meson/issues/4023 - # - # Separate system and prefix paths, and ensure that prefix paths are - # always searched first. - prefix_libpaths = OrderedSet() - # We also store this raw_link_args on the object later - raw_link_args = self._convert_mingw_paths(self._split_args(out_raw)) - for arg in raw_link_args: - if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')): - path = arg[2:] - if not os.path.isabs(path): - # Resolve the path as a compiler in the build directory would - path = os.path.join(self.env.get_build_dir(), path) - prefix_libpaths.add(path) - # Library paths are not always ordered in a meaningful way - # - # Instead of relying on pkg-config or pkgconf to provide -L flags in a - # specific order, we reorder library paths ourselves, according to th - # order specified in PKG_CONFIG_PATH. See: - # https://github.com/mesonbuild/meson/issues/4271 - # - # Only prefix_libpaths are reordered here because there should not be - # too many system_libpaths to cause library version issues. - pkg_config_path: T.List[str] = self.env.coredata.options[OptionKey('pkg_config_path', machine=self.for_machine)].value - pkg_config_path = self._convert_mingw_paths(pkg_config_path) - prefix_libpaths = sort_libpaths(prefix_libpaths, pkg_config_path) - system_libpaths = OrderedSet() - full_args = self._convert_mingw_paths(self._split_args(out)) - for arg in full_args: - if arg.startswith(('-L-l', '-L-L')): - # These are D language arguments, not library paths - continue - if arg.startswith('-L') and arg[2:] not in prefix_libpaths: - system_libpaths.add(arg[2:]) - # Use this re-ordered path list for library resolution - libpaths = list(prefix_libpaths) + list(system_libpaths) - # Track -lfoo libraries to avoid duplicate work - libs_found = OrderedSet() - # Track not-found libraries to know whether to add library paths - libs_notfound = [] - libtype = LibType.STATIC if self.static else LibType.PREFER_SHARED - # Generate link arguments for this library - link_args = [] - for lib in full_args: - if lib.startswith(('-L-l', '-L-L')): - # These are D language arguments, add them as-is - pass - elif lib.startswith('-L'): - # We already handled library paths above - continue - elif lib.startswith('-l'): - # Don't resolve the same -lfoo argument again - if lib in libs_found: - continue - if self.clib_compiler: - args = self.clib_compiler.find_library(lib[2:], self.env, - libpaths, libtype) - # If the project only uses a non-clib language such as D, Rust, - # C#, Python, etc, all we can do is limp along by adding the - # arguments as-is and then adding the libpaths at the end. - else: - args = None - if args is not None: - libs_found.add(lib) - # Replace -l arg with full path to library if available - # else, library is either to be ignored, or is provided by - # the compiler, can't be resolved, and should be used as-is - if args: - if not args[0].startswith('-l'): - lib = args[0] - else: - continue - else: - # Library wasn't found, maybe we're looking in the wrong - # places or the library will be provided with LDFLAGS or - # LIBRARY_PATH from the environment (on macOS), and many - # other edge cases that we can't account for. - # - # Add all -L paths and use it as -lfoo - if lib in libs_notfound: - continue - if self.static: - mlog.warning('Static library {!r} not found for dependency {!r}, may ' - 'not be statically linked'.format(lib[2:], self.name)) - libs_notfound.append(lib) - elif 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) - self.is_libtool = True - lib = shared_lib - if lib in link_args: - continue - link_args.append(lib) - # Add all -Lbar args if we have -lfoo args in link_args - if libs_notfound: - # Order of -L flags doesn't matter with ld, but it might with other - # linkers such as MSVC, so prepend them. - link_args = ['-L' + lp for lp in prefix_libpaths] + link_args - return link_args, raw_link_args - - def _set_libs(self): - env = None - libcmd = ['--libs'] - - if self.static: - libcmd.append('--static') - - libcmd.append(self.name) - - # Force pkg-config to output -L fields even if they are system - # paths so we can do manual searching with cc.find_library() later. - env = os.environ.copy() - env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1' - ret, out, err = self._call_pkgbin(libcmd, env=env) - if ret != 0: - raise DependencyException('Could not generate libs for %s:\n%s\n' % - (self.name, err)) - # Also get the 'raw' output without -Lfoo system paths for adding -L - # args with -lfoo when a library can't be found, and also in - # gnome.generate_gir + gnome.gtkdoc which need -L -l arguments. - ret, out_raw, err_raw = self._call_pkgbin(libcmd) - if ret != 0: - raise DependencyException('Could not generate libs for %s:\n\n%s' % - (self.name, out_raw)) - self.link_args, self.raw_link_args = self._search_libs(out, out_raw) - - def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: - options = ['--variable=' + variable_name, self.name] - - if 'define_variable' in kwargs: - definition = kwargs.get('define_variable', []) - if not isinstance(definition, list): - raise DependencyException('define_variable takes a list') - - if len(definition) != 2 or not all(isinstance(i, str) for i in definition): - raise DependencyException('define_variable must be made up of 2 strings for VARIABLENAME and VARIABLEVALUE') - - options = ['--define-variable=' + '='.join(definition)] + options - - ret, out, err = self._call_pkgbin(options) - variable = '' - if ret != 0: - if self.required: - raise DependencyException('dependency %s not found:\n%s\n' % - (self.name, err)) - else: - variable = out.strip() - - # pkg-config doesn't distinguish between empty and non-existent variables - # use the variable list to check for variable existence - if not variable: - ret, out, _ = self._call_pkgbin(['--print-variables', self.name]) - if not re.search(r'^' + variable_name + r'$', out, re.MULTILINE): - if 'default' in kwargs: - variable = kwargs['default'] - else: - mlog.warning(f"pkgconfig variable '{variable_name}' not defined for dependency {self.name}.") - - mlog.debug(f'Got pkgconfig variable {variable_name} : {variable}') - return variable - - @staticmethod - def get_methods(): - return [DependencyMethods.PKGCONFIG] - - def check_pkgconfig(self, pkgbin): - if not pkgbin.found(): - mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}') - return None - try: - p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2] - if p.returncode != 0: - mlog.warning('Found pkg-config {!r} but it failed when run' - ''.format(' '.join(pkgbin.get_command()))) - return None - except FileNotFoundError: - mlog.warning('We thought we found pkg-config {!r} but now it\'s not there. How odd!' - ''.format(' '.join(pkgbin.get_command()))) - return None - except PermissionError: - msg = 'Found pkg-config {!r} but didn\'t have permissions to run it.'.format(' '.join(pkgbin.get_command())) - if not self.env.machines.build.is_windows(): - msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' - mlog.warning(msg) - return None - return out.strip() - - 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 self.env.machines[self.for_machine].is_darwin(): - 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) - - def log_tried(self): - return self.type_name - - def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, - configtool: T.Optional[str] = None, internal: T.Optional[str] = None, - default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: - if pkgconfig: - kwargs = {} - if default_value is not None: - kwargs['default'] = default_value - if pkgconfig_define is not None: - kwargs['define_variable'] = pkgconfig_define - try: - return self.get_pkgconfig_variable(pkgconfig, kwargs) - except DependencyException: - pass - if default_value is not None: - return default_value - raise DependencyException(f'Could not get pkg-config variable and no default provided for {self!r}') - -class CMakeDependency(ExternalDependency): - # The class's copy of the CMake path. Avoids having to search for it - # multiple times in the same Meson invocation. - class_cmakeinfo = PerMachine(None, None) - # Version string for the minimum CMake version - class_cmake_version = '>=3.4' - # CMake generators to try (empty for no generator) - class_cmake_generators = ['', 'Ninja', 'Unix Makefiles', 'Visual Studio 10 2010'] - class_working_generator = None - - def _gen_exception(self, msg): - return DependencyException(f'Dependency {self.name} not found: {msg}') - - def _main_cmake_file(self) -> str: - return 'CMakeLists.txt' - - def _extra_cmake_opts(self) -> T.List[str]: - return [] - - def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: - # Map the input module list to something else - # This function will only be executed AFTER the initial CMake - # interpreter pass has completed. Thus variables defined in the - # CMakeLists.txt can be accessed here. - # - # Both the modules and components inputs contain the original lists. - return modules - - def _map_component_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: - # Map the input components list to something else. This - # function will be executed BEFORE the initial CMake interpreter - # pass. Thus variables from the CMakeLists.txt can NOT be accessed. - # - # Both the modules and components inputs contain the original lists. - return components - - def _original_module_name(self, module: str) -> str: - # Reverse the module mapping done by _map_module_list for - # one module - return module - - def __init__(self, name: str, environment: Environment, kwargs, language: T.Optional[str] = None): - # Gather a list of all languages to support - self.language_list = [] # type: T.List[str] - if language is None: - compilers = None - if kwargs.get('native', False): - compilers = environment.coredata.compilers.build - else: - compilers = environment.coredata.compilers.host - - candidates = ['c', 'cpp', 'fortran', 'objc', 'objcxx'] - self.language_list += [x for x in candidates if x in compilers] - else: - self.language_list += [language] - - # Add additional languages if required - if 'fortran' in self.language_list: - self.language_list += ['c'] - - # Ensure that the list is unique - self.language_list = list(set(self.language_list)) - - super().__init__('cmake', environment, kwargs, language=language) - self.name = name - self.is_libtool = False - # Store a copy of the CMake path on the object itself so it is - # stored in the pickled coredata and recovered. - self.cmakebin = None - self.cmakeinfo = None - - # Where all CMake "build dirs" are located - self.cmake_root_dir = environment.scratch_dir - - # T.List of successfully found modules - self.found_modules = [] - - # Initialize with None before the first return to avoid - # AttributeError exceptions in derived classes - self.traceparser = None # type: CMakeTraceParser - - # TODO further evaluate always using MachineChoice.BUILD - self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent) - if not self.cmakebin.found(): - self.cmakebin = None - msg = f'CMake binary for machine {self.for_machine} not found. Giving up.' - if self.required: - raise DependencyException(msg) - mlog.debug(msg) - return - - # Setup the trace parser - self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) - - cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) - cm_args = check_cmake_args(cm_args) - if CMakeDependency.class_cmakeinfo[self.for_machine] is None: - CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info(cm_args) - self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine] - if self.cmakeinfo is None: - raise self._gen_exception('Unable to obtain CMake system information') - - package_version = kwargs.get('cmake_package_version', '') - if not isinstance(package_version, str): - raise DependencyException('Keyword "cmake_package_version" must be a string.') - components = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'components'))] - modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] - modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] - cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) - cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] - if cm_path: - cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) - if not self._preliminary_find_check(name, cm_path, self.cmakebin.get_cmake_prefix_paths(), environment.machines[self.for_machine]): - mlog.debug('Preliminary CMake check failed. Aborting.') - return - self._detect_dep(name, package_version, modules, components, cm_args) - - def __repr__(self): - s = '<{0} {1}: {2} {3}>' - return s.format(self.__class__.__name__, self.name, self.is_found, - self.version_reqs) - - def _get_cmake_info(self, cm_args): - mlog.debug("Extracting basic cmake information") - res = {} - - # Try different CMake generators since specifying no generator may fail - # in cygwin for some reason - gen_list = [] - # First try the last working generator - if CMakeDependency.class_working_generator is not None: - gen_list += [CMakeDependency.class_working_generator] - gen_list += CMakeDependency.class_cmake_generators - - temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) - toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) - toolchain.write() - - for i in gen_list: - mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) - - # Prepare options - cmake_opts = temp_parser.trace_args() + toolchain.get_cmake_args() + ['.'] - cmake_opts += cm_args - if len(i) > 0: - cmake_opts = ['-G', i] + cmake_opts - - # Run CMake - ret1, out1, err1 = self._call_cmake(cmake_opts, 'CMakePathInfo.txt') - - # Current generator was successful - if ret1 == 0: - CMakeDependency.class_working_generator = i - break - - mlog.debug(f'CMake failed to gather system information for generator {i} with error code {ret1}') - mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') - - # Check if any generator succeeded - if ret1 != 0: - return None - - try: - temp_parser.parse(err1) - except MesonException: - return None - - def process_paths(l: T.List[str]) -> T.Set[str]: - if mesonlib.is_windows(): - # Cannot split on ':' on Windows because its in the drive letter - l = [x.split(os.pathsep) for x in l] - else: - # https://github.com/mesonbuild/meson/issues/7294 - l = [re.split(r':|;', x) for x in l] - l = [x for sublist in l for x in sublist] - return set(l) - - # Extract the variables and sanity check them - root_paths = process_paths(temp_parser.get_cmake_var('MESON_FIND_ROOT_PATH')) - root_paths.update(process_paths(temp_parser.get_cmake_var('MESON_CMAKE_SYSROOT'))) - root_paths = sorted(root_paths) - root_paths = list(filter(lambda x: os.path.isdir(x), root_paths)) - module_paths = process_paths(temp_parser.get_cmake_var('MESON_PATHS_LIST')) - rooted_paths = [] - for j in [Path(x) for x in root_paths]: - for i in [Path(x) for x in module_paths]: - rooted_paths.append(str(j / i.relative_to(i.anchor))) - module_paths = sorted(module_paths.union(rooted_paths)) - module_paths = list(filter(lambda x: os.path.isdir(x), module_paths)) - archs = temp_parser.get_cmake_var('MESON_ARCH_LIST') - - common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share'] - for i in archs: - common_paths += [os.path.join('lib', i)] - - res = { - 'module_paths': module_paths, - 'cmake_root': temp_parser.get_cmake_var('MESON_CMAKE_ROOT')[0], - 'archs': archs, - 'common_paths': common_paths - } - - mlog.debug(' -- Module search paths: {}'.format(res['module_paths'])) - mlog.debug(' -- CMake root: {}'.format(res['cmake_root'])) - mlog.debug(' -- CMake architectures: {}'.format(res['archs'])) - mlog.debug(' -- CMake lib search paths: {}'.format(res['common_paths'])) - - return res - - @staticmethod - @functools.lru_cache(maxsize=None) - def _cached_listdir(path: str) -> T.Tuple[T.Tuple[str, str]]: - try: - return tuple((x, str(x).lower()) for x in os.listdir(path)) - except OSError: - return () - - @staticmethod - @functools.lru_cache(maxsize=None) - def _cached_isdir(path: str) -> bool: - try: - return os.path.isdir(path) - except OSError: - return False - - def _preliminary_find_check(self, name: str, module_path: T.List[str], prefix_path: T.List[str], machine: MachineInfo) -> bool: - lname = str(name).lower() - - # Checks , /cmake, /CMake - def find_module(path: str) -> bool: - for i in [path, os.path.join(path, 'cmake'), os.path.join(path, 'CMake')]: - if not self._cached_isdir(i): - continue - - # Check the directory case insensitive - content = self._cached_listdir(i) - candidates = ['Find{}.cmake', '{}Config.cmake', '{}-config.cmake'] - candidates = [x.format(name).lower() for x in candidates] - if any([x[1] in candidates for x in content]): - return True - return False - - # Search in /(lib/|lib*|share) for cmake files - def search_lib_dirs(path: str) -> bool: - for i in [os.path.join(path, x) for x in self.cmakeinfo['common_paths']]: - if not self._cached_isdir(i): - continue - - # Check /(lib/|lib*|share)/cmake/*/ - cm_dir = os.path.join(i, 'cmake') - if self._cached_isdir(cm_dir): - content = self._cached_listdir(cm_dir) - content = list(filter(lambda x: x[1].startswith(lname), content)) - for k in content: - if find_module(os.path.join(cm_dir, k[0])): - return True - - # /(lib/|lib*|share)/*/ - # /(lib/|lib*|share)/*/(cmake|CMake)/ - content = self._cached_listdir(i) - content = list(filter(lambda x: x[1].startswith(lname), content)) - for k in content: - if find_module(os.path.join(i, k[0])): - return True - - return False - - # Check the user provided and system module paths - for i in module_path + [os.path.join(self.cmakeinfo['cmake_root'], 'Modules')]: - if find_module(i): - return True - - # Check the user provided prefix paths - for i in prefix_path: - if search_lib_dirs(i): - return True - - # Check PATH - system_env = [] # type: T.List[str] - for i in os.environ.get('PATH', '').split(os.pathsep): - if i.endswith('/bin') or i.endswith('\\bin'): - i = i[:-4] - if i.endswith('/sbin') or i.endswith('\\sbin'): - i = i[:-5] - system_env += [i] - - # Check the system paths - for i in self.cmakeinfo['module_paths'] + system_env: - if find_module(i): - return True - - if search_lib_dirs(i): - return True - - content = self._cached_listdir(i) - content = list(filter(lambda x: x[1].startswith(lname), content)) - for k in content: - if search_lib_dirs(os.path.join(i, k[0])): - return True - - # Mac framework support - if machine.is_darwin(): - for j in ['{}.framework', '{}.app']: - j = j.format(lname) - if j in content: - if find_module(os.path.join(i, j[0], 'Resources')) or find_module(os.path.join(i, j[0], 'Version')): - return True - - # Check the environment path - env_path = os.environ.get(f'{name}_DIR') - if env_path and find_module(env_path): - return True - - return False - - def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]], args: T.List[str]): - # Detect a dependency with CMake using the '--find-package' mode - # and the trace output (stderr) - # - # When the trace output is enabled CMake prints all functions with - # parameters to stderr as they are executed. Since CMake 3.4.0 - # variables ("${VAR}") are also replaced in the trace output. - mlog.debug('\nDetermining dependency {!r} with CMake executable ' - '{!r}'.format(name, self.cmakebin.executable_path())) - - # Try different CMake generators since specifying no generator may fail - # in cygwin for some reason - gen_list = [] - # First try the last working generator - if CMakeDependency.class_working_generator is not None: - gen_list += [CMakeDependency.class_working_generator] - gen_list += CMakeDependency.class_cmake_generators - - # Map the components - comp_mapped = self._map_component_list(modules, components) - toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) - toolchain.write() - - for i in gen_list: - mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) - - # Prepare options - cmake_opts = [] - cmake_opts += [f'-DNAME={name}'] - cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo['archs']))] - cmake_opts += [f'-DVERSION={package_version}'] - cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] - cmake_opts += args - cmake_opts += self.traceparser.trace_args() - cmake_opts += toolchain.get_cmake_args() - cmake_opts += self._extra_cmake_opts() - cmake_opts += ['.'] - if len(i) > 0: - cmake_opts = ['-G', i] + cmake_opts - - # Run CMake - ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file()) - - # Current generator was successful - if ret1 == 0: - CMakeDependency.class_working_generator = i - break - - mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}') - mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') - - # Check if any generator succeeded - if ret1 != 0: - return - - try: - self.traceparser.parse(err1) - except CMakeException as e: - e = self._gen_exception(str(e)) - if self.required: - raise - else: - self.compile_args = [] - self.link_args = [] - self.is_found = False - self.reason = e - return - - # Whether the package is found or not is always stored in PACKAGE_FOUND - self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND') - if not self.is_found: - return - - # Try to detect the version - vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION') - - if len(vers_raw) > 0: - self.version = vers_raw[0] - self.version.strip('"\' ') - - # Post-process module list. Used in derived classes to modify the - # module list (append prepend a string, etc.). - modules = self._map_module_list(modules, components) - autodetected_module_list = False - - # Try guessing a CMake target if none is provided - if len(modules) == 0: - for i in self.traceparser.targets: - tg = i.lower() - lname = name.lower() - if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''): - mlog.debug(f'Guessed CMake target \'{i}\'') - modules = [(i, True)] - autodetected_module_list = True - break - - # Failed to guess a target --> try the old-style method - if len(modules) == 0: - incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x] - defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x] - libs = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] - - # Try to use old style variables if no module is specified - if len(libs) > 0: - self.compile_args = list(map(lambda x: f'-I{x}', incDirs)) + defs - self.link_args = libs - mlog.debug(f'using old-style CMake variables for dependency {name}') - mlog.debug(f'Include Dirs: {incDirs}') - mlog.debug(f'Compiler Definitions: {defs}') - mlog.debug(f'Libraries: {libs}') - return - - # Even the old-style approach failed. Nothing else we can do here - self.is_found = False - raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n' - 'Try to explicitly specify one or more targets with the "modules" property.\n' - 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys()))) - - # Set dependencies with CMake targets - # recognise arguments we should pass directly to the linker - reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-pthread|-delayload:[a-zA-Z0-9_\.]+|[a-zA-Z0-9_]+\.lib)$') - reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$') - processed_targets = [] - incDirs = [] - compileDefinitions = [] - compileOptions = [] - libraries = [] - for i, required in modules: - if i not in self.traceparser.targets: - if not required: - mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found') - continue - raise self._gen_exception('CMake: invalid module {} for {}.\n' - 'Try to explicitly specify one or more targets with the "modules" property.\n' - 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys()))) - - targets = [i] - if not autodetected_module_list: - self.found_modules += [i] - - while len(targets) > 0: - curr = targets.pop(0) - - # Skip already processed targets - if curr in processed_targets: - continue - - tgt = self.traceparser.targets[curr] - cfgs = [] - cfg = '' - otherDeps = [] - mlog.debug(tgt) - - if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties: - incDirs += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x] - - if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties: - compileDefinitions += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x] - - if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties: - compileOptions += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x] - - if 'IMPORTED_CONFIGURATIONS' in tgt.properties: - cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x] - cfg = cfgs[0] - - if OptionKey('b_vscrt') in self.env.coredata.options: - is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug' - if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: - is_debug = True - else: - is_debug = self.env.coredata.get_option(OptionKey('debug')) - if is_debug: - if 'DEBUG' in cfgs: - cfg = 'DEBUG' - elif 'RELEASE' in cfgs: - cfg = 'RELEASE' - else: - if 'RELEASE' in cfgs: - cfg = 'RELEASE' - - if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties: - libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x] - elif 'IMPORTED_IMPLIB' in tgt.properties: - libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x] - elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties: - libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x] - elif 'IMPORTED_LOCATION' in tgt.properties: - libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x] - - if 'INTERFACE_LINK_LIBRARIES' in tgt.properties: - otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x] - - if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties: - otherDeps += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x] - elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties: - otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x] - - for j in otherDeps: - if j in self.traceparser.targets: - targets += [j] - elif reg_is_lib.match(j): - libraries += [j] - elif os.path.isabs(j) and os.path.exists(j): - libraries += [j] - elif self.env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(j): - # On Windows, CMake library dependencies can be passed as bare library names, - # e.g. 'version' should translate into 'version.lib'. CMake brute-forces a - # combination of prefix/suffix combinations to find the right library, however - # as we do not have a compiler environment available to us, we cannot do the - # same, but must assume any bare argument passed which is not also a CMake - # target must be a system library we should try to link against - libraries += [f"{j}.lib"] - else: - mlog.warning('CMake: Dependency', mlog.bold(j), 'for', mlog.bold(name), 'target', mlog.bold(self._original_module_name(curr)), 'was not found') - - processed_targets += [curr] - - # Make sure all elements in the lists are unique and sorted - incDirs = sorted(set(incDirs)) - compileDefinitions = sorted(set(compileDefinitions)) - compileOptions = sorted(set(compileOptions)) - libraries = sorted(set(libraries)) - - mlog.debug(f'Include Dirs: {incDirs}') - mlog.debug(f'Compiler Definitions: {compileDefinitions}') - mlog.debug(f'Compiler Options: {compileOptions}') - mlog.debug(f'Libraries: {libraries}') - - self.compile_args = compileOptions + compileDefinitions + [f'-I{x}' for x in incDirs] - self.link_args = libraries - - def _get_build_dir(self) -> Path: - build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}' - build_dir.mkdir(parents=True, exist_ok=True) - return build_dir - - def _setup_cmake_dir(self, cmake_file: str) -> Path: - # Setup the CMake build environment and return the "build" directory - build_dir = self._get_build_dir() - - # Remove old CMake cache so we can try out multiple generators - cmake_cache = build_dir / 'CMakeCache.txt' - cmake_files = build_dir / 'CMakeFiles' - if cmake_cache.exists(): - cmake_cache.unlink() - shutil.rmtree(cmake_files.as_posix(), ignore_errors=True) - - # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt - cmake_txt = mesondata['dependencies/data/' + cmake_file].data - - # In general, some Fortran CMake find_package() also require C language enabled, - # even if nothing from C is directly used. An easy Fortran example that fails - # without C language is - # find_package(Threads) - # To make this general to - # any other language that might need this, we use a list for all - # languages and expand in the cmake Project(... LANGUAGES ...) statement. - from ..cmake import language_map - cmake_language = [language_map[x] for x in self.language_list if x in language_map] - if not cmake_language: - cmake_language += ['NONE'] - - cmake_txt = textwrap.dedent(""" - cmake_minimum_required(VERSION ${{CMAKE_VERSION}}) - project(MesonTemp LANGUAGES {}) - """).format(' '.join(cmake_language)) + cmake_txt - - cm_file = build_dir / 'CMakeLists.txt' - cm_file.write_text(cmake_txt) - mlog.cmd_ci_include(cm_file.absolute().as_posix()) - - return build_dir - - def _call_cmake(self, args, cmake_file: str, env=None): - build_dir = self._setup_cmake_dir(cmake_file) - return self.cmakebin.call(args, build_dir, env=env) - - @staticmethod - def get_methods(): - return [DependencyMethods.CMAKE] - - def log_tried(self): - return self.type_name - - def log_details(self) -> str: - modules = [self._original_module_name(x) for x in self.found_modules] - modules = sorted(set(modules)) - if modules: - return 'modules: ' + ', '.join(modules) - return '' - - def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, - configtool: T.Optional[str] = None, internal: T.Optional[str] = None, - default_value: T.Optional[str] = None, - pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: - if cmake and self.traceparser is not None: - try: - v = self.traceparser.vars[cmake] - except KeyError: - pass - else: - if len(v) == 1: - return v[0] - elif v: - return v - if default_value is not None: - return default_value - raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') - -class DubDependency(ExternalDependency): - class_dubbin = None - - def __init__(self, name, environment, kwargs): - super().__init__('dub', environment, kwargs, language='d') - self.name = name - self.compiler = super().get_compiler() - self.module_path = None - - if 'required' in kwargs: - self.required = kwargs.get('required') - - if DubDependency.class_dubbin is None: - self.dubbin = self._check_dub() - DubDependency.class_dubbin = self.dubbin - else: - self.dubbin = DubDependency.class_dubbin - - if not self.dubbin: - if self.required: - raise DependencyException('DUB not found.') - self.is_found = False - return - - mlog.debug('Determining dependency {!r} with DUB executable ' - '{!r}'.format(name, self.dubbin.get_path())) - - # we need to know the target architecture - arch = self.compiler.arch - - # Ask dub for the package - ret, res = self._call_dubbin(['describe', name, '--arch=' + arch]) - - if ret != 0: - self.is_found = False - return - - comp = self.compiler.get_id().replace('llvm', 'ldc').replace('gcc', 'gdc') - packages = [] - description = json.loads(res) - for package in description['packages']: - packages.append(package['name']) - if package['name'] == name: - self.is_found = True - - not_lib = True - if 'targetType' in package: - if package['targetType'] in ['library', 'sourceLibrary', 'staticLibrary', 'dynamicLibrary']: - not_lib = False - - if not_lib: - mlog.error(mlog.bold(name), "found but it isn't a library") - self.is_found = False - return - - self.module_path = self._find_right_lib_path(package['path'], comp, description, True, package['targetFileName']) - if not os.path.exists(self.module_path): - # check if the dependency was built for other archs - archs = [['x86_64'], ['x86'], ['x86', 'x86_mscoff']] - for a in archs: - description_a = copy.deepcopy(description) - description_a['architecture'] = a - arch_module_path = self._find_right_lib_path(package['path'], comp, description_a, True, package['targetFileName']) - if arch_module_path: - mlog.error(mlog.bold(name), "found but it wasn't compiled for", mlog.bold(arch)) - self.is_found = False - return - - mlog.error(mlog.bold(name), "found but it wasn't compiled with", mlog.bold(comp)) - self.is_found = False - return - - self.version = package['version'] - self.pkg = package - - if self.pkg['targetFileName'].endswith('.a'): - self.static = True - - self.compile_args = [] - for flag in self.pkg['dflags']: - self.link_args.append(flag) - for path in self.pkg['importPaths']: - self.compile_args.append('-I' + os.path.join(self.pkg['path'], path)) - - self.link_args = self.raw_link_args = [] - for flag in self.pkg['lflags']: - self.link_args.append(flag) - - self.link_args.append(os.path.join(self.module_path, self.pkg['targetFileName'])) - - # Handle dependencies - libs = [] - - def add_lib_args(field_name, target): - if field_name in target['buildSettings']: - for lib in target['buildSettings'][field_name]: - if lib not in libs: - libs.append(lib) - if os.name != 'nt': - pkgdep = PkgConfigDependency(lib, environment, {'required': 'true', 'silent': 'true'}) - for arg in pkgdep.get_compile_args(): - self.compile_args.append(arg) - for arg in pkgdep.get_link_args(): - self.link_args.append(arg) - for arg in pkgdep.get_link_args(raw=True): - self.raw_link_args.append(arg) - - for target in description['targets']: - if target['rootPackage'] in packages: - add_lib_args('libs', target) - add_lib_args(f'libs-{platform.machine()}', target) - for file in target['buildSettings']['linkerFiles']: - lib_path = self._find_right_lib_path(file, comp, description) - if lib_path: - self.link_args.append(lib_path) - else: - self.is_found = False - - def get_compiler(self): - return self.compiler - - def _find_right_lib_path(self, default_path, comp, description, folder_only=False, file_name=''): - module_path = lib_file_name = '' - if folder_only: - module_path = default_path - lib_file_name = file_name - else: - module_path = os.path.dirname(default_path) - lib_file_name = os.path.basename(default_path) - module_build_path = os.path.join(module_path, '.dub', 'build') - - # If default_path is a path to lib file and - # directory of lib don't have subdir '.dub/build' - if not os.path.isdir(module_build_path) and os.path.isfile(default_path): - if folder_only: - return module_path - else: - return default_path - - # Get D version implemented in the compiler - # gdc doesn't support this - ret, res = self._call_dubbin(['--version']) - - if ret != 0: - mlog.error('Failed to run {!r}', mlog.bold(comp)) - return - - d_ver = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2 - if d_ver is not None: - d_ver = d_ver.group().rsplit('.', 1)[0].replace('v', '').replace('.', '') # Fix structure. Ex.: 2081 - else: - d_ver = '' # gdc - - if not os.path.isdir(module_build_path): - return '' - - # Ex.: library-debug-linux.posix-x86_64-ldc_2081-EF934983A3319F8F8FF2F0E107A363BA - build_name = '-{}-{}-{}-{}_{}'.format(description['buildType'], '.'.join(description['platform']), '.'.join(description['architecture']), comp, d_ver) - for entry in os.listdir(module_build_path): - if build_name in entry: - for file in os.listdir(os.path.join(module_build_path, entry)): - if file == lib_file_name: - if folder_only: - return os.path.join(module_build_path, entry) - else: - return os.path.join(module_build_path, entry, lib_file_name) - - return '' - - def _call_dubbin(self, args, env=None): - p, out = Popen_safe(self.dubbin.get_command() + args, env=env)[0:2] - return p.returncode, out.strip() - - def _call_copmbin(self, args, env=None): - p, out = Popen_safe(self.compiler.get_exelist() + args, env=env)[0:2] - return p.returncode, out.strip() - - def _check_dub(self): - dubbin = ExternalProgram('dub', silent=True) - if dubbin.found(): - try: - p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2] - if p.returncode != 0: - mlog.warning('Found dub {!r} but couldn\'t run it' - ''.format(' '.join(dubbin.get_command()))) - # Set to False instead of None to signify that we've already - # searched for it and not found it - dubbin = False - except (FileNotFoundError, PermissionError): - dubbin = False - else: - dubbin = False - if dubbin: - mlog.log('Found DUB:', mlog.bold(dubbin.get_path()), - '(%s)' % out.strip()) - else: - mlog.log('Found DUB:', mlog.red('NO')) - return dubbin - - @staticmethod - def get_methods(): - return [DependencyMethods.DUB] - class ExternalLibrary(ExternalDependency): def __init__(self, name, link_args, environment, language, silent=False): super().__init__('library', environment, {}, language=language) @@ -1880,363 +445,6 @@ class ExternalLibrary(ExternalDependency): return new -class ExtraFrameworkDependency(ExternalDependency): - system_framework_paths = None - - def __init__(self, name, env, kwargs, language: T.Optional[str] = None): - paths = kwargs.get('paths', []) - super().__init__('extraframeworks', env, kwargs, language=language) - self.name = name - # Full path to framework directory - self.framework_path = None - if not self.clib_compiler: - raise DependencyException('No C-like compilers are available') - if self.system_framework_paths is None: - try: - self.system_framework_paths = self.clib_compiler.find_framework_paths(self.env) - except MesonException as e: - if 'non-clang' in str(e): - # Apple frameworks can only be found (and used) with the - # system compiler. It is not available so bail immediately. - self.is_found = False - return - raise - self.detect(name, paths) - - def detect(self, name, paths): - if not paths: - paths = self.system_framework_paths - for p in paths: - mlog.debug(f'Looking for framework {name} in {p}') - # We need to know the exact framework path because it's used by the - # Qt5 dependency class, and for setting the include path. We also - # want to avoid searching in an invalid framework path which wastes - # time and can cause a false positive. - framework_path = self._get_framework_path(p, name) - if framework_path is None: - continue - # We want to prefer the specified paths (in order) over the system - # paths since these are "extra" frameworks. - # For example, Python2's framework is in /System/Library/Frameworks and - # Python3's framework is in /Library/Frameworks, but both are called - # Python.framework. We need to know for sure that the framework was - # found in the path we expect. - allow_system = p in self.system_framework_paths - args = self.clib_compiler.find_framework(name, self.env, [p], allow_system) - if args is None: - continue - self.link_args = args - self.framework_path = framework_path.as_posix() - self.compile_args = ['-F' + self.framework_path] - # We need to also add -I includes to the framework because all - # cross-platform projects such as OpenGL, Python, Qt, GStreamer, - # etc do not use "framework includes": - # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html - incdir = self._get_framework_include_path(framework_path) - if incdir: - self.compile_args += ['-I' + incdir] - self.is_found = True - return - - def _get_framework_path(self, path, name): - p = Path(path) - lname = name.lower() - for d in p.glob('*.framework/'): - if lname == d.name.rsplit('.', 1)[0].lower(): - return d - return None - - def _get_framework_latest_version(self, path): - versions = [] - for each in path.glob('Versions/*'): - # macOS filesystems are usually case-insensitive - if each.name.lower() == 'current': - continue - versions.append(Version(each.name)) - if len(versions) == 0: - # most system frameworks do not have a 'Versions' directory - return 'Headers' - return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s) - - def _get_framework_include_path(self, path): - # According to the spec, 'Headers' must always be a symlink to the - # Headers directory inside the currently-selected version of the - # framework, but sometimes frameworks are broken. Look in 'Versions' - # for the currently-selected version or pick the latest one. - trials = ('Headers', 'Versions/Current/Headers', - self._get_framework_latest_version(path)) - for each in trials: - trial = path / each - if trial.is_dir(): - return trial.as_posix() - return None - - @staticmethod - def get_methods(): - return [DependencyMethods.EXTRAFRAMEWORK] - - def log_info(self): - return self.framework_path - - def log_tried(self): - return 'framework' - - -class DependencyFactory: - - """Factory to get dependencies from multiple sources. - - This class provides an initializer that takes a set of names and classes - for various kinds of dependencies. When the initialized object is called - it returns a list of callables return Dependency objects to try in order. - - :name: The name of the dependency. This will be passed as the name - parameter of the each dependency unless it is overridden on a per - type basis. - :methods: An ordered list of DependencyMethods. This is the order - dependencies will be returned in unless they are removed by the - _process_method function - :*_name: This will overwrite the name passed to the coresponding class. - For example, if the name is 'zlib', but cmake calls the dependency - 'Z', then using `cmake_name='Z'` will pass the name as 'Z' to cmake. - :*_class: A *type* or callable that creates a class, and has the - signature of an ExternalDependency - :system_class: If you pass DependencyMethods.SYSTEM in methods, you must - set this argument. - """ - - def __init__(self, name: str, methods: T.List[DependencyMethods], *, - extra_kwargs: T.Optional[T.Dict[str, T.Any]] = None, - pkgconfig_name: T.Optional[str] = None, - pkgconfig_class: 'T.Type[PkgConfigDependency]' = PkgConfigDependency, - cmake_name: T.Optional[str] = None, - cmake_class: 'T.Type[CMakeDependency]' = CMakeDependency, - configtool_class: 'T.Optional[T.Type[ConfigToolDependency]]' = None, - framework_name: T.Optional[str] = None, - framework_class: 'T.Type[ExtraFrameworkDependency]' = ExtraFrameworkDependency, - system_class: 'T.Type[ExternalDependency]' = ExternalDependency): - - if DependencyMethods.CONFIG_TOOL in methods and not configtool_class: - raise DependencyException('A configtool must have a custom class') - - self.extra_kwargs = extra_kwargs or {} - self.methods = methods - self.classes = { - # Just attach the correct name right now, either the generic name - # or the method specific name. - DependencyMethods.EXTRAFRAMEWORK: functools.partial(framework_class, framework_name or name), - DependencyMethods.PKGCONFIG: functools.partial(pkgconfig_class, pkgconfig_name or name), - DependencyMethods.CMAKE: functools.partial(cmake_class, cmake_name or name), - DependencyMethods.SYSTEM: functools.partial(system_class, name), - DependencyMethods.CONFIG_TOOL: None, - } - if configtool_class is not None: - self.classes[DependencyMethods.CONFIG_TOOL] = functools.partial(configtool_class, name) - - @staticmethod - def _process_method(method: DependencyMethods, env: Environment, for_machine: MachineChoice) -> bool: - """Report whether a method is valid or not. - - If the method is valid, return true, otherwise return false. This is - used in a list comprehension to filter methods that are not possible. - - By default this only remove EXTRAFRAMEWORK dependencies for non-mac platforms. - """ - # Extra frameworks are only valid for macOS and other apple products - if (method is DependencyMethods.EXTRAFRAMEWORK and - not env.machines[for_machine].is_darwin()): - return False - return True - - def __call__(self, env: Environment, for_machine: MachineChoice, - kwargs: T.Dict[str, T.Any]) -> T.List['DependencyType']: - """Return a list of Dependencies with the arguments already attached.""" - methods = process_method_kw(self.methods, kwargs) - nwargs = self.extra_kwargs.copy() - nwargs.update(kwargs) - - return [functools.partial(self.classes[m], env, nwargs) for m in methods - if self._process_method(m, env, for_machine)] - - -def get_dep_identifier(name, kwargs) -> T.Tuple: - identifier = (name, ) - from ..interpreter import permitted_dependency_kwargs - assert len(permitted_dependency_kwargs) == 19, \ - 'Extra kwargs have been added to dependency(), please review if it makes sense to handle it here' - for key, value in kwargs.items(): - # 'version' is irrelevant for caching; the caller must check version matches - # 'native' is handled above with `for_machine` - # 'required' is irrelevant for caching; the caller handles it separately - # 'fallback' and 'allow_fallback' is not part of the cache because, - # once a dependency has been found through a fallback, it should - # be used for the rest of the Meson run. - # 'default_options' is only used in fallback case - # 'not_found_message' has no impact on the dependency lookup - # 'include_type' is handled after the dependency lookup - if key in ('version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options', - 'not_found_message', 'include_type'): - continue - # All keyword arguments are strings, ints, or lists (or lists of lists) - if isinstance(value, list): - value = frozenset(listify(value)) - identifier += (key, value) - return identifier - -display_name_map = { - 'boost': 'Boost', - 'cuda': 'CUDA', - 'dub': 'DUB', - 'gmock': 'GMock', - 'gtest': 'GTest', - 'hdf5': 'HDF5', - 'llvm': 'LLVM', - 'mpi': 'MPI', - 'netcdf': 'NetCDF', - 'openmp': 'OpenMP', - 'wxwidgets': 'WxWidgets', -} - -def find_external_dependency(name, env, kwargs): - assert(name) - required = kwargs.get('required', True) - if not isinstance(required, bool): - raise DependencyException('Keyword "required" must be a boolean.') - if not isinstance(kwargs.get('method', ''), str): - raise DependencyException('Keyword "method" must be a string.') - lname = name.lower() - if lname not in _packages_accept_language and 'language' in kwargs: - raise DependencyException(f'{name} dependency does not accept "language" keyword argument') - if not isinstance(kwargs.get('version', ''), (str, list)): - raise DependencyException('Keyword "Version" must be string or list.') - - # display the dependency name with correct casing - display_name = display_name_map.get(lname, lname) - - for_machine = MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST - - type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency' - - # build a list of dependency methods to try - candidates = _build_external_dependency_list(name, env, for_machine, kwargs) - - pkg_exc = [] - pkgdep = [] - details = '' - - for c in candidates: - # try this dependency method - try: - d = c() - d._check_version() - pkgdep.append(d) - except DependencyException as e: - pkg_exc.append(e) - mlog.debug(str(e)) - else: - pkg_exc.append(None) - details = d.log_details() - if details: - details = '(' + details + ') ' - if 'language' in kwargs: - details += 'for ' + d.language + ' ' - - # if the dependency was found - if d.found(): - - info = [] - if d.version: - info.append(mlog.normal_cyan(d.version)) - - log_info = d.log_info() - if log_info: - info.append('(' + log_info + ')') - - mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.green('YES'), *info) - - return d - - # otherwise, the dependency could not be found - tried_methods = [d.log_tried() for d in pkgdep if d.log_tried()] - if tried_methods: - tried = '{}'.format(mlog.format_list(tried_methods)) - else: - tried = '' - - mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.red('NO'), - f'(tried {tried})' if tried else '') - - if required: - # if an exception occurred with the first detection method, re-raise it - # (on the grounds that it came from the preferred dependency detection - # method) - if pkg_exc and pkg_exc[0]: - raise pkg_exc[0] - - # we have a list of failed ExternalDependency objects, so we can report - # the methods we tried to find the dependency - raise DependencyException('Dependency "%s" not found' % (name) + - (', tried %s' % (tried) if tried else '')) - - return NotFoundDependency(env) - - -def _build_external_dependency_list(name: str, env: Environment, for_machine: MachineChoice, - kwargs: T.Dict[str, T.Any]) -> T.List['DependencyType']: - # First check if the method is valid - if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]: - raise DependencyException('method {!r} is invalid'.format(kwargs['method'])) - - # Is there a specific dependency detector for this dependency? - lname = name.lower() - if lname in packages: - # Create the list of dependency object constructors using a factory - # class method, if one exists, otherwise the list just consists of the - # constructor - if isinstance(packages[lname], type) and issubclass(packages[lname], Dependency): - dep = [functools.partial(packages[lname], env, kwargs)] - else: - dep = packages[lname](env, for_machine, kwargs) - return dep - - candidates = [] - - # If it's explicitly requested, use the dub detection method (only) - if 'dub' == kwargs.get('method', ''): - candidates.append(functools.partial(DubDependency, name, env, kwargs)) - return candidates - - # If it's explicitly requested, use the pkgconfig detection method (only) - if 'pkg-config' == kwargs.get('method', ''): - candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) - return candidates - - # If it's explicitly requested, use the CMake detection method (only) - if 'cmake' == kwargs.get('method', ''): - candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) - return candidates - - # If it's explicitly requested, use the Extraframework detection method (only) - if 'extraframework' == kwargs.get('method', ''): - # On OSX, also try framework dependency detector - if env.machines[for_machine].is_darwin(): - candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) - return candidates - - # Otherwise, just use the pkgconfig and cmake dependency detector - if 'auto' == kwargs.get('method', 'auto'): - candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) - - # On OSX, also try framework dependency detector - if env.machines[for_machine].is_darwin(): - candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) - - # Only use CMake as a last resort, since it might not work 100% (see #6113) - candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) - - return candidates - - def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]: """Sort according to @@ -2260,7 +468,6 @@ def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]: return (max_index, reversed_max_length) return sorted(libpaths, key=key_func) - def strip_system_libdirs(environment, for_machine: MachineChoice, link_args): """Remove -L arguments. @@ -2271,7 +478,6 @@ def strip_system_libdirs(environment, for_machine: MachineChoice, link_args): exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)} return [l for l in link_args if l not in exclude] - def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs) -> T.List[DependencyMethods]: method = kwargs.get('method', 'auto') # type: T.Union[DependencyMethods, str] if isinstance(method, DependencyMethods): @@ -2305,31 +511,6 @@ def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs) -> T.List return methods - -if T.TYPE_CHECKING: - FactoryType = T.TypeVar('FactoryType', bound=T.Callable[..., T.List[T.Callable[[], 'Dependency']]]) - - -def factory_methods(methods: T.Set[DependencyMethods]) -> T.Callable[['FactoryType'], 'FactoryType']: - """Decorator for handling methods for dependency factory functions. - - This helps to make factory functions self documenting - >>> @factory_methods([DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE]) - >>> def factory(env: Environment, for_machine: MachineChoice, kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List[T.Callable[[], 'Dependency']]: - >>> pass - """ - - def inner(func: 'FactoryType') -> 'FactoryType': - - @functools.wraps(func) - def wrapped(env: Environment, for_machine: MachineChoice, kwargs: T.Dict[str, T.Any]) -> T.List[T.Callable[[], 'Dependency']]: - return func(env, for_machine, kwargs, process_method_kw(methods, kwargs)) - - return T.cast('FactoryType', wrapped) - - return inner - - def detect_compiler(name: str, env: Environment, for_machine: MachineChoice, language: T.Optional[str]) -> T.Optional['Compiler']: """Given a language and environment find the compiler used.""" diff --git a/mesonbuild/dependencies/boost.py b/mesonbuild/dependencies/boost.py index 2c735bc..c8bc594 100644 --- a/mesonbuild/dependencies/boost.py +++ b/mesonbuild/dependencies/boost.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import re import functools import typing as T @@ -22,7 +21,8 @@ from .. import mlog from .. import mesonlib from ..environment import Environment -from .base import DependencyException, ExternalDependency, PkgConfigDependency +from .base import DependencyException, ExternalDependency +from .pkgconfig import PkgConfigDependency from .misc import threads_factory if T.TYPE_CHECKING: diff --git a/mesonbuild/dependencies/cmake.py b/mesonbuild/dependencies/cmake.py new file mode 100644 index 0000000..3b332b9 --- /dev/null +++ b/mesonbuild/dependencies/cmake.py @@ -0,0 +1,655 @@ +# Copyright 2013-2021 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. + +from .base import ExternalDependency, DependencyException, DependencyMethods +from ..mesonlib import is_windows, MesonException, OptionKey, PerMachine, stringlistify, extract_as_list +from ..mesondata import mesondata +from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args +from .. import mlog +from pathlib import Path +import functools +import re +import os +import shutil +import textwrap +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + from ..mesonlib import MachineInfo + +class CMakeDependency(ExternalDependency): + # The class's copy of the CMake path. Avoids having to search for it + # multiple times in the same Meson invocation. + class_cmakeinfo = PerMachine(None, None) + # Version string for the minimum CMake version + class_cmake_version = '>=3.4' + # CMake generators to try (empty for no generator) + class_cmake_generators = ['', 'Ninja', 'Unix Makefiles', 'Visual Studio 10 2010'] + class_working_generator = None + + def _gen_exception(self, msg): + return DependencyException(f'Dependency {self.name} not found: {msg}') + + def _main_cmake_file(self) -> str: + return 'CMakeLists.txt' + + def _extra_cmake_opts(self) -> T.List[str]: + return [] + + def _map_module_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: + # Map the input module list to something else + # This function will only be executed AFTER the initial CMake + # interpreter pass has completed. Thus variables defined in the + # CMakeLists.txt can be accessed here. + # + # Both the modules and components inputs contain the original lists. + return modules + + def _map_component_list(self, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]]) -> T.List[T.Tuple[str, bool]]: + # Map the input components list to something else. This + # function will be executed BEFORE the initial CMake interpreter + # pass. Thus variables from the CMakeLists.txt can NOT be accessed. + # + # Both the modules and components inputs contain the original lists. + return components + + def _original_module_name(self, module: str) -> str: + # Reverse the module mapping done by _map_module_list for + # one module + return module + + def __init__(self, name: str, environment: 'Environment', kwargs, language: T.Optional[str] = None): + # Gather a list of all languages to support + self.language_list = [] # type: T.List[str] + if language is None: + compilers = None + if kwargs.get('native', False): + compilers = environment.coredata.compilers.build + else: + compilers = environment.coredata.compilers.host + + candidates = ['c', 'cpp', 'fortran', 'objc', 'objcxx'] + self.language_list += [x for x in candidates if x in compilers] + else: + self.language_list += [language] + + # Add additional languages if required + if 'fortran' in self.language_list: + self.language_list += ['c'] + + # Ensure that the list is unique + self.language_list = list(set(self.language_list)) + + super().__init__('cmake', environment, kwargs, language=language) + self.name = name + self.is_libtool = False + # Store a copy of the CMake path on the object itself so it is + # stored in the pickled coredata and recovered. + self.cmakebin = None + self.cmakeinfo = None + + # Where all CMake "build dirs" are located + self.cmake_root_dir = environment.scratch_dir + + # T.List of successfully found modules + self.found_modules = [] + + # Initialize with None before the first return to avoid + # AttributeError exceptions in derived classes + self.traceparser = None # type: CMakeTraceParser + + # TODO further evaluate always using MachineChoice.BUILD + self.cmakebin = CMakeExecutor(environment, CMakeDependency.class_cmake_version, self.for_machine, silent=self.silent) + if not self.cmakebin.found(): + self.cmakebin = None + msg = f'CMake binary for machine {self.for_machine} not found. Giving up.' + if self.required: + raise DependencyException(msg) + mlog.debug(msg) + return + + # Setup the trace parser + self.traceparser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) + + cm_args = stringlistify(extract_as_list(kwargs, 'cmake_args')) + cm_args = check_cmake_args(cm_args) + if CMakeDependency.class_cmakeinfo[self.for_machine] is None: + CMakeDependency.class_cmakeinfo[self.for_machine] = self._get_cmake_info(cm_args) + self.cmakeinfo = CMakeDependency.class_cmakeinfo[self.for_machine] + if self.cmakeinfo is None: + raise self._gen_exception('Unable to obtain CMake system information') + + package_version = kwargs.get('cmake_package_version', '') + if not isinstance(package_version, str): + raise DependencyException('Keyword "cmake_package_version" must be a string.') + components = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'components'))] + modules = [(x, True) for x in stringlistify(extract_as_list(kwargs, 'modules'))] + modules += [(x, False) for x in stringlistify(extract_as_list(kwargs, 'optional_modules'))] + cm_path = stringlistify(extract_as_list(kwargs, 'cmake_module_path')) + cm_path = [x if os.path.isabs(x) else os.path.join(environment.get_source_dir(), x) for x in cm_path] + if cm_path: + cm_args.append('-DCMAKE_MODULE_PATH=' + ';'.join(cm_path)) + if not self._preliminary_find_check(name, cm_path, self.cmakebin.get_cmake_prefix_paths(), environment.machines[self.for_machine]): + mlog.debug('Preliminary CMake check failed. Aborting.') + return + self._detect_dep(name, package_version, modules, components, cm_args) + + def __repr__(self): + s = '<{0} {1}: {2} {3}>' + return s.format(self.__class__.__name__, self.name, self.is_found, + self.version_reqs) + + def _get_cmake_info(self, cm_args): + mlog.debug("Extracting basic cmake information") + res = {} + + # Try different CMake generators since specifying no generator may fail + # in cygwin for some reason + gen_list = [] + # First try the last working generator + if CMakeDependency.class_working_generator is not None: + gen_list += [CMakeDependency.class_working_generator] + gen_list += CMakeDependency.class_cmake_generators + + temp_parser = CMakeTraceParser(self.cmakebin.version(), self._get_build_dir()) + toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) + toolchain.write() + + for i in gen_list: + mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) + + # Prepare options + cmake_opts = temp_parser.trace_args() + toolchain.get_cmake_args() + ['.'] + cmake_opts += cm_args + if len(i) > 0: + cmake_opts = ['-G', i] + cmake_opts + + # Run CMake + ret1, out1, err1 = self._call_cmake(cmake_opts, 'CMakePathInfo.txt') + + # Current generator was successful + if ret1 == 0: + CMakeDependency.class_working_generator = i + break + + mlog.debug(f'CMake failed to gather system information for generator {i} with error code {ret1}') + mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') + + # Check if any generator succeeded + if ret1 != 0: + return None + + try: + temp_parser.parse(err1) + except MesonException: + return None + + def process_paths(l: T.List[str]) -> T.Set[str]: + if is_windows(): + # Cannot split on ':' on Windows because its in the drive letter + l = [x.split(os.pathsep) for x in l] + else: + # https://github.com/mesonbuild/meson/issues/7294 + l = [re.split(r':|;', x) for x in l] + l = [x for sublist in l for x in sublist] + return set(l) + + # Extract the variables and sanity check them + root_paths = process_paths(temp_parser.get_cmake_var('MESON_FIND_ROOT_PATH')) + root_paths.update(process_paths(temp_parser.get_cmake_var('MESON_CMAKE_SYSROOT'))) + root_paths = sorted(root_paths) + root_paths = list(filter(lambda x: os.path.isdir(x), root_paths)) + module_paths = process_paths(temp_parser.get_cmake_var('MESON_PATHS_LIST')) + rooted_paths = [] + for j in [Path(x) for x in root_paths]: + for i in [Path(x) for x in module_paths]: + rooted_paths.append(str(j / i.relative_to(i.anchor))) + module_paths = sorted(module_paths.union(rooted_paths)) + module_paths = list(filter(lambda x: os.path.isdir(x), module_paths)) + archs = temp_parser.get_cmake_var('MESON_ARCH_LIST') + + common_paths = ['lib', 'lib32', 'lib64', 'libx32', 'share'] + for i in archs: + common_paths += [os.path.join('lib', i)] + + res = { + 'module_paths': module_paths, + 'cmake_root': temp_parser.get_cmake_var('MESON_CMAKE_ROOT')[0], + 'archs': archs, + 'common_paths': common_paths + } + + mlog.debug(' -- Module search paths: {}'.format(res['module_paths'])) + mlog.debug(' -- CMake root: {}'.format(res['cmake_root'])) + mlog.debug(' -- CMake architectures: {}'.format(res['archs'])) + mlog.debug(' -- CMake lib search paths: {}'.format(res['common_paths'])) + + return res + + @staticmethod + @functools.lru_cache(maxsize=None) + def _cached_listdir(path: str) -> T.Tuple[T.Tuple[str, str]]: + try: + return tuple((x, str(x).lower()) for x in os.listdir(path)) + except OSError: + return () + + @staticmethod + @functools.lru_cache(maxsize=None) + def _cached_isdir(path: str) -> bool: + try: + return os.path.isdir(path) + except OSError: + return False + + def _preliminary_find_check(self, name: str, module_path: T.List[str], prefix_path: T.List[str], machine: 'MachineInfo') -> bool: + lname = str(name).lower() + + # Checks , /cmake, /CMake + def find_module(path: str) -> bool: + for i in [path, os.path.join(path, 'cmake'), os.path.join(path, 'CMake')]: + if not self._cached_isdir(i): + continue + + # Check the directory case insensitive + content = self._cached_listdir(i) + candidates = ['Find{}.cmake', '{}Config.cmake', '{}-config.cmake'] + candidates = [x.format(name).lower() for x in candidates] + if any([x[1] in candidates for x in content]): + return True + return False + + # Search in /(lib/|lib*|share) for cmake files + def search_lib_dirs(path: str) -> bool: + for i in [os.path.join(path, x) for x in self.cmakeinfo['common_paths']]: + if not self._cached_isdir(i): + continue + + # Check /(lib/|lib*|share)/cmake/*/ + cm_dir = os.path.join(i, 'cmake') + if self._cached_isdir(cm_dir): + content = self._cached_listdir(cm_dir) + content = list(filter(lambda x: x[1].startswith(lname), content)) + for k in content: + if find_module(os.path.join(cm_dir, k[0])): + return True + + # /(lib/|lib*|share)/*/ + # /(lib/|lib*|share)/*/(cmake|CMake)/ + content = self._cached_listdir(i) + content = list(filter(lambda x: x[1].startswith(lname), content)) + for k in content: + if find_module(os.path.join(i, k[0])): + return True + + return False + + # Check the user provided and system module paths + for i in module_path + [os.path.join(self.cmakeinfo['cmake_root'], 'Modules')]: + if find_module(i): + return True + + # Check the user provided prefix paths + for i in prefix_path: + if search_lib_dirs(i): + return True + + # Check PATH + system_env = [] # type: T.List[str] + for i in os.environ.get('PATH', '').split(os.pathsep): + if i.endswith('/bin') or i.endswith('\\bin'): + i = i[:-4] + if i.endswith('/sbin') or i.endswith('\\sbin'): + i = i[:-5] + system_env += [i] + + # Check the system paths + for i in self.cmakeinfo['module_paths'] + system_env: + if find_module(i): + return True + + if search_lib_dirs(i): + return True + + content = self._cached_listdir(i) + content = list(filter(lambda x: x[1].startswith(lname), content)) + for k in content: + if search_lib_dirs(os.path.join(i, k[0])): + return True + + # Mac framework support + if machine.is_darwin(): + for j in ['{}.framework', '{}.app']: + j = j.format(lname) + if j in content: + if find_module(os.path.join(i, j[0], 'Resources')) or find_module(os.path.join(i, j[0], 'Version')): + return True + + # Check the environment path + env_path = os.environ.get(f'{name}_DIR') + if env_path and find_module(env_path): + return True + + return False + + def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[str, bool]], components: T.List[T.Tuple[str, bool]], args: T.List[str]): + # Detect a dependency with CMake using the '--find-package' mode + # and the trace output (stderr) + # + # When the trace output is enabled CMake prints all functions with + # parameters to stderr as they are executed. Since CMake 3.4.0 + # variables ("${VAR}") are also replaced in the trace output. + mlog.debug('\nDetermining dependency {!r} with CMake executable ' + '{!r}'.format(name, self.cmakebin.executable_path())) + + # Try different CMake generators since specifying no generator may fail + # in cygwin for some reason + gen_list = [] + # First try the last working generator + if CMakeDependency.class_working_generator is not None: + gen_list += [CMakeDependency.class_working_generator] + gen_list += CMakeDependency.class_cmake_generators + + # Map the components + comp_mapped = self._map_component_list(modules, components) + toolchain = CMakeToolchain(self.cmakebin, self.env, self.for_machine, CMakeExecScope.DEPENDENCY, self._get_build_dir()) + toolchain.write() + + for i in gen_list: + mlog.debug('Try CMake generator: {}'.format(i if len(i) > 0 else 'auto')) + + # Prepare options + cmake_opts = [] + cmake_opts += [f'-DNAME={name}'] + cmake_opts += ['-DARCHS={}'.format(';'.join(self.cmakeinfo['archs']))] + cmake_opts += [f'-DVERSION={package_version}'] + cmake_opts += ['-DCOMPS={}'.format(';'.join([x[0] for x in comp_mapped]))] + cmake_opts += args + cmake_opts += self.traceparser.trace_args() + cmake_opts += toolchain.get_cmake_args() + cmake_opts += self._extra_cmake_opts() + cmake_opts += ['.'] + if len(i) > 0: + cmake_opts = ['-G', i] + cmake_opts + + # Run CMake + ret1, out1, err1 = self._call_cmake(cmake_opts, self._main_cmake_file()) + + # Current generator was successful + if ret1 == 0: + CMakeDependency.class_working_generator = i + break + + mlog.debug(f'CMake failed for generator {i} and package {name} with error code {ret1}') + mlog.debug(f'OUT:\n{out1}\n\n\nERR:\n{err1}\n\n') + + # Check if any generator succeeded + if ret1 != 0: + return + + try: + self.traceparser.parse(err1) + except CMakeException as e: + e = self._gen_exception(str(e)) + if self.required: + raise + else: + self.compile_args = [] + self.link_args = [] + self.is_found = False + self.reason = e + return + + # Whether the package is found or not is always stored in PACKAGE_FOUND + self.is_found = self.traceparser.var_to_bool('PACKAGE_FOUND') + if not self.is_found: + return + + # Try to detect the version + vers_raw = self.traceparser.get_cmake_var('PACKAGE_VERSION') + + if len(vers_raw) > 0: + self.version = vers_raw[0] + self.version.strip('"\' ') + + # Post-process module list. Used in derived classes to modify the + # module list (append prepend a string, etc.). + modules = self._map_module_list(modules, components) + autodetected_module_list = False + + # Try guessing a CMake target if none is provided + if len(modules) == 0: + for i in self.traceparser.targets: + tg = i.lower() + lname = name.lower() + if f'{lname}::{lname}' == tg or lname == tg.replace('::', ''): + mlog.debug(f'Guessed CMake target \'{i}\'') + modules = [(i, True)] + autodetected_module_list = True + break + + # Failed to guess a target --> try the old-style method + if len(modules) == 0: + incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x] + defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x] + libs = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x] + + # Try to use old style variables if no module is specified + if len(libs) > 0: + self.compile_args = list(map(lambda x: f'-I{x}', incDirs)) + defs + self.link_args = libs + mlog.debug(f'using old-style CMake variables for dependency {name}') + mlog.debug(f'Include Dirs: {incDirs}') + mlog.debug(f'Compiler Definitions: {defs}') + mlog.debug(f'Libraries: {libs}') + return + + # Even the old-style approach failed. Nothing else we can do here + self.is_found = False + raise self._gen_exception('CMake: failed to guess a CMake target for {}.\n' + 'Try to explicitly specify one or more targets with the "modules" property.\n' + 'Valid targets are:\n{}'.format(name, list(self.traceparser.targets.keys()))) + + # Set dependencies with CMake targets + # recognise arguments we should pass directly to the linker + reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-pthread|-delayload:[a-zA-Z0-9_\.]+|[a-zA-Z0-9_]+\.lib)$') + reg_is_maybe_bare_lib = re.compile(r'^[a-zA-Z0-9_]+$') + processed_targets = [] + incDirs = [] + compileDefinitions = [] + compileOptions = [] + libraries = [] + for i, required in modules: + if i not in self.traceparser.targets: + if not required: + mlog.warning('CMake: T.Optional module', mlog.bold(self._original_module_name(i)), 'for', mlog.bold(name), 'was not found') + continue + raise self._gen_exception('CMake: invalid module {} for {}.\n' + 'Try to explicitly specify one or more targets with the "modules" property.\n' + 'Valid targets are:\n{}'.format(self._original_module_name(i), name, list(self.traceparser.targets.keys()))) + + targets = [i] + if not autodetected_module_list: + self.found_modules += [i] + + while len(targets) > 0: + curr = targets.pop(0) + + # Skip already processed targets + if curr in processed_targets: + continue + + tgt = self.traceparser.targets[curr] + cfgs = [] + cfg = '' + otherDeps = [] + mlog.debug(tgt) + + if 'INTERFACE_INCLUDE_DIRECTORIES' in tgt.properties: + incDirs += [x for x in tgt.properties['INTERFACE_INCLUDE_DIRECTORIES'] if x] + + if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties: + compileDefinitions += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x] + + if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties: + compileOptions += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x] + + if 'IMPORTED_CONFIGURATIONS' in tgt.properties: + cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x] + cfg = cfgs[0] + + if OptionKey('b_vscrt') in self.env.coredata.options: + is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug' + if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}: + is_debug = True + else: + is_debug = self.env.coredata.get_option(OptionKey('debug')) + if is_debug: + if 'DEBUG' in cfgs: + cfg = 'DEBUG' + elif 'RELEASE' in cfgs: + cfg = 'RELEASE' + else: + if 'RELEASE' in cfgs: + cfg = 'RELEASE' + + if f'IMPORTED_IMPLIB_{cfg}' in tgt.properties: + libraries += [x for x in tgt.properties[f'IMPORTED_IMPLIB_{cfg}'] if x] + elif 'IMPORTED_IMPLIB' in tgt.properties: + libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x] + elif f'IMPORTED_LOCATION_{cfg}' in tgt.properties: + libraries += [x for x in tgt.properties[f'IMPORTED_LOCATION_{cfg}'] if x] + elif 'IMPORTED_LOCATION' in tgt.properties: + libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x] + + if 'INTERFACE_LINK_LIBRARIES' in tgt.properties: + otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x] + + if f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}' in tgt.properties: + otherDeps += [x for x in tgt.properties[f'IMPORTED_LINK_DEPENDENT_LIBRARIES_{cfg}'] if x] + elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties: + otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x] + + for j in otherDeps: + if j in self.traceparser.targets: + targets += [j] + elif reg_is_lib.match(j): + libraries += [j] + elif os.path.isabs(j) and os.path.exists(j): + libraries += [j] + elif self.env.machines.build.is_windows() and reg_is_maybe_bare_lib.match(j): + # On Windows, CMake library dependencies can be passed as bare library names, + # e.g. 'version' should translate into 'version.lib'. CMake brute-forces a + # combination of prefix/suffix combinations to find the right library, however + # as we do not have a compiler environment available to us, we cannot do the + # same, but must assume any bare argument passed which is not also a CMake + # target must be a system library we should try to link against + libraries += [f"{j}.lib"] + else: + mlog.warning('CMake: Dependency', mlog.bold(j), 'for', mlog.bold(name), 'target', mlog.bold(self._original_module_name(curr)), 'was not found') + + processed_targets += [curr] + + # Make sure all elements in the lists are unique and sorted + incDirs = sorted(set(incDirs)) + compileDefinitions = sorted(set(compileDefinitions)) + compileOptions = sorted(set(compileOptions)) + libraries = sorted(set(libraries)) + + mlog.debug(f'Include Dirs: {incDirs}') + mlog.debug(f'Compiler Definitions: {compileDefinitions}') + mlog.debug(f'Compiler Options: {compileOptions}') + mlog.debug(f'Libraries: {libraries}') + + self.compile_args = compileOptions + compileDefinitions + [f'-I{x}' for x in incDirs] + self.link_args = libraries + + def _get_build_dir(self) -> Path: + build_dir = Path(self.cmake_root_dir) / f'cmake_{self.name}' + build_dir.mkdir(parents=True, exist_ok=True) + return build_dir + + def _setup_cmake_dir(self, cmake_file: str) -> Path: + # Setup the CMake build environment and return the "build" directory + build_dir = self._get_build_dir() + + # Remove old CMake cache so we can try out multiple generators + cmake_cache = build_dir / 'CMakeCache.txt' + cmake_files = build_dir / 'CMakeFiles' + if cmake_cache.exists(): + cmake_cache.unlink() + shutil.rmtree(cmake_files.as_posix(), ignore_errors=True) + + # Insert language parameters into the CMakeLists.txt and write new CMakeLists.txt + cmake_txt = mesondata['dependencies/data/' + cmake_file].data + + # In general, some Fortran CMake find_package() also require C language enabled, + # even if nothing from C is directly used. An easy Fortran example that fails + # without C language is + # find_package(Threads) + # To make this general to + # any other language that might need this, we use a list for all + # languages and expand in the cmake Project(... LANGUAGES ...) statement. + from ..cmake import language_map + cmake_language = [language_map[x] for x in self.language_list if x in language_map] + if not cmake_language: + cmake_language += ['NONE'] + + cmake_txt = textwrap.dedent(""" + cmake_minimum_required(VERSION ${{CMAKE_VERSION}}) + project(MesonTemp LANGUAGES {}) + """).format(' '.join(cmake_language)) + cmake_txt + + cm_file = build_dir / 'CMakeLists.txt' + cm_file.write_text(cmake_txt) + mlog.cmd_ci_include(cm_file.absolute().as_posix()) + + return build_dir + + def _call_cmake(self, args, cmake_file: str, env=None): + build_dir = self._setup_cmake_dir(cmake_file) + return self.cmakebin.call(args, build_dir, env=env) + + @staticmethod + def get_methods(): + return [DependencyMethods.CMAKE] + + def log_tried(self): + return self.type_name + + def log_details(self) -> str: + modules = [self._original_module_name(x) for x in self.found_modules] + modules = sorted(set(modules)) + if modules: + return 'modules: ' + ', '.join(modules) + return '' + + def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, + configtool: T.Optional[str] = None, internal: T.Optional[str] = None, + default_value: T.Optional[str] = None, + pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: + if cmake and self.traceparser is not None: + try: + v = self.traceparser.vars[cmake] + except KeyError: + pass + else: + if len(v) == 1: + return v[0] + elif v: + return v + if default_value is not None: + return default_value + raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') diff --git a/mesonbuild/dependencies/coarrays.py b/mesonbuild/dependencies/coarrays.py index 84c3412..98bb0ed 100644 --- a/mesonbuild/dependencies/coarrays.py +++ b/mesonbuild/dependencies/coarrays.py @@ -15,7 +15,10 @@ import functools import typing as T -from .base import CMakeDependency, DependencyMethods, ExternalDependency, PkgConfigDependency, detect_compiler, factory_methods +from .base import DependencyMethods, ExternalDependency, detect_compiler +from .cmake import CMakeDependency +from .pkgconfig import PkgConfigDependency +from .factory import factory_methods if T.TYPE_CHECKING: from . base import DependencyType diff --git a/mesonbuild/dependencies/configtool.py b/mesonbuild/dependencies/configtool.py new file mode 100644 index 0000000..d07b0c3 --- /dev/null +++ b/mesonbuild/dependencies/configtool.py @@ -0,0 +1,173 @@ +# Copyright 2013-2021 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. + +from .base import ExternalDependency, DependencyException, DependencyMethods +from ..mesonlib import listify, Popen_safe, split_args, version_compare, version_compare_many +from ..programs import find_external_program +from .. import mlog +import re +import typing as T + +class ConfigToolDependency(ExternalDependency): + + """Class representing dependencies found using a config tool. + + Takes the following extra keys in kwargs that it uses internally: + :tools List[str]: A list of tool names to use + :version_arg str: The argument to pass to the tool to get it's version + :returncode_value int: The value of the correct returncode + Because some tools are stupid and don't return 0 + """ + + tools = None + tool_name = None + version_arg = '--version' + __strip_version = re.compile(r'^[0-9][0-9.]+') + + def __init__(self, name, environment, kwargs, language: T.Optional[str] = None): + super().__init__('config-tool', environment, kwargs, language=language) + self.name = name + # You may want to overwrite the class version in some cases + self.tools = listify(kwargs.get('tools', self.tools)) + if not self.tool_name: + self.tool_name = self.tools[0] + if 'version_arg' in kwargs: + self.version_arg = kwargs['version_arg'] + + req_version = kwargs.get('version', None) + tool, version = self.find_config(req_version, kwargs.get('returncode_value', 0)) + self.config = tool + self.is_found = self.report_config(version, req_version) + if not self.is_found: + self.config = None + return + self.version = version + + def _sanitize_version(self, version): + """Remove any non-numeric, non-point version suffixes.""" + m = self.__strip_version.match(version) + if m: + # Ensure that there isn't a trailing '.', such as an input like + # `1.2.3.git-1234` + return m.group(0).rstrip('.') + return version + + def find_config(self, versions: T.Optional[T.List[str]] = None, returncode: int = 0) \ + -> T.Tuple[T.Optional[str], T.Optional[str]]: + """Helper method that searches for config tool binaries in PATH and + returns the one that best matches the given version requirements. + """ + if not isinstance(versions, list) and versions is not None: + versions = listify(versions) + best_match = (None, None) # type: T.Tuple[T.Optional[str], T.Optional[str]] + for potential_bin in find_external_program( + self.env, self.for_machine, self.tool_name, + self.tool_name, self.tools, allow_default_for_cross=False): + if not potential_bin.found(): + continue + tool = potential_bin.get_command() + try: + p, out = Popen_safe(tool + [self.version_arg])[:2] + except (FileNotFoundError, PermissionError): + continue + if p.returncode != returncode: + continue + + out = self._sanitize_version(out.strip()) + # Some tools, like pcap-config don't supply a version, but also + # don't fail with --version, in that case just assume that there is + # only one version and return it. + if not out: + return (tool, None) + if versions: + is_found = version_compare_many(out, versions)[0] + # This allows returning a found version without a config tool, + # which is useful to inform the user that you found version x, + # but y was required. + if not is_found: + tool = None + if best_match[1]: + if version_compare(out, '> {}'.format(best_match[1])): + best_match = (tool, out) + else: + best_match = (tool, out) + + return best_match + + def report_config(self, version, req_version): + """Helper method to print messages about the tool.""" + + found_msg = [mlog.bold(self.tool_name), 'found:'] + + if self.config is None: + found_msg.append(mlog.red('NO')) + if version is not None and req_version is not None: + found_msg.append(f'found {version!r} but need {req_version!r}') + elif req_version: + found_msg.append(f'need {req_version!r}') + else: + found_msg += [mlog.green('YES'), '({})'.format(' '.join(self.config)), version] + + mlog.log(*found_msg) + + return self.config is not None + + def get_config_value(self, args: T.List[str], stage: str) -> T.List[str]: + p, out, err = Popen_safe(self.config + args) + if p.returncode != 0: + if self.required: + raise DependencyException( + 'Could not generate {} for {}.\n{}'.format( + stage, self.name, err)) + return [] + return split_args(out) + + @staticmethod + def get_methods(): + return [DependencyMethods.AUTO, DependencyMethods.CONFIG_TOOL] + + def get_configtool_variable(self, variable_name): + p, out, _ = Popen_safe(self.config + [f'--{variable_name}']) + if p.returncode != 0: + if self.required: + raise DependencyException( + 'Could not get variable "{}" for dependency {}'.format( + variable_name, self.name)) + variable = out.strip() + mlog.debug(f'Got config-tool variable {variable_name} : {variable}') + return variable + + def log_tried(self): + return self.type_name + + def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, + configtool: T.Optional[str] = None, internal: T.Optional[str] = None, + default_value: T.Optional[str] = None, + pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: + if configtool: + # In the not required case '' (empty string) will be returned if the + # variable is not found. Since '' is a valid value to return we + # set required to True here to force and error, and use the + # finally clause to ensure it's restored. + restore = self.required + self.required = True + try: + return self.get_configtool_variable(configtool) + except DependencyException: + pass + finally: + self.required = restore + if default_value is not None: + return default_value + raise DependencyException(f'Could not get config-tool variable and no default provided for {self!r}') diff --git a/mesonbuild/dependencies/detect.py b/mesonbuild/dependencies/detect.py new file mode 100644 index 0000000..e70478e --- /dev/null +++ b/mesonbuild/dependencies/detect.py @@ -0,0 +1,209 @@ +# Copyright 2013-2021 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. + +from .base import Dependency, DependencyException, DependencyMethods, NotFoundDependency +from .cmake import CMakeDependency +from .dub import DubDependency +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency + +from ..mesonlib import listify, MachineChoice, PerMachine +from .. import mlog +import functools +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + from .factory import DependencyType + +# These must be defined in this file to avoid cyclical references. +packages = {} +_packages_accept_language = set() + +def get_dep_identifier(name, kwargs) -> T.Tuple: + identifier = (name, ) + from ..interpreter import permitted_dependency_kwargs + assert len(permitted_dependency_kwargs) == 19, \ + 'Extra kwargs have been added to dependency(), please review if it makes sense to handle it here' + for key, value in kwargs.items(): + # 'version' is irrelevant for caching; the caller must check version matches + # 'native' is handled above with `for_machine` + # 'required' is irrelevant for caching; the caller handles it separately + # 'fallback' and 'allow_fallback' is not part of the cache because, + # once a dependency has been found through a fallback, it should + # be used for the rest of the Meson run. + # 'default_options' is only used in fallback case + # 'not_found_message' has no impact on the dependency lookup + # 'include_type' is handled after the dependency lookup + if key in ('version', 'native', 'required', 'fallback', 'allow_fallback', 'default_options', + 'not_found_message', 'include_type'): + continue + # All keyword arguments are strings, ints, or lists (or lists of lists) + if isinstance(value, list): + value = frozenset(listify(value)) + identifier += (key, value) + return identifier + +display_name_map = { + 'boost': 'Boost', + 'cuda': 'CUDA', + 'dub': 'DUB', + 'gmock': 'GMock', + 'gtest': 'GTest', + 'hdf5': 'HDF5', + 'llvm': 'LLVM', + 'mpi': 'MPI', + 'netcdf': 'NetCDF', + 'openmp': 'OpenMP', + 'wxwidgets': 'WxWidgets', +} + +def find_external_dependency(name, env, kwargs): + assert(name) + required = kwargs.get('required', True) + if not isinstance(required, bool): + raise DependencyException('Keyword "required" must be a boolean.') + if not isinstance(kwargs.get('method', ''), str): + raise DependencyException('Keyword "method" must be a string.') + lname = name.lower() + if lname not in _packages_accept_language and 'language' in kwargs: + raise DependencyException(f'{name} dependency does not accept "language" keyword argument') + if not isinstance(kwargs.get('version', ''), (str, list)): + raise DependencyException('Keyword "Version" must be string or list.') + + # display the dependency name with correct casing + display_name = display_name_map.get(lname, lname) + + for_machine = MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST + + type_text = PerMachine('Build-time', 'Run-time')[for_machine] + ' dependency' + + # build a list of dependency methods to try + candidates = _build_external_dependency_list(name, env, for_machine, kwargs) + + pkg_exc = [] + pkgdep = [] + details = '' + + for c in candidates: + # try this dependency method + try: + d = c() + d._check_version() + pkgdep.append(d) + except DependencyException as e: + pkg_exc.append(e) + mlog.debug(str(e)) + else: + pkg_exc.append(None) + details = d.log_details() + if details: + details = '(' + details + ') ' + if 'language' in kwargs: + details += 'for ' + d.language + ' ' + + # if the dependency was found + if d.found(): + + info = [] + if d.version: + info.append(mlog.normal_cyan(d.version)) + + log_info = d.log_info() + if log_info: + info.append('(' + log_info + ')') + + mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.green('YES'), *info) + + return d + + # otherwise, the dependency could not be found + tried_methods = [d.log_tried() for d in pkgdep if d.log_tried()] + if tried_methods: + tried = '{}'.format(mlog.format_list(tried_methods)) + else: + tried = '' + + mlog.log(type_text, mlog.bold(display_name), details + 'found:', mlog.red('NO'), + f'(tried {tried})' if tried else '') + + if required: + # if an exception occurred with the first detection method, re-raise it + # (on the grounds that it came from the preferred dependency detection + # method) + if pkg_exc and pkg_exc[0]: + raise pkg_exc[0] + + # we have a list of failed ExternalDependency objects, so we can report + # the methods we tried to find the dependency + raise DependencyException('Dependency "%s" not found' % (name) + + (', tried %s' % (tried) if tried else '')) + + return NotFoundDependency(env) + + +def _build_external_dependency_list(name: str, env: 'Environment', for_machine: MachineChoice, + kwargs: T.Dict[str, T.Any]) -> T.List['DependencyType']: + # First check if the method is valid + if 'method' in kwargs and kwargs['method'] not in [e.value for e in DependencyMethods]: + raise DependencyException('method {!r} is invalid'.format(kwargs['method'])) + + # Is there a specific dependency detector for this dependency? + lname = name.lower() + if lname in packages: + # Create the list of dependency object constructors using a factory + # class method, if one exists, otherwise the list just consists of the + # constructor + if isinstance(packages[lname], type) and issubclass(packages[lname], Dependency): + dep = [functools.partial(packages[lname], env, kwargs)] + else: + dep = packages[lname](env, for_machine, kwargs) + return dep + + candidates = [] + + # If it's explicitly requested, use the dub detection method (only) + if 'dub' == kwargs.get('method', ''): + candidates.append(functools.partial(DubDependency, name, env, kwargs)) + return candidates + + # If it's explicitly requested, use the pkgconfig detection method (only) + if 'pkg-config' == kwargs.get('method', ''): + candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) + return candidates + + # If it's explicitly requested, use the CMake detection method (only) + if 'cmake' == kwargs.get('method', ''): + candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) + return candidates + + # If it's explicitly requested, use the Extraframework detection method (only) + if 'extraframework' == kwargs.get('method', ''): + # On OSX, also try framework dependency detector + if env.machines[for_machine].is_darwin(): + candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) + return candidates + + # Otherwise, just use the pkgconfig and cmake dependency detector + if 'auto' == kwargs.get('method', 'auto'): + candidates.append(functools.partial(PkgConfigDependency, name, env, kwargs)) + + # On OSX, also try framework dependency detector + if env.machines[for_machine].is_darwin(): + candidates.append(functools.partial(ExtraFrameworkDependency, name, env, kwargs)) + + # Only use CMake as a last resort, since it might not work 100% (see #6113) + candidates.append(functools.partial(CMakeDependency, name, env, kwargs)) + + return candidates diff --git a/mesonbuild/dependencies/dev.py b/mesonbuild/dependencies/dev.py index 07bf87a..345b24c 100644 --- a/mesonbuild/dependencies/dev.py +++ b/mesonbuild/dependencies/dev.py @@ -25,10 +25,11 @@ import typing as T from .. import mesonlib, mlog from ..mesonlib import version_compare, stringlistify, extract_as_list, MachineChoice from ..environment import get_llvm_tool_names -from .base import ( - DependencyException, DependencyMethods, ExternalDependency, PkgConfigDependency, - strip_system_libdirs, ConfigToolDependency, CMakeDependency, DependencyFactory, -) +from .base import DependencyException, DependencyMethods, ExternalDependency, strip_system_libdirs +from .cmake import CMakeDependency +from .configtool import ConfigToolDependency +from .pkgconfig import PkgConfigDependency +from .factory import DependencyFactory from .misc import threads_factory from ..compilers.c import AppleClangCCompiler from ..compilers.cpp import AppleClangCPPCompiler diff --git a/mesonbuild/dependencies/dub.py b/mesonbuild/dependencies/dub.py new file mode 100644 index 0000000..1b844fe --- /dev/null +++ b/mesonbuild/dependencies/dub.py @@ -0,0 +1,227 @@ +# Copyright 2013-2021 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. + +from .base import ExternalDependency, DependencyException, DependencyMethods +from .pkgconfig import PkgConfigDependency +from ..mesonlib import Popen_safe +from ..programs import ExternalProgram +from .. import mlog +import re +import os +import copy +import json +import platform + +class DubDependency(ExternalDependency): + class_dubbin = None + + def __init__(self, name, environment, kwargs): + super().__init__('dub', environment, kwargs, language='d') + self.name = name + self.compiler = super().get_compiler() + self.module_path = None + + if 'required' in kwargs: + self.required = kwargs.get('required') + + if DubDependency.class_dubbin is None: + self.dubbin = self._check_dub() + DubDependency.class_dubbin = self.dubbin + else: + self.dubbin = DubDependency.class_dubbin + + if not self.dubbin: + if self.required: + raise DependencyException('DUB not found.') + self.is_found = False + return + + mlog.debug('Determining dependency {!r} with DUB executable ' + '{!r}'.format(name, self.dubbin.get_path())) + + # we need to know the target architecture + arch = self.compiler.arch + + # Ask dub for the package + ret, res = self._call_dubbin(['describe', name, '--arch=' + arch]) + + if ret != 0: + self.is_found = False + return + + comp = self.compiler.get_id().replace('llvm', 'ldc').replace('gcc', 'gdc') + packages = [] + description = json.loads(res) + for package in description['packages']: + packages.append(package['name']) + if package['name'] == name: + self.is_found = True + + not_lib = True + if 'targetType' in package: + if package['targetType'] in ['library', 'sourceLibrary', 'staticLibrary', 'dynamicLibrary']: + not_lib = False + + if not_lib: + mlog.error(mlog.bold(name), "found but it isn't a library") + self.is_found = False + return + + self.module_path = self._find_right_lib_path(package['path'], comp, description, True, package['targetFileName']) + if not os.path.exists(self.module_path): + # check if the dependency was built for other archs + archs = [['x86_64'], ['x86'], ['x86', 'x86_mscoff']] + for a in archs: + description_a = copy.deepcopy(description) + description_a['architecture'] = a + arch_module_path = self._find_right_lib_path(package['path'], comp, description_a, True, package['targetFileName']) + if arch_module_path: + mlog.error(mlog.bold(name), "found but it wasn't compiled for", mlog.bold(arch)) + self.is_found = False + return + + mlog.error(mlog.bold(name), "found but it wasn't compiled with", mlog.bold(comp)) + self.is_found = False + return + + self.version = package['version'] + self.pkg = package + + if self.pkg['targetFileName'].endswith('.a'): + self.static = True + + self.compile_args = [] + for flag in self.pkg['dflags']: + self.link_args.append(flag) + for path in self.pkg['importPaths']: + self.compile_args.append('-I' + os.path.join(self.pkg['path'], path)) + + self.link_args = self.raw_link_args = [] + for flag in self.pkg['lflags']: + self.link_args.append(flag) + + self.link_args.append(os.path.join(self.module_path, self.pkg['targetFileName'])) + + # Handle dependencies + libs = [] + + def add_lib_args(field_name, target): + if field_name in target['buildSettings']: + for lib in target['buildSettings'][field_name]: + if lib not in libs: + libs.append(lib) + if os.name != 'nt': + pkgdep = PkgConfigDependency(lib, environment, {'required': 'true', 'silent': 'true'}) + for arg in pkgdep.get_compile_args(): + self.compile_args.append(arg) + for arg in pkgdep.get_link_args(): + self.link_args.append(arg) + for arg in pkgdep.get_link_args(raw=True): + self.raw_link_args.append(arg) + + for target in description['targets']: + if target['rootPackage'] in packages: + add_lib_args('libs', target) + add_lib_args(f'libs-{platform.machine()}', target) + for file in target['buildSettings']['linkerFiles']: + lib_path = self._find_right_lib_path(file, comp, description) + if lib_path: + self.link_args.append(lib_path) + else: + self.is_found = False + + def get_compiler(self): + return self.compiler + + def _find_right_lib_path(self, default_path, comp, description, folder_only=False, file_name=''): + module_path = lib_file_name = '' + if folder_only: + module_path = default_path + lib_file_name = file_name + else: + module_path = os.path.dirname(default_path) + lib_file_name = os.path.basename(default_path) + module_build_path = os.path.join(module_path, '.dub', 'build') + + # If default_path is a path to lib file and + # directory of lib don't have subdir '.dub/build' + if not os.path.isdir(module_build_path) and os.path.isfile(default_path): + if folder_only: + return module_path + else: + return default_path + + # Get D version implemented in the compiler + # gdc doesn't support this + ret, res = self._call_dubbin(['--version']) + + if ret != 0: + mlog.error('Failed to run {!r}', mlog.bold(comp)) + return + + d_ver = re.search('v[0-9].[0-9][0-9][0-9].[0-9]', res) # Ex.: v2.081.2 + if d_ver is not None: + d_ver = d_ver.group().rsplit('.', 1)[0].replace('v', '').replace('.', '') # Fix structure. Ex.: 2081 + else: + d_ver = '' # gdc + + if not os.path.isdir(module_build_path): + return '' + + # Ex.: library-debug-linux.posix-x86_64-ldc_2081-EF934983A3319F8F8FF2F0E107A363BA + build_name = '-{}-{}-{}-{}_{}'.format(description['buildType'], '.'.join(description['platform']), '.'.join(description['architecture']), comp, d_ver) + for entry in os.listdir(module_build_path): + if build_name in entry: + for file in os.listdir(os.path.join(module_build_path, entry)): + if file == lib_file_name: + if folder_only: + return os.path.join(module_build_path, entry) + else: + return os.path.join(module_build_path, entry, lib_file_name) + + return '' + + def _call_dubbin(self, args, env=None): + p, out = Popen_safe(self.dubbin.get_command() + args, env=env)[0:2] + return p.returncode, out.strip() + + def _call_copmbin(self, args, env=None): + p, out = Popen_safe(self.compiler.get_exelist() + args, env=env)[0:2] + return p.returncode, out.strip() + + def _check_dub(self): + dubbin = ExternalProgram('dub', silent=True) + if dubbin.found(): + try: + p, out = Popen_safe(dubbin.get_command() + ['--version'])[0:2] + if p.returncode != 0: + mlog.warning('Found dub {!r} but couldn\'t run it' + ''.format(' '.join(dubbin.get_command()))) + # Set to False instead of None to signify that we've already + # searched for it and not found it + dubbin = False + except (FileNotFoundError, PermissionError): + dubbin = False + else: + dubbin = False + if dubbin: + mlog.log('Found DUB:', mlog.bold(dubbin.get_path()), + '(%s)' % out.strip()) + else: + mlog.log('Found DUB:', mlog.red('NO')) + return dubbin + + @staticmethod + def get_methods(): + return [DependencyMethods.DUB] diff --git a/mesonbuild/dependencies/factory.py b/mesonbuild/dependencies/factory.py new file mode 100644 index 0000000..760dba2 --- /dev/null +++ b/mesonbuild/dependencies/factory.py @@ -0,0 +1,125 @@ +# Copyright 2013-2021 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. + +from .base import Dependency, ExternalDependency +from .base import DependencyException, DependencyMethods +from .base import process_method_kw +from .cmake import CMakeDependency +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency +from ..mesonlib import MachineChoice +import functools +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + from .base import DependencyType + from .configtool import ConfigToolDependency + FactoryType = T.TypeVar('FactoryType', bound=T.Callable[..., T.List[T.Callable[[], 'Dependency']]]) + +class DependencyFactory: + + """Factory to get dependencies from multiple sources. + + This class provides an initializer that takes a set of names and classes + for various kinds of dependencies. When the initialized object is called + it returns a list of callables return Dependency objects to try in order. + + :name: The name of the dependency. This will be passed as the name + parameter of the each dependency unless it is overridden on a per + type basis. + :methods: An ordered list of DependencyMethods. This is the order + dependencies will be returned in unless they are removed by the + _process_method function + :*_name: This will overwrite the name passed to the coresponding class. + For example, if the name is 'zlib', but cmake calls the dependency + 'Z', then using `cmake_name='Z'` will pass the name as 'Z' to cmake. + :*_class: A *type* or callable that creates a class, and has the + signature of an ExternalDependency + :system_class: If you pass DependencyMethods.SYSTEM in methods, you must + set this argument. + """ + + def __init__(self, name: str, methods: T.List[DependencyMethods], *, + extra_kwargs: T.Optional[T.Dict[str, T.Any]] = None, + pkgconfig_name: T.Optional[str] = None, + pkgconfig_class: 'T.Type[PkgConfigDependency]' = PkgConfigDependency, + cmake_name: T.Optional[str] = None, + cmake_class: 'T.Type[CMakeDependency]' = CMakeDependency, + configtool_class: 'T.Optional[T.Type[ConfigToolDependency]]' = None, + framework_name: T.Optional[str] = None, + framework_class: 'T.Type[ExtraFrameworkDependency]' = ExtraFrameworkDependency, + system_class: 'T.Type[ExternalDependency]' = ExternalDependency): + + if DependencyMethods.CONFIG_TOOL in methods and not configtool_class: + raise DependencyException('A configtool must have a custom class') + + self.extra_kwargs = extra_kwargs or {} + self.methods = methods + self.classes = { + # Just attach the correct name right now, either the generic name + # or the method specific name. + DependencyMethods.EXTRAFRAMEWORK: functools.partial(framework_class, framework_name or name), + DependencyMethods.PKGCONFIG: functools.partial(pkgconfig_class, pkgconfig_name or name), + DependencyMethods.CMAKE: functools.partial(cmake_class, cmake_name or name), + DependencyMethods.SYSTEM: functools.partial(system_class, name), + DependencyMethods.CONFIG_TOOL: None, + } + if configtool_class is not None: + self.classes[DependencyMethods.CONFIG_TOOL] = functools.partial(configtool_class, name) + + @staticmethod + def _process_method(method: DependencyMethods, env: 'Environment', for_machine: MachineChoice) -> bool: + """Report whether a method is valid or not. + + If the method is valid, return true, otherwise return false. This is + used in a list comprehension to filter methods that are not possible. + + By default this only remove EXTRAFRAMEWORK dependencies for non-mac platforms. + """ + # Extra frameworks are only valid for macOS and other apple products + if (method is DependencyMethods.EXTRAFRAMEWORK and + not env.machines[for_machine].is_darwin()): + return False + return True + + def __call__(self, env: 'Environment', for_machine: MachineChoice, + kwargs: T.Dict[str, T.Any]) -> T.List['DependencyType']: + """Return a list of Dependencies with the arguments already attached.""" + methods = process_method_kw(self.methods, kwargs) + nwargs = self.extra_kwargs.copy() + nwargs.update(kwargs) + + return [functools.partial(self.classes[m], env, nwargs) for m in methods + if self._process_method(m, env, for_machine)] + + +def factory_methods(methods: T.Set[DependencyMethods]) -> T.Callable[['FactoryType'], 'FactoryType']: + """Decorator for handling methods for dependency factory functions. + + This helps to make factory functions self documenting + >>> @factory_methods([DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE]) + >>> def factory(env: Environment, for_machine: MachineChoice, kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods]) -> T.List[T.Callable[[], 'Dependency']]: + >>> pass + """ + + def inner(func: 'FactoryType') -> 'FactoryType': + + @functools.wraps(func) + def wrapped(env: 'Environment', for_machine: MachineChoice, kwargs: T.Dict[str, T.Any]) -> T.List[T.Callable[[], 'Dependency']]: + return func(env, for_machine, kwargs, process_method_kw(methods, kwargs)) + + return T.cast('FactoryType', wrapped) + + return inner diff --git a/mesonbuild/dependencies/framework.py b/mesonbuild/dependencies/framework.py new file mode 100644 index 0000000..bdc2b5d --- /dev/null +++ b/mesonbuild/dependencies/framework.py @@ -0,0 +1,120 @@ +# Copyright 2013-2021 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. + +from .base import ExternalDependency, DependencyException, DependencyMethods +from ..mesonlib import MesonException, Version +from .. import mlog +from pathlib import Path +import typing as T + +class ExtraFrameworkDependency(ExternalDependency): + system_framework_paths = None + + def __init__(self, name, env, kwargs, language: T.Optional[str] = None): + paths = kwargs.get('paths', []) + super().__init__('extraframeworks', env, kwargs, language=language) + self.name = name + # Full path to framework directory + self.framework_path = None + if not self.clib_compiler: + raise DependencyException('No C-like compilers are available') + if self.system_framework_paths is None: + try: + self.system_framework_paths = self.clib_compiler.find_framework_paths(self.env) + except MesonException as e: + if 'non-clang' in str(e): + # Apple frameworks can only be found (and used) with the + # system compiler. It is not available so bail immediately. + self.is_found = False + return + raise + self.detect(name, paths) + + def detect(self, name, paths): + if not paths: + paths = self.system_framework_paths + for p in paths: + mlog.debug(f'Looking for framework {name} in {p}') + # We need to know the exact framework path because it's used by the + # Qt5 dependency class, and for setting the include path. We also + # want to avoid searching in an invalid framework path which wastes + # time and can cause a false positive. + framework_path = self._get_framework_path(p, name) + if framework_path is None: + continue + # We want to prefer the specified paths (in order) over the system + # paths since these are "extra" frameworks. + # For example, Python2's framework is in /System/Library/Frameworks and + # Python3's framework is in /Library/Frameworks, but both are called + # Python.framework. We need to know for sure that the framework was + # found in the path we expect. + allow_system = p in self.system_framework_paths + args = self.clib_compiler.find_framework(name, self.env, [p], allow_system) + if args is None: + continue + self.link_args = args + self.framework_path = framework_path.as_posix() + self.compile_args = ['-F' + self.framework_path] + # We need to also add -I includes to the framework because all + # cross-platform projects such as OpenGL, Python, Qt, GStreamer, + # etc do not use "framework includes": + # https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Tasks/IncludingFrameworks.html + incdir = self._get_framework_include_path(framework_path) + if incdir: + self.compile_args += ['-I' + incdir] + self.is_found = True + return + + def _get_framework_path(self, path, name): + p = Path(path) + lname = name.lower() + for d in p.glob('*.framework/'): + if lname == d.name.rsplit('.', 1)[0].lower(): + return d + return None + + def _get_framework_latest_version(self, path): + versions = [] + for each in path.glob('Versions/*'): + # macOS filesystems are usually case-insensitive + if each.name.lower() == 'current': + continue + versions.append(Version(each.name)) + if len(versions) == 0: + # most system frameworks do not have a 'Versions' directory + return 'Headers' + return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s) + + def _get_framework_include_path(self, path): + # According to the spec, 'Headers' must always be a symlink to the + # Headers directory inside the currently-selected version of the + # framework, but sometimes frameworks are broken. Look in 'Versions' + # for the currently-selected version or pick the latest one. + trials = ('Headers', 'Versions/Current/Headers', + self._get_framework_latest_version(path)) + for each in trials: + trial = path / each + if trial.is_dir(): + return trial.as_posix() + return None + + @staticmethod + def get_methods(): + return [DependencyMethods.EXTRAFRAMEWORK] + + def log_info(self): + return self.framework_path + + def log_tried(self): + return 'framework' diff --git a/mesonbuild/dependencies/hdf5.py b/mesonbuild/dependencies/hdf5.py index 59c7382..87cd8f4 100644 --- a/mesonbuild/dependencies/hdf5.py +++ b/mesonbuild/dependencies/hdf5.py @@ -22,10 +22,10 @@ import subprocess from pathlib import Path from ..mesonlib import OrderedSet, join_args -from .base import ( - DependencyException, DependencyMethods, ConfigToolDependency, - PkgConfigDependency, factory_methods -) +from .base import DependencyException, DependencyMethods +from .configtool import ConfigToolDependency +from .pkgconfig import PkgConfigDependency +from .factory import factory_methods import typing as T if T.TYPE_CHECKING: diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py index 5bf2f46..84cf04e 100644 --- a/mesonbuild/dependencies/misc.py +++ b/mesonbuild/dependencies/misc.py @@ -24,11 +24,11 @@ from .. import mlog from .. import mesonlib from ..environment import detect_cpu_family -from .base import ( - DependencyException, DependencyMethods, ExternalDependency, - PkgConfigDependency, CMakeDependency, ConfigToolDependency, - factory_methods, DependencyFactory, -) +from .base import DependencyException, DependencyMethods, ExternalDependency +from .cmake import CMakeDependency +from .configtool import ConfigToolDependency +from .pkgconfig import PkgConfigDependency +from .factory import DependencyFactory, factory_methods if T.TYPE_CHECKING: from ..environment import Environment, MachineChoice diff --git a/mesonbuild/dependencies/mpi.py b/mesonbuild/dependencies/mpi.py index d4b324c..242dc34 100644 --- a/mesonbuild/dependencies/mpi.py +++ b/mesonbuild/dependencies/mpi.py @@ -17,8 +17,10 @@ import typing as T import os import re -from .base import (DependencyMethods, PkgConfigDependency, factory_methods, - ConfigToolDependency, detect_compiler, ExternalDependency) +from .base import DependencyMethods, detect_compiler, ExternalDependency +from .configtool import ConfigToolDependency +from .pkgconfig import PkgConfigDependency +from .factory import factory_methods from ..environment import detect_cpu_family if T.TYPE_CHECKING: diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py new file mode 100644 index 0000000..de55d3e --- /dev/null +++ b/mesonbuild/dependencies/pkgconfig.py @@ -0,0 +1,472 @@ +# Copyright 2013-2021 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. + +from .base import ExternalDependency, DependencyException, DependencyMethods, sort_libpaths +from ..mesonlib import LibType, MachineChoice, OptionKey, OrderedSet, PerMachine, Popen_safe +from ..programs import find_external_program +from .. import mlog +from pathlib import PurePath +import re +import os +import shlex +import typing as T + +if T.TYPE_CHECKING: + from ..environment import Environment + +class PkgConfigDependency(ExternalDependency): + # The class's copy of the pkg-config path. Avoids having to search for it + # multiple times in the same Meson invocation. + class_pkgbin = PerMachine(None, None) + # We cache all pkg-config subprocess invocations to avoid redundant calls + pkgbin_cache = {} + + def __init__(self, name, environment: 'Environment', kwargs, language: T.Optional[str] = None): + super().__init__('pkgconfig', environment, kwargs, language=language) + self.name = name + self.is_libtool = False + # 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 + + # Only search for pkg-config for each machine the first time and store + # the result in the class definition + if PkgConfigDependency.class_pkgbin[self.for_machine] is False: + mlog.debug('Pkg-config binary for %s is cached as not found.' % self.for_machine) + elif PkgConfigDependency.class_pkgbin[self.for_machine] is not None: + mlog.debug('Pkg-config binary for %s is cached.' % self.for_machine) + else: + assert PkgConfigDependency.class_pkgbin[self.for_machine] is None + mlog.debug('Pkg-config binary for %s is not cached.' % self.for_machine) + for potential_pkgbin in find_external_program( + self.env, self.for_machine, 'pkgconfig', 'Pkg-config', + environment.default_pkgconfig, allow_default_for_cross=False): + version_if_ok = self.check_pkgconfig(potential_pkgbin) + if not version_if_ok: + continue + if not self.silent: + mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()), + '(%s)' % version_if_ok) + PkgConfigDependency.class_pkgbin[self.for_machine] = potential_pkgbin + break + else: + if not self.silent: + mlog.log('Found Pkg-config:', mlog.red('NO')) + # Set to False instead of None to signify that we've already + # searched for it and not found it + PkgConfigDependency.class_pkgbin[self.for_machine] = False + + self.pkgbin = PkgConfigDependency.class_pkgbin[self.for_machine] + if self.pkgbin is False: + self.pkgbin = None + msg = 'Pkg-config binary for machine %s not found. Giving up.' % self.for_machine + if self.required: + raise DependencyException(msg) + else: + mlog.debug(msg) + return + + mlog.debug('Determining dependency {!r} with pkg-config executable ' + '{!r}'.format(name, self.pkgbin.get_path())) + ret, self.version, _ = self._call_pkgbin(['--modversion', name]) + if ret != 0: + return + + self.is_found = True + + try: + # 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() + except DependencyException as e: + mlog.debug(f"pkg-config error with '{name}': {e}") + if self.required: + raise + else: + self.compile_args = [] + self.link_args = [] + self.is_found = False + self.reason = e + + 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_real(self, args, env): + cmd = self.pkgbin.get_command() + args + p, out, err = Popen_safe(cmd, env=env) + rc, out, err = p.returncode, out.strip(), err.strip() + call = ' '.join(cmd) + mlog.debug(f"Called `{call}` -> {rc}\n{out}") + return rc, out, err + + @staticmethod + def setup_env(env: T.MutableMapping[str, str], environment: 'Environment', for_machine: MachineChoice, + extra_path: T.Optional[str] = None) -> None: + extra_paths: T.List[str] = environment.coredata.options[OptionKey('pkg_config_path', machine=for_machine)].value[:] + if extra_path and extra_path not in extra_paths: + extra_paths.append(extra_path) + sysroot = environment.properties[for_machine].get_sys_root() + if sysroot: + env['PKG_CONFIG_SYSROOT_DIR'] = sysroot + new_pkg_config_path = ':'.join([p for p in extra_paths]) + env['PKG_CONFIG_PATH'] = new_pkg_config_path + + pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir() + if pkg_config_libdir_prop: + new_pkg_config_libdir = ':'.join([p for p in pkg_config_libdir_prop]) + env['PKG_CONFIG_LIBDIR'] = new_pkg_config_libdir + # Dump all PKG_CONFIG environment variables + for key, value in env.items(): + if key.startswith('PKG_'): + mlog.debug(f'env[{key}]: {value}') + + def _call_pkgbin(self, args, env=None): + # Always copy the environment since we're going to modify it + # with pkg-config variables + if env is None: + env = os.environ.copy() + else: + env = env.copy() + + PkgConfigDependency.setup_env(env, self.env, self.for_machine) + + fenv = frozenset(env.items()) + targs = tuple(args) + cache = PkgConfigDependency.pkgbin_cache + if (self.pkgbin, targs, fenv) not in cache: + cache[(self.pkgbin, targs, fenv)] = self._call_pkgbin_real(args, env) + return cache[(self.pkgbin, targs, fenv)] + + def _convert_mingw_paths(self, args: T.List[str]) -> T.List[str]: + ''' + Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo + paths so convert them to C:/foo. We cannot resolve other paths starting + with / like /home/foo so leave them as-is so that the user gets an + error/warning from the compiler/linker. + ''' + if not self.env.machines.build.is_windows(): + return args + converted = [] + for arg in args: + pargs = [] + # Library search path + if arg.startswith('-L/'): + pargs = PurePath(arg[2:]).parts + tmpl = '-L{}:/{}' + elif arg.startswith('-I/'): + pargs = PurePath(arg[2:]).parts + tmpl = '-I{}:/{}' + # Full path to library or .la file + elif arg.startswith('/'): + pargs = PurePath(arg).parts + tmpl = '{}:/{}' + elif arg.startswith(('-L', '-I')) or (len(arg) > 2 and arg[1] == ':'): + # clean out improper '\\ ' as comes from some Windows pkg-config files + arg = arg.replace('\\ ', ' ') + if len(pargs) > 1 and len(pargs[1]) == 1: + arg = tmpl.format(pargs[1], '/'.join(pargs[2:])) + converted.append(arg) + return converted + + def _split_args(self, cmd): + # pkg-config paths follow Unix conventions, even on Windows; split the + # output using shlex.split rather than mesonlib.split_args + return shlex.split(cmd) + + def _set_cargs(self): + env = None + if self.language == 'fortran': + # gfortran doesn't appear to look in system paths for INCLUDE files, + # so don't allow pkg-config to suppress -I flags for system paths + env = os.environ.copy() + env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' + ret, out, err = self._call_pkgbin(['--cflags', self.name], env=env) + if ret != 0: + raise DependencyException('Could not generate cargs for %s:\n%s\n' % + (self.name, err)) + self.compile_args = self._convert_mingw_paths(self._split_args(out)) + + def _search_libs(self, out, out_raw): + ''' + @out: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs + @out_raw: pkg-config --libs + + We always look for the file ourselves instead of depending on the + compiler to find it with -lfoo or foo.lib (if possible) because: + 1. We want to be able to select static or shared + 2. We need the full path of the library to calculate RPATH values + 3. De-dup of libraries is easier when we have absolute paths + + Libraries that are provided by the toolchain or are not found by + find_library() will be added with -L -l pairs. + ''' + # Library paths should be safe to de-dup + # + # First, figure out what library paths to use. Originally, we were + # doing this as part of the loop, but due to differences in the order + # of -L values between pkg-config and pkgconf, we need to do that as + # a separate step. See: + # https://github.com/mesonbuild/meson/issues/3951 + # https://github.com/mesonbuild/meson/issues/4023 + # + # Separate system and prefix paths, and ensure that prefix paths are + # always searched first. + prefix_libpaths = OrderedSet() + # We also store this raw_link_args on the object later + raw_link_args = self._convert_mingw_paths(self._split_args(out_raw)) + for arg in raw_link_args: + if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')): + path = arg[2:] + if not os.path.isabs(path): + # Resolve the path as a compiler in the build directory would + path = os.path.join(self.env.get_build_dir(), path) + prefix_libpaths.add(path) + # Library paths are not always ordered in a meaningful way + # + # Instead of relying on pkg-config or pkgconf to provide -L flags in a + # specific order, we reorder library paths ourselves, according to th + # order specified in PKG_CONFIG_PATH. See: + # https://github.com/mesonbuild/meson/issues/4271 + # + # Only prefix_libpaths are reordered here because there should not be + # too many system_libpaths to cause library version issues. + pkg_config_path: T.List[str] = self.env.coredata.options[OptionKey('pkg_config_path', machine=self.for_machine)].value + pkg_config_path = self._convert_mingw_paths(pkg_config_path) + prefix_libpaths = sort_libpaths(prefix_libpaths, pkg_config_path) + system_libpaths = OrderedSet() + full_args = self._convert_mingw_paths(self._split_args(out)) + for arg in full_args: + if arg.startswith(('-L-l', '-L-L')): + # These are D language arguments, not library paths + continue + if arg.startswith('-L') and arg[2:] not in prefix_libpaths: + system_libpaths.add(arg[2:]) + # Use this re-ordered path list for library resolution + libpaths = list(prefix_libpaths) + list(system_libpaths) + # Track -lfoo libraries to avoid duplicate work + libs_found = OrderedSet() + # Track not-found libraries to know whether to add library paths + libs_notfound = [] + libtype = LibType.STATIC if self.static else LibType.PREFER_SHARED + # Generate link arguments for this library + link_args = [] + for lib in full_args: + if lib.startswith(('-L-l', '-L-L')): + # These are D language arguments, add them as-is + pass + elif lib.startswith('-L'): + # We already handled library paths above + continue + elif lib.startswith('-l'): + # Don't resolve the same -lfoo argument again + if lib in libs_found: + continue + if self.clib_compiler: + args = self.clib_compiler.find_library(lib[2:], self.env, + libpaths, libtype) + # If the project only uses a non-clib language such as D, Rust, + # C#, Python, etc, all we can do is limp along by adding the + # arguments as-is and then adding the libpaths at the end. + else: + args = None + if args is not None: + libs_found.add(lib) + # Replace -l arg with full path to library if available + # else, library is either to be ignored, or is provided by + # the compiler, can't be resolved, and should be used as-is + if args: + if not args[0].startswith('-l'): + lib = args[0] + else: + continue + else: + # Library wasn't found, maybe we're looking in the wrong + # places or the library will be provided with LDFLAGS or + # LIBRARY_PATH from the environment (on macOS), and many + # other edge cases that we can't account for. + # + # Add all -L paths and use it as -lfoo + if lib in libs_notfound: + continue + if self.static: + mlog.warning('Static library {!r} not found for dependency {!r}, may ' + 'not be statically linked'.format(lib[2:], self.name)) + libs_notfound.append(lib) + elif 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) + self.is_libtool = True + lib = shared_lib + if lib in link_args: + continue + link_args.append(lib) + # Add all -Lbar args if we have -lfoo args in link_args + if libs_notfound: + # Order of -L flags doesn't matter with ld, but it might with other + # linkers such as MSVC, so prepend them. + link_args = ['-L' + lp for lp in prefix_libpaths] + link_args + return link_args, raw_link_args + + def _set_libs(self): + env = None + libcmd = ['--libs'] + + if self.static: + libcmd.append('--static') + + libcmd.append(self.name) + + # Force pkg-config to output -L fields even if they are system + # paths so we can do manual searching with cc.find_library() later. + env = os.environ.copy() + env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1' + ret, out, err = self._call_pkgbin(libcmd, env=env) + if ret != 0: + raise DependencyException('Could not generate libs for %s:\n%s\n' % + (self.name, err)) + # Also get the 'raw' output without -Lfoo system paths for adding -L + # args with -lfoo when a library can't be found, and also in + # gnome.generate_gir + gnome.gtkdoc which need -L -l arguments. + ret, out_raw, err_raw = self._call_pkgbin(libcmd) + if ret != 0: + raise DependencyException('Could not generate libs for %s:\n\n%s' % + (self.name, out_raw)) + self.link_args, self.raw_link_args = self._search_libs(out, out_raw) + + def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: + options = ['--variable=' + variable_name, self.name] + + if 'define_variable' in kwargs: + definition = kwargs.get('define_variable', []) + if not isinstance(definition, list): + raise DependencyException('define_variable takes a list') + + if len(definition) != 2 or not all(isinstance(i, str) for i in definition): + raise DependencyException('define_variable must be made up of 2 strings for VARIABLENAME and VARIABLEVALUE') + + options = ['--define-variable=' + '='.join(definition)] + options + + ret, out, err = self._call_pkgbin(options) + variable = '' + if ret != 0: + if self.required: + raise DependencyException('dependency %s not found:\n%s\n' % + (self.name, err)) + else: + variable = out.strip() + + # pkg-config doesn't distinguish between empty and non-existent variables + # use the variable list to check for variable existence + if not variable: + ret, out, _ = self._call_pkgbin(['--print-variables', self.name]) + if not re.search(r'^' + variable_name + r'$', out, re.MULTILINE): + if 'default' in kwargs: + variable = kwargs['default'] + else: + mlog.warning(f"pkgconfig variable '{variable_name}' not defined for dependency {self.name}.") + + mlog.debug(f'Got pkgconfig variable {variable_name} : {variable}') + return variable + + @staticmethod + def get_methods(): + return [DependencyMethods.PKGCONFIG] + + def check_pkgconfig(self, pkgbin): + if not pkgbin.found(): + mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}') + return None + try: + p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2] + if p.returncode != 0: + mlog.warning('Found pkg-config {!r} but it failed when run' + ''.format(' '.join(pkgbin.get_command()))) + return None + except FileNotFoundError: + mlog.warning('We thought we found pkg-config {!r} but now it\'s not there. How odd!' + ''.format(' '.join(pkgbin.get_command()))) + return None + except PermissionError: + msg = 'Found pkg-config {!r} but didn\'t have permissions to run it.'.format(' '.join(pkgbin.get_command())) + if not self.env.machines.build.is_windows(): + msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' + mlog.warning(msg) + return None + return out.strip() + + 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 self.env.machines[self.for_machine].is_darwin(): + 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) + + def log_tried(self): + return self.type_name + + def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, + configtool: T.Optional[str] = None, internal: T.Optional[str] = None, + default_value: T.Optional[str] = None, + pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: + if pkgconfig: + kwargs = {} + if default_value is not None: + kwargs['default'] = default_value + if pkgconfig_define is not None: + kwargs['define_variable'] = pkgconfig_define + try: + return self.get_pkgconfig_variable(pkgconfig, kwargs) + except DependencyException: + pass + if default_value is not None: + return default_value + raise DependencyException(f'Could not get pkg-config variable and no default provided for {self!r}') diff --git a/mesonbuild/dependencies/qt.py b/mesonbuild/dependencies/qt.py index 1059871..4eef71e 100644 --- a/mesonbuild/dependencies/qt.py +++ b/mesonbuild/dependencies/qt.py @@ -21,11 +21,11 @@ import re import os import typing as T -from . import ( - ExtraFrameworkDependency, DependencyException, DependencyMethods, - PkgConfigDependency, -) -from .base import ConfigToolDependency, DependencyFactory +from .base import DependencyException, DependencyMethods +from .configtool import ConfigToolDependency +from .framework import ExtraFrameworkDependency +from .pkgconfig import PkgConfigDependency +from .factory import DependencyFactory from .. import mlog from .. import mesonlib diff --git a/mesonbuild/dependencies/scalapack.py b/mesonbuild/dependencies/scalapack.py index 0147e0b..f81f819 100644 --- a/mesonbuild/dependencies/scalapack.py +++ b/mesonbuild/dependencies/scalapack.py @@ -17,8 +17,11 @@ import functools import os import typing as T -from .base import CMakeDependency, DependencyMethods, PkgConfigDependency -from .base import factory_methods, DependencyException +from .base import DependencyMethods +from .base import DependencyException +from .cmake import CMakeDependency +from .pkgconfig import PkgConfigDependency +from .factory import factory_methods if T.TYPE_CHECKING: from ..environment import Environment, MachineChoice diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py index 26103ce..3d68e46 100644 --- a/mesonbuild/dependencies/ui.py +++ b/mesonbuild/dependencies/ui.py @@ -27,7 +27,8 @@ from ..environment import detect_cpu_family from .base import DependencyException, DependencyMethods from .base import ExternalDependency -from .base import ConfigToolDependency, DependencyFactory +from .configtool import ConfigToolDependency +from .factory import DependencyFactory if T.TYPE_CHECKING: from ..environment import Environment diff --git a/mesonbuild/modules/dlang.py b/mesonbuild/modules/dlang.py index f08803d..e2a55b6 100644 --- a/mesonbuild/modules/dlang.py +++ b/mesonbuild/modules/dlang.py @@ -26,7 +26,7 @@ from ..mesonlib import ( Popen_safe, MesonException ) -from ..dependencies.base import DubDependency +from ..dependencies import DubDependency from ..programs import ExternalProgram from ..interpreter import DependencyHolder diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index c2f713c..5cebd28 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -17,7 +17,7 @@ from pathlib import PurePath from .. import build from .. import dependencies -from ..dependencies.misc import ThreadDependency +from ..dependencies import ThreadDependency from .. import mesonlib from .. import mlog from . import ModuleReturnValue diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index b7070df..0e3df9b 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -30,7 +30,7 @@ from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitt from ..build import known_shmod_kwargs from .. import mlog from ..environment import detect_cpu_family -from ..dependencies.base import ( +from ..dependencies import ( DependencyMethods, ExternalDependency, PkgConfigDependency, NotFoundDependency ) diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index dd69611..aecfe50 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mesonbuild.dependencies.base import find_external_dependency +from mesonbuild.dependencies import find_external_dependency import os import shutil import typing as T diff --git a/mesonbuild/modules/unstable_external_project.py b/mesonbuild/modules/unstable_external_project.py index cd6cce4..f10c7aa 100644 --- a/mesonbuild/modules/unstable_external_project.py +++ b/mesonbuild/modules/unstable_external_project.py @@ -24,7 +24,7 @@ from ..interpreterbase import InterpreterException, FeatureNew from ..interpreterbase import permittedKwargs, typed_pos_args from ..interpreter import DependencyHolder from ..compilers.compilers import CFLAGS_MAPPING, CEXE_MAPPING -from ..dependencies.base import InternalDependency, PkgConfigDependency +from ..dependencies import InternalDependency, PkgConfigDependency from ..mesonlib import OptionKey class ExternalProject(ModuleObject): -- cgit v1.1