Skip to content

Add support for targeting iOS. #731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2025
Merged
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
34 changes: 25 additions & 9 deletions docs/reference/environment-variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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``, ...
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/meson-compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'''
Expand Down
18 changes: 18 additions & 0 deletions mesonpy/_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,28 @@
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]

Check warning on line 169 in mesonpy/_tags.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_tags.py#L166-L169

Added lines #L166 - L169 were not covered by tests

# 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('-', '_')

Check warning on line 174 in mesonpy/_tags.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_tags.py#L174

Added line #L174 was not covered by tests

return f"ios_{version[0]}_{version[1]}_{multiarch}"

Check warning on line 176 in mesonpy/_tags.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_tags.py#L176

Added line #L176 was not covered by tests


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()

Check warning on line 184 in mesonpy/_tags.py

View check run for this annotation

Codecov / codecov/patch

mesonpy/_tags.py#L184

Added line #L184 was not covered by tests
if _32_BIT_INTERPRETER:
# 32-bit Python running on a 64-bit kernel.
if platform == 'linux-x86_64':
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
]
Expand Down Expand Up @@ -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"',
]
Expand Down
38 changes: 38 additions & 0 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
20 changes: 20 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sysconfig

from collections import defaultdict
from unittest.mock import Mock

import packaging.tags
import pytest
Expand Down Expand Up @@ -74,6 +75,25 @@
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 warning on line 85 in tests/test_tags.py

View check run for this annotation

Codecov / codecov/patch

tests/test_tags.py#L82-L85

Added lines #L82 - L85 were not covered by tests

# Check the default value
assert next(packaging.tags.ios_platforms((13, 0))) == mesonpy._tags.get_platform_tag()

Check warning on line 88 in tests/test_tags.py

View check run for this annotation

Codecov / codecov/patch

tests/test_tags.py#L88

Added line #L88 was not covered by tests

# 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()

Check warning on line 94 in tests/test_tags.py

View check run for this annotation

Codecov / codecov/patch

tests/test_tags.py#L91-L94

Added lines #L91 - L94 were not covered by tests


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()})
Expand Down
Loading