diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b6bb7c866..3ae094dcd 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -80,7 +80,7 @@ jobs:
env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD_FRONTEND: 'build[uv]'
- CIBW_ENABLE: "cpython-prerelease cpython-freethreading pypy"
+ CIBW_ENABLE: "cpython-prerelease cpython-freethreading pypy graalpy"
- name: Run a sample build (GitHub Action, only)
uses: ./
diff --git a/README.md b/README.md
index 12b328001..2d9af6b11 100644
--- a/README.md
+++ b/README.md
@@ -36,14 +36,15 @@ While cibuildwheel itself requires a recent Python version to run (we support th
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | 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 | N/A | N/A |
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A |
+| GraalPy 24.2 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | N/A | ✅¹ | N/A | N/A | N/A | N/A | N/A |
-¹ PyPy is only supported for manylinux wheels.
+¹ PyPy & GraalPy are only supported for manylinux wheels.
² Windows arm64 support is experimental.
³ Free-threaded mode requires opt-in using [`CIBW_ENABLE`](https://cibuildwheel.pypa.io/en/stable/options/#enable).
⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.
⁵ manylinux armv7l support is experimental. As there are no RHEL based image for this architecture, it's using an Ubuntu based image instead.
-- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), and Windows wheels for CPython and PyPy
+- Builds manylinux, musllinux, macOS 10.9+ (10.13+ for Python 3.12+), 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
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index eaf93958e..1cc707fd9 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -6,7 +6,7 @@ pr:
jobs:
- job: linux_311
- timeoutInMinutes: 120
+ timeoutInMinutes: 180
pool: {vmImage: 'Ubuntu-22.04'}
steps:
- task: UsePythonVersion@0
@@ -20,6 +20,7 @@ jobs:
- job: macos_311
pool: {vmImage: 'macOS-13'}
+ timeoutInMinutes: 120
steps:
- task: UsePythonVersion@0
inputs:
diff --git a/bin/update_pythons.py b/bin/update_pythons.py
index 7b8ea1474..75de60a64 100755
--- a/bin/update_pythons.py
+++ b/bin/update_pythons.py
@@ -5,6 +5,7 @@
import difflib
import logging
import operator
+import re
import tomllib
from collections.abc import Mapping, MutableMapping
from pathlib import Path
@@ -44,13 +45,19 @@ class ConfigWinPP(TypedDict):
url: str
+class ConfigWinGP(TypedDict):
+ identifier: str
+ version: str
+ url: str
+
+
class ConfigApple(TypedDict):
identifier: str
version: str
url: str
-AnyConfig = ConfigWinCP | ConfigWinPP | ConfigApple
+AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple
# 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:
)
+class GraalPyVersions:
+ def __init__(self) -> None:
+ 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 = ConfigApple
+ 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")
@@ -294,6 +367,8 @@ def __init__(self) -> None:
self.ios_cpython = CPythonIOSVersions()
+ self.graalpy = GraalPyVersions()
+
def update_config(self, config: MutableMapping[str, str]) -> None:
identifier = config["identifier"]
version = Version(config["version"])
@@ -311,6 +386,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"):
@@ -322,6 +399,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"):
diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py
index 321352e8c..80c2fb02e 100644
--- a/cibuildwheel/logger.py
+++ b/cibuildwheel/logger.py
@@ -243,6 +243,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)
diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py
index 4b4a064d8..420e16139 100644
--- a/cibuildwheel/platforms/macos.py
+++ b/cibuildwheel/platforms/macos.py
@@ -103,7 +103,7 @@ def get_python_configurations(
# skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)]
- # filter-out some cross-compilation configs with PyPy:
+ # filter-out some cross-compilation configs with PyPy and GraalPy:
# can't build arm64 on x86_64
# rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64":
@@ -111,7 +111,7 @@ def get_python_configurations(
python_configurations = [
c
for c in python_configurations
- if not (c.identifier.startswith("pp") and c.identifier.endswith("arm64"))
+ if not (c.identifier.startswith(("pp", "gp")) and c.identifier.endswith("arm64"))
]
removed_elements = python_configurations_before - set(python_configurations)
if removed_elements:
@@ -191,6 +191,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,
@@ -212,6 +228,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)
diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py
index a63890644..91e72404b 100644
--- a/cibuildwheel/platforms/windows.py
+++ b/cibuildwheel/platforms/windows.py
@@ -123,6 +123,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,
@@ -239,6 +253,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)
@@ -314,6 +330,49 @@ def setup_python(
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
setup_rust_cross_compile(tmp, python_configuration, python_libs_base, env)
+ if implementation_id.startswith("gp"):
+ # GraalPy fails to discover compilers, setup the relevant environment
+ # variables. Adapted from
+ # https://github.com/microsoft/vswhere/wiki/Start-Developer-Command-Prompt
+ # Remove when https://github.com/oracle/graalpython/issues/492 is fixed.
+ vcpath = subprocess.check_output(
+ [
+ Path(os.environ["PROGRAMFILES(X86)"])
+ / "Microsoft Visual Studio"
+ / "Installer"
+ / "vswhere.exe",
+ "-products",
+ "*",
+ "-latest",
+ "-property",
+ "installationPath",
+ ],
+ text=True,
+ ).strip()
+ log.notice(f"Discovering Visual Studio for GraalPy at {vcpath}")
+ env.update(
+ dict(
+ [
+ envvar.strip().split("=", 1)
+ for envvar in subprocess.check_output(
+ [
+ f"{vcpath}\\Common7\\Tools\\vsdevcmd.bat",
+ "-no_logo",
+ "-arch=amd64",
+ "-host_arch=amd64",
+ "&&",
+ "set",
+ ],
+ shell=True,
+ text=True,
+ env=env,
+ )
+ .strip()
+ .split("\n")
+ ]
+ )
+ )
+
return base_python, env
@@ -342,6 +401,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("build")
+
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
log.build_start(config.identifier)
@@ -390,6 +450,22 @@ def build(options: Options, tmp_path: Path) -> None:
build_frontend, build_options.build_verbosity, build_options.config_settings
)
+ if (
+ config.identifier.startswith("gp")
+ and build_frontend.name == "build"
+ and "--no-isolation" not in extra_flags
+ and "-n" not in extra_flags
+ ):
+ # GraalPy fails to discover its standard library when a venv is created
+ # from a virtualenv seeded executable. See
+ # https://github.com/oracle/graalpython/issues/491 and remove this once
+ # fixed upstream.
+ log.notice(
+ "Disabling build isolation to workaround GraalPy bug. If the build fails, consider using pip or build[uv] as build frontend."
+ )
+ shell("graalpy -m pip install setuptools wheel", env=env)
+ extra_flags = [*extra_flags, "-n"]
+
build_env = env.copy()
if pip_version is not None:
build_env["VIRTUALENV_PIP"] = pip_version
@@ -414,6 +490,7 @@ def build(options: Options, tmp_path: Path) -> None:
elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags.append("--installer=uv")
+
call(
"python",
"-m",
diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml
index d2ad15e09..d77ac1e2d 100644
--- a/cibuildwheel/resources/build-platforms.toml
+++ b/cibuildwheel/resources/build-platforms.toml
@@ -18,6 +18,7 @@ python_configurations = [
{ 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 = "pp311-manylinux_x86_64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
+ { identifier = "gp242-manylinux_x86_64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
@@ -57,6 +58,7 @@ python_configurations = [
{ 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 = "pp311-manylinux_aarch64", version = "3.11", path_str = "/opt/python/pp311-pypy311_pp73" },
+ { identifier = "gp242-manylinux_aarch64", version = "3.11", path_str = "/opt/python/graalpy311-graalpy242_311_native" },
{ 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" },
{ identifier = "pp310-manylinux_i686", version = "3.10", path_str = "/opt/python/pp310-pypy310_pp73" },
@@ -143,6 +145,8 @@ python_configurations = [
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_x86_64.tar.bz2" },
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_arm64.tar.bz2" },
+ { 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" },
+ { 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" },
]
[windows]
@@ -171,6 +175,7 @@ python_configurations = [
{ 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.19-win64.zip" },
{ identifier = "pp311-win_amd64", version = "3.11", arch = "64", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.19-win64.zip" },
+ { 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" },
]
[pyodide]
diff --git a/cibuildwheel/selector.py b/cibuildwheel/selector.py
index 98c7a6601..48578712d 100644
--- a/cibuildwheel/selector.py
+++ b/cibuildwheel/selector.py
@@ -33,6 +33,7 @@ class EnableGroup(StrEnum):
CPythonPrerelease = "cpython-prerelease"
PyPy = "pypy"
CPythonExperimentalRiscV64 = "cpython-experimental-riscv64"
+ GraalPy = "graalpy"
@classmethod
def all_groups(cls) -> frozenset["EnableGroup"]:
@@ -75,6 +76,8 @@ def __call__(self, build_id: str) -> bool:
build_id, "*_riscv64"
):
return False
+ if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"):
+ return False
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
diff --git a/docs/options.md b/docs/options.md
index 9d906b0c9..5006d8e28 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -323,6 +323,7 @@ values are:
- `cpython-experimental-riscv64`: Enable experimental riscv64 builds. Those builds
are disabled by default as they can't be uploaded to PyPI and a PEP will most likely
be required before this can happen.
+- `graalpy`: Enable GraalPy.
!!! caution
diff --git a/test/conftest.py b/test/conftest.py
index 8c0c9504c..f2b575df0 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -56,9 +56,8 @@ def docker_warmup(request: pytest.FixtureRequest) -> None:
images = [build_options.manylinux_images[arch] for arch in archs] + [
build_options.musllinux_images[arch] for arch in archs
]
- # exclude GraalPy as it's not a target for cibuildwheel
command = (
- "manylinux-interpreters ensure $(manylinux-interpreters list 2>/dev/null | grep -v graalpy) &&"
+ "manylinux-interpreters ensure-all &&"
"cpython3.13 -m pip download -d /tmp setuptools wheel pytest"
)
for image in images:
diff --git a/test/test_abi_variants.py b/test/test_abi_variants.py
index ec529849d..5c7c27f54 100644
--- a/test/test_abi_variants.py
+++ b/test/test_abi_variants.py
@@ -41,9 +41,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-* gp242-* {single_python_tag}-* cp313t-*"
},
)
@@ -59,7 +59,11 @@ 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 "-graalpy242" in w
+ or "-cp313t" in w
]
assert set(actual_wheels) == set(expected_wheels)
diff --git a/test/test_before_all.py b/test/test_before_all.py
index a46031ee7..c7ec86240 100644
--- a/test/test_before_all.py
+++ b/test/test_before_all.py
@@ -38,8 +38,8 @@ def test(tmp_path):
# build the wheels
before_all_command = (
- """python -c "import os, sys;open('{project}/text_info.txt', 'w').write('sample text '+os.environ.get('TEST_VAL', ''))" && """
- '''python -c "import sys; open('{project}/python_prefix.txt', 'w').write(sys.prefix)"'''
+ """python -c "import os, pathlib, sys; pathlib.Path('{project}/text_info.txt').write_text('sample text '+os.environ.get('TEST_VAL', ''))" && """
+ '''python -c "import pathlib, sys; pathlib.Path('{project}/python_prefix.txt').write_text(sys.prefix)"'''
)
actual_wheels = utils.cibuildwheel_run(
project_dir,
diff --git a/test/test_before_build.py b/test/test_before_build.py
index 19a770133..9494de7bd 100644
--- a/test/test_before_build.py
+++ b/test/test_before_build.py
@@ -41,8 +41,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 pathlib, sys; pathlib.Path('{project}/pythonversion_bb.txt').write_text(sys.version)" && """
+ f'''python -c "import pathlib, sys; pathlib.Path('{{project}}/pythonprefix_bb.txt').write_text({SYS_PREFIX})"'''
)
frontend = "build"
if utils.platform != "pyodide":
diff --git a/test/test_before_test.py b/test/test_before_test.py
index 7e766abaa..a3700f843 100644
--- a/test/test_before_test.py
+++ b/test/test_before_test.py
@@ -7,7 +7,7 @@
from pathlib import Path
from unittest import TestCase
-PROJECT_DIR = Path(__file__).joinpath("..", "..").resolve()
+PROJECT_DIR = Path(__file__).parent.parent.resolve()
class TestBeforeTest(TestCase):
@@ -39,8 +39,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 pathlib, sys; pathlib.Path('{project}/pythonversion_bt.txt').write_text(sys.version)"''',
+ '''python -c "import pathlib, sys; pathlib.Path('{project}/pythonprefix_bt.txt').write_text(sys.prefix)"''',
]
if utils.platform == "pyodide":
@@ -63,7 +63,9 @@ def test(tmp_path, build_frontend_env):
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
- "CIBW_TEST_COMMAND_WINDOWS": "pytest ./test",
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 is fixed
+ "CIBW_TEST_COMMAND_WINDOWS": "where graalpy || pytest ./test",
**build_frontend_env,
},
)
diff --git a/test/test_dependency_versions.py b/test/test_dependency_versions.py
index b8d80f3bc..d4b9416f3 100644
--- a/test/test_dependency_versions.py
+++ b/test/test_dependency_versions.py
@@ -128,6 +128,17 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv):
build_environment = {}
+ if (
+ utils.platform == "windows"
+ and method == "file"
+ and build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] == "build"
+ ):
+ # GraalPy fails to discover its standard library when a venv is created
+ # from a virtualenv seeded executable. See
+ # https://github.com/oracle/graalpython/issues/491 and remove this once
+ # fixed upstream.
+ build_frontend_env_nouv["CIBW_SKIP"] = "gp*"
+
for package_name, version in tool_versions.items():
env_name = f"EXPECTED_{package_name.upper()}_VERSION"
build_environment[env_name] = version
@@ -147,4 +158,13 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv):
# also check that we got the right wheels
expected_wheels = utils.expected_wheels("spam", "0.1.0")
+ if (
+ utils.platform == "windows"
+ and method == "file"
+ and build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] == "build"
+ ):
+ # See reference to https://github.com/oracle/graalpython/issues/491
+ # above
+ expected_wheels = [w for w in expected_wheels if "graalpy" not in w]
+
assert set(actual_wheels) == set(expected_wheels)
diff --git a/test/test_pep518.py b/test/test_pep518.py
index 2147520f9..c861f0da3 100644
--- a/test/test_pep518.py
+++ b/test/test_pep518.py
@@ -33,11 +33,26 @@ def test_pep518(tmp_path, build_frontend_env):
project_dir = tmp_path / "project"
basic_project.generate(project_dir)
+ # GraalPy fails to discover its standard library when a venv is created
+ # from a virtualenv seeded executable. See
+ # https://github.com/oracle/graalpython/issues/491 and remove this once
+ # fixed upstream.
+ if build_frontend_env["CIBW_BUILD_FRONTEND"] == "build" and utils.platform == "windows":
+ build_frontend_env["CIBW_SKIP"] = "gp*"
+
# build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir, add_env=build_frontend_env)
# check that the expected wheels are produced
expected_wheels = utils.expected_wheels("spam", "0.1.0")
+
+ # GraalPy fails to discover its standard library when a venv is created
+ # from a virtualenv seeded executable. See
+ # https://github.com/oracle/graalpython/issues/491 and remove this once
+ # fixed upstream.
+ if build_frontend_env["CIBW_BUILD_FRONTEND"] == "build" and utils.platform == "windows":
+ expected_wheels = [w for w in expected_wheels if "graalpy" not in w]
+
assert set(actual_wheels) == set(expected_wheels)
# These checks ensure an extra file is not created when using custom
diff --git a/test/test_testing.py b/test/test_testing.py
index 16f00aa60..9b7364f99 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -81,7 +81,9 @@ def test(tmp_path):
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
- "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || pytest ./test",
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 is fixed
+ "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
},
)
@@ -102,7 +104,9 @@ def test_extras_require(tmp_path):
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
- "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || pytest ./test",
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 is fixed
+ "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
},
single_python=True,
)
@@ -134,7 +138,9 @@ def test_dependency_groups(tmp_path):
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
- "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || pytest ./test",
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 is fixed
+ "CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
},
single_python=True,
)
@@ -189,7 +195,9 @@ def test_bare_pytest_invocation(tmp_path: Path, test_runner: str) -> None:
add_env={
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
"CIBW_TEST_COMMAND": (
- "python -m pytest"
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 fixed
+ "graalpy.exe -c 1 || python -m pytest"
if test_runner == "pytest"
else "python -m unittest discover test spam_test.py"
),
@@ -211,7 +219,9 @@ def test_test_sources(tmp_path):
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "pytest",
- "CIBW_TEST_COMMAND_WINDOWS": "pytest",
+ # pytest fails on GraalPy 24.2.0 on Windows so we skip it there
+ # until https://github.com/oracle/graalpython/issues/490 is fixed
+ "CIBW_TEST_COMMAND_WINDOWS": "where graalpy || pytest",
"CIBW_TEST_SOURCES": "test",
},
)
diff --git a/test/utils.py b/test/utils.py
index e05d3a2dd..957ee7295 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -55,7 +55,7 @@ def cibuildwheel_get_build_identifiers(
cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)]
if env is None:
env = os.environ.copy()
- env["CIBW_ENABLE"] = "cpython-freethreading pypy"
+ env["CIBW_ENABLE"] = "cpython-freethreading pypy graalpy"
if prerelease_pythons:
env["CIBW_ENABLE"] += " cpython-prerelease"
@@ -257,6 +257,10 @@ def _expected_wheels(
"pp310-pypy310_pp73",
"pp311-pypy311_pp73",
]
+ if machine_arch in ["x86_64", "AMD64", "aarch64", "arm64"]:
+ python_abi_tags += [
+ "graalpy311-graalpy242_311_native",
+ ]
if single_python:
python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION)
@@ -286,7 +290,7 @@ def _expected_wheels(
for manylinux_version in manylinux_versions
)
]
- 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.append(
".".join(
f"{musllinux_version}_{machine_arch}"
diff --git a/unit_test/linux_build_steps_test.py b/unit_test/linux_build_steps_test.py
index cd295a5e9..aa4363c13 100644
--- a/unit_test/linux_build_steps_test.py
+++ b/unit_test/linux_build_steps_test.py
@@ -24,7 +24,7 @@ def test_linux_container_split(tmp_path: Path, monkeypatch: pytest.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]]
diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py
index 48723213b..66ee4c952 100644
--- a/unit_test/main_tests/main_options_test.py
+++ b/unit_test/main_tests/main_options_test.py
@@ -423,9 +423,9 @@ def test_debug_traceback(monkeypatch, method, capfd):
@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
def test_enable(method, intercepted_build_args, monkeypatch):
if method == "command_line":
- monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "pypy"])
+ monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "pypy", "--enable", "graalpy"])
elif method == "env_var":
- monkeypatch.setenv("CIBW_ENABLE", "pypy")
+ monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy")
main()
@@ -434,18 +434,20 @@ def test_enable(method, intercepted_build_args, monkeypatch):
if method == "unset":
assert enable_groups == frozenset()
else:
- assert enable_groups == frozenset([EnableGroup.PyPy])
+ assert enable_groups == frozenset([EnableGroup.PyPy, EnableGroup.GraalPy])
def test_enable_arg_inherits(intercepted_build_args, monkeypatch):
- monkeypatch.setenv("CIBW_ENABLE", "pypy")
+ monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy")
monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "cpython-prerelease"])
main()
enable_groups = intercepted_build_args.args[0].globals.build_selector.enable
- assert enable_groups == frozenset((EnableGroup.PyPy, EnableGroup.CPythonPrerelease))
+ assert enable_groups == frozenset(
+ (EnableGroup.PyPy, EnableGroup.GraalPy, EnableGroup.CPythonPrerelease)
+ )
def test_enable_arg_error_message(monkeypatch, capsys):
diff --git a/unit_test/option_prepare_test.py b/unit_test/option_prepare_test.py
index 4a24190c1..d5dc7d191 100644
--- a/unit_test/option_prepare_test.py
+++ b/unit_test/option_prepare_test.py
@@ -14,7 +14,7 @@
from cibuildwheel.util import file
DEFAULT_IDS = {"cp38", "cp39", "cp310", "cp311", "cp312", "cp313"}
-ALL_IDS = DEFAULT_IDS | {"cp313t", "pp38", "pp39", "pp310", "pp311"}
+ALL_IDS = DEFAULT_IDS | {"cp313t", "pp38", "pp39", "pp310", "pp311", "gp242"}
@pytest.fixture
@@ -103,7 +103,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
[tool.cibuildwheel]
manylinux-x86_64-image = "manylinux_2_28"
musllinux-x86_64-image = "musllinux_1_2"
-enable = ["pypy", "cpython-freethreading"]
+enable = ["pypy", "graalpy", "cpython-freethreading"]
# Before Python 3.10, use manylinux2014
[[tool.cibuildwheel.overrides]]
@@ -155,6 +155,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
"pp39",
"pp310",
"pp311",
+ "gp242",
}
}
assert kwargs["options"].build_options("cp39-manylinux_x86_64").before_all == ""
@@ -176,6 +177,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
"pp39",
"pp310",
"pp311",
+ "gp242",
]
}
@@ -185,14 +187,16 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
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_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]}
- assert identifiers == {f"{x}-musllinux_x86_64" for x in ALL_IDS if "pp" not in x}
+ assert identifiers == {
+ f"{x}-musllinux_x86_64" for x in ALL_IDS if "pp" not in x and "gp" not in x
+ }
kwargs = build_in_container.call_args_list[5][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
@@ -200,4 +204,6 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
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
+ }