Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support GraalPy #1538

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ What does it do?
| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | 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 |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
| GraalPy 24.1 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A |

<sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
<sup>¹ PyPy & GraalPy are only supported for manylinux wheels.</sup><br>
<sup>² Windows arm64 support is experimental.</sup><br>
<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>
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>

- Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython and PyPy
- Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython, PyPy, and GraalPy
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
- Bundles shared library dependencies on Linux and macOS through [auditwheel](https://github.com/pypa/auditwheel) and [delocate](https://github.com/matthew-brett/delocate)
- Runs your library's tests against the wheel-installed version of your library
Expand Down
81 changes: 80 additions & 1 deletion bin/update_pythons.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import copy
import difflib
import logging
import re
from collections.abc import Mapping, MutableMapping
from pathlib import Path
from typing import Any, Final, Literal, TypedDict, Union
Expand Down Expand Up @@ -44,13 +45,19 @@ class ConfigWinPP(TypedDict):
url: str


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


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


AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigMacOS]
AnyConfig = Union[ConfigWinCP, ConfigWinPP, ConfigWinGP, ConfigMacOS]


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


class GraalPyVersions:
def __init__(self):
response = requests.get("https://api.github.com/repos/oracle/graalpython/releases")
response.raise_for_status()

releases = response.json()
gp_version_re = re.compile(r"-(\d+\.\d+\.\d+)$")
cp_version_re = re.compile(r"Python (\d+\.\d+(?:\.\d+)?)")
for release in releases:
m = gp_version_re.search(release["tag_name"])
if m:
release["graalpy_version"] = Version(m.group(1))
m = cp_version_re.search(release["body"])
if m:
release["python_version"] = Version(m.group(1))

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:
if "x86_64" in identifier or "amd64" in identifier:
arch = "x86_64"
elif "arm64" in identifier or "aarch64" in identifier:
arch = "aarch64"
else:
msg = f"{identifier} not supported yet on GraalPy"
raise RuntimeError(msg)

releases = [r for r in self.releases if spec.contains(r["python_version"])]
releases = sorted(releases, key=lambda r: r["graalpy_version"])

if not releases:
msg = f"GraalPy {arch} not found for {spec}!"
raise RuntimeError(msg)

release = releases[-1]
version = release["python_version"]
gpversion = release["graalpy_version"]

if "macosx" in identifier:
arch = "x86_64" if "x86_64" in identifier else "arm64"
config = ConfigMacOS
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"
raise RuntimeError(msg)

arch = "amd64" if arch == "x86_64" else "aarch64"
ext = "zip" if "win" in identifier else "tar.gz"
(url,) = (
rf["browser_download_url"]
for rf in release["assets"]
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
and rf["name"].startswith(f"graalpy-{gpversion.major}")
)

return config(
identifier=identifier,
version=f"{version.major}.{version.minor}",
url=url,
)


class PyPyVersions:
def __init__(self, arch_str: ArchStr):
response = requests.get("https://downloads.python.org/pypy/versions.json")
Expand Down Expand Up @@ -250,6 +323,8 @@ def __init__(self) -> None:
self.macos_pypy = PyPyVersions("64")
self.macos_pypy_arm64 = PyPyVersions("ARM64")

self.graalpy = GraalPyVersions()

def update_config(self, config: MutableMapping[str, str]) -> None:
identifier = config["identifier"]
version = Version(config["version"])
Expand All @@ -267,6 +342,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
config_update = self.macos_pypy.update_version_macos(spec)
elif "macosx_arm64" in identifier:
config_update = self.macos_pypy_arm64.update_version_macos(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win32" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_32.update_version_windows(spec)
elif "win32" in identifier and identifier.startswith("cp"):
Expand All @@ -278,6 +355,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
config_update = self.windows_64.update_version_windows(spec)
elif identifier.startswith("pp"):
config_update = self.windows_pypy_64.update_version_windows(spec)
elif identifier.startswith("gp"):
config_update = self.graalpy.update_version(identifier, spec)
elif "t-win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_arm64.update_version_windows(spec)
elif "win_arm64" in identifier and identifier.startswith("cp"):
Expand Down
2 changes: 2 additions & 0 deletions cibuildwheel/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ def build_description_from_identifier(identifier: str) -> str:
build_description += "CPython"
elif python_interpreter == "pp":
build_description += "PyPy"
elif python_interpreter == "gp":
build_description += "GraalPy"
else:
msg = f"unknown python {python_interpreter!r}"
raise Exception(msg)
Expand Down
18 changes: 18 additions & 0 deletions cibuildwheel/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ def can_use_uv(python_configuration: PythonConfiguration) -> bool:
return all(conditions)


def install_graalpy(tmp: Path, url: str) -> Path:
graalpy_archive = url.rsplit("/", 1)[-1]
extension = ".tar.gz"
assert graalpy_archive.endswith(extension)
installation_path = CIBW_CACHE_PATH / graalpy_archive[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
downloaded_archive = tmp / graalpy_archive
download(url, downloaded_archive)
installation_path.mkdir(parents=True)
# GraalPy top-folder name is inconsistent with archive name
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
downloaded_archive.unlink()
return installation_path / "bin" / "graalpy"


def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
Expand All @@ -222,6 +238,8 @@ def setup_python(

elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
else:
msg = "Unknown Python implementation"
raise ValueError(msg)
Expand Down
2 changes: 1 addition & 1 deletion cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ def globals(self) -> GlobalOptions:
output_dir = args.output_dir

build_config = (
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "*"
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "[!g]*"
)
skip_config = self.reader.get("skip", env_plat=False, option_format=ListFormat(sep=" "))
test_skip = self.reader.get("test-skip", env_plat=False, option_format=ListFormat(sep=" "))
Expand Down
5 changes: 5 additions & 0 deletions cibuildwheel/resources/build-platforms.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ python_configurations = [
{ identifier = "pp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "gp241-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy241_311_native" },
{ identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
{ identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
Expand Down Expand Up @@ -53,6 +54,7 @@ python_configurations = [
{ identifier = "pp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
{ identifier = "pp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
{ identifier = "gp241-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy241_311_native" },
{ identifier = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
{ identifier = "pp38-manylinux_i686", version = "3.8", path_str = "/opt/python/pp38-pypy38_pp73" },
{ identifier = "pp39-manylinux_i686", version = "3.9", path_str = "/opt/python/pp39-pypy39_pp73" },
Expand Down Expand Up @@ -136,6 +138,8 @@ python_configurations = [
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" },
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_x86_64.tar.bz2" },
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_arm64.tar.bz2" },
{ 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" },
{ 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" },
]

[windows]
Expand Down Expand Up @@ -168,6 +172,7 @@ python_configurations = [
{ identifier = "pp38-win_amd64", version = "3.8", arch = "64", url = "https://downloads.python.org/pypy/pypy3.8-v7.3.11-win64.zip" },
{ identifier = "pp39-win_amd64", version = "3.9", arch = "64", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
{ identifier = "pp310-win_amd64", version = "3.10", arch = "64", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.17-win64.zip" },
{ 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" },
]

[pyodide]
Expand Down
16 changes: 16 additions & 0 deletions cibuildwheel/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
return installation_path / "python.exe"


def install_graalpy(tmp: Path, url: str) -> Path:
zip_filename = url.rsplit("/", 1)[-1]
extension = ".zip"
assert zip_filename.endswith(extension)
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
graalpy_zip = tmp / zip_filename
download(url, graalpy_zip)
# Extract to the parent directory because the zip file still contains a directory
extract_zip(graalpy_zip, installation_path.parent)
return installation_path / "bin" / "graalpy.exe"


def setup_setuptools_cross_compile(
tmp: Path,
python_configuration: PythonConfiguration,
Expand Down Expand Up @@ -257,6 +271,8 @@ def setup_python(
elif implementation_id.startswith("pp"):
assert python_configuration.url is not None
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url or "")
else:
msg = "Unknown Python implementation"
raise ValueError(msg)
Expand Down
4 changes: 2 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]:
if platform == "pyodide" and frontend == "pip":
pytest.skip("Can't use pip as build frontend for pyodide platform")

return {"CIBW_BUILD_FRONTEND": frontend}
return {"CIBW_BUILD": "gp*", "CIBW_BUILD_FRONTEND": frontend}


@pytest.fixture
Expand All @@ -43,7 +43,7 @@ def build_frontend_env(build_frontend_env_nouv: dict[str, str]) -> dict[str, str
if frontend != "build" or platform == "pyodide" or find_uv() is None:
return build_frontend_env_nouv

return {"CIBW_BUILD_FRONTEND": "build[uv]"}
return {"CIBW_BUILD": "gp*", "CIBW_BUILD_FRONTEND": "build[uv]"}


@pytest.fixture
Expand Down
6 changes: 3 additions & 3 deletions test/test_abi_variants.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ def test_abi3(tmp_path):
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
# free_threaded and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
# free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
# also limit the number of builds for test performance reasons
"CIBW_BUILD": f"cp39-* cp310-* pp310-* {single_python_tag}-* cp313t-*"
"CIBW_BUILD": f"cp39-* cp310-* pp310-* gp241-* {single_python_tag}-* cp313t-*"
},
)

Expand All @@ -72,7 +72,7 @@ def test_abi3(tmp_path):
expected_wheels = [
w.replace("cp310-cp310", "cp310-abi3")
for w in expected_wheels
if "-cp39" in w or "-cp310" in w or "-pp310" in w or "-cp313t" in w
if "-cp39" in w or "-cp310" in w or "-pp310" in w or "-gp241" in w or "-cp313t" in w
]
assert set(actual_wheels) == set(expected_wheels)

Expand Down
4 changes: 2 additions & 2 deletions test/test_before_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def test(tmp_path):
project_with_before_build_asserts.generate(project_dir)

before_build = (
"""python -c "import sys; open('{project}/pythonversion_bb.txt', 'w').write(sys.version)" && """
f'''python -c "import sys; open('{{project}}/pythonprefix_bb.txt', 'w').write({SYS_PREFIX})"'''
"""python -c "import sys; f = open('{project}/pythonversion_bb.txt', 'w'); f.write(sys.version); f.close()" && """
f'''python -c "import sys; f = open('{{project}}/pythonprefix_bb.txt', 'w'); f.write({SYS_PREFIX}); f.close()"'''
)

# build the wheels
Expand Down
4 changes: 2 additions & 2 deletions test/test_before_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def test(tmp_path, build_frontend_env):
test_projects.new_c_project().generate(test_project_dir)

before_test_steps = [
'''python -c "import os, sys; open('{project}/pythonversion_bt.txt', 'w').write(sys.version)"''',
'''python -c "import os, sys; open('{project}/pythonprefix_bt.txt', 'w').write(sys.prefix)"''',
'''python -c "import os, sys; f = open('{project}/pythonversion_bt.txt', 'w'); f.write(sys.version); f.close()"''',
'''python -c "import os, sys; f = open('{project}/pythonprefix_bt.txt', 'w'); f.write(sys.prefix); f.close()"''',
]

if utils.platform == "pyodide":
Expand Down
10 changes: 8 additions & 2 deletions test/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ def expected_wheels(
"pp38-pypy38_pp73",
"pp39-pypy39_pp73",
"pp310-pypy310_pp73",
"graalpy311-graalpy241_311_native",
]

if platform == "macos" and machine_arch == "arm64":
Expand All @@ -219,6 +220,7 @@ def expected_wheels(
"pp38-pypy38_pp73",
"pp39-pypy39_pp73",
"pp310-pypy310_pp73",
"graalpy311-graalpy241_311_native",
]

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

if machine_arch == "x86_64" and not single_arch:
if (
machine_arch == "x86_64"
and not single_arch
and not python_abi_tag.startswith("graalpy")
):
architectures.append("i686")

if len(manylinux_versions) > 0:
Expand All @@ -256,7 +262,7 @@ def expected_wheels(
)
for architecture in architectures
]
if len(musllinux_versions) > 0 and not python_abi_tag.startswith("pp"):
if len(musllinux_versions) > 0 and not python_abi_tag.startswith(("pp", "graalpy")):
platform_tags.extend(
[
".".join(
Expand Down
2 changes: 1 addition & 1 deletion unit_test/linux_build_steps_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_linux_container_split(tmp_path: Path, monkeypatch):
manylinux-x86_64-image = "normal_container_image"
manylinux-i686-image = "normal_container_image"
build = "*-manylinux_x86_64"
skip = "pp*"
skip = "[gp]p*"
archs = "x86_64 i686"

[[tool.cibuildwheel.overrides]]
Expand Down
Loading
Loading