From 4d67dd19e5b7dcec6716840d30984fa41eef55c6 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 12 Jul 2021 16:03:06 -0700 Subject: [PATCH] modules/python: use factory for dependency This removes the odd 'pkgdep' attribute thing, and makes it behave more like a proper dependency --- mesonbuild/modules/python.py | 173 +++++++++++++++++------------------ 1 file changed, 83 insertions(+), 90 deletions(-) diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index a8e55bdf8..69ee89f33 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -13,8 +13,9 @@ # limitations under the License. from pathlib import Path -import os +import functools import json +import os import shutil import typing as T @@ -23,6 +24,7 @@ from .. import mesonlib from .. import mlog from ..build import known_shmod_kwargs from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency +from ..dependencies.base import process_method_kw from ..environment import detect_cpu_family from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs from ..interpreterbase import ( @@ -30,13 +32,14 @@ from ..interpreterbase import ( InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, FeatureNew, FeatureNewKwargs, disablerIfNotFound ) -from ..mesonlib import MachineChoice, MesonException +from ..mesonlib import MachineChoice from ..programs import ExternalProgram, NonExistingExternalProgram if T.TYPE_CHECKING: from . import ModuleState from ..build import SharedModule, Data - from ..dependencies import ExternalDependency + from ..dependencies import ExternalDependency, Dependency + from ..dependencies.factory import DependencyGenerator from ..environment import Environment from ..interpreter import Interpreter from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs @@ -49,17 +52,18 @@ mod_kwargs.update(known_shmod_kwargs) mod_kwargs -= {'name_prefix', 'name_suffix'} -class PythonDependency(SystemDependency): +if T.TYPE_CHECKING: + _Base = ExternalDependency +else: + _Base = object + +class _PythonDependencyBase(_Base): - def __init__(self, python_holder: 'PythonInstallation', environment: 'Environment', - kwargs: T.Dict[str, T.Any]): - super().__init__('python', environment, kwargs) - self.name = 'python' - self.static = kwargs.get('static', False) - self.embed = kwargs.get('embed', False) - self.version = python_holder.version + def __init__(self, python_holder: 'PythonInstallation', embed: bool): + self.name = 'python' # override the name from the "real" dependency lookup + self.embed = embed + self.version: str = python_holder.version self.platform = python_holder.platform - self.pkgdep = None self.variables = python_holder.variables self.paths = python_holder.paths self.link_libpython = python_holder.link_libpython @@ -69,73 +73,26 @@ class PythonDependency(SystemDependency): else: self.major_version = 2 - # We first try to find the necessary python variables using pkgconfig - if DependencyMethods.PKGCONFIG in self.methods and not python_holder.is_pypy: - pkg_version = self.variables.get('LDVERSION') or self.version - pkg_libdir = self.variables.get('LIBPC') - pkg_embed = '-embed' if self.embed and mesonlib.version_compare(self.version, '>=3.8') else '' - pkg_name = f'python-{pkg_version}{pkg_embed}' - # If python-X.Y.pc exists in LIBPC, we will try to use it - if pkg_libdir is not None and Path(os.path.join(pkg_libdir, f'{pkg_name}.pc')).is_file(): - old_pkg_libdir = os.environ.get('PKG_CONFIG_LIBDIR') - old_pkg_path = os.environ.get('PKG_CONFIG_PATH') +class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): - os.environ.pop('PKG_CONFIG_PATH', None) + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + PkgConfigDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - if pkg_libdir: - os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir - try: - self.pkgdep = PkgConfigDependency(pkg_name, environment, kwargs) - mlog.debug(f'Found "{pkg_name}" via pkgconfig lookup in LIBPC ({pkg_libdir})') - py_lookup_method = 'pkgconfig' - except MesonException as e: - mlog.debug(f'"{pkg_name}" could not be found in LIBPC ({pkg_libdir})') - mlog.debug(e) +class PythonSystemDependency(SystemDependency, _PythonDependencyBase): - if old_pkg_path is not None: - os.environ['PKG_CONFIG_PATH'] = old_pkg_path + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + SystemDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) - if old_pkg_libdir is not None: - os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir - else: - os.environ.pop('PKG_CONFIG_LIBDIR', None) - else: - mlog.debug(f'"{pkg_name}" could not be found in LIBPC ({pkg_libdir}), this is likely due to a relocated python installation') - - # If lookup via LIBPC failed, try to use fallback PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH mechanisms - if self.pkgdep is None or not self.pkgdep.found(): - try: - self.pkgdep = PkgConfigDependency(pkg_name, environment, kwargs) - mlog.debug(f'Found "{pkg_name}" via fallback pkgconfig lookup in PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH') - py_lookup_method = 'pkgconfig-fallback' - except MesonException as e: - mlog.debug(f'"{pkg_name}" could not be found via fallback pkgconfig lookup in PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH') - mlog.debug(e) - - if self.pkgdep and self.pkgdep.found(): - self.compile_args = self.pkgdep.get_compile_args() - self.link_args = self.pkgdep.get_link_args() - self.is_found = True - self.pcdep = self.pkgdep - else: - self.pkgdep = None - - # Finally, try to find python via SYSCONFIG as a final measure - if DependencyMethods.SYSCONFIG in self.methods: - if mesonlib.is_windows(): - self._find_libpy_windows(environment) - else: - self._find_libpy(python_holder, environment) - if self.is_found: - mlog.debug(f'Found "python-{self.version}" via SYSCONFIG module') - py_lookup_method = 'sysconfig' - - if self.is_found: - mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.green(f'YES ({py_lookup_method})')) + if mesonlib.is_windows(): + self._find_libpy_windows(environment) else: - mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.red('NO')) + self._find_libpy(installation, environment) def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: if python_holder.is_pypy: @@ -253,20 +210,45 @@ class PythonDependency(SystemDependency): self.is_found = True - @staticmethod - def get_methods() -> T.List[DependencyMethods]: - if mesonlib.is_windows(): - return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSCONFIG] - elif mesonlib.is_osx(): - return [DependencyMethods.PKGCONFIG, DependencyMethods.EXTRAFRAMEWORK] - else: - return [DependencyMethods.PKGCONFIG, DependencyMethods.SYSCONFIG] - def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: - if self.pkgdep: - return self.pkgdep.get_pkgconfig_variable(variable_name, kwargs) - else: - return super().get_pkgconfig_variable(variable_name, kwargs) +def python_factory(env: 'Environment', for_machine: 'MachineChoice', + kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods], + installation: 'PythonInstallation') -> T.List['DependencyGenerator']: + # We can't use the factory_methods decorator here, as we need to pass the + # extra installation argument + embed = kwargs.get('embed', False) + candidates: T.List['DependencyGenerator'] = [] + + if DependencyMethods.PKGCONFIG in methods: + pkg_version = installation.variables.get('LDVERSION') or installation.version + pkg_libdir = installation.variables.get('LIBPC') + pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.version, '>=3.8') else '' + pkg_name = f'python-{pkg_version}{pkg_embed}' + + # If python-X.Y.pc exists in LIBPC, we will try to use it + def wrap_in_pythons_pc_dir(name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], + installation: 'PythonInstallation') -> 'ExternalDependency': + old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) + old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) + if pkg_libdir: + os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir + try: + return PythonPkgConfigDependency(name, env, kwargs, installation) + finally: + if old_pkg_libdir is not None: + os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir + if old_pkg_path is not None: + os.environ['PKG_CONFIG_PATH'] = old_pkg_path + + candidates.extend([ + functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation), + functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation) + ]) + + if DependencyMethods.SYSTEM in methods: + candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) + + return candidates INTROSPECT_COMMAND = '''\ @@ -388,7 +370,7 @@ class PythonInstallation(ExternalProgramHolder): if not self.link_libpython: new_deps = [] for dep in mesonlib.extract_as_list(kwargs, 'dependencies'): - if isinstance(dep, PythonDependency): + if isinstance(dep, _PythonDependencyBase): dep = dep.get_partial_dependency(compile_args=True) new_deps.append(dep) kwargs['dependencies'] = new_deps @@ -408,15 +390,26 @@ class PythonInstallation(ExternalProgramHolder): @permittedKwargs(permitted_dependency_kwargs | {'embed'}) @FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed']) @noPosargs - def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'ExternalDependency': + def dependency_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'Dependency': disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) + + # it's theoretically (though not practically) possible for the else clse + # to not bind dep, let's ensure it is. + dep: 'Dependency' = NotFoundDependency(self.interpreter.environment) if disabled: mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') - dep = NotFoundDependency(self.interpreter.environment) else: - dep = PythonDependency(self, self.interpreter.environment, kwargs) + for d in python_factory(self.interpreter.environment, + MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST, + kwargs, + process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs), + self): + dep = d() + if dep.found(): + break if required and not dep.found(): raise mesonlib.MesonException('Python dependency not found') + return dep @typed_pos_args('install_data', varargs=(str, mesonlib.File)) @@ -509,7 +502,7 @@ class PythonModule(ExtensionModule): return None @staticmethod - def _check_version(name_or_path: str, version) -> bool: + def _check_version(name_or_path: str, version: str) -> bool: if name_or_path == 'python2': return mesonlib.version_compare(version, '< 3.0') elif name_or_path == 'python3':