Repository: pypa/pip (python)

Issue:
Add support for Android
### What's the problem this feature will solve?

Android has been an [officially supported Python platform](https://peps.python.org/pep-0011/) since Python 3.13. More recently, its wheel tag format was added to the [PyPA specs page](https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#android), and that format is now accepted by PyPI (e.g. see [here](https://pypi.org/project/pyspamsum/#files)). 

pip should be able to install these wheels in two contexts:

* Directly on an Android device, e.g. via the Termux app. 
* Through an app packaging tool such as Briefcase, using a command like `pip install --platform android_21_arm64_v8a mypackage`.

### Describe the solution you'd like

To avoid repeating the confusion around adding iOS support (#13053), I think the best sequence would be:

* Merge https://github.com/pypa/packaging/pull/880
* Make a `packaging` release.
* Update pip's vendored copy of `packaging`.
* Merge remaining changes to pip:
  * #13303 

### Alternative Solutions

Briefcase and Chaquopy currently use a forked version of pip that's several years old. They therefore lack many of pip's recent features, such as the new resolver.

Termux and Kivy don't support Android wheels at all, and compile everything from source, which is much slower.

### Code of Conduct

- [x] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).

Pull request description:
Add support for Android tags
* Closes #13299

As discussed in the linked issue, this PR depends on an update to `packaging`. I've tested it locally by copying the changes from https://github.com/pypa/packaging/pull/880, but it'll have to remain in draft state until the `packaging` PR has been merged and vendored into pip.

Patch:
diff --git a/news/13299.feature.rst b/news/13299.feature.rst
new file mode 100644
index 000000000..413901dae
--- /dev/null
+++ b/news/13299.feature.rst
@@ -0,0 +1,1 @@
+Support for :pep:`738` Android wheels.
diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py
index 43d6a4aa4..edbc7c37a 100644
--- a/src/pip/_internal/utils/compatibility_tags.py
+++ b/src/pip/_internal/utils/compatibility_tags.py
@@ -6,6 +6,7 @@ from typing import List, Optional, Tuple
 from pip._vendor.packaging.tags import (
     PythonVersion,
     Tag,
+    android_platforms,
     compatible_tags,
     cpython_tags,
     generic_tags,
@@ -63,6 +64,16 @@ def _ios_platforms(arch: str) -> List[str]:
     return arches
 
 
+def _android_platforms(arch: str) -> List[str]:
+    match = re.fullmatch(r"android_(\d+)_(.+)", arch)
+    if match:
+        api_level, abi = match.groups()
+        return list(android_platforms(int(api_level), abi))
+    else:
+        # arch pattern didn't match (?!)
+        return [arch]
+
+
 def _custom_manylinux_platforms(arch: str) -> List[str]:
     arches = [arch]
     arch_prefix, arch_sep, arch_suffix = arch.partition("_")
@@ -90,6 +101,8 @@ def _get_custom_platforms(arch: str) -> List[str]:
         arches = _mac_platforms(arch)
     elif arch.startswith("ios"):
         arches = _ios_platforms(arch)
+    elif arch_prefix == "android":
+        arches = _android_platforms(arch)
     elif arch_prefix in ["manylinux2014", "manylinux2010"]:
         arches = _custom_manylinux_platforms(arch)
     else:


Test patch:
diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py
index 46991e348..d9212ac8d 100644
--- a/tests/unit/test_models_wheel.py
+++ b/tests/unit/test_models_wheel.py
@@ -180,6 +180,56 @@ class TestWheelFile:
         w = Wheel("simple-0.1-cp313-none-ios_15_1_arm64_iphoneos.whl")
         assert not w.supported(tags=tags)
 
+    def test_android(self) -> None:
+        arm_old = compatibility_tags.get_supported(
+            "313", platforms=["android_21_arm64_v8a"], impl="cp"
+        )
+        arm_new = compatibility_tags.get_supported(
+            "313", platforms=["android_30_arm64_v8a"], impl="cp"
+        )
+        x86_old = compatibility_tags.get_supported(
+            "313", platforms=["android_21_x86_64"], impl="cp"
+        )
+        x86_new = compatibility_tags.get_supported(
+            "313", platforms=["android_30_x86_64"], impl="cp"
+        )
+
+        w = Wheel("simple-0.1-cp313-none-android_21_arm64_v8a.whl")
+        assert w.supported(arm_old)
+        assert w.supported(arm_new)
+        assert not w.supported(x86_old)
+        assert not w.supported(x86_new)
+
+        w = Wheel("simple-0.1-cp313-none-android_22_arm64_v8a.whl")
+        assert not w.supported(arm_old)
+        assert w.supported(arm_new)
+        assert not w.supported(x86_old)
+        assert not w.supported(x86_new)
+
+        w = Wheel("simple-0.1-cp313-none-android_31_arm64_v8a.whl")
+        assert not w.supported(arm_old)
+        assert not w.supported(arm_new)
+        assert not w.supported(x86_old)
+        assert not w.supported(x86_new)
+
+        w = Wheel("simple-0.1-cp313-none-android_20_x86_64.whl")
+        assert not w.supported(arm_old)
+        assert not w.supported(arm_new)
+        assert w.supported(x86_old)
+        assert w.supported(x86_new)
+
+        w = Wheel("simple-0.1-cp313-none-android_30_x86_64.whl")
+        assert not w.supported(arm_old)
+        assert not w.supported(arm_new)
+        assert not w.supported(x86_old)
+        assert w.supported(x86_new)
+
+        w = Wheel("simple-0.1-cp313-none-android_31_x86_64.whl")
+        assert not w.supported(arm_old)
+        assert not w.supported(arm_new)
+        assert not w.supported(x86_old)
+        assert not w.supported(x86_new)
+
     def test_support_index_min(self) -> None:
         """
         Test results from `support_index_min`