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 + }