Skip to content

Commit

Permalink
ENH: fix support for Python limited API / stable ABI wheels
Browse files Browse the repository at this point in the history
Meson gained support for building Python extension modules targeting
the Python limited API, therefore it is time for meson-python to
properly support tagging the build wheels as targeting the stable ABI.

Unfortunately there isn't a reliable and cross-platform way to detect
when extension modules are build for the limited API. Therefore, we
need to add an explicit "limited-api" configuration option in the
[tool.meson-python] section in pyproject.toml.
  • Loading branch information
dnicolodi committed Aug 15, 2023
1 parent 2e99c7b commit 48e87a0
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 48 deletions.
55 changes: 24 additions & 31 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,8 @@ def _init_colors() -> Dict[str, str]:


_SUFFIXES = importlib.machinery.all_suffixes()
_EXTENSION_SUFFIXES = importlib.machinery.EXTENSION_SUFFIXES
_EXTENSION_SUFFIX_REGEX = re.compile(r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$')
assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES)
_EXTENSION_SUFFIX_REGEX = re.compile(r'^[^.]+\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd|dll)$')
assert all(re.match(_EXTENSION_SUFFIX_REGEX, f'foo{x}') for x in importlib.machinery.EXTENSION_SUFFIXES)


# Map Meson installation path placeholders to wheel installation paths.
Expand Down Expand Up @@ -344,34 +343,20 @@ def entrypoints_txt(self) -> bytes:

@cached_property
def _stable_abi(self) -> Optional[str]:
"""Determine stabe ABI compatibility.
Examine all files installed in {platlib} that look like
extension modules (extension .pyd on Windows, .dll on Cygwin,
and .so on other platforms) and, if they all share the same
PEP 3149 filename stable ABI tag, return it.
Other files are ignored.
"""
soext = sorted(_EXTENSION_SUFFIXES, key=len)[0]
abis = []

for path, _ in self._wheel_files['platlib']:
# NOTE: When searching for shared objects files, we assume the host
# and build machines have the same soext, even though that we might
# be cross compiling.
if path.suffix == soext:
match = re.match(r'^[^.]+(.*)$', path.name)
assert match is not None
suffix = match.group(1)
match = _EXTENSION_SUFFIX_REGEX.match(suffix)
if self._project._limited_api:
# Verify stabe ABI compatibility: examine files installed
# in {platlib} that look like extension modules, and raise
# an exception if any of them has a Python version
# specific extension filename suffix ABI tag.
for path, _ in self._wheel_files['platlib']:
match = _EXTENSION_SUFFIX_REGEX.match(path.name)
if match:
abis.append(match.group('abi'))

stable = [x for x in abis if x and re.match(r'abi\d+', x)]
if len(stable) > 0 and len(stable) == len(abis) and all(x == stable[0] for x in stable[1:]):
return stable[0]
abi = match.group('abi')
if abi is not None and abi != 'abi3':
raise BuildError(
f'The package declares compatibility with Python limited API but extension '
f'module {os.fspath(path)!r} is tagged for a specific Python version.')
return 'abi3'
return None

@property
Expand Down Expand Up @@ -576,10 +561,16 @@ def _strings(value: Any, name: str) -> List[str]:
raise ConfigError(f'Configuration entry "{name}" must be a list of strings')
return value

def _bool(value: Any, name: str) -> bool:
if not isinstance(value, bool):
raise ConfigError(f'Configuration entry "{name}" must be a boolean')
return value

scheme = _table({
'limited-api': _bool,
'args': _table({
name: _strings for name in _MESON_ARGS_KEYS
})
}),
})

table = pyproject.get('tool', {}).get('meson-python', {})
Expand Down Expand Up @@ -648,6 +639,7 @@ def __init__(
self._meson_native_file = self._build_dir / 'meson-python-native-file.ini'
self._meson_cross_file = self._build_dir / 'meson-python-cross-file.ini'
self._meson_args: MesonArgs = collections.defaultdict(list)
self._limited_api = False

_check_meson_version()

Expand Down Expand Up @@ -693,6 +685,7 @@ def __init__(
pyproject_config = _validate_pyproject_config(pyproject)
for key, value in pyproject_config.get('args', {}).items():
self._meson_args[key].extend(value)
self._limited_api = pyproject_config.get('limited-api', False)

# meson arguments from the command line take precedence over
# arguments from the configuration file thus are added later
Expand Down
17 changes: 0 additions & 17 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
PLATFORM = adjust_packaging_platform_tag(tag.platform)

SUFFIX = sysconfig.get_config_var('EXT_SUFFIX')
ABI3SUFFIX = next((x for x in mesonpy._EXTENSION_SUFFIXES if '.abi3.' in x), None)


def test_wheel_tag():
Expand Down Expand Up @@ -76,19 +75,3 @@ def test_tag_platlib_wheel(monkeypatch):
'platlib': [f'extension{SUFFIX}'],
})
assert str(builder.tag) == f'{INTERPRETER}-{ABI}-{PLATFORM}'


@pytest.mark.skipif(not ABI3SUFFIX, reason='Stable ABI not supported by Python interpreter')
def test_tag_stable_abi(monkeypatch):
builder = wheel_builder_test_factory(monkeypatch, {
'platlib': [f'extension{ABI3SUFFIX}'],
})
assert str(builder.tag) == f'{INTERPRETER}-abi3-{PLATFORM}'


@pytest.mark.skipif(not ABI3SUFFIX, reason='Stable ABI not supported by Python interpreter')
def test_tag_mixed_abi(monkeypatch):
builder = wheel_builder_test_factory(monkeypatch, {
'platlib': [f'extension{ABI3SUFFIX}', f'another{SUFFIX}'],
})
assert str(builder.tag) == f'{INTERPRETER}-{ABI}-{PLATFORM}'
18 changes: 18 additions & 0 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,21 @@ def test_skip_subprojects(package_subproject, tmp_path, arg):
'subproject-1.0.0.dist-info/WHEEL',
'subproject.py',
}


# Requires Meson 1.3.0, see https://github.com/mesonbuild/meson/pull/11745.
@pytest.mark.skipif(MESON_VERSION < (1, 2, 99), reason='Meson version too old')
def test_limited_api(wheel_limited_api):
artifact = wheel.wheelfile.WheelFile(wheel_limited_api)
name = artifact.parsed_filename
assert name.group('pyver') == INTERPRETER
assert name.group('abi') == 'abi3'
assert name.group('plat') == PLATFORM


# Requires Meson 1.3.0, see https://github.com/mesonbuild/meson/pull/11745.
@pytest.mark.skipif(MESON_VERSION < (1, 2, 99), reason='Meson version too old')
def test_limited_api_bad(package_limited_api, tmp_path):
with pytest.raises(mesonpy.BuildError, match='The package declares compatibility with Python limited API but '):
with mesonpy.Project.with_temp_working_dir(meson_args={'setup': ['-Dextra=true']}) as project:
project.wheel(tmp_path)

0 comments on commit 48e87a0

Please sign in to comment.