Skip to content

Commit

Permalink
Add support for GraalPy
Browse files Browse the repository at this point in the history
  • Loading branch information
timfel committed Jul 16, 2024
1 parent de84624 commit 80668a0
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 18 deletions.
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.0 |||| 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 available using the [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) option. Free-threaded mode requires opt-in.</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"
identifier = f"gp{gpversion.major}{gpversion.minor}-macosx_{arch}"
config = ConfigMacOS
platform = "macos"
elif "win" in identifier:
arch = "amd64" if "x86_64" in identifier else "arm64"
identifier = f"gp{gpversion.major}{gpversion.minor}-win_{arch}"
config = ConfigWinGP
platform = "linux"
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"
(url,) = (
rf["browser_download_url"]
for rf in release["assets"]
if rf["name"].endswith(f"{platform}-{arch}.tar.gz")
)

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 @@ -193,6 +193,22 @@ def install_pypy(tmp: Path, url: str) -> Path:
return installation_path / "bin" / "pypy3"


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 @@ -219,6 +235,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 @@ -512,7 +512,7 @@ def globals(self) -> GlobalOptions:
package_dir = args.package_dir
output_dir = args.output_dir

build_config = self.reader.get("build", env_plat=False, list_sep=" ") or "*"
build_config = self.reader.get("build", env_plat=False, list_sep=" ") or "[!g]*"
skip_config = self.reader.get("skip", env_plat=False, list_sep=" ")
test_skip = self.reader.get("test-skip", env_plat=False, list_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 = "gp240-manylinux_x86_64", version = "3.10", path_str = "/opt/python/graalpy310-graalpy240_310_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 = "gp240-manylinux_aarch64", version = "3.10", path_str = "/opt/python/graalpy310-graalpy240_310_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.16-macos_x86_64.tar.bz2" },
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.16-macos_arm64.tar.bz2" },
{ identifier = "gp240-macosx_x86_64", version = "3.10", url = "https://github.com/oracle/graalpython/releases/download/graal-24.0.1/graalpy-24.0.1-macos-amd64.tar.gz" },
{ identifier = "gp240-macosx_arm64", version = "3.10", url = "https://github.com/oracle/graalpython/releases/download/graal-24.0.1/graalpy-24.0.1-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.16-win64.zip" },
{ identifier = "gp240-win_amd64", version = "3.10", arch = "64", url = "https://github.com/oracle/graalpython/releases/download/graal-24.0.1/graalpy-24.0.1-windows-amd64.tar.gz" },
]

[pyodide]
Expand Down
4 changes: 2 additions & 2 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-* gp240-* {single_python_tag}-* cp313t-*"
},
)

Expand Down
6 changes: 4 additions & 2 deletions test/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def expected_wheels(
"pp38-pypy38_pp73",
"pp39-pypy39_pp73",
"pp310-pypy310_pp73",
"graalpy310-graalpy240_310_native",
]

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

if single_python:
Expand All @@ -246,7 +248,7 @@ 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 @@ -257,7 +259,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
23 changes: 14 additions & 9 deletions unit_test/option_prepare_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"pp38",
"pp39",
"pp310",
"gp240",
}


Expand Down Expand Up @@ -83,7 +84,7 @@ def test_build_default_launches(monkeypatch):
assert kwargs["container"]["enforce_32_bit"]

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS if "gp" not in x}

kwargs = build_in_container.call_args_list[2][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
Expand All @@ -92,7 +93,7 @@ def test_build_default_launches(monkeypatch):

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-musllinux_x86_64" for x in ALL_IDS for x in ALL_IDS if "pp" not in x
f"{x}-musllinux_x86_64" for x in ALL_IDS for x in ALL_IDS if "pp" not in x and "gp" not in x
}

kwargs = build_in_container.call_args_list[3][1]
Expand All @@ -101,7 +102,9 @@ def test_build_default_launches(monkeypatch):
assert kwargs["container"]["enforce_32_bit"]

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x}
assert identifiers == {
f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x and "gp" not in x
}


@pytest.mark.usefixtures("mock_build_container")
Expand Down Expand Up @@ -155,7 +158,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-manylinux_x86_64"
for x in ALL_IDS - {"cp36", "cp310", "cp311", "cp312", "pp37", "pp38", "pp39", "pp310"}
for x in ALL_IDS - {"cp36", "cp310", "cp311", "cp312", "pp37", "pp38", "pp39", "pp310", "gp240"}
}
assert kwargs["options"].build_options("cp37-manylinux_x86_64").before_all == ""

Expand All @@ -166,7 +169,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-manylinux_x86_64"
for x in ["cp310", "cp311", "cp312", "pp37", "pp38", "pp39", "pp310"]
for x in ["cp310", "cp311", "cp312", "pp37", "pp38", "pp39", "pp310", "gp240"]
}

kwargs = build_in_container.call_args_list[3][1]
Expand All @@ -175,7 +178,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
assert kwargs["container"]["enforce_32_bit"]

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS}
assert identifiers == {f"{x}-manylinux_i686" for x in ALL_IDS if "gp" not in x}

kwargs = build_in_container.call_args_list[4][1]
assert "quay.io/pypa/musllinux_1_1_x86_64" in kwargs["container"]["image"]
Expand All @@ -184,7 +187,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-musllinux_x86_64" for x in ALL_IDS & {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x
f"{x}-musllinux_x86_64" for x in ALL_IDS & {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x and "gp" not in x
}

kwargs = build_in_container.call_args_list[5][1]
Expand All @@ -193,7 +196,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
assert not kwargs["container"]["enforce_32_bit"]
identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {
f"{x}-musllinux_x86_64" for x in ALL_IDS - {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x
f"{x}-musllinux_x86_64" for x in ALL_IDS - {"cp36", "cp37", "cp38", "cp39"} if "pp" not in x and "gp" not in x
}

kwargs = build_in_container.call_args_list[6][1]
Expand All @@ -202,4 +205,6 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
assert kwargs["container"]["enforce_32_bit"]

identifiers = {x.identifier for x in kwargs["platform_configs"]}
assert identifiers == {f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x}
assert identifiers == {
f"{x}-musllinux_i686" for x in ALL_IDS if "pp" not in x and "gp" not in x
}

0 comments on commit 80668a0

Please sign in to comment.