The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
18 KiB
431 lines
18 KiB
# SPDX-License-Identifier: Apache-2.0 |
|
# Copyright 2022 The Meson development team |
|
|
|
from __future__ import annotations |
|
|
|
import functools, json, os, textwrap |
|
from pathlib import Path |
|
import typing as T |
|
|
|
from .. import mesonlib, mlog |
|
from .base import process_method_kw, DependencyException, DependencyMethods, DependencyTypeName, ExternalDependency, SystemDependency |
|
from .configtool import ConfigToolDependency |
|
from .detect import packages |
|
from .factory import DependencyFactory |
|
from .framework import ExtraFrameworkDependency |
|
from .pkgconfig import PkgConfigDependency |
|
from ..environment import detect_cpu_family |
|
from ..programs import ExternalProgram |
|
|
|
if T.TYPE_CHECKING: |
|
from typing_extensions import TypedDict |
|
|
|
from .factory import DependencyGenerator |
|
from ..environment import Environment |
|
from ..mesonlib import MachineChoice |
|
|
|
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 |
|
limited_api_suffix: str |
|
variables: T.Dict[str, str] |
|
version: str |
|
|
|
_Base = ExternalDependency |
|
else: |
|
_Base = object |
|
|
|
|
|
class Pybind11ConfigToolDependency(ConfigToolDependency): |
|
|
|
tools = ['pybind11-config'] |
|
|
|
# any version of the tool is valid, since this is header-only |
|
allow_default_for_cross = True |
|
|
|
# pybind11 in 2.10.4 added --version, sanity-check another flag unique to it |
|
# in the meantime |
|
skip_version = '--pkgconfigdir' |
|
|
|
def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any]): |
|
super().__init__(name, environment, kwargs) |
|
if not self.is_found: |
|
return |
|
self.compile_args = self.get_config_value(['--includes'], 'compile_args') |
|
|
|
|
|
class NumPyConfigToolDependency(ConfigToolDependency): |
|
|
|
tools = ['numpy-config'] |
|
|
|
def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any]): |
|
super().__init__(name, environment, kwargs) |
|
if not self.is_found: |
|
return |
|
self.compile_args = self.get_config_value(['--cflags'], 'compile_args') |
|
|
|
|
|
class BasicPythonExternalProgram(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 |
|
self.cached_version = None |
|
|
|
# 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': 'sentinel', |
|
'suffix': 'sentinel', |
|
'limited_api_suffix': 'sentinel', |
|
'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) -> bool: |
|
# Sanity check, we expect to have something that at least quacks in tune |
|
|
|
import importlib.resources |
|
|
|
with importlib.resources.path('mesonbuild.scripts', 'python_info.py') as f: |
|
cmd = self.get_command() + [str(f)] |
|
env = os.environ.copy() |
|
env['SETUPTOOLS_USE_DISTUTILS'] = 'stdlib' |
|
p, stdout, stderr = mesonlib.Popen_safe(cmd, env=env) |
|
|
|
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) |
|
return True |
|
else: |
|
return False |
|
|
|
|
|
class _PythonDependencyBase(_Base): |
|
|
|
def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool): |
|
self.embed = embed |
|
self.version: str = python_holder.info['version'] |
|
self.platform = python_holder.info['platform'] |
|
self.variables = python_holder.info['variables'] |
|
self.paths = python_holder.info['paths'] |
|
self.is_pypy = python_holder.info['is_pypy'] |
|
# 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.info['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: 'BasicPythonExternalProgram', |
|
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: 'BasicPythonExternalProgram'): |
|
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: 'BasicPythonExternalProgram'): |
|
SystemDependency.__init__(self, name, environment, kwargs) |
|
_PythonDependencyBase.__init__(self, installation, kwargs.get('embed', False)) |
|
|
|
# match pkg-config behavior |
|
if self.link_libpython: |
|
# link args |
|
if mesonlib.is_windows(): |
|
self.find_libpy_windows(environment, limited_api=False) |
|
else: |
|
self.find_libpy(environment) |
|
else: |
|
self.is_found = True |
|
|
|
# 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 mesonlib.is_windows() and self.get_windows_python_arch().endswith('64') and mesonlib.version_compare(self.version, '<3.12'): |
|
self.compile_args += ['-DMS_WIN64='] |
|
|
|
if not self.clib_compiler.has_header('Python.h', '', environment, extra_args=self.compile_args)[0]: |
|
self.is_found = False |
|
|
|
def find_libpy(self, environment: 'Environment') -> None: |
|
if self.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 = True |
|
|
|
def get_windows_python_arch(self) -> str: |
|
if self.platform.startswith('mingw'): |
|
if 'x86_64' in self.platform: |
|
return 'x86_64' |
|
elif 'i686' in self.platform: |
|
return 'x86' |
|
elif 'aarch64' in self.platform: |
|
return 'aarch64' |
|
else: |
|
raise DependencyException(f'MinGW Python built with unknown platform {self.platform!r}, please file a bug') |
|
elif self.platform == 'win32': |
|
return 'x86' |
|
elif self.platform in {'win64', 'win-amd64'}: |
|
return 'x86_64' |
|
elif self.platform in {'win-arm64'}: |
|
return 'aarch64' |
|
raise DependencyException('Unknown Windows Python platform {self.platform!r}') |
|
|
|
def get_windows_link_args(self, limited_api: bool) -> 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: |
|
if limited_api: |
|
vernum = vernum[0] |
|
libpath = Path('libs') / f'python{vernum}.lib' |
|
# For a debug build, pyconfig.h may force linking with |
|
# pythonX_d.lib (see meson#10776). This cannot be avoided |
|
# and won't work unless we also have a debug build of |
|
# Python itself (except with pybind11, which has an ugly |
|
# hack to work around this) - so emit a warning to explain |
|
# the cause of the expected link error. |
|
buildtype = self.env.coredata.get_option(mesonlib.OptionKey('buildtype')) |
|
assert isinstance(buildtype, str) |
|
debug = self.env.coredata.get_option(mesonlib.OptionKey('debug')) |
|
# `debugoptimized` buildtype may not set debug=True currently, see gh-11645 |
|
is_debug_build = debug or buildtype == 'debug' |
|
vscrt_debug = False |
|
if mesonlib.OptionKey('b_vscrt') in self.env.coredata.options: |
|
vscrt = self.env.coredata.options[mesonlib.OptionKey('b_vscrt')].value |
|
if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: |
|
vscrt_debug = True |
|
if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): |
|
mlog.warning(textwrap.dedent('''\ |
|
Using a debug build type with MSVC or an MSVC-compatible compiler |
|
when the Python interpreter is not also a debug build will almost |
|
certainly result in a failed build. Prefer using a release build |
|
type or a debug Python interpreter. |
|
''')) |
|
# base_prefix to allow for virtualenvs. |
|
lib = Path(self.variables.get('base_prefix')) / libpath |
|
elif self.platform.startswith('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', limited_api: bool = False) -> None: |
|
''' |
|
Find python3 libraries on Windows and also verify that the arch matches |
|
what we are building for. |
|
''' |
|
try: |
|
pyarch = self.get_windows_python_arch() |
|
except DependencyException as e: |
|
mlog.log(str(e)) |
|
self.is_found = False |
|
return |
|
arch = detect_cpu_family(env.coredata.compilers.host) |
|
if arch != pyarch: |
|
mlog.log('Need', mlog.bold(self.name), f'for {arch}, but found {pyarch}') |
|
self.is_found = False |
|
return |
|
# This can fail if the library is not found |
|
largs = self.get_windows_link_args(limited_api) |
|
if largs is None: |
|
self.is_found = False |
|
return |
|
self.link_args = largs |
|
self.is_found = True |
|
|
|
@staticmethod |
|
def log_tried() -> str: |
|
return 'sysconfig' |
|
|
|
def python_factory(env: 'Environment', for_machine: 'MachineChoice', |
|
kwargs: T.Dict[str, T.Any], |
|
installation: T.Optional['BasicPythonExternalProgram'] = None) -> T.List['DependencyGenerator']: |
|
# We can't use the factory_methods decorator here, as we need to pass the |
|
# extra installation argument |
|
methods = process_method_kw({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM}, kwargs) |
|
embed = kwargs.get('embed', False) |
|
candidates: T.List['DependencyGenerator'] = [] |
|
from_installation = installation is not None |
|
# When not invoked through the python module, default installation. |
|
if installation is None: |
|
installation = BasicPythonExternalProgram('python3', mesonlib.python_command) |
|
installation.sanity() |
|
pkg_version = installation.info['variables'].get('LDVERSION') or installation.info['version'] |
|
|
|
if DependencyMethods.PKGCONFIG in methods: |
|
if from_installation: |
|
pkg_libdir = installation.info['variables'].get('LIBPC') |
|
pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.info['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: 'BasicPythonExternalProgram') -> 'ExternalDependency': |
|
if not pkg_libdir: |
|
# there is no LIBPC, so we can't search in it |
|
empty = ExternalDependency(DependencyTypeName('pkgconfig'), env, {}) |
|
empty.name = 'python' |
|
return empty |
|
|
|
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: str, value: str) -> None: |
|
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)) |
|
else: |
|
candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) |
|
|
|
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 |
|
|
|
packages['python3'] = python_factory |
|
|
|
packages['pybind11'] = pybind11_factory = DependencyFactory( |
|
'pybind11', |
|
[DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.CMAKE], |
|
configtool_class=Pybind11ConfigToolDependency, |
|
) |
|
|
|
packages['numpy'] = numpy_factory = DependencyFactory( |
|
'numpy', |
|
[DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL], |
|
configtool_class=NumPyConfigToolDependency, |
|
)
|
|
|