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 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
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
6 changes: 6 additions & 0 deletions src/pip/_vendor/distlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ def _build_shebang(self, executable, post_interp):
"""
if os.name != 'posix':
simple_shebang = True
elif getattr(sys, "cross_compiling", False):
# In a cross-compiling environment, the shebang will likely be a
# script; this *must* be invoked with the "safe" version of the
# shebang, or else using os.exec() to run the entry script will
# fail, raising "OSError 8 [Errno 8] Exec format error".
simple_shebang = False
else:
# Add 3 for '#!' prefix and newline suffix.
shebang_length = len(executable) + len(post_interp) + 3
Expand Down
69 changes: 64 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,63 @@ 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
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 @@ -512,6 +569,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
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
17 changes: 15 additions & 2 deletions tools/vendoring/patches/distlib.patch
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ index cfa45d2af..e16292b83 100644
@@ -49,6 +49,24 @@ if __name__ == '__main__':
sys.exit(%(func)s())
'''

+# Pre-fetch the contents of all executable wrapper stubs.
+# This is to address https://github.com/pypa/pip/issues/12666.
+# When updating pip, we rename the old pip in place before installing the
Expand All @@ -24,9 +24,22 @@ index cfa45d2af..e16292b83 100644
+ if r.name.endswith(".exe")
+}
+

def enquote_executable(executable):
if ' ' in executable:
@@ -164,6 +164,12 @@ class ScriptMaker(object):
"""
if os.name != 'posix':
simple_shebang = True
+ elif getattr(sys, "cross_compiling", False):
+ # In a cross-compiling environment, the shebang will likely be a
+ # script; this *must* be invoked with the "safe" version of the
+ # shebang, or else using os.exec() to run the entry script will
+ # fail, raising "OSError 8 [Errno 8] Exec format error".
+ simple_shebang = False
else:
# Add 3 for '#!' prefix and newline suffix.
shebang_length = len(executable) + len(post_interp) + 3
@@ -409,15 +427,11 @@ class ScriptMaker(object):
bits = '32'
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
Expand Down
122 changes: 122 additions & 0 deletions tools/vendoring/patches/packaging.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py
index 6667d2990..cb11c60b8 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,63 @@ 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
+ 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_"):
@@ -512,6 +569,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