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 <henryschreineriii@gmail.com>

* Include GraalPy in docker_warmup and remove workaround for installing it late

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
This commit is contained in:
Tim Felgentreff
2025-04-28 18:25:17 -04:00
committed by GitHub
co-authored by pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Henry Schreiner
parent 5f8d06ff55
commit 020a91baa8
22 changed files with 287 additions and 38 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
env: env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64 CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD_FRONTEND: 'build[uv]' 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) - name: Run a sample build (GitHub Action, only)
uses: ./ uses: ./
+3 -2
View File
@@ -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.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.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 | | 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 |
<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>² Windows arm64 support is experimental.</sup><br>
<sup>³ Free-threaded mode requires opt-in using [`CIBW_ENABLE`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br> <sup>³ Free-threaded mode requires opt-in using [`CIBW_ENABLE`](https://cibuildwheel.pypa.io/en/stable/options/#enable).</sup><br>
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br> <sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>
<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> <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>
- 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 - 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) - 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 - Runs your library's tests against the wheel-installed version of your library
+2 -1
View File
@@ -6,7 +6,7 @@ pr:
jobs: jobs:
- job: linux_311 - job: linux_311
timeoutInMinutes: 120 timeoutInMinutes: 180
pool: {vmImage: 'Ubuntu-22.04'} pool: {vmImage: 'Ubuntu-22.04'}
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
@@ -20,6 +20,7 @@ jobs:
- job: macos_311 - job: macos_311
pool: {vmImage: 'macOS-13'} pool: {vmImage: 'macOS-13'}
timeoutInMinutes: 120
steps: steps:
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
+80 -1
View File
@@ -5,6 +5,7 @@ import copy
import difflib import difflib
import logging import logging
import operator import operator
import re
import tomllib import tomllib
from collections.abc import Mapping, MutableMapping from collections.abc import Mapping, MutableMapping
from pathlib import Path from pathlib import Path
@@ -44,13 +45,19 @@ class ConfigWinPP(TypedDict):
url: str url: str
class ConfigWinGP(TypedDict):
identifier: str
version: str
url: str
class ConfigApple(TypedDict): class ConfigApple(TypedDict):
identifier: str identifier: str
version: str version: str
url: 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 # The following set of "Versions" classes allow the initial call to the APIs to
@@ -106,6 +113,72 @@ class WindowsVersions:
) )
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: class PyPyVersions:
def __init__(self, arch_str: ArchStr): def __init__(self, arch_str: ArchStr):
response = requests.get("https://downloads.python.org/pypy/versions.json") response = requests.get("https://downloads.python.org/pypy/versions.json")
@@ -294,6 +367,8 @@ class AllVersions:
self.ios_cpython = CPythonIOSVersions() self.ios_cpython = CPythonIOSVersions()
self.graalpy = GraalPyVersions()
def update_config(self, config: MutableMapping[str, str]) -> None: def update_config(self, config: MutableMapping[str, str]) -> None:
identifier = config["identifier"] identifier = config["identifier"]
version = Version(config["version"]) version = Version(config["version"])
@@ -311,6 +386,8 @@ class AllVersions:
config_update = self.macos_pypy.update_version_macos(spec) config_update = self.macos_pypy.update_version_macos(spec)
elif "macosx_arm64" in identifier: elif "macosx_arm64" in identifier:
config_update = self.macos_pypy_arm64.update_version_macos(spec) 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"): elif "t-win32" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_32.update_version_windows(spec) config_update = self.windows_t_32.update_version_windows(spec)
elif "win32" in identifier and identifier.startswith("cp"): elif "win32" in identifier and identifier.startswith("cp"):
@@ -322,6 +399,8 @@ class AllVersions:
config_update = self.windows_64.update_version_windows(spec) config_update = self.windows_64.update_version_windows(spec)
elif identifier.startswith("pp"): elif identifier.startswith("pp"):
config_update = self.windows_pypy_64.update_version_windows(spec) 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"): elif "t-win_arm64" in identifier and identifier.startswith("cp"):
config_update = self.windows_t_arm64.update_version_windows(spec) config_update = self.windows_t_arm64.update_version_windows(spec)
elif "win_arm64" in identifier and identifier.startswith("cp"): elif "win_arm64" in identifier and identifier.startswith("cp"):
+2
View File
@@ -243,6 +243,8 @@ def build_description_from_identifier(identifier: str) -> str:
build_description += "CPython" build_description += "CPython"
elif python_interpreter == "pp": elif python_interpreter == "pp":
build_description += "PyPy" build_description += "PyPy"
elif python_interpreter == "gp":
build_description += "GraalPy"
else: else:
msg = f"unknown python {python_interpreter!r}" msg = f"unknown python {python_interpreter!r}"
raise Exception(msg) raise Exception(msg)
+20 -2
View File
@@ -103,7 +103,7 @@ def get_python_configurations(
# skip builds as required by BUILD/SKIP # skip builds as required by BUILD/SKIP
python_configurations = [c for c in python_configurations if build_selector(c.identifier)] 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 # can't build arm64 on x86_64
# rosetta allows to build x86_64 on arm64 # rosetta allows to build x86_64 on arm64
if platform.machine() == "x86_64": if platform.machine() == "x86_64":
@@ -111,7 +111,7 @@ def get_python_configurations(
python_configurations = [ python_configurations = [
c c
for c in python_configurations 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) removed_elements = python_configurations_before - set(python_configurations)
if removed_elements: if removed_elements:
@@ -191,6 +191,22 @@ def install_pypy(tmp: Path, url: str) -> Path:
return installation_path / "bin" / "pypy3" 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( def setup_python(
tmp: Path, tmp: Path,
python_configuration: PythonConfiguration, python_configuration: PythonConfiguration,
@@ -212,6 +228,8 @@ def setup_python(
elif implementation_id.startswith("pp"): elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.url) base_python = install_pypy(tmp, python_configuration.url)
elif implementation_id.startswith("gp"):
base_python = install_graalpy(tmp, python_configuration.url)
else: else:
msg = "Unknown Python implementation" msg = "Unknown Python implementation"
raise ValueError(msg) raise ValueError(msg)
+77
View File
@@ -123,6 +123,20 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
return installation_path / "python.exe" 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( def setup_setuptools_cross_compile(
tmp: Path, tmp: Path,
python_configuration: PythonConfiguration, python_configuration: PythonConfiguration,
@@ -239,6 +253,8 @@ def setup_python(
elif implementation_id.startswith("pp"): elif implementation_id.startswith("pp"):
assert python_configuration.url is not None assert python_configuration.url is not None
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url) 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: else:
msg = "Unknown Python implementation" msg = "Unknown Python implementation"
raise ValueError(msg) raise ValueError(msg)
@@ -314,6 +330,49 @@ def setup_python(
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env) setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
setup_rust_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 return base_python, env
@@ -342,6 +401,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("build") build_frontend = build_options.build_frontend or BuildFrontendConfig("build")
use_uv = build_frontend.name == "build[uv]" and can_use_uv(config) use_uv = build_frontend.name == "build[uv]" and can_use_uv(config)
log.build_start(config.identifier) 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 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() build_env = env.copy()
if pip_version is not None: if pip_version is not None:
build_env["VIRTUALENV_PIP"] = pip_version 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]": 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: if use_uv and "--no-isolation" not in extra_flags and "-n" not in extra_flags:
extra_flags.append("--installer=uv") extra_flags.append("--installer=uv")
call( call(
"python", "python",
"-m", "-m",
@@ -18,6 +18,7 @@ python_configurations = [
{ identifier = "pp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/pp39-pypy39_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 = "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 = "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 = "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 = "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" }, { 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 = "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 = "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 = "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 = "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 = "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" }, { 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 = "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_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 = "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] [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 = "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 = "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 = "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] [pyodide]
+3
View File
@@ -33,6 +33,7 @@ class EnableGroup(StrEnum):
CPythonPrerelease = "cpython-prerelease" CPythonPrerelease = "cpython-prerelease"
PyPy = "pypy" PyPy = "pypy"
CPythonExperimentalRiscV64 = "cpython-experimental-riscv64" CPythonExperimentalRiscV64 = "cpython-experimental-riscv64"
GraalPy = "graalpy"
@classmethod @classmethod
def all_groups(cls) -> frozenset["EnableGroup"]: def all_groups(cls) -> frozenset["EnableGroup"]:
@@ -75,6 +76,8 @@ class BuildSelector:
build_id, "*_riscv64" build_id, "*_riscv64"
): ):
return False 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_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id) should_skip = selector_matches(self.skip_config, build_id)
+1
View File
@@ -323,6 +323,7 @@ values are:
- `cpython-experimental-riscv64`: Enable experimental riscv64 builds. Those builds - `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 are disabled by default as they can't be uploaded to PyPI and a PEP will most likely
be required before this can happen. be required before this can happen.
- `graalpy`: Enable GraalPy.
!!! caution !!! caution
+1 -2
View File
@@ -56,9 +56,8 @@ def docker_warmup(request: pytest.FixtureRequest) -> None:
images = [build_options.manylinux_images[arch] for arch in archs] + [ images = [build_options.manylinux_images[arch] for arch in archs] + [
build_options.musllinux_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 = ( 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" "cpython3.13 -m pip download -d /tmp setuptools wheel pytest"
) )
for image in images: for image in images:
+7 -3
View File
@@ -41,9 +41,9 @@ def test_abi3(tmp_path):
actual_wheels = utils.cibuildwheel_run( actual_wheels = utils.cibuildwheel_run(
project_dir, project_dir,
add_env={ 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 # 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 = [ expected_wheels = [
w.replace("cp310-cp310", "cp310-abi3") w.replace("cp310-cp310", "cp310-abi3")
for w in expected_wheels 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) assert set(actual_wheels) == set(expected_wheels)
+2 -2
View File
@@ -38,8 +38,8 @@ def test(tmp_path):
# build the wheels # build the wheels
before_all_command = ( 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 os, pathlib, sys; pathlib.Path('{project}/text_info.txt').write_text('sample text '+os.environ.get('TEST_VAL', ''))" && """
'''python -c "import sys; open('{project}/python_prefix.txt', 'w').write(sys.prefix)"''' '''python -c "import pathlib, sys; pathlib.Path('{project}/python_prefix.txt').write_text(sys.prefix)"'''
) )
actual_wheels = utils.cibuildwheel_run( actual_wheels = utils.cibuildwheel_run(
project_dir, project_dir,
+2 -2
View File
@@ -41,8 +41,8 @@ def test(tmp_path):
project_with_before_build_asserts.generate(project_dir) project_with_before_build_asserts.generate(project_dir)
before_build = ( before_build = (
"""python -c "import sys; open('{project}/pythonversion_bb.txt', 'w').write(sys.version)" && """ """python -c "import pathlib, sys; pathlib.Path('{project}/pythonversion_bb.txt').write_text(sys.version)" && """
f'''python -c "import sys; open('{{project}}/pythonprefix_bb.txt', 'w').write({SYS_PREFIX})"''' f'''python -c "import pathlib, sys; pathlib.Path('{{project}}/pythonprefix_bb.txt').write_text({SYS_PREFIX})"'''
) )
frontend = "build" frontend = "build"
if utils.platform != "pyodide": if utils.platform != "pyodide":
+6 -4
View File
@@ -7,7 +7,7 @@ import os
from pathlib import Path from pathlib import Path
from unittest import TestCase from unittest import TestCase
PROJECT_DIR = Path(__file__).joinpath("..", "..").resolve() PROJECT_DIR = Path(__file__).parent.parent.resolve()
class TestBeforeTest(TestCase): class TestBeforeTest(TestCase):
@@ -39,8 +39,8 @@ def test(tmp_path, build_frontend_env):
test_projects.new_c_project().generate(test_project_dir) test_projects.new_c_project().generate(test_project_dir)
before_test_steps = [ before_test_steps = [
'''python -c "import os, sys; open('{project}/pythonversion_bt.txt', 'w').write(sys.version)"''', '''python -c "import pathlib, sys; pathlib.Path('{project}/pythonversion_bt.txt').write_text(sys.version)"''',
'''python -c "import os, sys; open('{project}/pythonprefix_bt.txt', 'w').write(sys.prefix)"''', '''python -c "import pathlib, sys; pathlib.Path('{project}/pythonprefix_bt.txt').write_text(sys.prefix)"''',
] ]
if utils.platform == "pyodide": 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 # the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux. # mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test", "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, **build_frontend_env,
}, },
) )
+20
View File
@@ -128,6 +128,17 @@ def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv):
build_environment = {} 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(): for package_name, version in tool_versions.items():
env_name = f"EXPECTED_{package_name.upper()}_VERSION" env_name = f"EXPECTED_{package_name.upper()}_VERSION"
build_environment[env_name] = 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 # also check that we got the right wheels
expected_wheels = utils.expected_wheels("spam", "0.1.0") 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) assert set(actual_wheels) == set(expected_wheels)
+15
View File
@@ -33,11 +33,26 @@ def test_pep518(tmp_path, build_frontend_env):
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
basic_project.generate(project_dir) 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 # build the wheels
actual_wheels = utils.cibuildwheel_run(project_dir, add_env=build_frontend_env) actual_wheels = utils.cibuildwheel_run(project_dir, add_env=build_frontend_env)
# check that the expected wheels are produced # check that the expected wheels are produced
expected_wheels = utils.expected_wheels("spam", "0.1.0") 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) assert set(actual_wheels) == set(expected_wheels)
# These checks ensure an extra file is not created when using custom # These checks ensure an extra file is not created when using custom
+15 -5
View File
@@ -81,7 +81,9 @@ def test(tmp_path):
# the 'false ||' bit is to ensure this command runs in a shell on # the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux. # mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test", "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 # the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux. # mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test", "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, 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 # the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux. # mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test", "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, single_python=True,
) )
@@ -189,7 +195,9 @@ def test_bare_pytest_invocation(tmp_path: Path, test_runner: str) -> None:
add_env={ add_env={
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "", "CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
"CIBW_TEST_COMMAND": ( "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" if test_runner == "pytest"
else "python -m unittest discover test spam_test.py" else "python -m unittest discover test spam_test.py"
), ),
@@ -211,7 +219,9 @@ def test_test_sources(tmp_path):
add_env={ add_env={
"CIBW_TEST_REQUIRES": "pytest", "CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "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", "CIBW_TEST_SOURCES": "test",
}, },
) )
+6 -2
View File
@@ -55,7 +55,7 @@ def cibuildwheel_get_build_identifiers(
cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)] cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)]
if env is None: if env is None:
env = os.environ.copy() env = os.environ.copy()
env["CIBW_ENABLE"] = "cpython-freethreading pypy" env["CIBW_ENABLE"] = "cpython-freethreading pypy graalpy"
if prerelease_pythons: if prerelease_pythons:
env["CIBW_ENABLE"] += " cpython-prerelease" env["CIBW_ENABLE"] += " cpython-prerelease"
@@ -257,6 +257,10 @@ def _expected_wheels(
"pp310-pypy310_pp73", "pp310-pypy310_pp73",
"pp311-pypy311_pp73", "pp311-pypy311_pp73",
] ]
if machine_arch in ["x86_64", "AMD64", "aarch64", "arm64"]:
python_abi_tags += [
"graalpy311-graalpy242_311_native",
]
if single_python: if single_python:
python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION) python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION)
@@ -286,7 +290,7 @@ def _expected_wheels(
for manylinux_version in manylinux_versions 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( platform_tags.append(
".".join( ".".join(
f"{musllinux_version}_{machine_arch}" f"{musllinux_version}_{machine_arch}"
+1 -1
View File
@@ -24,7 +24,7 @@ def test_linux_container_split(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
manylinux-x86_64-image = "normal_container_image" manylinux-x86_64-image = "normal_container_image"
manylinux-i686-image = "normal_container_image" manylinux-i686-image = "normal_container_image"
build = "*-manylinux_x86_64" build = "*-manylinux_x86_64"
skip = "pp*" skip = "[gp]p*"
archs = "x86_64 i686" archs = "x86_64 i686"
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
+7 -5
View File
@@ -423,9 +423,9 @@ def test_debug_traceback(monkeypatch, method, capfd):
@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
def test_enable(method, intercepted_build_args, monkeypatch): def test_enable(method, intercepted_build_args, monkeypatch):
if method == "command_line": 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": elif method == "env_var":
monkeypatch.setenv("CIBW_ENABLE", "pypy") monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy")
main() main()
@@ -434,18 +434,20 @@ def test_enable(method, intercepted_build_args, monkeypatch):
if method == "unset": if method == "unset":
assert enable_groups == frozenset() assert enable_groups == frozenset()
else: else:
assert enable_groups == frozenset([EnableGroup.PyPy]) assert enable_groups == frozenset([EnableGroup.PyPy, EnableGroup.GraalPy])
def test_enable_arg_inherits(intercepted_build_args, monkeypatch): 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"]) monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "cpython-prerelease"])
main() main()
enable_groups = intercepted_build_args.args[0].globals.build_selector.enable 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): def test_enable_arg_error_message(monkeypatch, capsys):
+11 -5
View File
@@ -14,7 +14,7 @@ from cibuildwheel.oci_container import OCIPlatform
from cibuildwheel.util import file from cibuildwheel.util import file
DEFAULT_IDS = {"cp38", "cp39", "cp310", "cp311", "cp312", "cp313"} 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 @pytest.fixture
@@ -103,7 +103,7 @@ def test_build_with_override_launches(monkeypatch, tmp_path):
[tool.cibuildwheel] [tool.cibuildwheel]
manylinux-x86_64-image = "manylinux_2_28" manylinux-x86_64-image = "manylinux_2_28"
musllinux-x86_64-image = "musllinux_1_2" musllinux-x86_64-image = "musllinux_1_2"
enable = ["pypy", "cpython-freethreading"] enable = ["pypy", "graalpy", "cpython-freethreading"]
# Before Python 3.10, use manylinux2014 # Before Python 3.10, use manylinux2014
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
@@ -155,6 +155,7 @@ before-all = "true"
"pp39", "pp39",
"pp310", "pp310",
"pp311", "pp311",
"gp242",
} }
} }
assert kwargs["options"].build_options("cp39-manylinux_x86_64").before_all == "" assert kwargs["options"].build_options("cp39-manylinux_x86_64").before_all == ""
@@ -176,6 +177,7 @@ before-all = "true"
"pp39", "pp39",
"pp310", "pp310",
"pp311", "pp311",
"gp242",
] ]
} }
@@ -185,14 +187,16 @@ before-all = "true"
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386 assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]} 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] kwargs = build_in_container.call_args_list[4][1]
assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_x86_64" in kwargs["container"]["image"]
assert kwargs["container"]["cwd"] == PurePosixPath("/project") assert kwargs["container"]["cwd"] == PurePosixPath("/project")
assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64 assert kwargs["container"]["oci_platform"] == OCIPlatform.AMD64
identifiers = {x.identifier for x in kwargs["platform_configs"]} 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] kwargs = build_in_container.call_args_list[5][1]
assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"] assert "quay.io/pypa/musllinux_1_2_i686" in kwargs["container"]["image"]
@@ -200,4 +204,6 @@ before-all = "true"
assert kwargs["container"]["oci_platform"] == OCIPlatform.i386 assert kwargs["container"]["oci_platform"] == OCIPlatform.i386
identifiers = {x.identifier for x in kwargs["platform_configs"]} 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
}