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/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 6667d299085..bcc6ea041b7 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,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: diff --git a/tests/functional/test_bad_url.py b/tests/functional/test_bad_url.py index bc3a987e6f2..b1938789867 100644 --- a/tests/functional/test_bad_url.py +++ b/tests/functional/test_bad_url.py @@ -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) assert proc.returncode == 1 expect = ( "ERROR: 404 Client Error: FileNotFoundError for url: file:///unexistent_file" 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/packaging.patch b/tools/vendoring/patches/packaging.patch new file mode 100644 index 00000000000..e952981068d --- /dev/null +++ b/tools/vendoring/patches/packaging.patch @@ -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: