Skip to content

Commit 07ccbbe

Browse files
committed
Add support for GraalPy
1 parent d85128f commit 07ccbbe

File tree

14 files changed

+163
-22
lines changed

14 files changed

+163
-22
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
env:
8181
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
8282
CIBW_BUILD_FRONTEND: 'build[uv]'
83-
CIBW_ENABLE: "cpython-prerelease cpython-freethreading pypy"
83+
CIBW_ENABLE: "cpython-prerelease cpython-freethreading pypy graalpy"
8484

8585
- name: Run a sample build (GitHub Action, only)
8686
uses: ./

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ While cibuildwheel itself requires a recent Python version to run (we support th
3636
| PyPy 3.9 v7.3 |||| 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 |
3838
| PyPy 3.11 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A |
39+
| GraalPy 24.2 |||| N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A | N/A | N/A |
3940

40-
<sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
41+
<sup>¹ PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
4142
<sup>² Windows arm64 support is experimental.</sup><br>
4243
<sup>³ Free-threaded mode requires opt-in using [`CIBW_ENABLE`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
4344
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>
4445
<sup>⁵ manylinux armv7l support is experimental. As there are no RHEL based image for this architecture, it's using an Ubuntu based image instead.</sup><br>
4546

46-
- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), and Windows wheels for CPython and PyPy
47+
- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), and Windows wheels for CPython, PyPy, and GraalPy
4748
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
4849
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
4950
- Runs your library's tests against the wheel-installed version of your library

bin/update_pythons.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import difflib
66
import logging
77
import operator
8+
import re
89
import tomllib
910
from collections.abc import Mapping, MutableMapping
1011
from pathlib import Path
@@ -44,13 +45,19 @@ class ConfigWinPP(TypedDict):
4445
url: str
4546

4647

48+
class ConfigWinGP(TypedDict):
49+
identifier: str
50+
version: str
51+
url: str
52+
53+
4754
class ConfigApple(TypedDict):
4855
identifier: str
4956
version: str
5057
url: str
5158

5259

53-
AnyConfig = ConfigWinCP | ConfigWinPP | ConfigApple
60+
AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple
5461

5562

5663
# The following set of "Versions" classes allow the initial call to the APIs to
@@ -106,6 +113,72 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
106113
)
107114

108115

116+
class GraalPyVersions:
117+
def __init__(self) -> None:
118+
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
119+
response.raise_for_status()
120+
121+
releases = response.json()
122+
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
123+
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
124+
for release in releases:
125+
m = gp_version_re.search(release["tag_name"])
126+
if m:
127+
release["graalpy_version"] = Version(m.group(1))
128+
m = cp_version_re.search(release["body"])
129+
if m:
130+
release["python_version"] = Version(m.group(1))
131+
132+
self.releases = [r for r in releases if "graalpy_version" in r and "python_version" in r]
133+
134+
def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
135+
if "x86_64" in identifier or "amd64" in identifier:
136+
arch = "x86_64"
137+
elif "arm64" in identifier or "aarch64" in identifier:
138+
arch = "aarch64"
139+
else:
140+
msg = f"{identifier} not supported yet on GraalPy"
141+
raise RuntimeError(msg)
142+
143+
releases = [r for r in self.releases if spec.contains(r["python_version"])]
144+
releases = sorted(releases, key=lambda r: r["graalpy_version"])
145+
146+
if not releases:
147+
msg = f"GraalPy {arch} not found for {spec}!"
148+
raise RuntimeError(msg)
149+
150+
release = releases[-1]
151+
version = release["python_version"]
152+
gpversion = release["graalpy_version"]
153+
154+
if "macosx" in identifier:
155+
arch = "x86_64" if "x86_64" in identifier else "arm64"
156+
config = ConfigApple
157+
platform = "macos"
158+
elif "win" in identifier:
159+
arch = "aarch64" if "arm64" in identifier else "x86_64"
160+
config = ConfigWinGP
161+
platform = "windows"
162+
else:
163+
msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
164+
raise RuntimeError(msg)
165+
166+
arch = "amd64" if arch == "x86_64" else "aarch64"
167+
ext = "zip" if "win" in identifier else "tar.gz"
168+
(url,) = (
169+
rf["browser_download_url"]
170+
for rf in release["assets"]
171+
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
172+
and rf["name"].startswith(f"graalpy-{gpversion.major}")
173+
)
174+
175+
return config(
176+
identifier=identifier,
177+
version=f"{version.major}.{version.minor}",
178+
url=url,
179+
)
180+
181+
109182
class PyPyVersions:
110183
def __init__(self, arch_str: ArchStr):
111184
response = requests.get("https://downloads.python.org/pypy/versions.json")
@@ -294,6 +367,8 @@ def __init__(self) -> None:
294367

295368
self.ios_cpython = CPythonIOSVersions()
296369

370+
self.graalpy = GraalPyVersions()
371+
297372
def update_config(self, config: MutableMapping[str, str]) -> None:
298373
identifier = config["identifier"]
299374
version = Version(config["version"])
@@ -311,6 +386,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
311386
config_update = self.macos_pypy.update_version_macos(spec)
312387
elif "macosx_arm64" in identifier:
313388
config_update = self.macos_pypy_arm64.update_version_macos(spec)
389+
elif identifier.startswith("gp"):
390+
config_update = self.graalpy.update_version(identifier, spec)
314391
elif "t-win32" in identifier and identifier.startswith("cp"):
315392
config_update = self.windows_t_32.update_version_windows(spec)
316393
elif "win32" in identifier and identifier.startswith("cp"):
@@ -322,6 +399,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
322399
config_update = self.windows_64.update_version_windows(spec)
323400
elif identifier.startswith("pp"):
324401
config_update = self.windows_pypy_64.update_version_windows(spec)
402+
elif identifier.startswith("gp"):
403+
config_update = self.graalpy.update_version(identifier, spec)
325404
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
326405
config_update = self.windows_t_arm64.update_version_windows(spec)
327406
elif "win_arm64" in identifier and identifier.startswith("cp"):

cibuildwheel/logger.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ def build_description_from_identifier(identifier: str) -> str:
241241
build_description += "CPython"
242242
elif python_interpreter == "pp":
243243
build_description += "PyPy"
244+
elif python_interpreter == "gp":
245+
build_description += "GraalPy"
244246
else:
245247
msg = f"unknown python {python_interpreter!r}"
246248
raise Exception(msg)

cibuildwheel/platforms/macos.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ def get_python_configurations(
104104
# skip builds as required by BUILD/SKIP
105105
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
106106

107-
# filter-out some cross-compilation configs with PyPy:
107+
# filter-out some cross-compilation configs with PyPy and GraalPy:
108108
# can't build arm64 on x86_64
109109
# rosetta allows to build x86_64 on arm64
110110
if platform.machine() == "x86_64":
111111
python_configurations_before = set(python_configurations)
112112
python_configurations = [
113113
c
114114
for c in python_configurations
115-
if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
115+
if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
116116
]
117117
removed_elements = python_configurations_before - set(python_configurations)
118118
if removed_elements:
@@ -192,6 +192,22 @@ def install_pypy(tmp: Path, url: str) -> Path:
192192
return installation_path / "bin" / "pypy3"
193193

194194

195+
def install_graalpy(tmp: Path, url: str) -> Path:
196+
graalpy_archive = url.rsplit("/", 1)[-1]
197+
extension = ".tar.gz"
198+
assert graalpy_archive.endswith(extension)
199+
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
200+
with FileLock(str(installation_path) + ".lock"):
201+
if not installation_path.exists():
202+
downloaded_archive = tmp / graalpy_archive
203+
download(url, downloaded_archive)
204+
installation_path.mkdir(parents=True)
205+
# GraalPy top-folder name is inconsistent with archive name
206+
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
207+
downloaded_archive.unlink()
208+
return installation_path / "bin" / "graalpy"
209+
210+
195211
def setup_python(
196212
tmp: Path,
197213
python_configuration: PythonConfiguration,
@@ -213,6 +229,8 @@ def setup_python(
213229

214230
elif implementation_id.startswith("pp"):
215231
base_python = install_pypy(tmp, python_configuration.url)
232+
elif implementation_id.startswith("gp"):
233+
base_python = install_graalpy(tmp, python_configuration.url)
216234
else:
217235
msg = "Unknown Python implementation"
218236
raise ValueError(msg)

cibuildwheel/platforms/windows.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
124124
return installation_path / "python.exe"
125125

126126

127+
def install_graalpy(tmp: Path, url: str) -> Path:
128+
zip_filename = url.rsplit("/", 1)[-1]
129+
extension = ".zip"
130+
assert zip_filename.endswith(extension)
131+
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
132+
with FileLock(str(installation_path) + ".lock"):
133+
if not installation_path.exists():
134+
graalpy_zip = tmp / zip_filename
135+
download(url, graalpy_zip)
136+
# Extract to the parent directory because the zip file still contains a directory
137+
extract_zip(graalpy_zip, installation_path.parent)
138+
return installation_path / "bin" / "graalpy.exe"
139+
140+
127141
def setup_setuptools_cross_compile(
128142
tmp: Path,
129143
python_configuration: PythonConfiguration,
@@ -240,6 +254,8 @@ def setup_python(
240254
elif implementation_id.startswith("pp"):
241255
assert python_configuration.url is not None
242256
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
257+
elif implementation_id.startswith("gp"):
258+
base_python = install_graalpy(tmp, python_configuration.url or "")
243259
else:
244260
msg = "Unknown Python implementation"
245261
raise ValueError(msg)

cibuildwheel/resources/build-platforms.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ python_configurations = [
1818
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
1919
{ identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
2020
{ identifier = "pp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
21+
{ identifier = "gp242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
2122
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
2223
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
2324
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -50,6 +51,7 @@ python_configurations = [
5051
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
5152
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
5253
{ identifier = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
54+
{ identifier = "gp242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
5355
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
5456
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
5557
{ identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
@@ -129,6 +131,8 @@ python_configurations = [
129131
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
130132
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_x86_64.tar.bz2" },
131133
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_arm64.tar.bz2" },
134+
{ identifier = "gp242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.0/graalpy-24.2.0-macos-amd64.tar.gz" },
135+
{ identifier = "gp242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.0/graalpy-24.2.0-macos-aarch64.tar.gz" },
132136
]
133137

134138
[windows]
@@ -157,6 +161,7 @@ python_configurations = [
157161
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
158162
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip" },
159163
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-win64.zip" },
164+
{ identifier = "gp242-win_amd64", version = "3.11", arch = "64", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.0/graalpy-24.2.0-windows-amd64.zip" },
160165
]
161166

162167
[pyodide]

cibuildwheel/selector.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class EnableGroup(StrEnum):
3232
CPythonFreeThreading = "cpython-freethreading"
3333
CPythonPrerelease = "cpython-prerelease"
3434
PyPy = "pypy"
35+
GraalPy = "graalpy"
3536

3637
@classmethod
3738
def all_groups(cls) -> frozenset["EnableGroup"]:
@@ -70,6 +71,8 @@ def __call__(self, build_id: str) -> bool:
7071
return False
7172
if EnableGroup.PyPy not in self.enable and fnmatch(build_id, "pp*"):
7273
return False
74+
if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"):
75+
return False
7376

7477
should_build = selector_matches(self.build_config, build_id)
7578
should_skip = selector_matches(self.skip_config, build_id)

docs/options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ values are:
559559
The build identifiers for those variants have a `t` suffix in their
560560
`python_tag` (e.g. `cp313t-manylinux_x86_64`).
561561
- `pypy`: Enable PyPy.
562+
- `graalpy`: Enable GraalPy.
562563

563564

564565
!!! caution

test/test_abi_variants.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def test_abi3(tmp_path):
4141
actual_wheels = utils.cibuildwheel_run(
4242
project_dir,
4343
add_env={
44-
# free_threaded and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
44+
# free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
4545
# also limit the number of builds for test performance reasons
46-
"CIBW_BUILD": f"cp39-* cp310-* pp310-* {single_python_tag}-* cp313t-*"
46+
"CIBW_BUILD": f"cp39-* cp310-* pp310-* gp242-* {single_python_tag}-* cp313t-*"
4747
},
4848
)
4949

@@ -59,7 +59,11 @@ def test_abi3(tmp_path):
5959
expected_wheels = [
6060
w.replace("cp310-cp310", "cp310-abi3")
6161
for w in expected_wheels
62-
if "-cp39" in w or "-cp310" in w or "-pp310" in w or "-cp313t" in w
62+
if "-cp39" in w
63+
or "-cp310" in w
64+
or "-pp310" in w
65+
or "-graalpy242" in w
66+
or "-cp313t" in w
6367
]
6468
assert set(actual_wheels) == set(expected_wheels)
6569

test/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def cibuildwheel_get_build_identifiers(
5555
cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)]
5656
if env is None:
5757
env = os.environ.copy()
58-
env["CIBW_ENABLE"] = "cpython-freethreading pypy"
58+
env["CIBW_ENABLE"] = "cpython-freethreading pypy graalpy"
5959
if prerelease_pythons:
6060
env["CIBW_ENABLE"] += " cpython-prerelease"
6161

@@ -253,6 +253,10 @@ def _expected_wheels(
253253
"pp310-pypy310_pp73",
254254
"pp311-pypy311_pp73",
255255
]
256+
if machine_arch in ["x86_64", "AMD64", "aarch64", "arm64"]:
257+
python_abi_tags += [
258+
"graalpy311-graalpy242_311_native",
259+
]
256260

257261
if single_python:
258262
python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION)
@@ -283,7 +287,7 @@ def _expected_wheels(
283287
for manylinux_version in manylinux_versions
284288
)
285289
]
286-
if len(musllinux_versions) > 0 and not python_abi_tag.startswith("pp"):
290+
if len(musllinux_versions) > 0 and not python_abi_tag.startswith(("pp", "graalpy")):
287291
platform_tags.append(
288292
".".join(
289293
f"{musllinux_version}_{machine_arch}"

unit_test/linux_build_steps_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_linux_container_split(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
2424
manylinux-x86_64-image = "normal_container_image"
2525
manylinux-i686-image = "normal_container_image"
2626
build = "*-manylinux_x86_64"
27-
skip = "pp*"
27+
skip = "[gp]p*"
2828
archs = "x86_64 i686"
2929
3030
[[tool.cibuildwheel.overrides]]

0 commit comments

Comments
 (0)