Skip to content

Commit 51dbf8b

Browse files
committed
Add support for GraalPy
1 parent 08e5b97 commit 51dbf8b

11 files changed

+155
-19
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ What does it do?
3636
| PyPy 3.8 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
3737
| PyPy 3.9 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
3838
| PyPy 3.10 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
39+
| GraalPy 24.1 |||| 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>³ CPython 3.13 is built by default using Python RCs, starting with cibuildwheel 2.20. Free-threaded mode will still require opt-in using [`CIBW_FREE_THREADED_SUPPORT`](https://cibuildwheel.pypa.io/en/stable/options/#free-threaded-support).</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

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

bin/update_pythons.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import copy
66
import difflib
77
import logging
8+
import re
89
from collections.abc import Mapping, MutableMapping
910
from pathlib import Path
1011
from typing import Any, Final, Literal, TypedDict, Union
@@ -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 ConfigMacOS(TypedDict):
4855
identifier: str
4956
version: str
5057
url: str
5158

5259

53-
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
60+
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigWinGP, ConfigMacOS]
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):
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 = ConfigMacOS
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")
@@ -250,6 +323,8 @@ def __init__(self) -> None:
250323
self.macos_pypy = PyPyVersions("64")
251324
self.macos_pypy_arm64 = PyPyVersions("ARM64")
252325

326+
self.graalpy = GraalPyVersions()
327+
253328
def update_config(self, config: MutableMapping[str, str]) -> None:
254329
identifier = config["identifier"]
255330
version = Version(config["version"])
@@ -267,6 +342,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
267342
config_update = self.macos_pypy.update_version_macos(spec)
268343
elif "macosx_arm64" in identifier:
269344
config_update = self.macos_pypy_arm64.update_version_macos(spec)
345+
elif identifier.startswith("gp"):
346+
config_update = self.graalpy.update_version(identifier, spec)
270347
elif "t-win32" in identifier and identifier.startswith("cp"):
271348
config_update = self.windows_t_32.update_version_windows(spec)
272349
elif "win32" in identifier and identifier.startswith("cp"):
@@ -278,6 +355,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
278355
config_update = self.windows_64.update_version_windows(spec)
279356
elif identifier.startswith("pp"):
280357
config_update = self.windows_pypy_64.update_version_windows(spec)
358+
elif identifier.startswith("gp"):
359+
config_update = self.graalpy.update_version(identifier, spec)
281360
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
282361
config_update = self.windows_t_arm64.update_version_windows(spec)
283362
elif "win_arm64" in identifier and identifier.startswith("cp"):

cibuildwheel/logger.py

+2
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ def build_description_from_identifier(identifier: str) -> str:
212212
build_description += "CPython"
213213
elif python_interpreter == "pp":
214214
build_description += "PyPy"
215+
elif python_interpreter == "gp":
216+
build_description += "GraalPy"
215217
else:
216218
msg = f"unknown python {python_interpreter!r}"
217219
raise Exception(msg)

cibuildwheel/macos.py

+18
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ def can_use_uv(python_configuration: PythonConfiguration) -> bool:
198198
return all(conditions)
199199

200200

201+
def install_graalpy(tmp: Path, url: str) -> Path:
202+
graalpy_archive = url.rsplit("/", 1)[-1]
203+
extension = ".tar.gz"
204+
assert graalpy_archive.endswith(extension)
205+
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
206+
with FileLock(str(installation_path) + ".lock"):
207+
if not installation_path.exists():
208+
downloaded_archive = tmp / graalpy_archive
209+
download(url, downloaded_archive)
210+
installation_path.mkdir(parents=True)
211+
# GraalPy top-folder name is inconsistent with archive name
212+
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
213+
downloaded_archive.unlink()
214+
return installation_path / "bin" / "graalpy"
215+
216+
201217
def setup_python(
202218
tmp: Path,
203219
python_configuration: PythonConfiguration,
@@ -222,6 +238,8 @@ def setup_python(
222238

223239
elif implementation_id.startswith("pp"):
224240
base_python = install_pypy(tmp, python_configuration.url)
241+
elif implementation_id.startswith("gp"):
242+
base_python = install_graalpy(tmp, python_configuration.url)
225243
else:
226244
msg = "Unknown Python implementation"
227245
raise ValueError(msg)

cibuildwheel/options.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ def globals(self) -> GlobalOptions:
595595
output_dir = args.output_dir
596596

597597
build_config = (
598-
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "*"
598+
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "[!g]*"
599599
)
600600
skip_config = self.reader.get("skip", env_plat=False, option_format=ListFormat(sep=" "))
601601
test_skip = self.reader.get("test-skip", env_plat=False, option_format=ListFormat(sep=" "))

cibuildwheel/resources/build-platforms.toml

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ python_configurations = [
2222
{ identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
2323
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
2424
{ identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
25+
{ identifier = "gp241-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy241_311_native" },
2526
{ identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
2627
{ identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
2728
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
@@ -53,6 +54,7 @@ python_configurations = [
5354
{ identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
5455
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
5556
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
57+
{ identifier = "gp241-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy241_311_native" },
5658
{ identifier = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
5759
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
5860
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
@@ -136,6 +138,8 @@ python_configurations = [
136138
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" },
137139
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_x86_64.tar.bz2" },
138140
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_arm64.tar.bz2" },
141+
{ identifier = "gp241-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.1.0/graalpy-24.1.0-macos-amd64.tar.gz" },
142+
{ identifier = "gp241-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.1.0/graalpy-24.1.0-macos-aarch64.tar.gz" },
139143
]
140144

141145
[windows]
@@ -168,6 +172,7 @@ python_configurations = [
168172
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
169173
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
170174
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-win64.zip" },
175+
{ identifier = "gp241-win_amd64", version = "3.11", arch = "64", url = "https://github.com/oracle/graalpython/releases/download/graal-24.1.0/graalpy-24.1.0-windows-amd64.zip" },
171176
]
172177

173178
[pyodide]

cibuildwheel/windows.py

+16
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
138138
return installation_path / "python.exe"
139139

140140

141+
def install_graalpy(tmp: Path, url: str) -> Path:
142+
zip_filename = url.rsplit("/", 1)[-1]
143+
extension = ".zip"
144+
assert zip_filename.endswith(extension)
145+
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
146+
with FileLock(str(installation_path) + ".lock"):
147+
if not installation_path.exists():
148+
graalpy_zip = tmp / zip_filename
149+
download(url, graalpy_zip)
150+
# Extract to the parent directory because the zip file still contains a directory
151+
extract_zip(graalpy_zip, installation_path.parent)
152+
return installation_path / "bin" / "graalpy.exe"
153+
154+
141155
def setup_setuptools_cross_compile(
142156
tmp: Path,
143157
python_configuration: PythonConfiguration,
@@ -257,6 +271,8 @@ def setup_python(
257271
elif implementation_id.startswith("pp"):
258272
assert python_configuration.url is not None
259273
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
274+
elif implementation_id.startswith("gp"):
275+
base_python = install_graalpy(tmp, python_configuration.url or "")
260276
else:
261277
msg = "Unknown Python implementation"
262278
raise ValueError(msg)

test/test_abi_variants.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ def test_abi3(tmp_path):
5454
actual_wheels = utils.cibuildwheel_run(
5555
project_dir,
5656
add_env={
57-
# free_threaded and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
57+
# free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
5858
# also limit the number of builds for test performance reasons
59-
"CIBW_BUILD": f"cp39-* cp310-* pp310-* {single_python_tag}-* cp313t-*"
59+
"CIBW_BUILD": f"cp39-* cp310-* pp310-* gp241-* {single_python_tag}-* cp313t-*"
6060
},
6161
)
6262

@@ -72,7 +72,7 @@ def test_abi3(tmp_path):
7272
expected_wheels = [
7373
w.replace("cp310-cp310", "cp310-abi3")
7474
for w in expected_wheels
75-
if "-cp39" in w or "-cp310" in w or "-pp310" in w or "-cp313t" in w
75+
if "-cp39" in w or "-cp310" in w or "-pp310" in w or "-gp241" in w or "-cp313t" in w
7676
]
7777
assert set(actual_wheels) == set(expected_wheels)
7878

test/utils.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def expected_wheels(
204204
"pp38-pypy38_pp73",
205205
"pp39-pypy39_pp73",
206206
"pp310-pypy310_pp73",
207+
"graalpy311-graalpy241_311_native",
207208
]
208209

209210
if platform == "macos" and machine_arch == "arm64":
@@ -219,6 +220,7 @@ def expected_wheels(
219220
"pp38-pypy38_pp73",
220221
"pp39-pypy39_pp73",
221222
"pp310-pypy310_pp73",
223+
"graalpy311-graalpy241_311_native",
222224
]
223225

224226
if single_python:
@@ -245,7 +247,11 @@ def expected_wheels(
245247
if platform == "linux":
246248
architectures = [arch_name_for_linux(machine_arch)]
247249

248-
if machine_arch == "x86_64" and not single_arch:
250+
if (
251+
machine_arch == "x86_64"
252+
and not single_arch
253+
and not python_abi_tag.startswith("graalpy")
254+
):
249255
architectures.append("i686")
250256

251257
if len(manylinux_versions) > 0:
@@ -256,7 +262,7 @@ def expected_wheels(
256262
)
257263
for architecture in architectures
258264
]
259-
if len(musllinux_versions) > 0 and not python_abi_tag.startswith("pp"):
265+
if len(musllinux_versions) > 0 and not python_abi_tag.startswith(("pp", "graalpy")):
260266
platform_tags.extend(
261267
[
262268
".".join(

unit_test/linux_build_steps_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_linux_container_split(tmp_path: Path, 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)