Skip to content

Commit 020a91b

Browse files
timfelpre-commit-ci[bot]henryiii
authored
feat: support GraalPy (#1538)
* Properly close files used for testing * Add support for GraalPy * Help GraalPy discover build tools on Windows * Expect manylinux-interpreters ensure graalpy* warning in pip * Workaround GraalPy bugs on Windows * Workaround oracle/graalpython#491 also when uv is not available * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update azure-pipelines.yml * Update azure-pipelines.yml * refacotor: use pathlib.write_text Signed-off-by: Henry Schreiner <[email protected]> * Include GraalPy in docker_warmup and remove workaround for installing it late --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 5f8d06f commit 020a91b

22 files changed

+287
-38
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

azure-pipelines.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pr:
66

77
jobs:
88
- job: linux_311
9-
timeoutInMinutes: 120
9+
timeoutInMinutes: 180
1010
pool: {vmImage: 'Ubuntu-22.04'}
1111
steps:
1212
- task: UsePythonVersion@0
@@ -20,6 +20,7 @@ jobs:
2020
2121
- job: macos_311
2222
pool: {vmImage: 'macOS-13'}
23+
timeoutInMinutes: 120
2324
steps:
2425
- task: UsePythonVersion@0
2526
inputs:

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
@@ -243,6 +243,8 @@ def build_description_from_identifier(identifier: str) -> str:
243243
build_description += "CPython"
244244
elif python_interpreter == "pp":
245245
build_description += "PyPy"
246+
elif python_interpreter == "gp":
247+
build_description += "GraalPy"
246248
else:
247249
msg = f"unknown python {python_interpreter!r}"
248250
raise Exception(msg)

cibuildwheel/platforms/macos.py

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

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

193193

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

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

cibuildwheel/platforms/windows.py

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

125125

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

333+
if implementation_id.startswith("gp"):
334+
# GraalPy fails to discover compilers, setup the relevant environment
335+
# variables. Adapted from
336+
# https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
337+
# Remove when https://github.com/oracle/graalpython/issues/492 is fixed.
338+
vcpath = subprocess.check_output(
339+
[
340+
Path(os.environ["PROGRAMFILES(X86)"])
341+
/ "Microsoft Visual Studio"
342+
/ "Installer"
343+
/ "vswhere.exe",
344+
"-products",
345+
"*",
346+
"-latest",
347+
"-property",
348+
"installationPath",
349+
],
350+
text=True,
351+
).strip()
352+
log.notice(f"Discovering Visual Studio for GraalPy at {vcpath}")
353+
env.update(
354+
dict(
355+
[
356+
envvar.strip().split("=", 1)
357+
for envvar in subprocess.check_output(
358+
[
359+
f"{vcpath}\\Common7\\Tools\\vsdevcmd.bat",
360+
"-no_logo",
361+
"-arch=amd64",
362+
"-host_arch=amd64",
363+
"&&",
364+
"set",
365+
],
366+
shell=True,
367+
text=True,
368+
env=env,
369+
)
370+
.strip()
371+
.split("\n")
372+
]
373+
)
374+
)
375+
317376
return base_python, env
318377

319378

@@ -342,6 +401,7 @@ def build(options: Options, tmp_path: Path) -> None:
342401
for config in python_configurations:
343402
build_options = options.build_options(config.identifier)
344403
build_frontend = build_options.build_frontend or BuildFrontendConfig("build")
404+
345405
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
346406
log.build_start(config.identifier)
347407

@@ -390,6 +450,22 @@ def build(options: Options, tmp_path: Path) -> None:
390450
build_frontend, build_options.build_verbosity, build_options.config_settings
391451
)
392452

453+
if (
454+
config.identifier.startswith("gp")
455+
and build_frontend.name == "build"
456+
and "--no-isolation" not in extra_flags
457+
and "-n" not in extra_flags
458+
):
459+
# GraalPy fails to discover its standard library when a venv is created
460+
# from a virtualenv seeded executable. See
461+
# https://github.com/oracle/graalpython/issues/491 and remove this once
462+
# fixed upstream.
463+
log.notice(
464+
"Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
465+
)
466+
shell("graalpy -m pip install setuptools wheel", env=env)
467+
extra_flags = [*extra_flags, "-n"]
468+
393469
build_env = env.copy()
394470
if pip_version is not None:
395471
build_env["VIRTUALENV_PIP"] = pip_version
@@ -414,6 +490,7 @@ def build(options: Options, tmp_path: Path) -> None:
414490
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
415491
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
416492
extra_flags.append("--installer=uv")
493+
417494
call(
418495
"python",
419496
"-m",

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" },
@@ -57,6 +58,7 @@ python_configurations = [
5758
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
5859
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
5960
{ identifier = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
61+
{ identifier = "gp242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
6062
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
6163
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
6264
{ identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
@@ -143,6 +145,8 @@ python_configurations = [
143145
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
144146
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_x86_64.tar.bz2" },
145147
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_arm64.tar.bz2" },
148+
{ 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" },
149+
{ 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" },
146150
]
147151

148152
[windows]
@@ -171,6 +175,7 @@ python_configurations = [
171175
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
172176
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip" },
173177
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-win64.zip" },
178+
{ 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" },
174179
]
175180

176181
[pyodide]

0 commit comments

Comments
 (0)