Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
12 changes: 8 additions & 4 deletions cibuildwheel/platforms/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,17 +508,21 @@ 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)

# If cibuildwheel was called without activating its environment, the `bin`
# directory will not be on the PATH.
cibw_bin = Path(sys.executable).parent
call(cibw_bin / "patchelf", "--set-soname", new_soname, dst_path)

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


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