diff --git a/docs/reference/environment-variables.rst b/docs/reference/environment-variables.rst index d305ab96..5b4de015 100644 --- a/docs/reference/environment-variables.rst +++ b/docs/reference/environment-variables.rst @@ -61,6 +61,22 @@ Environment variables used by meson-python .. _cross build definition file: https://mesonbuild.com/Cross-compilation.html .. _cibuildwheel: https://cibuildwheel.readthedocs.io/en/stable/ +.. envvar:: IPHONEOS_DEPLOYMENT_TARGET + + This environment variable is used to specify the target iOS platform version + to the Xcode development tools. If this environment variable is set, + ``meson-python`` will use the specified iOS version for the Python wheel + platform tag, instead than the iOS platform default of 13.0. + + This variable must be set to a major/minor version, for example ``13.0`` or + ``15.4``. + + Note that ``IPHONEOS_DEPLOYMENT_TARGET`` is the only supported mechanism + for specifying the target iOS version. Although the iOS toolchain supports + the use of ``-mios-version-min`` compile and link flags to set the target iOS + version, ``meson-python`` will not set the Python wheel platform tag + correctly unless ``IPHONEOS_DEPLOYMENT_TARGET`` is set. + .. envvar:: FORCE_COLOR Setting this environment variable to any value forces the use of ANSI @@ -69,11 +85,10 @@ Environment variables used by meson-python .. envvar:: MACOSX_DEPLOYMENT_TARGET - This environment variables is used of specifying the target macOS platform - major version to the Xcode development tools. If this environment variable - is set, ``meson-python`` will use the specified macOS version for the - Python wheel platform tag instead than the macOS version of the build - machine. + This environment variable is used to specify the target macOS platform major + version to the Xcode development tools. If this environment variable is set, + ``meson-python`` will use the specified macOS version for the Python wheel + platform tag instead than the macOS version of the build machine. This variable must be set to macOS major versions only: ``10.9`` to ``10.15``, ``11``, ``12``, ``13``, ... @@ -84,10 +99,11 @@ Environment variables used by meson-python are currently designed to specify compatibility only with major version number granularity. - Another way of specifying the target macOS platform is to use the - ``-mmacosx-version-min`` compile and link flags. However, it is not - possible for ``meson-python`` to detect this, and it will not set the - Python wheel platform tag accordingly. + Note that ``MACOSX_DEPLOYMENT_TARGET`` is the only supported mechanism for + specifying the target macOS version. Although the macOS toolchain supports + the use of ``-mmacosx-version-min`` compile and link flags to set the target + macOS version, ``meson-python`` will not set the Python wheel platform tag + correctly unless ``MACOSX_DEPLOYMENT_TARGET`` is set. .. envvar:: MESON diff --git a/docs/reference/meson-compatibility.rst b/docs/reference/meson-compatibility.rst index 4f2ff406..73258e73 100644 --- a/docs/reference/meson-compatibility.rst +++ b/docs/reference/meson-compatibility.rst @@ -52,6 +52,10 @@ versions. populate the package license and license files from the ones declared via the ``project()`` call in ``meson.build``. +.. option:: 1.9.0 + + Meson 1.9.0 or later is required to support building for iOS. + Build front-ends by default build packages in an isolated Python environment where build dependencies are installed. Most often, unless a package or its build dependencies declare explicitly a version diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 5bab479a..35ef729a 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -719,6 +719,30 @@ def __init__( ''') self._meson_cross_file.write_text(cross_file_data, encoding='utf-8') self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) + elif sysconfig.get_platform().startswith('ios-'): + ios_ver = platform.ios_ver() # type: ignore[attr-defined] + + arch = platform.machine() + family = 'aarch64' if arch == 'arm64' else arch + subsystem = 'ios-simulator' if ios_ver.is_simulator else 'ios' + + cross_file_data = textwrap.dedent(f''' + [binaries] + c = '{arch}-apple-{subsystem}-clang' + cpp = '{arch}-apple-{subsystem}-clang++' + objc = '{arch}-apple-{subsystem}-clang' + objcpp = '{arch}-apple-{subsystem}-clang++' + ar = '{arch}-apple-{subsystem}-ar' + + [host_machine] + system = 'ios' + subsystem = {subsystem!r} + cpu = {arch!r} + cpu_family = {family!r} + endian = 'little' + ''') + self._meson_cross_file.write_text(cross_file_data, encoding='utf-8') + self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) # write the native file native_file_data = textwrap.dedent(f''' diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index 37fc8411..1dab7541 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -160,10 +160,28 @@ def _get_macosx_platform_tag() -> str: return f'macosx_{major}_{minor}_{arch}' +def _get_ios_platform_tag() -> str: + # Override the iOS version if one is provided via the + # IPHONEOS_DEPLOYMENT_TARGET environment variable. + try: + version = tuple(map(int, os.environ.get('IPHONEOS_DEPLOYMENT_TARGET', '').split('.')))[:2] + 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 determiniation. + multiarch = sys.implementation._multiarch.replace('-', '_') + + return f"ios_{version[0]}_{version[1]}_{multiarch}" + + def get_platform_tag() -> str: platform = sysconfig.get_platform() if platform.startswith('macosx'): return _get_macosx_platform_tag() + 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': diff --git a/pyproject.toml b/pyproject.toml index 733d69a2..d67f99d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ backend-path = ['.'] requires = [ 'meson >= 0.64.0; python_version < "3.12"', 'meson >= 1.2.3; python_version >= "3.12"', - 'packaging >= 23.2', + 'packaging >= 23.2; sys_platform != "ios"', + 'packaging >= 24.2; sys_platform == "ios"', 'pyproject-metadata >= 0.9.0', 'tomli >= 1.0.0; python_version < "3.11"', ] @@ -37,7 +38,8 @@ classifiers = [ dependencies = [ 'meson >= 0.64.0; python_version < "3.12"', 'meson >= 1.2.3; python_version >= "3.12"', - 'packaging >= 23.2', + 'packaging >= 23.2; sys_platform != "ios"', + 'packaging >= 24.2; sys_platform == "ios"', 'pyproject-metadata >= 0.9.0', 'tomli >= 1.0.0; python_version < "3.11"', ] diff --git a/tests/test_project.py b/tests/test_project.py index 9d57ffb6..2f8fe0f4 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -4,10 +4,14 @@ import ast import os +import platform import shutil import sys +import sysconfig import textwrap +from unittest.mock import Mock + if sys.version_info < (3, 11): import tomli as tomllib @@ -373,3 +377,37 @@ def test_archflags_envvar_parsing_invalid(package_purelib_and_platlib, monkeypat finally: # revert environment variable setting done by the in-process build os.environ.pop('_PYTHON_HOST_PLATFORM', None) + + +@pytest.mark.skipif(sys.version_info < (3, 13), reason='requires Python 3.13 or higher') +@pytest.mark.parametrize('multiarch', [ + 'arm64-iphoneos', + 'arm64-iphonesimulator', + 'x86_64-iphonesimulator', +]) +def test_ios_project(package_simple, monkeypatch, multiarch, tmp_path): + arch, abi = multiarch.split('-') + subsystem = 'ios-simulator' if abi == 'iphonesimulator' else 'ios' + + # Mock being on iOS + monkeypatch.setattr(sys, 'platform', 'ios') + monkeypatch.setattr(platform, 'machine', Mock(return_value=arch)) + monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value=f"ios-13.0-{multiarch}")) + ios_ver = platform.IOSVersionInfo('iOS', '13.0', 'iPhone', multiarch.endswith('simulator')) + monkeypatch.setattr(platform, 'ios_ver', Mock(return_value=ios_ver)) + + # Create an iOS project. + project = mesonpy.Project(source_dir=package_simple, build_dir=tmp_path) + + # Meson configuration points at the cross file + assert project._meson_args['setup'] == ['--cross-file', os.fspath(tmp_path / 'meson-python-cross-file.ini')] + + # Meson config files exist, and have some relevant keys + assert (tmp_path / 'meson-python-native-file.ini').exists() + assert (tmp_path / 'meson-python-cross-file.ini').exists() + + cross_config = (tmp_path / 'meson-python-cross-file.ini').read_text() + + assert "\nsystem = 'ios'\n" in cross_config + assert f"\nc = '{arch}-apple-{subsystem}-clang'\n" in cross_config + assert f"\nsubsystem = '{subsystem}'\n" in cross_config diff --git a/tests/test_tags.py b/tests/test_tags.py index 433628b8..75d5b650 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -10,6 +10,7 @@ import sysconfig from collections import defaultdict +from unittest.mock import Mock import packaging.tags import pytest @@ -74,6 +75,25 @@ def test_python_host_platform(monkeypatch): assert mesonpy._tags.get_platform_tag().endswith('x86_64') +@pytest.mark.skipif(sys.version_info < (3, 13), reason='requires Python 3.13 or higher') +@pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test') +def test_ios_platform_tag(monkeypatch): + # Mock being on iOS + monkeypatch.setattr(sys.implementation, '_multiarch', 'arm64-iphoneos') + monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value='ios-13.0-arm64-iphoneos')) + ios_ver = platform.IOSVersionInfo('iOS', '13.0', 'iPhone', False) + monkeypatch.setattr(platform, 'ios_ver', Mock(return_value=ios_ver)) + + # Check the default value + assert next(packaging.tags.ios_platforms((13, 0))) == mesonpy._tags.get_platform_tag() + + # Check the value when IPHONEOS_DEPLOYMENT_TARGET is set. + for major in range(13, 20): + for minor in range(3): + monkeypatch.setenv('IPHONEOS_DEPLOYMENT_TARGET', f'{major}.{minor}') + assert next(packaging.tags.ios_platforms((major, minor))) == mesonpy._tags.get_platform_tag() + + def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) manifest.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()})