Skip to content
Open
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
8 changes: 0 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,6 @@ jobs:
/usr/local/share/powershell
df -h

# https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/
- name: Enable KVM for Android emulator
if: runner.os == 'Linux' && runner.arch == 'X64'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

# for oci_container unit tests
- name: Set up QEMU
if: runner.os == 'Linux'
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ While cibuildwheel itself requires a recent Python version to run (we support th
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | ✅⁴ |
| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | N/A |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | ✅⁴ |
| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | ✅⁴ |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | ✅ | N/A |
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | N/A |
Expand Down
109 changes: 40 additions & 69 deletions bin/update_pythons.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3


import copy
import difflib
import logging
import operator
Expand Down Expand Up @@ -34,47 +33,20 @@
ArchStr = Literal["32", "64", "ARM64"]


class ConfigWinCP(TypedDict):
class Config(TypedDict):
identifier: str
version: str
arch: str


class ConfigWinPP(TypedDict):
identifier: str
version: str
arch: str
url: str


class ConfigWinGP(TypedDict):
identifier: str
version: str
url: str


class ConfigApple(TypedDict):
identifier: str
version: str
url: str


class ConfigAndroid(TypedDict):
identifier: str
version: str
class ConfigUrl(Config):
url: str


class ConfigPyodide(TypedDict):
identifier: str
version: str
class ConfigPyodide(Config):
default_pyodide_version: str
node_version: str


AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple | ConfigAndroid | ConfigPyodide


# The following set of "Versions" classes allow the initial call to the APIs to
# be cached and reused in the `update_version_*` methods.

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

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

def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
def update_version_windows(self, spec: Specifier) -> Config | None:
# Specifier.filter selects all non pre-releases that match the spec,
# unless there are only pre-releases, then it selects pre-releases
# instead (like pip)
Expand All @@ -121,10 +93,9 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
flags = "t" if self.free_threaded else ""
version = versions[0]
identifier = f"cp{version.major}{version.minor}{flags}-{self.arch}"
return ConfigWinCP(
return Config(
identifier=identifier,
version=self.version_dict[version],
arch=self.arch_str,
)


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

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

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

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

return config(
return ConfigUrl(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
Expand Down Expand Up @@ -223,7 +192,7 @@ def get_arch_file(self, release: Mapping[str, Any]) -> str:
]
return urls[0] if urls else ""

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

return ConfigWinPP(
return ConfigUrl(
identifier=identifier,
version=f"{version.major}.{version.minor}",
arch=self.arch,
url=url,
)

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

return ConfigApple(
return ConfigUrl(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
Expand Down Expand Up @@ -298,16 +266,11 @@ def __init__(self) -> None:
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
self.versions_dict[version] = uri

def update_version_macos(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigApple | None:
def update_version(self, identifier: str, spec: Specifier, file_ident: str) -> ConfigUrl | None:
# see note above on Specifier.filter
unsorted_versions = spec.filter(self.versions_dict)
sorted_versions = sorted(unsorted_versions, reverse=True)

macver = "x10.9" if version <= Version("3.8.9999") else "11"
file_ident = f"macos{macver}.pkg"

for new_version in sorted_versions:
# Find the first patch version that contains the requested file
uri = self.versions_dict[new_version]
Expand All @@ -319,17 +282,25 @@ def update_version_macos(

urls = [rf["url"] for rf in file_info if file_ident in rf["url"]]
if urls:
return ConfigApple(
return ConfigUrl(
identifier=identifier,
version=f"{new_version.major}.{new_version.minor}",
url=urls[0],
)

return None

def update_version_macos(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigUrl | None:
macver = "x10.9" if version <= Version("3.8.9999") else "11"
return self.update_version(identifier, spec, f"macos{macver}.pkg")

def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
return self.update_version(identifier, spec, android_triplet(identifier))

class AndroidVersions:
# This should be replaced with official python.org downloads once they're available.

class MavenVersions:
MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"

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

def update_version_android(
self, identifier: str, version: Version, spec: Specifier
) -> ConfigAndroid | None:
def update_version_android(self, identifier: str, spec: Specifier) -> ConfigUrl | None:
sorted_versions = sorted(spec.filter(self.versions), reverse=True)

# Return a config using the highest version for the given specifier.
if sorted_versions:
max_version = sorted_versions[0]
triplet = android_triplet(identifier)
return ConfigAndroid(
return ConfigUrl(
identifier=identifier,
version=str(version),
version=f"{max_version.major}.{max_version.minor}",
url=f"{self.MAVEN_URL}/{max_version}/python-{max_version}-{triplet}.tar.gz",
)
else:
Expand Down Expand Up @@ -390,11 +359,11 @@ def __init__(self) -> None:
if filename.endswith("-iOS-support"):
self.versions_dict[version][int(build[1:])] = asset["browser_download_url"]

def update_version_ios(self, identifier: str, version: Version) -> ConfigApple | None:
def update_version_ios(self, identifier: str, version: Version) -> ConfigUrl | None:
# Return a config using the highest build number for the given version.
urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())]
if urls:
return ConfigApple(
return ConfigUrl(
identifier=identifier,
version=str(version),
url=urls[-1],
Expand Down Expand Up @@ -450,11 +419,11 @@ def __init__(self) -> None:
self.windows_t_arm64 = WindowsVersions("ARM64", True)
self.windows_pypy_64 = PyPyVersions("64")

self.macos_cpython = CPythonVersions()
self.cpython = CPythonVersions()
self.macos_pypy = PyPyVersions("64")
self.macos_pypy_arm64 = PyPyVersions("ARM64")

self.android = AndroidVersions()
self.maven = MavenVersions()
self.ios_cpython = CPythonIOSVersions()

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

# We need to use ** in update due to MyPy (probably a bug)
if "macosx" in identifier:
if identifier.startswith("cp"):
config_update = self.macos_cpython.update_version_macos(identifier, version, spec)
config_update = self.cpython.update_version_macos(identifier, version, spec)
elif identifier.startswith("pp"):
if "macosx_x86_64" in identifier:
config_update = self.macos_pypy.update_version_macos(spec)
Expand All @@ -498,7 +466,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
elif "win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_arm64.update_version_windows(spec)
elif "android" in identifier:
config_update = self.android.update_version_android(identifier, version, spec)
# Python 3.13 is released by Chaquopy on Maven Central.
# Python 3.14 and newer have official releases on python.org.
versions = self.maven if identifier.startswith("cp313") else self.cpython
config_update = versions.update_version_android(identifier, spec)
elif "ios" in identifier:
config_update = self.ios_cpython.update_version_ios(identifier, version)
elif "pyodide" in identifier:
Expand All @@ -507,10 +478,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
)

assert config_update is not None, f"{identifier} not found!"
config.update(**config_update)

if config != orig_config:
log.info(" Updated %s to %s", orig_config, config)
if config_update != config:
log.info(" Updated %s to %s", config, config_update)
config.clear()
config.update(**config_update)


@click.command()
Expand Down
25 changes: 18 additions & 7 deletions cibuildwheel/platforms/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shlex
import shutil
import subprocess
import sys
import sysconfig
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from os.path import relpath
Expand Down Expand Up @@ -205,8 +205,8 @@ def setup_env(
build_env = build_options.environment.as_dictionary(build_env)
build_env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
for command in ["python", "pip"]:
which = call("which", command, env=build_env, capture_stdout=True).strip()
if which != f"{venv_dir}/bin/{command}":
command_path = call("which", command, env=build_env, capture_stdout=True).strip()
if command_path != f"{venv_dir}/bin/{command}":
msg = (
f"{command} available on PATH doesn't match our installed instance. If you "
f"have modified PATH, ensure that you don't overwrite cibuildwheel's entry "
Expand Down Expand Up @@ -508,17 +508,28 @@ def repair_default(
new_soname = soname_with_hash(src_path)
dst_path = libs_dir / new_soname
shutil.copyfile(src_path, dst_path)
call("patchelf", "--set-soname", new_soname, dst_path)
call(which("patchelf"), "--set-soname", new_soname, dst_path)

for path in paths_to_patch:
call("patchelf", "--replace-needed", old_soname, new_soname, path)
call(which("patchelf"), "--replace-needed", old_soname, new_soname, path)
call(
"patchelf",
which("patchelf"),
"--set-rpath",
f"${{ORIGIN}}/{relpath(libs_dir, path.parent)}",
path,
)
call(sys.executable, "-m", "wheel", "pack", unpacked_dir, "-d", repaired_wheel_dir)
call(which("wheel"), "pack", unpacked_dir, "-d", repaired_wheel_dir)


# If cibuildwheel was called without activating its environment, its scripts directory
# will not be on the PATH.
def which(cmd: str) -> str:
scripts_dir = sysconfig.get_path("scripts")
result = shutil.which(cmd, path=scripts_dir + os.pathsep + os.environ["PATH"])
if result is None:
msg = f"Couldn't find {cmd!r} in {scripts_dir} or on the PATH"
raise errors.FatalError(msg)
return result


def elf_file_filter(paths: Iterable[Path]) -> Iterator[tuple[Path, ELFFile]]:
Expand Down
9 changes: 8 additions & 1 deletion cibuildwheel/platforms/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,17 @@ def get_nuget_args(
@dataclasses.dataclass(frozen=True, kw_only=True)
class PythonConfiguration:
version: str
arch: str
identifier: str
url: str | None = None

@property
def arch(self) -> str:
return {
"win32": "32",
"win_amd64": "64",
"win_arm64": "ARM64",
}[self.identifier.split("-")[-1]]


def all_python_configurations() -> list[PythonConfiguration]:
config_dicts = resources.read_python_configs("windows")
Expand Down
Loading
Loading