Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ class _WheelBuilder():
_manifest: Dict[str, List[_Entry]]
_limited_api: bool
_allow_windows_shared_libs: bool
_build_details: mesonpy._tags.BuildDetails

@property
def _has_internal_libs(self) -> bool:
Expand All @@ -364,14 +365,14 @@ def _pure(self) -> bool:
def tag(self) -> mesonpy._tags.Tag:
"""Wheel tags."""
if self._pure:
return mesonpy._tags.Tag('py3', 'none', 'any')
return mesonpy._tags.Tag('py3', 'none', 'any', build_details=self._build_details)
if not self._has_extension_modules:
# The wheel has platform dependent code (is not pure) but
# does not contain any extension module (does not
# distribute any file in {platlib}) thus use generic
# implementation and ABI tags.
return mesonpy._tags.Tag('py3', 'none', None)
return mesonpy._tags.Tag(None, self._stable_abi, None)
return mesonpy._tags.Tag('py3', 'none', None, build_details=self._build_details)
return mesonpy._tags.Tag(None, self._stable_abi, None, build_details=self._build_details)

@property
def name(self) -> str:
Expand Down Expand Up @@ -834,6 +835,29 @@ def __init__(
''')
self._meson_native_file.write_text(native_file_data, encoding='utf-8')

# Starting with version 1.10, Meson can consume a `build-details.json`
# file following the specification is PEP 739 to obtain required
# information to build extension modules without having to run the
# interpreter. The path to the `build-details.json` can be specified
# passing the with the `-Dpython.build_config=` option to `meson
# setup`. Extract the value passed to this option and use the details
# in the `build-details.json` file to compute the wheel tag.
self._build_details: mesonpy._tags.BuildDetails | None = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation is now wrong: self._build_details cannot be None. Also, now that we use this attribute to always store information on the interpreter we can consider to name it in a less confusing way (now, at a first look it could seem that this attribute contains information about the build of the Meson project). Maybe self._info is not too bad of a name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is None temporarily. Unless you want me to undo the optimization and set the default value unconditionally, then override if python_build_config arg is provided.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind renaming it, though info sounds a bit unclear. Maybe tag_info? Should I also rename the classes and the argument elsewhere?

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-D', action='append', default=[])
args, _ = parser.parse_known_args(self._meson_args['setup'])
for arg in reversed(args.D):
name, value = arg.split('=', 1)
if name == 'python.build_config':
try:
with open(value, 'r', encoding='utf8') as f:
self._build_details = json.load(f)
except OSError as err:
raise ConfigError(f'The file specified as "python.build_config" cannot be opened: {err}') from err
break
if self._build_details is None:
self._build_details = mesonpy._tags.introspect_build_details()

# reconfigure if we have a valid Meson build directory. Meson
# uses the presence of the 'meson-private/coredata.dat' file
# in the build directory as indication that the build
Expand Down Expand Up @@ -1151,13 +1175,15 @@ def sdist(self, directory: Path) -> pathlib.Path:
def wheel(self, directory: Path) -> pathlib.Path:
"""Generates a wheel in the specified directory."""
self.build()
builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs)
builder = _WheelBuilder(
self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._build_details)
return builder.build(directory)

def editable(self, directory: Path) -> pathlib.Path:
"""Generates an editable wheel in the specified directory."""
self.build()
builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs)
builder = _EditableWheelBuilder(
self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._build_details)
return builder.build(directory, self._source_dir, self._build_dir, self._build_command, self._editable_verbose)


Expand Down Expand Up @@ -1323,7 +1349,7 @@ def build_editable(
if not config_settings:
config_settings = {}
if 'build-dir' not in config_settings and 'builddir' not in config_settings:
config_settings['build-dir'] = 'build/' + mesonpy._tags.get_abi_tag()
config_settings['build-dir'] = 'build/' + mesonpy._tags.get_abi_tag(mesonpy._tags.introspect_build_details())

out = pathlib.Path(wheel_directory)
with _project(config_settings) as project:
Expand Down
181 changes: 125 additions & 56 deletions mesonpy/_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@
import struct
import sys
import sysconfig
import typing


if typing.TYPE_CHECKING: # pragma: no cover
from typing import TypedDict

class _Abi(TypedDict):
extension_suffix: str

class _ImplementationVersion(TypedDict):
major: int
minor: int

class _Implementation(TypedDict):
name: str
version: _ImplementationVersion

class BuildDetails(TypedDict):
abi: _Abi
implementation: _Implementation
platform: str


# https://peps.python.org/pep-0425/#python-tag
Expand All @@ -24,22 +45,100 @@
_32_BIT_INTERPRETER = struct.calcsize('P') == 4


def get_interpreter_tag() -> str:
name = sys.implementation.name
def _get_macosx_platform() -> str:
ver, _, arch = platform.mac_ver()
major, minor = map(int, ver.split('.')[:2])

# Python built with older macOS SDK on macOS 11, reports an
# nonexistent macOS 10.16 version instead of the real version.
#
# The packaging module introduced a workaround
# https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da
#
# This results in packaging versions up to 21.3 generating
# platform tags like "macosx_10_16_x86_64" and later versions
# generating "macosx_11_0_x86_64". Using the latter would be more
# correct but prevents the resulting wheel from being installed on
# systems using packaging 21.3 or earlier (pip 22.3 or earlier).
#
# Fortunately packaging versions carrying the workaround still
# accepts "macosx_10_16_x86_64" as a compatible platform tag. We
# can therefore ignore the issue and generate the slightly
# incorrect tag.
Comment on lines +52 to +67

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the code this comments refers to?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this comment is saying that we don't need any code to handle that case. It was followed by empty line, then another comment. I've figured out it's better to move it where we get the platform, since it applied to what the system gives us.


if _32_BIT_INTERPRETER:
# 32-bit Python running on a 64-bit kernel.
if arch == 'ppc64':
arch = 'ppc'
if arch == 'x86_64':
arch = 'i386'

return f'macosx-{major}.{minor}-{arch}'


def _get_ios_platform() -> str:
ver = platform.ios_ver().release
major, minor = map(int, ver.split('.')[:2])

# Although _multiarch is an internal implementation detail, it's a core part
# of how CPython is implemented on iOS; this attribute is also relied upon
# by `packaging` as part of tag determination.
multiarch = sys.implementation._multiarch.replace('-', '_')

return f'ios-{major}.{minor}-{multiarch}'


def introspect_build_details() -> BuildDetails:
platform = sysconfig.get_platform()
if platform.startswith('macosx'):
platform = _get_macosx_platform()
elif platform.startswith('ios'):
platform = _get_ios_platform()
elif _32_BIT_INTERPRETER:
# 32-bit Python running on a 64-bit kernel.
if platform == 'linux-x86_64':
platform = 'linux_i686'
if platform == 'linux-aarch64':
platform = 'linux_armv7l'

return {
'abi': {
# PyPy reports a $SOABI that does not agree with $EXT_SUFFIX.
# Using $EXT_SUFFIX will not break when PyPy will fix this.
# See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and
# https://github.com/pypa/packaging/pull/607.
Comment on lines +106 to +109

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment belongs here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been here:

# PyPy reports a $SOABI that does not agree with $EXT_SUFFIX.
# Using $EXT_SUFFIX will not break when PyPy will fix this.
# See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and
# https://github.com/pypa/packaging/pull/607.
empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.')

and it seems related to why we're using EXT_SUFFIX. Where else should I put it?

'extension_suffix': str(sysconfig.get_config_var('EXT_SUFFIX')),
},
'implementation': {
'name': sys.implementation.name,
'version': {
'major': sys.version_info.major,
'minor': sys.version_info.minor,
},
},
'platform': platform,
}


def get_interpreter_tag(build_details: BuildDetails) -> str:
name = build_details['implementation']['name']
_v = build_details['implementation']['version']
major = _v['major']
minor = _v['minor']
name = INTERPRETERS.get(name, name)
version = sys.version_info
return f'{name}{version[0]}{version[1]}'
return f'{name}{major}{minor}'


def get_abi_tag() -> str:
def get_abi_tag(build_details: BuildDetails) -> str:
# The best solution to obtain the Python ABI is to parse the
# $SOABI or $EXT_SUFFIX sysconfig variables as defined in PEP-314.

# PyPy reports a $SOABI that does not agree with $EXT_SUFFIX.
# Using $EXT_SUFFIX will not break when PyPy will fix this.
# See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and
# https://github.com/pypa/packaging/pull/607.
empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.')
ext_suffix = build_details['abi']['extension_suffix']
empty, abi, ext = ext_suffix.split('.')

# The packaging module initially based his understanding of the
# $SOABI variable on the inconsistent value reported by PyPy, and
Expand All @@ -60,8 +159,9 @@ def get_abi_tag() -> str:
return abi.replace('.', '_').replace('-', '_')


def _get_macosx_platform_tag() -> str:
ver, _, arch = platform.mac_ver()
def _get_macosx_platform_tag(platform: str) -> str:
name, ver, arch = platform.split('-', 2)
assert name == 'macosx'

# Override the architecture with the one provided in the
# _PYTHON_HOST_PLATFORM environment variable. This environment
Expand All @@ -81,24 +181,7 @@ def _get_macosx_platform_tag() -> str:
parts = os.environ.get('MACOSX_DEPLOYMENT_TARGET', '').split('.')[:2]
version = tuple(map(int, parts + ['0'] * (2 - len(parts))))
except ValueError:
version = tuple(map(int, ver.split('.')))[:2]

# Python built with older macOS SDK on macOS 11, reports an
# nonexistent macOS 10.16 version instead of the real version.
#
# The packaging module introduced a workaround
# https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da
#
# This results in packaging versions up to 21.3 generating
# platform tags like "macosx_10_16_x86_64" and later versions
# generating "macosx_11_0_x86_64". Using the latter would be more
# correct but prevents the resulting wheel from being installed on
# systems using packaging 21.3 or earlier (pip 22.3 or earlier).
#
# Fortunately packaging versions carrying the workaround still
# accepts "macosx_10_16_x86_64" as a compatible platform tag. We
# can therefore ignore the issue and generate the slightly
# incorrect tag.
version = tuple(map(int, ver.split('.')[:2]))

# The minimum macOS ABI version on arm64 is 11.0. The macOS SDK
# on arm64 silently bumps any compatibility version specified via
Expand All @@ -120,53 +203,39 @@ def _get_macosx_platform_tag() -> str:
# the patch level. Reset the patch level to zero.
minor = 0

if _32_BIT_INTERPRETER:
# 32-bit Python running on a 64-bit kernel.
if arch == 'ppc64':
arch = 'ppc'
if arch == 'x86_64':
arch = 'i386'

return f'macosx_{major}_{minor}_{arch}'


def _get_ios_platform_tag() -> str:
def _get_ios_platform_tag(platform: str) -> str:
name, version, multiarch = platform.split('-', 2)
assert name == 'ios'

# Override the iOS version if one is provided via the
# IPHONEOS_DEPLOYMENT_TARGET environment variable.
try:
parts = os.environ.get('IPHONEOS_DEPLOYMENT_TARGET', '').split('.')[:2]
version = tuple(map(int, parts + ['0'] * (2 - len(parts))))
version = '.'.join(map(int, parts + ['0'] * (2 - len(parts))))
except ValueError:
version = tuple(map(int, platform.ios_ver().release.split('.')))[:2] # type: ignore[attr-defined]

# Although _multiarch is an internal implementation detail, it's a core part
# of how CPython is implemented on iOS; this attribute is also relied upon
# by `packaging` as part of tag determination.
multiarch = sys.implementation._multiarch.replace('-', '_')
pass

return f'ios_{version[0]}_{version[1]}_{multiarch}'
return f'ios_{version.replace(".", "_")}_{multiarch.replace("-", "_")}'


def get_platform_tag() -> str:
platform = sysconfig.get_platform()
def get_platform_tag(build_details: BuildDetails) -> str:
platform = build_details['platform']
if platform.startswith('macosx'):
return _get_macosx_platform_tag()
return _get_macosx_platform_tag(platform)
if platform.startswith('ios'):
return _get_ios_platform_tag()
if _32_BIT_INTERPRETER:
# 32-bit Python running on a 64-bit kernel.
if platform == 'linux-x86_64':
return 'linux_i686'
if platform == 'linux-aarch64':
return 'linux_armv7l'
return _get_ios_platform_tag(platform)
return platform.replace('-', '_').replace('.', '_').lower()


class Tag:
def __init__(self, interpreter: str | None = None, abi: str | None = None, platform: str | None = None):
self.interpreter = interpreter or get_interpreter_tag()
self.abi = abi or get_abi_tag()
self.platform = platform or get_platform_tag()
def __init__(self, interpreter: str | None = None, abi: str | None = None, platform: str | None = None,
*, build_details: BuildDetails):
self.interpreter = interpreter or get_interpreter_tag(build_details)
self.abi = abi or get_abi_tag(build_details)
self.platform = platform or get_platform_tag(build_details)

def __str__(self) -> str:
return f'{self.interpreter}-{self.abi}-{self.platform}'
Loading
Loading