modules/python: use factory for dependency

This removes the odd 'pkgdep' attribute thing, and makes it behave more
like a proper dependency
This commit is contained in:
Dylan Baker 2021-07-12 16:03:06 -07:00
parent a881e849b5
commit 4d67dd19e5
1 changed files with 83 additions and 90 deletions

View File

@ -13,8 +13,9 @@
# limitations under the License. # limitations under the License.
from pathlib import Path from pathlib import Path
import os import functools
import json import json
import os
import shutil import shutil
import typing as T import typing as T
@ -23,6 +24,7 @@ from .. import mesonlib
from .. import mlog from .. import mlog
from ..build import known_shmod_kwargs from ..build import known_shmod_kwargs
from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency
from ..dependencies.base import process_method_kw
from ..environment import detect_cpu_family from ..environment import detect_cpu_family
from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs
from ..interpreterbase import ( from ..interpreterbase import (
@ -30,13 +32,14 @@ from ..interpreterbase import (
InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo,
FeatureNew, FeatureNewKwargs, disablerIfNotFound FeatureNew, FeatureNewKwargs, disablerIfNotFound
) )
from ..mesonlib import MachineChoice, MesonException from ..mesonlib import MachineChoice
from ..programs import ExternalProgram, NonExistingExternalProgram from ..programs import ExternalProgram, NonExistingExternalProgram
if T.TYPE_CHECKING: if T.TYPE_CHECKING:
from . import ModuleState from . import ModuleState
from ..build import SharedModule, Data from ..build import SharedModule, Data
from ..dependencies import ExternalDependency from ..dependencies import ExternalDependency, Dependency
from ..dependencies.factory import DependencyGenerator
from ..environment import Environment from ..environment import Environment
from ..interpreter import Interpreter from ..interpreter import Interpreter
from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs
@ -49,17 +52,18 @@ mod_kwargs.update(known_shmod_kwargs)
mod_kwargs -= {'name_prefix', 'name_suffix'} mod_kwargs -= {'name_prefix', 'name_suffix'}
class PythonDependency(SystemDependency): if T.TYPE_CHECKING:
_Base = ExternalDependency
else:
_Base = object
def __init__(self, python_holder: 'PythonInstallation', environment: 'Environment', class _PythonDependencyBase(_Base):
kwargs: T.Dict[str, T.Any]):
super().__init__('python', environment, kwargs) def __init__(self, python_holder: 'PythonInstallation', embed: bool):
self.name = 'python' self.name = 'python' # override the name from the "real" dependency lookup
self.static = kwargs.get('static', False) self.embed = embed
self.embed = kwargs.get('embed', False) self.version: str = python_holder.version
self.version = python_holder.version
self.platform = python_holder.platform self.platform = python_holder.platform
self.pkgdep = None
self.variables = python_holder.variables self.variables = python_holder.variables
self.paths = python_holder.paths self.paths = python_holder.paths
self.link_libpython = python_holder.link_libpython self.link_libpython = python_holder.link_libpython
@ -69,73 +73,26 @@ class PythonDependency(SystemDependency):
else: else:
self.major_version = 2 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 class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase):
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')
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: class PythonSystemDependency(SystemDependency, _PythonDependencyBase):
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)
if old_pkg_path is not None: def __init__(self, name: str, environment: 'Environment',
os.environ['PKG_CONFIG_PATH'] = old_pkg_path 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: if mesonlib.is_windows():
os.environ['PKG_CONFIG_LIBDIR'] = old_pkg_libdir self._find_libpy_windows(environment)
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: else:
self.pkgdep = None self._find_libpy(installation, environment)
# 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})'))
else:
mlog.log('Dependency', mlog.bold(self.name), 'found:', mlog.red('NO'))
def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None:
if python_holder.is_pypy: if python_holder.is_pypy:
@ -253,20 +210,45 @@ class PythonDependency(SystemDependency):
self.is_found = True 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: def python_factory(env: 'Environment', for_machine: 'MachineChoice',
if self.pkgdep: kwargs: T.Dict[str, T.Any], methods: T.List[DependencyMethods],
return self.pkgdep.get_pkgconfig_variable(variable_name, kwargs) installation: 'PythonInstallation') -> T.List['DependencyGenerator']:
else: # We can't use the factory_methods decorator here, as we need to pass the
return super().get_pkgconfig_variable(variable_name, kwargs) # 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 = '''\ INTROSPECT_COMMAND = '''\
@ -388,7 +370,7 @@ class PythonInstallation(ExternalProgramHolder):
if not self.link_libpython: if not self.link_libpython:
new_deps = [] new_deps = []
for dep in mesonlib.extract_as_list(kwargs, 'dependencies'): 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) dep = dep.get_partial_dependency(compile_args=True)
new_deps.append(dep) new_deps.append(dep)
kwargs['dependencies'] = new_deps kwargs['dependencies'] = new_deps
@ -408,15 +390,26 @@ class PythonInstallation(ExternalProgramHolder):
@permittedKwargs(permitted_dependency_kwargs | {'embed'}) @permittedKwargs(permitted_dependency_kwargs | {'embed'})
@FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed']) @FeatureNewKwargs('python_installation.dependency', '0.53.0', ['embed'])
@noPosargs @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) 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: if disabled:
mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled')
dep = NotFoundDependency(self.interpreter.environment)
else: 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(): if required and not dep.found():
raise mesonlib.MesonException('Python dependency not found') raise mesonlib.MesonException('Python dependency not found')
return dep return dep
@typed_pos_args('install_data', varargs=(str, mesonlib.File)) @typed_pos_args('install_data', varargs=(str, mesonlib.File))
@ -509,7 +502,7 @@ class PythonModule(ExtensionModule):
return None return None
@staticmethod @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': if name_or_path == 'python2':
return mesonlib.version_compare(version, '< 3.0') return mesonlib.version_compare(version, '< 3.0')
elif name_or_path == 'python3': elif name_or_path == 'python3':