Skip to content

Commit 0826975

Browse files
authored
Android updates (#2568)
* Add Python 3.14 for Android * Simplify GitHub Actions workflows * Update tests * Allow running `patchelf` even when the environment's `bin` directory is not on the PATH * Search in both the environment's bin directory and the PATH * Use `sysconfig.get_path` * Fix lint warning
1 parent 3f076ba commit 0826975

File tree

13 files changed

+149
-167
lines changed

13 files changed

+149
-167
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,6 @@ jobs:
105105
/usr/local/share/powershell
106106
df -h
107107
108-
# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
109-
- name: Enable KVM for Android emulator
110-
if: runner.os == 'Linux' && runner.arch == 'X64'
111-
run: |
112-
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
113-
sudo udevadm control --reload-rules
114-
sudo udevadm trigger --name-match=kvm
115-
116108
# for oci_container unit tests
117109
- name: Set up QEMU
118110
if: runner.os == 'Linux'

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ While cibuildwheel itself requires a recent Python version to run (we support th
2929
| CPython 3.9 ||||| ✅² |||||| ✅⁵ | N/A | N/A | N/A |
3030
| CPython 3.10 ||||| ✅² |||||| ✅⁵ | N/A | N/A | N/A |
3131
| CPython 3.11 ||||| ✅² |||||| ✅⁵ | N/A | N/A | N/A |
32-
| CPython 3.12 ||||| ✅² |||||| ✅⁵ | N/A | N/A | ✅⁴ |
33-
| CPython 3.13³ ||||| ✅² |||||| ✅⁵ ||| N/A |
34-
| CPython 3.14 ||||| ✅² |||||| ✅⁵ | N/A | N/A | N/A |
32+
| CPython 3.12 ||||| ✅² |||||| ✅⁵ | N/A | N/A | ✅⁴ |
33+
| CPython 3.13³ ||||| ✅² |||||| ✅⁵ | | | ✅⁴ |
34+
| CPython 3.14 ||||| ✅² |||||| ✅⁵ || | N/A |
3535
| PyPy 3.8 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
3636
| PyPy 3.9 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
3737
| PyPy 3.10 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |

bin/update_pythons.py

Lines changed: 40 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env python3
22

33

4-
import copy
54
import difflib
65
import logging
76
import operator
@@ -34,47 +33,20 @@
3433
ArchStr = Literal["32", "64", "ARM64"]
3534

3635

37-
class ConfigWinCP(TypedDict):
36+
class Config(TypedDict):
3837
identifier: str
3938
version: str
40-
arch: str
4139

4240

43-
class ConfigWinPP(TypedDict):
44-
identifier: str
45-
version: str
46-
arch: str
47-
url: str
48-
49-
50-
class ConfigWinGP(TypedDict):
51-
identifier: str
52-
version: str
53-
url: str
54-
55-
56-
class ConfigApple(TypedDict):
57-
identifier: str
58-
version: str
59-
url: str
60-
61-
62-
class ConfigAndroid(TypedDict):
63-
identifier: str
64-
version: str
41+
class ConfigUrl(Config):
6542
url: str
6643

6744

68-
class ConfigPyodide(TypedDict):
69-
identifier: str
70-
version: str
45+
class ConfigPyodide(Config):
7146
default_pyodide_version: str
7247
node_version: str
7348

7449

75-
AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple | ConfigAndroid | ConfigPyodide
76-
77-
7850
# The following set of "Versions" classes allow the initial call to the APIs to
7951
# be cached and reused in the `update_version_*` methods.
8052

@@ -106,7 +78,7 @@ def __init__(self, arch_str: ArchStr, free_threaded: bool) -> None:
10678

10779
self.version_dict = {Version(v): v for v in cp_info["versions"]}
10880

109-
def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
81+
def update_version_windows(self, spec: Specifier) -> Config | None:
11082
# Specifier.filter selects all non pre-releases that match the spec,
11183
# unless there are only pre-releases, then it selects pre-releases
11284
# instead (like pip)
@@ -121,10 +93,9 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
12193
flags = "t" if self.free_threaded else ""
12294
version = versions[0]
12395
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
124-
return ConfigWinCP(
96+
return Config(
12597
identifier=identifier,
12698
version=self.version_dict[version],
127-
arch=self.arch_str,
12899
)
129100

130101

@@ -146,7 +117,7 @@ def __init__(self) -> None:
146117

147118
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
148119

149-
def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
120+
def update_version(self, identifier: str, spec: Specifier) -> ConfigUrl:
150121
if "x86_64" in identifier or "amd64" in identifier:
151122
arch = "x86_64"
152123
elif "arm64" in identifier or "aarch64" in identifier:
@@ -172,11 +143,9 @@ def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
172143

173144
if "macosx" in identifier:
174145
arch = "x86_64" if "x86_64" in identifier else "arm64"
175-
config = ConfigApple
176146
platform = "macos"
177147
elif "win" in identifier:
178148
arch = "aarch64" if "arm64" in identifier else "x86_64"
179-
config = ConfigWinGP
180149
platform = "windows"
181150
else:
182151
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
@@ -191,7 +160,7 @@ def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
191160
and rf["name"].startswith(f"graalpy-{gpversion.major}")
192161
)
193162

194-
return config(
163+
return ConfigUrl(
195164
identifier=identifier,
196165
version=f"{version.major}.{version.minor}",
197166
url=url,
@@ -223,7 +192,7 @@ def get_arch_file(self, release: Mapping[str, Any]) -> str:
223192
]
224193
return urls[0] if urls else ""
225194

226-
def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
195+
def update_version_windows(self, spec: Specifier) -> ConfigUrl:
227196
releases = [r for r in self.releases if spec.contains(r["python_version"])]
228197
releases = sorted(releases, key=operator.itemgetter("pypy_version"))
229198
releases = [r for r in releases if self.get_arch_file(r)]
@@ -239,14 +208,13 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
239208
identifier = f"pp{version.major}{version.minor}-{version_arch}"
240209
url = self.get_arch_file(release)
241210

242-
return ConfigWinPP(
211+
return ConfigUrl(
243212
identifier=identifier,
244213
version=f"{version.major}.{version.minor}",
245-
arch=self.arch,
246214
url=url,
247215
)
248216

249-
def update_version_macos(self, spec: Specifier) -> ConfigApple:
217+
def update_version_macos(self, spec: Specifier) -> ConfigUrl:
250218
if self.arch not in {"64", "ARM64"}:
251219
msg = f"'{self.arch}' arch not supported yet on macOS"
252220
raise RuntimeError(msg)
@@ -270,7 +238,7 @@ def update_version_macos(self, spec: Specifier) -> ConfigApple:
270238
if "" in rf["platform"] == "darwin" and rf["arch"] == arch
271239
)
272240

273-
return ConfigApple(
241+
return ConfigUrl(
274242
identifier=identifier,
275243
version=f"{version.major}.{version.minor}",
276244
url=url,
@@ -298,16 +266,11 @@ def __init__(self) -> None:
298266
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
299267
self.versions_dict[version] = uri
300268

301-
def update_version_macos(
302-
self, identifier: str, version: Version, spec: Specifier
303-
) -> ConfigApple | None:
269+
def update_version(self, identifier: str, spec: Specifier, file_ident: str) -> ConfigUrl | None:
304270
# see note above on Specifier.filter
305271
unsorted_versions = spec.filter(self.versions_dict)
306272
sorted_versions = sorted(unsorted_versions, reverse=True)
307273

308-
macver = "x10.9" if version <= Version("3.8.9999") else "11"
309-
file_ident = f"macos{macver}.pkg"
310-
311274
for new_version in sorted_versions:
312275
# Find the first patch version that contains the requested file
313276
uri = self.versions_dict[new_version]
@@ -319,17 +282,25 @@ def update_version_macos(
319282

320283
urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
321284
if urls:
322-
return ConfigApple(
285+
return ConfigUrl(
323286
identifier=identifier,
324287
version=f"{new_version.major}.{new_version.minor}",
325288
url=urls[0],
326289
)
327290

328291
return None
329292

293+
def update_version_macos(
294+
self, identifier: str, version: Version, spec: Specifier
295+
) -> ConfigUrl | None:
296+
macver = "x10.9" if version <= Version("3.8.9999") else "11"
297+
return self.update_version(identifier, spec, f"macos{macver}.pkg")
298+
299+
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
300+
return self.update_version(identifier, spec, android_triplet(identifier))
330301

331-
class AndroidVersions:
332-
# This should be replaced with official python.org downloads once they're available.
302+
303+
class MavenVersions:
333304
MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"
334305

335306
def __init__(self) -> None:
@@ -343,18 +314,16 @@ def __init__(self) -> None:
343314
assert isinstance(version_str, str), version_str
344315
self.versions.append(Version(version_str))
345316

346-
def update_version_android(
347-
self, identifier: str, version: Version, spec: Specifier
348-
) -> ConfigAndroid | None:
317+
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
349318
sorted_versions = sorted(spec.filter(self.versions), reverse=True)
350319

351320
# Return a config using the highest version for the given specifier.
352321
if sorted_versions:
353322
max_version = sorted_versions[0]
354323
triplet = android_triplet(identifier)
355-
return ConfigAndroid(
324+
return ConfigUrl(
356325
identifier=identifier,
357-
version=str(version),
326+
version=f"{max_version.major}.{max_version.minor}",
358327
url=f"{self.MAVEN_URL}/{max_version}/python-{max_version}-{triplet}.tar.gz",
359328
)
360329
else:
@@ -390,11 +359,11 @@ def __init__(self) -> None:
390359
if filename.endswith("-iOS-support"):
391360
self.versions_dict[version][int(build[1:])] = asset["browser_download_url"]
392361

393-
def update_version_ios(self, identifier: str, version: Version) -> ConfigApple | None:
362+
def update_version_ios(self, identifier: str, version: Version) -> ConfigUrl | None:
394363
# Return a config using the highest build number for the given version.
395364
urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())]
396365
if urls:
397-
return ConfigApple(
366+
return ConfigUrl(
398367
identifier=identifier,
399368
version=str(version),
400369
url=urls[-1],
@@ -450,11 +419,11 @@ def __init__(self) -> None:
450419
self.windows_t_arm64 = WindowsVersions("ARM64", True)
451420
self.windows_pypy_64 = PyPyVersions("64")
452421

453-
self.macos_cpython = CPythonVersions()
422+
self.cpython = CPythonVersions()
454423
self.macos_pypy = PyPyVersions("64")
455424
self.macos_pypy_arm64 = PyPyVersions("ARM64")
456425

457-
self.android = AndroidVersions()
426+
self.maven = MavenVersions()
458427
self.ios_cpython = CPythonIOSVersions()
459428

460429
self.graalpy = GraalPyVersions()
@@ -466,13 +435,12 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
466435
version = Version(config["version"])
467436
spec = Specifier(f"=={version.major}.{version.minor}.*")
468437
log.info("Reading in %r -> %s @ %s", str(identifier), spec, version)
469-
orig_config = copy.copy(config)
470-
config_update: AnyConfig | None = None
438+
config_update: Config | None = None
471439

472440
# We need to use ** in update due to MyPy (probably a bug)
473441
if "macosx" in identifier:
474442
if identifier.startswith("cp"):
475-
config_update = self.macos_cpython.update_version_macos(identifier, version, spec)
443+
config_update = self.cpython.update_version_macos(identifier, version, spec)
476444
elif identifier.startswith("pp"):
477445
if "macosx_x86_64" in identifier:
478446
config_update = self.macos_pypy.update_version_macos(spec)
@@ -498,7 +466,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
498466
elif "win_arm64" in identifier and identifier.startswith("cp"):
499467
config_update = self.windows_arm64.update_version_windows(spec)
500468
elif "android" in identifier:
501-
config_update = self.android.update_version_android(identifier, version, spec)
469+
# Python 3.13 is released by Chaquopy on Maven Central.
470+
# Python 3.14 and newer have official releases on python.org.
471+
versions = self.maven if identifier.startswith("cp313") else self.cpython
472+
config_update = versions.update_version_android(identifier, spec)
502473
elif "ios" in identifier:
503474
config_update = self.ios_cpython.update_version_ios(identifier, version)
504475
elif "pyodide" in identifier:
@@ -507,10 +478,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
507478
)
508479

509480
assert config_update is not None, f"{identifier} not found!"
510-
config.update(**config_update)
511-
512-
if config != orig_config:
513-
log.info(" Updated %s to %s", orig_config, config)
481+
if config_update != config:
482+
log.info(" Updated %s to %s", config, config_update)
483+
config.clear()
484+
config.update(**config_update)
514485

515486

516487
@click.command()

cibuildwheel/platforms/android.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import shlex
77
import shutil
88
import subprocess
9-
import sys
9+
import sysconfig
1010
from collections.abc import Iterable, Iterator
1111
from dataclasses import dataclass
1212
from os.path import relpath
@@ -205,8 +205,8 @@ def setup_env(
205205
build_env = build_options.environment.as_dictionary(build_env)
206206
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
207207
for command in ["python", "pip"]:
208-
which = call("which", command, env=build_env, capture_stdout=True).strip()
209-
if which != f"{venv_dir}/bin/{command}":
208+
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
209+
if command_path != f"{venv_dir}/bin/{command}":
210210
msg = (
211211
f"{command} available on PATH doesn't match our installed instance. If you "
212212
f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
@@ -508,17 +508,28 @@ def repair_default(
508508
new_soname = soname_with_hash(src_path)
509509
dst_path = libs_dir / new_soname
510510
shutil.copyfile(src_path, dst_path)
511-
call("patchelf", "--set-soname", new_soname, dst_path)
511+
call(which("patchelf"), "--set-soname", new_soname, dst_path)
512512

513513
for path in paths_to_patch:
514-
call("patchelf", "--replace-needed", old_soname, new_soname, path)
514+
call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
515515
call(
516-
"patchelf",
516+
which("patchelf"),
517517
"--set-rpath",
518518
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
519519
path,
520520
)
521-
call(sys.executable, "-m", "wheel", "pack", unpacked_dir, "-d", repaired_wheel_dir)
521+
call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)
522+
523+
524+
# If cibuildwheel was called without activating its environment, its scripts directory
525+
# will not be on the PATH.
526+
def which(cmd: str) -> str:
527+
scripts_dir = sysconfig.get_path("scripts")
528+
result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
529+
if result is None:
530+
msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
531+
raise errors.FatalError(msg)
532+
return result
522533

523534

524535
def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:

cibuildwheel/platforms/windows.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,17 @@ def get_nuget_args(
5454
@dataclasses.dataclass(frozen=True, kw_only=True)
5555
class PythonConfiguration:
5656
version: str
57-
arch: str
5857
identifier: str
5958
url: str | None = None
6059

60+
@property
61+
def arch(self) -> str:
62+
return {
63+
"win32": "32",
64+
"win_amd64": "64",
65+
"win_arm64": "ARM64",
66+
}[self.identifier.split("-")[-1]]
67+
6168

6269
def all_python_configurations() -> list[PythonConfiguration]:
6370
config_dicts = resources.read_python_configs("windows")

0 commit comments

Comments
 (0)