diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index 3a912805a057..6cee1a216ff9 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -408,6 +408,7 @@ install prefix. For example: if the install prefix is `/usr` and the | platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | | purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | | allow_limited_api | true | true, false | Disables project-wide use of the Python Limited API (Since 1.3.0) | +| build_config | | File path | Specifies the Python build configuration file (PEP 739) (Since 1.7.0) | *Since 0.60.0* The `python.platlibdir` and `python.purelibdir` options are used by the python module methods `python.install_sources()` and diff --git a/mesonbuild/dependencies/pkgconfig.py b/mesonbuild/dependencies/pkgconfig.py index 447b69ea070f..03b25a2ffcfa 100644 --- a/mesonbuild/dependencies/pkgconfig.py +++ b/mesonbuild/dependencies/pkgconfig.py @@ -41,12 +41,13 @@ def set_program_override(pkg_bin: ExternalProgram, for_machine: MachineChoice) - PkgConfigInterface.pkg_bin_per_machine[for_machine] = pkg_bin @staticmethod - def instance(env: Environment, for_machine: MachineChoice, silent: bool) -> T.Optional[PkgConfigInterface]: + def instance(env: Environment, for_machine: MachineChoice, silent: bool, + extra_paths: T.Optional[T.List[str]] = None) -> T.Optional[PkgConfigInterface]: '''Return a pkg-config implementation singleton''' for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST impl = PkgConfigInterface.class_impl[for_machine] if impl is False: - impl = PkgConfigCLI(env, for_machine, silent, PkgConfigInterface.pkg_bin_per_machine[for_machine]) + impl = PkgConfigCLI(env, for_machine, silent, PkgConfigInterface.pkg_bin_per_machine[for_machine], extra_paths) if not impl.found(): impl = None if not impl and not silent: @@ -55,7 +56,9 @@ def instance(env: Environment, for_machine: MachineChoice, silent: bool) -> T.Op return impl @staticmethod - def _cli(env: Environment, for_machine: MachineChoice, silent: bool = False) -> T.Optional[PkgConfigCLI]: + def _cli(env: Environment, for_machine: MachineChoice, + extra_paths: T.Optional[T.List[str]] = None, + silent: bool = False) -> T.Optional[PkgConfigCLI]: '''Return the CLI pkg-config implementation singleton Even when we use another implementation internally, external tools might still need the CLI implementation. @@ -66,15 +69,16 @@ def _cli(env: Environment, for_machine: MachineChoice, silent: bool = False) -> if impl and not isinstance(impl, PkgConfigCLI): impl = PkgConfigInterface.class_cli_impl[for_machine] if impl is False: - impl = PkgConfigCLI(env, for_machine, silent, PkgConfigInterface.pkg_bin_per_machine[for_machine]) + impl = PkgConfigCLI(env, for_machine, silent, PkgConfigInterface.pkg_bin_per_machine[for_machine], extra_paths) if not impl.found(): impl = None PkgConfigInterface.class_cli_impl[for_machine] = impl return T.cast('T.Optional[PkgConfigCLI]', impl) # Trust me, mypy @staticmethod - def get_env(env: Environment, for_machine: MachineChoice, uninstalled: bool = False) -> EnvironmentVariables: - cli = PkgConfigInterface._cli(env, for_machine) + def get_env(env: Environment, for_machine: MachineChoice, uninstalled: bool = False, + extra_paths: T.Optional[T.List[str]] = None) -> EnvironmentVariables: + cli = PkgConfigInterface._cli(env, for_machine, extra_paths) return cli._get_env(uninstalled) if cli else EnvironmentVariables() @staticmethod @@ -123,11 +127,13 @@ class PkgConfigCLI(PkgConfigInterface): '''pkg-config CLI implementation''' def __init__(self, env: Environment, for_machine: MachineChoice, silent: bool, - pkgbin: T.Optional[ExternalProgram] = None) -> None: + pkgbin: T.Optional[ExternalProgram] = None, + extra_paths: T.Optional[T.List[str]] = None) -> None: super().__init__(env, for_machine) self._detect_pkgbin(pkgbin) if self.pkgbin and not silent: mlog.log('Found pkg-config:', mlog.green('YES'), mlog.bold(f'({self.pkgbin.get_path()})'), mlog.blue(self.pkgbin_version)) + self.extra_paths = extra_paths or [] def found(self) -> bool: return bool(self.pkgbin) @@ -256,7 +262,7 @@ def _check_pkgconfig(self, pkgbin: ExternalProgram) -> T.Optional[str]: def _get_env(self, uninstalled: bool = False) -> EnvironmentVariables: env = EnvironmentVariables() key = OptionKey('pkg_config_path', machine=self.for_machine) - extra_paths: T.List[str] = self.env.coredata.optstore.get_value(key)[:] + extra_paths: T.List[str] = self.env.coredata.optstore.get_value(key)[:] + self.extra_paths if uninstalled: bpath = self.env.get_build_dir() if bpath is not None: @@ -295,11 +301,13 @@ def _call_pkgbin(self, args: T.List[str], env: T.Optional[EnvironOrDict] = None) class PkgConfigDependency(ExternalDependency): def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any], - language: T.Optional[str] = None) -> None: + language: T.Optional[str] = None, + extra_paths: T.Optional[T.List[str]] = None) -> None: super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language) self.name = name self.is_libtool = False - pkgconfig = PkgConfigInterface.instance(self.env, self.for_machine, self.silent) + self.extra_paths = extra_paths + pkgconfig = PkgConfigInterface.instance(self.env, self.for_machine, self.silent, self.extra_paths) if not pkgconfig: msg = f'Pkg-config for machine {self.for_machine} not found. Giving up.' if self.required: diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py index 326e605d8543..d1b0942d793b 100644 --- a/mesonbuild/dependencies/python.py +++ b/mesonbuild/dependencies/python.py @@ -3,12 +3,12 @@ from __future__ import annotations -import functools, json, os, textwrap +import functools, json, os, sys, 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 .base import process_method_kw, DependencyException, DependencyMethods, ExternalDependency, SystemDependency from .configtool import ConfigToolDependency from .detect import packages from .factory import DependencyFactory @@ -45,6 +45,11 @@ class PythonIntrospectionDict(TypedDict): _Base = object +if sys.version_info >= (3, 8): + from functools import cached_property +else: + cached_property = property + class Pybind11ConfigToolDependency(ConfigToolDependency): tools = ['pybind11-config'] @@ -74,9 +79,97 @@ def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.An self.compile_args = self.get_config_value(['--cflags'], 'compile_args') +class PythonBuildConfig: + """PEP 739 build-details.json config file.""" + + """Schema version currently implemented.""" + IMPLEMENTED_VERSION: T.Final[str] = '1.0' + """Path keys — may be relative, need to be expanded.""" + _PATH_KEYS = ( + 'base_interpreter', + 'libpython.dynamic', + 'libpython.dynamic_stableabi', + 'libpython.static', + 'c_api.headers', + 'c_api.pkgconfig_path', + ) + + def __init__(self, path: str) -> None: + self._path = Path(path) + + try: + self._data = json.loads(self._path.read_text(encoding='utf8')) + except OSError as e: + raise DependencyException(f'Failed to read python.build_config: {e}') from e + + self._validate_data() + self._expand_paths() + + def __getitem__(self, key: str) -> T.Any: + value = self._data + for part in key.split('.'): + value = value[part] + return value + + def __contains__(self, key: str) -> bool: + try: + self[key] + except KeyError: + return False + else: + return True + + def get(self, key: str, default: T.Any = None) -> T.Any: + try: + self[key] + except KeyError: + return default + + def _validate_data(self) -> None: + schema_version = self._data['schema_version'] + if mesonlib.version_compare(schema_version, '< 1.0'): + raise DependencyException(f'Invalid schema_version in python.build_config: {schema_version}') + if mesonlib.version_compare(schema_version, '>= 2.0'): + raise DependencyException( + f'Unsupported schema_version {schema_version!r} in python.build_config, ' + f'but we only implement suport for {self.IMPLEMENTED_VERSION!r}' + ) + # Schema version that we currently understand + if mesonlib.version_compare(schema_version, f'> {self.IMPLEMENTED_VERSION}'): + mlog.log( + f'python.build_config has schema_version {schema_version!r}, ' + f'but we only implement suport for {self.IMPLEMENTED_VERSION!r}, ' + 'new functionality might be missing' + ) + + def _expand_paths(self) -> None: + """Expand relative path (they're relative to base_prefix).""" + for key in self._PATH_KEYS: + if key not in self: + continue + parent, _, child = key.rpartition('.') + container = self[parent] if parent else self._data + path = Path(container[child]) + if not path.is_absolute(): + container[child] = os.fspath(self.base_prefix / path) + + @property + def config_path(self) -> Path: + return self._path + + @cached_property + def base_prefix(self) -> Path: + path = Path(self._data['base_prefix']) + if path.is_absolute(): + return path + # Non-absolute paths are relative to the build config directory + return self.config_path.parent / path + + class BasicPythonExternalProgram(ExternalProgram): def __init__(self, name: str, command: T.Optional[T.List[str]] = None, - ext_prog: T.Optional[ExternalProgram] = None): + ext_prog: T.Optional[ExternalProgram] = None, + build_config_path: T.Optional[str] = None): if ext_prog is None: super().__init__(name, command=command, silent=True) else: @@ -86,6 +179,8 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None, self.cached_version = None self.version_arg = '--version' + self.build_config = PythonBuildConfig(build_config_path) if build_config_path else 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 @@ -106,6 +201,15 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None, } self.pure: bool = True + @property + def version(self) -> str: + if self.build_config: + value = self.build_config['language']['version'] + else: + value = self.info['variables'].get('LDVERSION') or self.info['version'] + assert isinstance(value, str) + return value + def _check_version(self, version: str) -> bool: if self.name == 'python2': return mesonlib.version_compare(version, '< 3.0') @@ -116,6 +220,14 @@ def _check_version(self, version: str) -> bool: def sanity(self) -> bool: # Sanity check, we expect to have something that at least quacks in tune + if self.build_config: + if not self.build_config['libpython']: + mlog.debug('This Python installation does not provide a libpython') + return False + if not self.build_config['c_api']: + mlog.debug('This Python installation does support the C API') + return False + import importlib.resources with importlib.resources.path('mesonbuild.scripts', 'python_info.py') as f: @@ -145,12 +257,24 @@ 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.build_config = python_holder.build_config + + if self.build_config: + self.version = self.build_config['language']['version'] + self.platform = self.build_config['platform'] + self.is_freethreaded = 't' in self.build_config['abi']['flags'] + self.link_libpython = self.build_config['libpython']['link_extensions'] + else: + self.version = python_holder.info['version'] + self.platform = python_holder.info['platform'] + self.is_freethreaded = python_holder.info['is_freethreaded'] + self.link_libpython = python_holder.info['link_libpython'] + # This data shouldn't be needed when build_config is set + self.is_pypy = python_holder.info['is_pypy'] + self.variables = python_holder.info['variables'] + self.paths = python_holder.info['paths'] - self.is_pypy = python_holder.info['is_pypy'] - self.is_freethreaded = python_holder.info['is_freethreaded'] + # 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 @@ -159,7 +283,9 @@ def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool): # 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 + if not self.link_libpython: + self.link_libpython = embed + self.info: T.Optional[T.Dict[str, str]] = None if mesonlib.version_compare(self.version, '>= 3.0'): self.major_version = 3 @@ -173,20 +299,27 @@ def __init__(self, python_holder: 'BasicPythonExternalProgram', embed: bool): self.compile_args += ['-DPy_GIL_DISABLED'] 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] + if self.build_config: + path = self.build_config['libpython'].get('dynamic') + if not path: + raise DependencyException('Python does not provide a dynamic libpython library') + libdirs = [os.path.dirname(path)] + libname = os.path.basename(path) 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 = [] + 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: @@ -212,6 +345,15 @@ def get_windows_python_arch(self) -> str: 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.build_config: + if self.static: + key = 'static' + elif limited_api: + key = 'dynamic-stableabi' + else: + key = 'dynamic' + return [self.build_config['libpython'][key]] + if self.platform.startswith('win'): vernum = self.variables.get('py_version_nodot') verdot = self.variables.get('py_version_short') @@ -309,19 +451,28 @@ def find_libpy_windows(self, env: 'Environment', limited_api: bool = False) -> N 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') + def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any], + installation: 'BasicPythonExternalProgram', embed: bool): + pkg_embed = '-embed' if embed and mesonlib.version_compare(installation.info['version'], '>=3.8') else '' + pkg_name = f'python-{installation.version}{pkg_embed}' + + if installation.build_config: + pkg_libdir = installation.build_config['c_api']['pkgconfig_path'] + pkg_libdir_origin = 'c_api.pkgconfig_path from the Python build config' else: - mlog.debug(f'Searching for {name!r} via fallback pkgconfig lookup in default paths') + pkg_libdir = installation.info['variables'].get('LIBPC') + pkg_libdir_origin = 'LIBPC' if pkg_libdir else 'the default paths' + mlog.debug(f'Searching for {pkg_libdir!r} via pkgconfig lookup in {pkg_libdir_origin}') + pkgconfig_paths = [pkg_libdir] if pkg_libdir else [] - PkgConfigDependency.__init__(self, name, environment, kwargs) + PkgConfigDependency.__init__(self, pkg_name, environment, kwargs, extra_paths=pkgconfig_paths) _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') + if pkg_libdir and not self.is_found: + mlog.debug( + f'{pkg_name!r} could not be found in {pkg_libdir_origin}, ' + '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'): @@ -354,10 +505,13 @@ def __init__(self, name: str, environment: 'Environment', self.is_found = True # compile args - inc_paths = mesonlib.OrderedSet([ - self.variables.get('INCLUDEPY'), - self.paths.get('include'), - self.paths.get('platinclude')]) + if self.build_config: + inc_paths = mesonlib.OrderedSet([self.build_config['c_api']['headers']]) + else: + 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] @@ -386,42 +540,10 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', 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)) + candidates.append(functools.partial(PythonPkgConfigDependency, env, kwargs, installation, embed)) else: candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) @@ -430,7 +552,7 @@ def set_env(name: str, value: str) -> None: if DependencyMethods.EXTRAFRAMEWORK in methods: nkwargs = kwargs.copy() - if mesonlib.version_compare(pkg_version, '>= 3'): + if mesonlib.version_compare(installation.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'] diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index 1b7a05640374..53145ab9ee37 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -115,17 +115,27 @@ def __init__(self, python: 'PythonExternalProgram', interpreter: 'Interpreter'): info = python.info prefix = self.interpreter.environment.coredata.get_option(OptionKey('prefix')) assert isinstance(prefix, str), 'for mypy' + + if python.build_config: + self.version = python.build_config['language']['version'] + self.platform = python.build_config['platform'] + self.suffix = python.build_config['abi']['extension_suffix'] + self.limited_api_suffix = python.build_config['abi']['stable_abi_suffix'] + self.link_libpython = python.build_config['libpython']['link_extensions'] + self.is_pypy = python.build_config['implementation']['name'] == 'pypy' + else: + self.version = info['version'] + self.platform = info['platform'] + self.suffix = info['suffix'] + self.limited_api_suffix = info['limited_api_suffix'] + self.link_libpython = info['link_libpython'] + self.is_pypy = info['is_pypy'] + self.variables = info['variables'] - self.suffix = info['suffix'] - self.limited_api_suffix = info['limited_api_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, @@ -256,8 +266,12 @@ def _dependency_method_impl(self, kwargs: TYPE_kwargs) -> Dependency: if dep is not None: return dep + build_config = self.interpreter.environment.coredata.get_option(OptionKey('python.build_config')) + new_kwargs = kwargs.copy() new_kwargs['required'] = False + if build_config: + new_kwargs['build_config'] = build_config candidates = python_factory(self.interpreter.environment, for_machine, new_kwargs, self.held_object) dep = find_external_dependency('python', self.interpreter.environment, new_kwargs, candidates) @@ -441,11 +455,13 @@ def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: return None def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_or_path: str, required: bool) -> MaybePythonProg: + build_config = self.interpreter.environment.coredata.get_option(OptionKey('python.build_config')) + if not name_or_path: - python = PythonExternalProgram('python3', mesonlib.python_command) + python = PythonExternalProgram('python3', mesonlib.python_command, build_config_path=build_config) else: tmp_python = ExternalProgram.from_entry(display_name, name_or_path) - python = PythonExternalProgram(display_name, ext_prog=tmp_python) + python = PythonExternalProgram(display_name, ext_prog=tmp_python, build_config_path=build_config) if not python.found() and mesonlib.is_windows(): pythonpath = self._get_win_pythonpath(name_or_path) @@ -459,7 +475,7 @@ def _find_installation_impl(self, state: 'ModuleState', display_name: str, name_ # it if not python.found() and name_or_path in {'python2', 'python3'}: tmp_python = ExternalProgram.from_entry(display_name, 'python') - python = PythonExternalProgram(name_or_path, ext_prog=tmp_python) + python = PythonExternalProgram(name_or_path, ext_prog=tmp_python, build_config_path=build_config) if python.found(): if python.sanity(state): diff --git a/mesonbuild/options.py b/mesonbuild/options.py index 1566f940c98c..3c48dab9c96d 100644 --- a/mesonbuild/options.py +++ b/mesonbuild/options.py @@ -663,6 +663,8 @@ def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffi BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')), (OptionKey('python.allow_limited_api'), BuiltinOption(UserBooleanOption, 'Whether to allow use of the Python Limited API', True)), + (OptionKey('python.build_config'), + BuiltinOption(UserStringOption, 'Config file containing the build details for the target Python installation.', '')), ]) BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items())) diff --git a/test cases/unit/125 python extension/foo.c b/test cases/unit/125 python extension/foo.c new file mode 100644 index 000000000000..0b39d704399b --- /dev/null +++ b/test cases/unit/125 python extension/foo.c @@ -0,0 +1,31 @@ +#define PY_SSIZE_T_CLEAN +#include + + +static PyObject * +bar_impl(PyObject *self) +{ + return Py_None; +} + + +static PyMethodDef foo_methods[] = { + {"bar", bar_impl, METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL} /* sentinel */ +}; + + +static struct PyModuleDef foo_module = { + PyModuleDef_HEAD_INIT, + "foo", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + foo_methods, /* m_methods */ +}; + + +PyMODINIT_FUNC +PyInit_foo(void) +{ + return PyModule_Create(&foo_module); +} diff --git a/test cases/unit/125 python extension/meson.build b/test cases/unit/125 python extension/meson.build new file mode 100644 index 000000000000..c9885d99a934 --- /dev/null +++ b/test cases/unit/125 python extension/meson.build @@ -0,0 +1,15 @@ +project('python extension', 'c') + +py = import('python').find_installation('') + +py.extension_module( + 'foo', 'foo.c', + install: true, +) + +py.extension_module( + 'foo_stable', 'foo.c', + install: true, + limited_api: '3.2', +) + diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 96576b0ee888..761a94c01209 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -12,6 +12,7 @@ import pickle import zipfile, tarfile import sys +import sysconfig from unittest import mock, SkipTest, skipIf, skipUnless from contextlib import contextmanager from glob import glob @@ -2929,6 +2930,88 @@ def test_pkg_config_libdir(self): self.wipe() self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env) + @skipIf(is_windows(), 'POSIX only') + def test_python_build_config_extensions(self): + testdir = os.path.join(self.unit_test_dir, + '125 python extension') + + VERSION_INFO_KEYS = ('major', 'minor', 'micro', 'releaselevel', 'serial') + EXTENSION_SUFFIX = '.extension-suffix.so' + STABLE_ABI_SUFFIX = '.stable-abi-suffix.so' + + python_build_config = { + 'schema_version': '1.0', + 'base_interpreter': sys.executable, + 'base_prefix': '/usr', + 'platform': sysconfig.get_platform(), + 'language': { + 'version': sysconfig.get_python_version(), + 'version_info': {key: getattr(sys.version_info, key) for key in VERSION_INFO_KEYS} + }, + 'implementation': { + attr: ( + getattr(sys.implementation, attr) + if attr != 'version' else + {key: getattr(sys.implementation.version, key) for key in VERSION_INFO_KEYS} + ) + for attr in dir(sys.implementation) + if not attr.startswith('__') + }, + 'abi': { + 'flags': [], + 'extension_suffix': EXTENSION_SUFFIX, + 'stable_abi_suffix': STABLE_ABI_SUFFIX, + }, + 'suffixes': { + 'source': ['.py'], + 'bytecode': ['.pyc'], + 'optimized_bytecode': ['.pyc'], + 'debug_bytecode': ['.pyc'], + 'extensions': [EXTENSION_SUFFIX, STABLE_ABI_SUFFIX, '.so'], + }, + 'libpython': { + 'dynamic': sysconfig.get_config_var('LDLIBRARY'), + 'dynamic_stableabi': sysconfig.get_config_var('PY3LIBRARY'), + 'static': sysconfig.get_config_var('LIBRARY'), + 'link_extensions': True, + }, + 'c_api': { + 'headers': sysconfig.get_path('include'), + 'pkgconfig_path': sysconfig.get_config_var('LIBPC'), + } + } + with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as python_build_config_file: + json.dump(python_build_config, fp=python_build_config_file) + + with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as cross_file: + cross_file.write( + textwrap.dedent(f''' + [binaries] + pkg-config = 'pkg-config' + + [built-in options] + python.build_config = '{python_build_config_file.name}' + '''.strip()) + ) + cross_file.flush() + + intro_installed_file = os.path.join(self.builddir, 'meson-info', 'intro-installed.json') + expected_files = [ + os.path.join(self.builddir, 'foo' + EXTENSION_SUFFIX), + os.path.join(self.builddir, 'foo_stable' + STABLE_ABI_SUFFIX), + ] + + for extra_args in ( + ['--python.build-config', python_build_config_file.name], + ['--cross-file', cross_file.name], + ): + with self.subTest(extra_args=extra_args): + self.init(testdir, extra_args=extra_args) + with open(intro_installed_file) as f: + intro_installed = json.load(f) + self.assertEqual(expected_files, list(intro_installed)) + self.wipe() + def __reconfigure(self): # Set an older version to force a reconfigure from scratch filename = os.path.join(self.privatedir, 'coredata.dat')