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 packaging #12962

Merged
merged 14 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions news/12961.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for PEP 730 iOS wheels was added.
27 changes: 25 additions & 2 deletions src/pip/_internal/utils/compatibility_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
generic_tags,
interpreter_name,
interpreter_version,
ios_platforms,
mac_platforms,
)

_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")
_apple_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")


def version_info_to_nodot(version_info: Tuple[int, ...]) -> str:
Expand All @@ -24,7 +25,7 @@ def version_info_to_nodot(version_info: Tuple[int, ...]) -> str:


def _mac_platforms(arch: str) -> List[str]:
match = _osx_arch_pat.match(arch)
match = _apple_arch_pat.match(arch)
if match:
name, major, minor, actual_arch = match.groups()
mac_version = (int(major), int(minor))
Expand All @@ -43,6 +44,26 @@ def _mac_platforms(arch: str) -> List[str]:
return arches


def _ios_platforms(arch: str) -> List[str]:
match = _apple_arch_pat.match(arch)
if match:
name, major, minor, actual_multiarch = match.groups()
ios_version = (int(major), int(minor))
arches = [
# Since we have always only checked that the platform starts
# with "ios", for backwards-compatibility we extract the
# actual prefix provided by the user in case they provided
# something like "ioscustom_". It may be good to remove
# this as undocumented or deprecate it in the future.
"{}_{}".format(name, arch[len("ios_") :])
for arch in ios_platforms(ios_version, actual_multiarch)
]
else:
# arch pattern didn't match (?!)
arches = [arch]
return arches


def _custom_manylinux_platforms(arch: str) -> List[str]:
arches = [arch]
arch_prefix, arch_sep, arch_suffix = arch.partition("_")
Expand All @@ -68,6 +89,8 @@ def _get_custom_platforms(arch: str) -> List[str]:
arch_prefix, arch_sep, arch_suffix = arch.partition("_")
if arch.startswith("macosx"):
arches = _mac_platforms(arch)
elif arch.startswith("ios"):
arches = _ios_platforms(arch)
elif arch_prefix in ["manylinux2014", "manylinux2010"]:
arches = _custom_manylinux_platforms(arch)
else:
Expand Down
63 changes: 58 additions & 5 deletions src/pip/_vendor/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 @@ -363,7 +363,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 @@ -396,7 +396,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 @@ -408,7 +408,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 @@ -424,7 +424,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 @@ -483,6 +483,57 @@ def mac_platforms(
)


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

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

# Consider any major.minor version from iOS 12.0 to the version prior to the
# version requested by platform. 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.
for major in range(12, version[0]):
for minor in range(0, 10):
yield ios_platform_template.format(
major=major, minor=minor, 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(0, version[1]):
yield ios_platform_template.format(
major=version[0], minor=minor, multiarch=multiarch
)

# Consider the actual X.Y version that was requested.
yield ios_platform_template.format(
major=version[0], minor=version[1], 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 @@ -512,6 +563,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
2 changes: 1 addition & 1 deletion tests/functional/test_bad_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def test_filenotfound_error_message(script: Any) -> None:
# Test the error message returned when using a bad 'file:' URL.
# make pip to fail and get an error message
# by running "pip install -r file:nonexistent_file"
proc = script.pip("install", "-r", "file:unexistent_file", expect_error=True)
proc = script.pip("install", "-r", "file:///unexistent_file", expect_error=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change has been submitted separately as #12964; it's included here to prove CI isn't failing because of this patch.

assert proc.returncode == 1
expect = (
"ERROR: 404 Client Error: FileNotFoundError for url: file:///unexistent_file"
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/test_models_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ def test_not_supported_multiarch_darwin(self) -> None:
assert not w.supported(tags=intel)
assert not w.supported(tags=universal)

def test_supported_ios_version(self) -> None:
"""
Wheels build for iOS 12.3 are supported on iOS 15.1
"""
tags = compatibility_tags.get_supported(
"313", platforms=["ios_15_1_arm64_iphoneos"], impl="cp"
)
w = Wheel("simple-0.1-cp313-none-ios_12_3_arm64_iphoneos.whl")
assert w.supported(tags=tags)
w = Wheel("simple-0.1-cp313-none-ios_15_1_arm64_iphoneos.whl")
assert w.supported(tags=tags)

def test_not_supported_ios_version(self) -> None:
"""
Wheels built for macOS 15.1 are not supported on 12.3
"""
tags = compatibility_tags.get_supported(
"313", platforms=["ios_12_3_arm64_iphoneos"], impl="cp"
)
w = Wheel("simple-0.1-cp313-none-ios_15_1_arm64_iphoneos.whl")
assert not w.supported(tags=tags)

def test_support_index_min(self) -> None:
"""
Test results from `support_index_min`
Expand Down
116 changes: 116 additions & 0 deletions tools/vendoring/patches/packaging.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py
index 6667d2990..bcc6ea041 100644
--- a/src/pip/_vendor/packaging/tags.py
+++ b/src/pip/_vendor/packaging/tags.py
@@ -25,7 +25,7 @@ from . import _manylinux, _musllinux
logger = logging.getLogger(__name__)

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

INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic.
@@ -363,7 +363,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):
@@ -396,7 +396,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.
@@ -408,7 +408,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.
@@ -424,7 +424,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:
@@ -483,6 +483,57 @@ def mac_platforms(
)


+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
+
+ ios_platform_template = "ios_{major}_{minor}_{multiarch}"
+
+ # Consider any major.minor version from iOS 12.0 to the version prior to the
+ # version requested by platform. 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.
+ for major in range(12, version[0]):
+ for minor in range(0, 10):
+ yield ios_platform_template.format(
+ major=major, minor=minor, 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(0, version[1]):
+ yield ios_platform_template.format(
+ major=version[0], minor=minor, multiarch=multiarch
+ )
+
+ # Consider the actual X.Y version that was requested.
+ yield ios_platform_template.format(
+ major=version[0], minor=version[1], multiarch=multiarch
+ )
+
+
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
linux = _normalize_string(sysconfig.get_platform())
if not linux.startswith("linux_"):
@@ -512,6 +563,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:
Loading