summaryrefslogtreecommitdiff
path: root/mesonbuild/modules/python.py
diff options
context:
space:
mode:
authorAndrew McNulty <amcn102@gmail.com>2023-04-24 09:52:28 +0200
committerEli Schwartz <eschwartz93@gmail.com>2023-08-14 20:02:09 -0400
commitc7308076966c1c55bc117ce9f7a7f49ac96acfa6 (patch)
tree826fcf546090c3a5155c1d730d34033563038d98 /mesonbuild/modules/python.py
parent9d323020321893093492bc7d538c311c61398a1e (diff)
downloadmeson-c7308076966c1c55bc117ce9f7a7f49ac96acfa6.tar.gz
Python: Add 'limited_api' kwarg to extension_module
This commit adds a new keyword arg to extension_module() that enables a user to target the Python Limited API, declaring the version of the limited API that they wish to target. Two new unittests have been added to test this functionality.
Diffstat (limited to 'mesonbuild/modules/python.py')
-rw-r--r--mesonbuild/modules/python.py83
1 files changed, 75 insertions, 8 deletions
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py
index d0067db5a..c8af224f8 100644
--- a/mesonbuild/modules/python.py
+++ b/mesonbuild/modules/python.py
@@ -13,7 +13,7 @@
# limitations under the License.
from __future__ import annotations
-import copy, json, os, shutil
+import copy, json, os, shutil, re
import typing as T
from . import ExtensionModule, ModuleInfo
@@ -32,7 +32,7 @@ from ..interpreterbase import (
InvalidArguments, typed_pos_args, typed_kwargs, KwargInfo,
FeatureNew, FeatureNewKwargs, disablerIfNotFound
)
-from ..mesonlib import MachineChoice
+from ..mesonlib import MachineChoice, OptionKey
from ..programs import ExternalProgram, NonExistingExternalProgram
if T.TYPE_CHECKING:
@@ -65,7 +65,7 @@ if T.TYPE_CHECKING:
MaybePythonProg = T.Union[NonExistingExternalProgram, 'PythonExternalProgram']
-mod_kwargs = {'subdir'}
+mod_kwargs = {'subdir', 'limited_api'}
mod_kwargs.update(known_shmod_kwargs)
mod_kwargs -= {'name_prefix', 'name_suffix'}
@@ -114,6 +114,7 @@ class PythonExternalProgram(BasicPythonExternalProgram):
_PURE_KW = KwargInfo('pure', (bool, NoneType))
_SUBDIR_KW = KwargInfo('subdir', str, default='')
+_LIMITED_API_KW = KwargInfo('limited_api', str, default='', since='1.3.0')
_DEFAULTABLE_SUBDIR_KW = KwargInfo('subdir', (str, NoneType))
class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
@@ -124,6 +125,7 @@ class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
assert isinstance(prefix, str), 'for mypy'
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)
@@ -148,7 +150,7 @@ class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
@permittedKwargs(mod_kwargs)
@typed_pos_args('python.extension_module', str, varargs=(str, mesonlib.File, CustomTarget, CustomTargetIndex, GeneratedList, StructuredSources, ExtractedObjects, BuildTarget))
- @typed_kwargs('python.extension_module', *_MOD_KWARGS, _DEFAULTABLE_SUBDIR_KW, allow_unknown=True)
+ @typed_kwargs('python.extension_module', *_MOD_KWARGS, _DEFAULTABLE_SUBDIR_KW, _LIMITED_API_KW, allow_unknown=True)
def extension_module_method(self, args: T.Tuple[str, T.List[BuildTargetSource]], kwargs: ExtensionModuleKw) -> 'SharedModule':
if 'install_dir' in kwargs:
if kwargs['subdir'] is not None:
@@ -161,9 +163,11 @@ class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
kwargs['install_dir'] = self._get_install_dir_impl(False, subdir)
+ target_suffix = self.suffix
+
new_deps = mesonlib.extract_as_list(kwargs, 'dependencies')
- has_pydep = any(isinstance(dep, _PythonDependencyBase) for dep in new_deps)
- if not has_pydep:
+ pydep = next((dep for dep in new_deps if isinstance(dep, _PythonDependencyBase)), None)
+ if pydep is None:
pydep = self._dependency_method_impl({})
if not pydep.found():
raise mesonlib.MesonException('Python dependency not found')
@@ -171,15 +175,62 @@ class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
FeatureNew.single_use('python_installation.extension_module with implicit dependency on python',
'0.63.0', self.subproject, 'use python_installation.dependency()',
self.current_node)
+
+ limited_api_version = kwargs.pop('limited_api')
+ allow_limited_api = self.interpreter.environment.coredata.get_option(OptionKey('allow_limited_api', module='python'))
+ if limited_api_version != '' and allow_limited_api:
+
+ target_suffix = self.limited_api_suffix
+
+ limited_api_version_hex = self._convert_api_version_to_py_version_hex(limited_api_version, pydep.version)
+ limited_api_definition = f'-DPy_LIMITED_API={limited_api_version_hex}'
+
+ new_c_args = mesonlib.extract_as_list(kwargs, 'c_args')
+ new_c_args.append(limited_api_definition)
+ kwargs['c_args'] = new_c_args
+
+ new_cpp_args = mesonlib.extract_as_list(kwargs, 'cpp_args')
+ new_cpp_args.append(limited_api_definition)
+ kwargs['cpp_args'] = new_cpp_args
+
+ # When compiled under MSVC, Python's PC/pyconfig.h forcibly inserts pythonMAJOR.MINOR.lib
+ # into the linker path when not running in debug mode via a series #pragma comment(lib, "")
+ # directives. We manually override these here as this interferes with the intended
+ # use of the 'limited_api' kwarg
+ for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
+ compilers = self.interpreter.environment.coredata.compilers[for_machine]
+ if any(compiler.get_id() == 'msvc' for compiler in compilers.values()):
+ pydep_copy = copy.copy(pydep)
+ pydep_copy.find_libpy_windows(self.env, limited_api=True)
+ if not pydep_copy.found():
+ raise mesonlib.MesonException('Python dependency supporting limited API not found')
+
+ new_deps.remove(pydep)
+ new_deps.append(pydep_copy)
+
+ pyver = pydep.version.replace('.', '')
+ python_windows_debug_link_exception = f'/NODEFAULTLIB:python{pyver}_d.lib'
+ python_windows_release_link_exception = f'/NODEFAULTLIB:python{pyver}.lib'
+
+ new_link_args = mesonlib.extract_as_list(kwargs, 'link_args')
+
+ is_debug = self.interpreter.environment.coredata.options[OptionKey('debug')].value
+ if is_debug:
+ new_link_args.append(python_windows_debug_link_exception)
+ else:
+ new_link_args.append(python_windows_release_link_exception)
+
+ kwargs['link_args'] = new_link_args
+
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)
+ split, target_suffix = target_suffix.rsplit('.', 1)
args = (args[0] + split, args[1])
kwargs['name_prefix'] = ''
- kwargs['name_suffix'] = suffix
+ kwargs['name_suffix'] = target_suffix
if 'gnu_symbol_visibility' not in kwargs and \
(self.is_pypy or mesonlib.version_compare(self.version, '>=3.9')):
@@ -187,6 +238,22 @@ class PythonInstallation(_ExternalProgramHolder['PythonExternalProgram']):
return self.interpreter.build_target(self.current_node, args, kwargs, SharedModule)
+ def _convert_api_version_to_py_version_hex(self, api_version: str, detected_version: str) -> str:
+ python_api_version_format = re.compile(r'[0-9]\.[0-9]{1,2}')
+ decimal_match = python_api_version_format.fullmatch(api_version)
+ if not decimal_match:
+ raise InvalidArguments(f'Python API version invalid: "{api_version}".')
+ if mesonlib.version_compare(api_version, '<3.2'):
+ raise InvalidArguments(f'Python Limited API version invalid: {api_version} (must be greater than 3.2)')
+ if mesonlib.version_compare(api_version, '>' + detected_version):
+ raise InvalidArguments(f'Python Limited API version too high: {api_version} (detected {detected_version})')
+
+ version_components = api_version.split('.')
+ major = int(version_components[0])
+ minor = int(version_components[1])
+
+ return '0x{:02x}{:02x}0000'.format(major, minor)
+
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)