diff --git a/news/12961.feature.rst b/news/12961.feature.rst new file mode 100644 index 00000000000..e4e982db13b --- /dev/null +++ b/news/12961.feature.rst @@ -0,0 +1 @@ +Support for PEP 730 iOS wheels was added. diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index b6ed9a78e55..2e7b7450dce 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -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: @@ -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)) @@ -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("_") @@ -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: diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py index e16292b8330..0982e6b726e 100644 --- a/src/pip/_vendor/distlib/scripts.py +++ b/src/pip/_vendor/distlib/scripts.py @@ -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 diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 6667d299085..703f0ed53c0 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -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. @@ -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: diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index ee4d88c744a..d323dab7167 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -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` diff --git a/tools/vendoring/patches/distlib.patch b/tools/vendoring/patches/distlib.patch index de2834710a3..962eab1d74f 100644 --- a/tools/vendoring/patches/distlib.patch +++ b/tools/vendoring/patches/distlib.patch @@ -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 @@ -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 '' diff --git a/tools/vendoring/patches/packaging.patch b/tools/vendoring/patches/packaging.patch new file mode 100644 index 00000000000..1d109e55f62 --- /dev/null +++ b/tools/vendoring/patches/packaging.patch @@ -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: