diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:41:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:41:38 +0000 |
commit | 7b6e527f440cd7e6f8be2b07cee320ee6ca18786 (patch) | |
tree | 4a2738d69fa2814659fdadddf5826282e73d81f4 /mesonbuild/modules/python.py | |
parent | Initial commit. (diff) | |
download | meson-upstream.tar.xz meson-upstream.zip |
Adding upstream version 1.0.1.upstream/1.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/modules/python.py | 835 |
1 files changed, 835 insertions, 0 deletions
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py new file mode 100644 index 0000000..16d3ac4 --- /dev/null +++ b/mesonbuild/modules/python.py @@ -0,0 +1,835 @@ +# Copyright 2018 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 __future__ import annotations + +from pathlib import Path +import copy +import functools +import json +import os +import shutil +import typing as T + +from . import ExtensionModule, ModuleInfo +from .. import mesonlib +from .. import mlog +from ..coredata import UserFeatureOption +from ..build import known_shmod_kwargs +from ..dependencies import DependencyMethods, PkgConfigDependency, NotFoundDependency, SystemDependency, ExtraFrameworkDependency +from ..dependencies.base import process_method_kw +from ..dependencies.detect import get_dep_identifier +from ..environment import detect_cpu_family +from ..interpreter import ExternalProgramHolder, extract_required_kwarg, permitted_dependency_kwargs +from ..interpreter import primitives as P_OBJ +from ..interpreter.type_checking import NoneType, PRESERVE_PATH_KW +from ..interpreterbase import ( + noPosargs, noKwargs, permittedKwargs, ContainerTypeInfo, + InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo, + FeatureNew, FeatureNewKwargs, disablerIfNotFound +) +from ..mesonlib import MachineChoice +from ..programs import ExternalProgram, NonExistingExternalProgram + +if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from . import ModuleState + from ..build import SharedModule, Data + from ..dependencies import ExternalDependency, Dependency + from ..dependencies.factory import DependencyGenerator + from ..environment import Environment + from ..interpreter import Interpreter + from ..interpreter.kwargs import ExtractRequired + from ..interpreterbase.interpreterbase import TYPE_var, TYPE_kwargs + + class PythonIntrospectionDict(TypedDict): + + install_paths: T.Dict[str, str] + is_pypy: bool + is_venv: bool + link_libpython: bool + sysconfig_paths: T.Dict[str, str] + paths: T.Dict[str, str] + platform: str + suffix: str + variables: T.Dict[str, str] + version: str + + class PyInstallKw(TypedDict): + + pure: T.Optional[bool] + subdir: str + install_tag: T.Optional[str] + + class FindInstallationKw(ExtractRequired): + + disabler: bool + modules: T.List[str] + pure: T.Optional[bool] + + _Base = ExternalDependency +else: + _Base = object + + +mod_kwargs = {'subdir'} +mod_kwargs.update(known_shmod_kwargs) +mod_kwargs -= {'name_prefix', 'name_suffix'} + + +class _PythonDependencyBase(_Base): + + def __init__(self, python_holder: 'PythonInstallation', embed: bool): + self.embed = embed + self.version: str = python_holder.version + self.platform = python_holder.platform + self.variables = python_holder.variables + self.paths = python_holder.paths + # The "-embed" version of python.pc / python-config was introduced in 3.8, + # and distutils extension linking was changed to be considered a non embed + # usage. Before then, this dependency always uses the embed=True handling + # because that is the only one that exists. + # + # On macOS and some Linux distros (Debian) distutils doesn't link extensions + # against libpython, even on 3.7 and below. We call into distutils and + # mirror its behavior. See https://github.com/mesonbuild/meson/issues/4117 + self.link_libpython = python_holder.link_libpython or embed + self.info: T.Optional[T.Dict[str, str]] = None + if mesonlib.version_compare(self.version, '>= 3.0'): + self.major_version = 3 + else: + self.major_version = 2 + + +class PythonPkgConfigDependency(PkgConfigDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation', + libpc: bool = False): + if libpc: + mlog.debug(f'Searching for {name!r} via pkgconfig lookup in LIBPC') + else: + mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths') + + PkgConfigDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) + + if libpc and not self.is_found: + mlog.debug(f'"python-{self.version}" could not be found in LIBPC, this is likely due to a relocated python installation') + + # pkg-config files are usually accurate starting with python 3.8 + if not self.link_libpython and mesonlib.version_compare(self.version, '< 3.8'): + self.link_args = [] + + +class PythonFrameworkDependency(ExtraFrameworkDependency, _PythonDependencyBase): + + def __init__(self, name: str, environment: 'Environment', + kwargs: T.Dict[str, T.Any], installation: 'PythonInstallation'): + ExtraFrameworkDependency.__init__(self, name, environment, kwargs) + _PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) + + +class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + + 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 mesonlib.is_windows(): + self._find_libpy_windows(environment) + else: + self._find_libpy(installation, environment) + + if not self.link_libpython: + # match pkg-config behavior + self.link_args = [] + + if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args): + self.is_found = False + + def _find_libpy(self, python_holder: 'PythonInstallation', environment: 'Environment') -> None: + if python_holder.is_pypy: + if self.major_version == 3: + libname = 'pypy3-c' + else: + libname = 'pypy-c' + libdir = os.path.join(self.variables.get('base'), 'bin') + libdirs = [libdir] + else: + libname = f'python{self.version}' + if 'DEBUG_EXT' in self.variables: + libname += self.variables['DEBUG_EXT'] + if 'ABIFLAGS' in self.variables: + libname += self.variables['ABIFLAGS'] + libdirs = [] + + largs = self.clib_compiler.find_library(libname, environment, libdirs) + if largs is not None: + self.link_args = largs + + self.is_found = largs is not None or not self.link_libpython + + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), + self.paths.get('include'), + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] + + def _get_windows_python_arch(self) -> T.Optional[str]: + if self.platform == 'mingw': + pycc = self.variables.get('CC') + if pycc.startswith('x86_64'): + return '64' + elif pycc.startswith(('i686', 'i386')): + return '32' + else: + mlog.log(f'MinGW Python built with unknown CC {pycc!r}, please file a bug') + return None + elif self.platform == 'win32': + return '32' + elif self.platform in {'win64', 'win-amd64'}: + return '64' + mlog.log(f'Unknown Windows Python platform {self.platform!r}') + return None + + def _get_windows_link_args(self) -> T.Optional[T.List[str]]: + if self.platform.startswith('win'): + vernum = self.variables.get('py_version_nodot') + verdot = self.variables.get('py_version_short') + imp_lower = self.variables.get('implementation_lower', 'python') + if self.static: + libpath = Path('libs') / f'libpython{vernum}.a' + else: + comp = self.get_compiler() + if comp.id == "gcc": + if imp_lower == 'pypy' and verdot == '3.8': + # The naming changed between 3.8 and 3.9 + libpath = Path('libpypy3-c.dll') + elif imp_lower == 'pypy': + libpath = Path(f'libpypy{verdot}-c.dll') + else: + libpath = Path(f'python{vernum}.dll') + else: + libpath = Path('libs') / f'python{vernum}.lib' + # base_prefix to allow for virtualenvs. + lib = Path(self.variables.get('base_prefix')) / libpath + elif self.platform == 'mingw': + if self.static: + libname = self.variables.get('LIBRARY') + else: + libname = self.variables.get('LDLIBRARY') + lib = Path(self.variables.get('LIBDIR')) / libname + else: + raise mesonlib.MesonBugException( + 'On a Windows path, but the OS doesn\'t appear to be Windows or MinGW.') + if not lib.exists(): + mlog.log('Could not find Python3 library {!r}'.format(str(lib))) + return None + return [str(lib)] + + def _find_libpy_windows(self, env: 'Environment') -> None: + ''' + Find python3 libraries on Windows and also verify that the arch matches + what we are building for. + ''' + pyarch = self._get_windows_python_arch() + if pyarch is None: + self.is_found = False + return + arch = detect_cpu_family(env.coredata.compilers.host) + if arch == 'x86': + arch = '32' + elif arch == 'x86_64': + arch = '64' + else: + # We can't cross-compile Python 3 dependencies on Windows yet + mlog.log(f'Unknown architecture {arch!r} for', + mlog.bold(self.name)) + self.is_found = False + return + # Pyarch ends in '32' or '64' + if arch != pyarch: + mlog.log('Need', mlog.bold(self.name), f'for {arch}-bit, but found {pyarch}-bit') + self.is_found = False + return + # This can fail if the library is not found + largs = self._get_windows_link_args() + if largs is None: + self.is_found = False + return + self.link_args = largs + # Compile args + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), + self.paths.get('include'), + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] + + # https://sourceforge.net/p/mingw-w64/mailman/message/30504611/ + # https://github.com/python/cpython/pull/100137 + if pyarch == '64' and mesonlib.version_compare(self.version, '<3.12'): + self.compile_args += ['-DMS_WIN64'] + + self.is_found = True + + +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'] = [] + pkg_version = installation.variables.get('LDVERSION') or installation.version + + if DependencyMethods.PKGCONFIG in methods: + 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': + if not pkg_libdir: + # there is no LIBPC, so we can't search in it + return NotFoundDependency('python', env) + + old_pkg_libdir = os.environ.pop('PKG_CONFIG_LIBDIR', None) + old_pkg_path = os.environ.pop('PKG_CONFIG_PATH', None) + os.environ['PKG_CONFIG_LIBDIR'] = pkg_libdir + try: + return PythonPkgConfigDependency(name, env, kwargs, installation, True) + finally: + def set_env(name, value): + if value is not None: + os.environ[name] = value + elif name in os.environ: + del os.environ[name] + set_env('PKG_CONFIG_LIBDIR', old_pkg_libdir) + set_env('PKG_CONFIG_PATH', old_pkg_path) + + candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) + # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, + # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. + if pkg_libdir is not None: + candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) + + if DependencyMethods.SYSTEM in methods: + candidates.append(functools.partial(PythonSystemDependency, 'python', env, kwargs, installation)) + + if DependencyMethods.EXTRAFRAMEWORK in methods: + nkwargs = kwargs.copy() + if mesonlib.version_compare(pkg_version, '>= 3'): + # There is a python in /System/Library/Frameworks, but that's python 2.x, + # Python 3 will always be in /Library + nkwargs['paths'] = ['/Library/Frameworks'] + candidates.append(functools.partial(PythonFrameworkDependency, 'Python', env, nkwargs, installation)) + + return candidates + + +INTROSPECT_COMMAND = '''\ +import os.path +import sysconfig +import json +import sys +import distutils.command.install + +def get_distutils_paths(scheme=None, prefix=None): + import distutils.dist + distribution = distutils.dist.Distribution() + install_cmd = distribution.get_command_obj('install') + if prefix is not None: + install_cmd.prefix = prefix + if scheme: + install_cmd.select_scheme(scheme) + install_cmd.finalize_options() + return { + 'data': install_cmd.install_data, + 'include': os.path.dirname(install_cmd.install_headers), + 'platlib': install_cmd.install_platlib, + 'purelib': install_cmd.install_purelib, + 'scripts': install_cmd.install_scripts, + } + +# On Debian derivatives, the Python interpreter shipped by the distribution uses +# a custom install scheme, deb_system, for the system install, and changes the +# default scheme to a custom one pointing to /usr/local and replacing +# site-packages with dist-packages. +# See https://github.com/mesonbuild/meson/issues/8739. +# XXX: We should be using sysconfig, but Debian only patches distutils. + +if 'deb_system' in distutils.command.install.INSTALL_SCHEMES: + paths = get_distutils_paths(scheme='deb_system') + install_paths = get_distutils_paths(scheme='deb_system', prefix='') +else: + paths = sysconfig.get_paths() + empty_vars = {'base': '', 'platbase': '', 'installed_base': ''} + install_paths = sysconfig.get_paths(vars=empty_vars) + +def links_against_libpython(): + from distutils.core import Distribution, Extension + cmd = Distribution().get_command_obj('build_ext') + cmd.ensure_finalized() + return bool(cmd.get_libraries(Extension('dummy', []))) + +variables = sysconfig.get_config_vars() +variables.update({'base_prefix': getattr(sys, 'base_prefix', sys.prefix)}) + +if sys.version_info < (3, 0): + suffix = variables.get('SO') +elif sys.version_info < (3, 8, 7): + # https://bugs.python.org/issue?@action=redirect&bpo=39825 + from distutils.sysconfig import get_config_var + suffix = get_config_var('EXT_SUFFIX') +else: + suffix = variables.get('EXT_SUFFIX') + +print(json.dumps({ + 'variables': variables, + 'paths': paths, + 'sysconfig_paths': sysconfig.get_paths(), + 'install_paths': install_paths, + 'version': sysconfig.get_python_version(), + 'platform': sysconfig.get_platform(), + 'is_pypy': '__pypy__' in sys.builtin_module_names, + 'is_venv': sys.prefix != variables['base_prefix'], + 'link_libpython': links_against_libpython(), + 'suffix': suffix, +})) +''' + + +class PythonExternalProgram(ExternalProgram): + def __init__(self, name: str, command: T.Optional[T.List[str]] = None, + ext_prog: T.Optional[ExternalProgram] = None): + if ext_prog is None: + super().__init__(name, command=command, silent=True) + else: + self.name = name + self.command = ext_prog.command + self.path = ext_prog.path + + # We want strong key values, so we always populate this with bogus data. + # Otherwise to make the type checkers happy we'd have to do .get() for + # everycall, even though we know that the introspection data will be + # complete + self.info: 'PythonIntrospectionDict' = { + 'install_paths': {}, + 'is_pypy': False, + 'is_venv': False, + 'link_libpython': False, + 'sysconfig_paths': {}, + 'paths': {}, + 'platform': 'sentinal', + 'variables': {}, + 'version': '0.0', + } + self.pure: bool = True + + def _check_version(self, version: str) -> bool: + if self.name == 'python2': + return mesonlib.version_compare(version, '< 3.0') + elif self.name == 'python3': + return mesonlib.version_compare(version, '>= 3.0') + return True + + def sanity(self, state: T.Optional['ModuleState'] = None) -> bool: + # Sanity check, we expect to have something that at least quacks in tune + from tempfile import NamedTemporaryFile + with NamedTemporaryFile(suffix='.py', delete=False, mode='w', encoding='utf-8') as tf: + tmpfilename = tf.name + tf.write(INTROSPECT_COMMAND) + cmd = self.get_command() + [tmpfilename] + p, stdout, stderr = mesonlib.Popen_safe(cmd) + os.unlink(tmpfilename) + try: + info = json.loads(stdout) + except json.JSONDecodeError: + info = None + mlog.debug('Could not introspect Python (%s): exit code %d' % (str(p.args), p.returncode)) + mlog.debug('Program stdout:\n') + mlog.debug(stdout) + mlog.debug('Program stderr:\n') + mlog.debug(stderr) + + if info is not None and self._check_version(info['version']): + self.info = T.cast('PythonIntrospectionDict', info) + self.platlib = self._get_path(state, 'platlib') + self.purelib = self._get_path(state, 'purelib') + return True + else: + return False + + def _get_path(self, state: T.Optional['ModuleState'], key: str) -> None: + rel_path = self.info['install_paths'][key][1:] + if not state: + # This happens only from run_project_tests.py + return rel_path + value = state.get_option(f'{key}dir', module='python') + if value: + if state.is_user_defined_option('install_env', module='python'): + raise mesonlib.MesonException(f'python.{key}dir and python.install_env are mutually exclusive') + return value + + install_env = state.get_option('install_env', module='python') + if install_env == 'auto': + install_env = 'venv' if self.info['is_venv'] else 'system' + + if install_env == 'system': + rel_path = os.path.join(self.info['variables']['prefix'], rel_path) + elif install_env == 'venv': + if not self.info['is_venv']: + raise mesonlib.MesonException('python.install_env cannot be set to "venv" unless you are in a venv!') + # inside a venv, deb_system is *never* active hence info['paths'] may be wrong + rel_path = self.info['sysconfig_paths'][key] + + return rel_path + + +_PURE_KW = KwargInfo('pure', (bool, NoneType)) +_SUBDIR_KW = KwargInfo('subdir', str, default='') + + +class PythonInstallation(ExternalProgramHolder): + def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): + ExternalProgramHolder.__init__(self, python, interpreter) + info = python.info + prefix = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('prefix')) + assert isinstance(prefix, str), 'for mypy' + self.variables = info['variables'] + self.suffix = info['suffix'] + self.paths = info['paths'] + self.pure = python.pure + self.platlib_install_path = os.path.join(prefix, python.platlib) + self.purelib_install_path = os.path.join(prefix, python.purelib) + self.version = info['version'] + self.platform = info['platform'] + self.is_pypy = info['is_pypy'] + self.link_libpython = info['link_libpython'] + self.methods.update({ + 'extension_module': self.extension_module_method, + 'dependency': self.dependency_method, + 'install_sources': self.install_sources_method, + 'get_install_dir': self.get_install_dir_method, + 'language_version': self.language_version_method, + 'found': self.found_method, + 'has_path': self.has_path_method, + 'get_path': self.get_path_method, + 'has_variable': self.has_variable_method, + 'get_variable': self.get_variable_method, + 'path': self.path_method, + }) + + @permittedKwargs(mod_kwargs) + def extension_module_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> 'SharedModule': + if 'install_dir' in kwargs: + if 'subdir' in kwargs: + raise InvalidArguments('"subdir" and "install_dir" are mutually exclusive') + else: + subdir = kwargs.pop('subdir', '') + if not isinstance(subdir, str): + raise InvalidArguments('"subdir" argument must be a string.') + + kwargs['install_dir'] = self._get_install_dir_impl(False, subdir) + + new_deps = mesonlib.extract_as_list(kwargs, 'dependencies') + has_pydep = any(isinstance(dep, _PythonDependencyBase) for dep in new_deps) + if not has_pydep: + pydep = self._dependency_method_impl({}) + if not pydep.found(): + raise mesonlib.MesonException('Python dependency not found') + new_deps.append(pydep) + FeatureNew.single_use('python_installation.extension_module with implicit dependency on python', + '0.63.0', self.subproject, 'use python_installation.dependency()', + self.current_node) + kwargs['dependencies'] = new_deps + + # msys2's python3 has "-cpython-36m.dll", we have to be clever + # FIXME: explain what the specific cleverness is here + split, suffix = self.suffix.rsplit('.', 1) + args[0] += split + + kwargs['name_prefix'] = '' + kwargs['name_suffix'] = suffix + + if 'gnu_symbol_visibility' not in kwargs and \ + (self.is_pypy or mesonlib.version_compare(self.version, '>=3.9')): + kwargs['gnu_symbol_visibility'] = 'inlineshidden' + + return self.interpreter.func_shared_module(None, args, kwargs) + + def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency: + for_machine = self.interpreter.machine_from_native_kwarg(kwargs) + identifier = get_dep_identifier(self._full_path(), kwargs) + + dep = self.interpreter.coredata.deps[for_machine].get(identifier) + if dep is not None: + return dep + + new_kwargs = kwargs.copy() + new_kwargs['required'] = False + methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs) + # it's theoretically (though not practically) possible to not bind dep, let's ensure it is. + dep: Dependency = NotFoundDependency('python', self.interpreter.environment) + for d in python_factory(self.interpreter.environment, for_machine, new_kwargs, methods, self): + dep = d() + if dep.found(): + break + + self.interpreter.coredata.deps[for_machine].put(identifier, dep) + return dep + + @disablerIfNotFound + @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') -> 'Dependency': + disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) + if disabled: + mlog.log('Dependency', mlog.bold('python'), 'skipped: feature', mlog.bold(feature), 'disabled') + return NotFoundDependency('python', self.interpreter.environment) + else: + dep = self._dependency_method_impl(kwargs) + if required and not dep.found(): + raise mesonlib.MesonException('Python dependency not found') + return dep + + @typed_pos_args('install_data', varargs=(str, mesonlib.File)) + @typed_kwargs( + 'python_installation.install_sources', + _PURE_KW, + _SUBDIR_KW, + PRESERVE_PATH_KW, + KwargInfo('install_tag', (str, NoneType), since='0.60.0') + ) + def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], + kwargs: 'PyInstallKw') -> 'Data': + tag = kwargs['install_tag'] or 'python-runtime' + pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure + install_dir = self._get_install_dir_impl(pure, kwargs['subdir']) + return self.interpreter.install_data_impl( + self.interpreter.source_strings_to_files(args[0]), + install_dir, + mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python', + install_dir_name=install_dir.optname, + preserve_path=kwargs['preserve_path']) + + @noPosargs + @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) + def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: + pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure + return self._get_install_dir_impl(pure, kwargs['subdir']) + + def _get_install_dir_impl(self, pure: bool, subdir: str) -> P_OBJ.OptionString: + if pure: + base = self.purelib_install_path + name = '{py_purelib}' + else: + base = self.platlib_install_path + name = '{py_platlib}' + + return P_OBJ.OptionString(os.path.join(base, subdir), os.path.join(name, subdir)) + + @noPosargs + @noKwargs + def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: + return self.version + + @typed_pos_args('python_installation.has_path', str) + @noKwargs + def has_path_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return args[0] in self.paths + + @typed_pos_args('python_installation.get_path', str, optargs=[object]) + @noKwargs + def get_path_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': + path_name, fallback = args + try: + return self.paths[path_name] + except KeyError: + if fallback is not None: + return fallback + raise InvalidArguments(f'{path_name} is not a valid path name') + + @typed_pos_args('python_installation.has_variable', str) + @noKwargs + def has_variable_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool: + return args[0] in self.variables + + @typed_pos_args('python_installation.get_variable', str, optargs=[object]) + @noKwargs + def get_variable_method(self, args: T.Tuple[str, T.Optional['TYPE_var']], kwargs: 'TYPE_kwargs') -> 'TYPE_var': + var_name, fallback = args + try: + return self.variables[var_name] + except KeyError: + if fallback is not None: + return fallback + raise InvalidArguments(f'{var_name} is not a valid variable name') + + @noPosargs + @noKwargs + @FeatureNew('Python module path method', '0.50.0') + def path_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: + return super().path_method(args, kwargs) + + +class PythonModule(ExtensionModule): + + INFO = ModuleInfo('python', '0.46.0') + + def __init__(self, interpreter: 'Interpreter') -> None: + super().__init__(interpreter) + self.installations: T.Dict[str, ExternalProgram] = {} + self.methods.update({ + 'find_installation': self.find_installation, + }) + + # https://www.python.org/dev/peps/pep-0397/ + @staticmethod + def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: + if name_or_path not in ['python2', 'python3']: + return None + if not shutil.which('py'): + # program not installed, return without an exception + return None + ver = {'python2': '-2', 'python3': '-3'}[name_or_path] + cmd = ['py', ver, '-c', "import sysconfig; print(sysconfig.get_config_var('BINDIR'))"] + _, stdout, _ = mesonlib.Popen_safe(cmd) + directory = stdout.strip() + if os.path.exists(directory): + return os.path.join(directory, 'python') + else: + return None + + def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> ExternalProgram: + if not name_or_path: + python = PythonExternalProgram('python3', mesonlib.python_command) + else: + tmp_python = ExternalProgram.from_entry(display_name, name_or_path) + python = PythonExternalProgram(display_name, ext_prog=tmp_python) + + if not python.found() and mesonlib.is_windows(): + pythonpath = self._get_win_pythonpath(name_or_path) + if pythonpath is not None: + name_or_path = pythonpath + python = PythonExternalProgram(name_or_path) + + # Last ditch effort, python2 or python3 can be named python + # on various platforms, let's not give up just yet, if an executable + # named python is available and has a compatible version, let's use + # it + if not python.found() and name_or_path in {'python2', 'python3'}: + python = PythonExternalProgram('python') + + if python.found(): + if python.sanity(state): + return python + else: + sanitymsg = f'{python} is not a valid python or it is missing distutils' + if required: + raise mesonlib.MesonException(sanitymsg) + else: + mlog.warning(sanitymsg, location=state.current_node) + + return NonExistingExternalProgram() + + @disablerIfNotFound + @typed_pos_args('python.find_installation', optargs=[str]) + @typed_kwargs( + 'python.find_installation', + KwargInfo('required', (bool, UserFeatureOption), default=True), + KwargInfo('disabler', bool, default=False, since='0.49.0'), + KwargInfo('modules', ContainerTypeInfo(list, str), listify=True, default=[], since='0.51.0'), + _PURE_KW.evolve(default=True, since='0.64.0'), + ) + def find_installation(self, state: 'ModuleState', args: T.Tuple[T.Optional[str]], + kwargs: 'FindInstallationKw') -> ExternalProgram: + feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0') + disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check) + + # FIXME: this code is *full* of sharp corners. It assumes that it's + # going to get a string value (or now a list of length 1), of `python2` + # or `python3` which is completely nonsense. On windows the value could + # easily be `['py', '-3']`, or `['py', '-3.7']` to get a very specific + # version of python. On Linux we might want a python that's not in + # $PATH, or that uses a wrapper of some kind. + np: T.List[str] = state.environment.lookup_binary_entry(MachineChoice.HOST, 'python') or [] + fallback = args[0] + display_name = fallback or 'python' + if not np and fallback is not None: + np = [fallback] + name_or_path = np[0] if np else None + + if disabled: + mlog.log('Program', name_or_path or 'python', 'found:', mlog.red('NO'), '(disabled by:', mlog.bold(feature), ')') + return NonExistingExternalProgram() + + python = self.installations.get(name_or_path) + if not python: + python = self._find_installation_impl(state, display_name, name_or_path, required) + self.installations[name_or_path] = python + + want_modules = kwargs['modules'] + found_modules: T.List[str] = [] + missing_modules: T.List[str] = [] + if python.found() and want_modules: + for mod in want_modules: + p, *_ = mesonlib.Popen_safe( + python.command + + ['-c', f'import {mod}']) + if p.returncode != 0: + missing_modules.append(mod) + else: + found_modules.append(mod) + + msg: T.List['mlog.TV_Loggable'] = ['Program', python.name] + if want_modules: + msg.append('({})'.format(', '.join(want_modules))) + msg.append('found:') + if python.found() and not missing_modules: + msg.extend([mlog.green('YES'), '({})'.format(' '.join(python.command))]) + else: + msg.append(mlog.red('NO')) + if found_modules: + msg.append('modules:') + msg.append(', '.join(found_modules)) + + mlog.log(*msg) + + if not python.found(): + if required: + raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python')) + return NonExistingExternalProgram() + elif missing_modules: + if required: + raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules))) + return NonExistingExternalProgram() + else: + python = copy.copy(python) + python.pure = kwargs['pure'] + return python + + raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') + + +def initialize(interpreter: 'Interpreter') -> PythonModule: + mod = PythonModule(interpreter) + mod.interpreter.append_holder_map(PythonExternalProgram, PythonInstallation) + return mod |