Skip to content
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

Add support for PEP 730 iOS tags. #832

Merged
merged 10 commits into from
Oct 2, 2024
15 changes: 15 additions & 0 deletions docs/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ to the implementation to provide.
compatibility


.. function:: ios_platforms(version=None, multiarch=None)

Yields the :attr:`~Tag.platform` tags for iOS.

:param tuple version: A two-item tuple representing the version of iOS.
Defaults to the current system's version.
:param str multiarch: The CPU architecture+ABI to be used. This should be in
the format by ``sys.implementation._multiarch`` (e.g.,
``arm64_iphoneos`` or ``x84_64_iphonesimulator``).
Defaults to the current system's multiarch value.

.. note::
Behavior of this method is undefined if invoked on non-iOS platforms
without providing explicit version and multiarch arguments.

.. function:: platform_tags(version=None, arch=None)

Yields the :attr:`~Tag.platform` tags for the running interpreter.
Expand Down
69 changes: 64 additions & 5 deletions src/packaging/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
logger = logging.getLogger(__name__)

PythonVersion = Sequence[int]
MacVersion = Tuple[int, int]
AppleVersion = Tuple[int, int]

INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic.
Expand Down Expand Up @@ -362,7 +362,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
return "i386"


def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
formats = [cpu_arch]
if cpu_arch == "x86_64":
if version < (10, 4):
Expand Down Expand Up @@ -395,7 +395,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:


def mac_platforms(
version: MacVersion | None = None, arch: str | None = None
version: AppleVersion | None = None, arch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for a macOS system.
Expand All @@ -407,7 +407,7 @@ def mac_platforms(
"""
version_str, _, cpu_arch = platform.mac_ver()
if version is None:
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
if version == (10, 16):
# When built against an older macOS SDK, Python will report macOS 10.16
# instead of the real version.
Expand All @@ -423,7 +423,7 @@ def mac_platforms(
stdout=subprocess.PIPE,
text=True,
).stdout
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
else:
version = version
if arch is None:
Expand Down Expand Up @@ -473,6 +473,63 @@ def mac_platforms(
yield f"macosx_{major_version}_{minor_version}_{binary_format}"


def ios_platforms(
version: AppleVersion | None = None, multiarch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for an iOS system.

:param version: A two-item tuple specifying the iOS version to generate
platform tags for. Defaults to the current iOS version.
:param multiarch: The CPU architecture+ABI to generate platform tags for -
(the value used by `sys.implementation._multiarch` e.g.,
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
multiarch value.
"""
if version is None:
# if iOS is the current platform, ios_ver *must* be defined. However,
# it won't exist for CPython versions before 3.13, which causes a mypy
# error.
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined]
version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))

if multiarch is None:
multiarch = sys.implementation._multiarch
multiarch = multiarch.replace("-", "_")

ios_platform_template = "ios_{major}_{minor}_{multiarch}"

# Consider any iOS major.minor version from the version requested, down to
# 12.0. 12.0 is the first iOS version that is known to have enough features
# to support CPython. Consider every possible minor release up to X.9. There
# highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
# candidates that won't ever match doesn't really hurt, and it saves us from
# having to keep an explicit list of known iOS versions in the code. Return
# the results descending order of version number.

# If the requested major version is less than 12, there won't be any matches.
if version[0] < 12:
return

# Consider the actual X.Y version that was requested.
yield ios_platform_template.format(
major=version[0], minor=version[1], multiarch=multiarch
)

# Consider every minor version from X.0 to the minor version prior to the
# version requested by the platform.
for minor in range(version[1] - 1, -1, -1):
yield ios_platform_template.format(
major=version[0], minor=minor, multiarch=multiarch
)

for major in range(version[0] - 1, 11, -1):
for minor in range(9, -1, -1):
yield ios_platform_template.format(
major=major, minor=minor, multiarch=multiarch
)


def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
linux = _normalize_string(sysconfig.get_platform())
if not linux.startswith("linux_"):
Expand Down Expand Up @@ -502,6 +559,8 @@ def platform_tags() -> Iterator[str]:
"""
if platform.system() == "Darwin":
return mac_platforms()
elif platform.system() == "iOS":
return ios_platforms()
elif platform.system() == "Linux":
return _linux_platforms()
else:
Expand Down
103 changes: 103 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ def mock(name):
return mock


@pytest.fixture
def mock_ios(monkeypatch):
# Monkeypatch the platform to be iOS
monkeypatch.setattr(sys, "platform", "ios")

# Mock a fake architecture that will fit the expected pattern, but
# wont actually be a legal multiarch.
monkeypatch.setattr(
sys.implementation,
"_multiarch",
"gothic-iphoneos",
raising=False,
)

# Mock the return value of platform.ios_ver.
def mock_ios_ver(*args):
return ("iOS", "13.2", "iPhone15,2", False)

if sys.version_info < (3, 13):
platform.ios_ver = mock_ios_ver
else:
monkeypatch.setattr(platform, "ios_ver", mock_ios_ver)


class TestTag:
def test_lowercasing(self):
tag = tags.Tag("PY3", "None", "ANY")
Expand Down Expand Up @@ -335,6 +359,84 @@ def test_macos_11(self, major, minor):
assert "macosx_12_0_universal2" in platforms


class TestIOSPlatforms:
def test_version_detection(self, mock_ios):
platforms = list(tags.ios_platforms(multiarch="arm64-iphoneos"))
assert platforms == [
"ios_13_2_arm64_iphoneos",
"ios_13_1_arm64_iphoneos",
"ios_13_0_arm64_iphoneos",
"ios_12_9_arm64_iphoneos",
"ios_12_8_arm64_iphoneos",
"ios_12_7_arm64_iphoneos",
"ios_12_6_arm64_iphoneos",
"ios_12_5_arm64_iphoneos",
"ios_12_4_arm64_iphoneos",
"ios_12_3_arm64_iphoneos",
"ios_12_2_arm64_iphoneos",
"ios_12_1_arm64_iphoneos",
"ios_12_0_arm64_iphoneos",
]

def test_multiarch_detection(self, mock_ios):
platforms = list(tags.ios_platforms(version=(12, 0)))
assert platforms == ["ios_12_0_gothic_iphoneos"]

def test_ios_platforms(self, mock_ios):
# Pre-iOS 12.0 releases won't match anything
platforms = list(tags.ios_platforms((7, 0), "arm64-iphoneos"))
assert platforms == []

# iOS 12.0 returns exactly 1 match
platforms = list(tags.ios_platforms((12, 0), "arm64-iphoneos"))
assert platforms == ["ios_12_0_arm64_iphoneos"]

# iOS 13.0 returns a match for 13.0, plus every 12.X
platforms = list(tags.ios_platforms((13, 0), "x86_64-iphonesimulator"))
assert platforms == [
"ios_13_0_x86_64_iphonesimulator",
"ios_12_9_x86_64_iphonesimulator",
"ios_12_8_x86_64_iphonesimulator",
"ios_12_7_x86_64_iphonesimulator",
"ios_12_6_x86_64_iphonesimulator",
"ios_12_5_x86_64_iphonesimulator",
"ios_12_4_x86_64_iphonesimulator",
"ios_12_3_x86_64_iphonesimulator",
"ios_12_2_x86_64_iphonesimulator",
"ios_12_1_x86_64_iphonesimulator",
"ios_12_0_x86_64_iphonesimulator",
]

# iOS 14.3 returns a match for 14.3-14.0, plus every 13.X and every 12.X
platforms = list(tags.ios_platforms((14, 3), "arm64-iphoneos"))
assert platforms == [
"ios_14_3_arm64_iphoneos",
"ios_14_2_arm64_iphoneos",
"ios_14_1_arm64_iphoneos",
"ios_14_0_arm64_iphoneos",
"ios_13_9_arm64_iphoneos",
"ios_13_8_arm64_iphoneos",
"ios_13_7_arm64_iphoneos",
"ios_13_6_arm64_iphoneos",
"ios_13_5_arm64_iphoneos",
"ios_13_4_arm64_iphoneos",
"ios_13_3_arm64_iphoneos",
"ios_13_2_arm64_iphoneos",
"ios_13_1_arm64_iphoneos",
"ios_13_0_arm64_iphoneos",
"ios_12_9_arm64_iphoneos",
"ios_12_8_arm64_iphoneos",
"ios_12_7_arm64_iphoneos",
"ios_12_6_arm64_iphoneos",
"ios_12_5_arm64_iphoneos",
"ios_12_4_arm64_iphoneos",
"ios_12_3_arm64_iphoneos",
"ios_12_2_arm64_iphoneos",
"ios_12_1_arm64_iphoneos",
"ios_12_0_arm64_iphoneos",
]


class TestManylinuxPlatform:
def teardown_method(self):
# Clear the version cache
Expand Down Expand Up @@ -619,6 +721,7 @@ def test_linux_not_linux(self, monkeypatch):
"platform_name,dispatch_func",
[
("Darwin", "mac_platforms"),
("iOS", "ios_platforms"),
("Linux", "_linux_platforms"),
("Generic", "_generic_platforms"),
],
Expand Down
Loading