diff --git a/.circleci/config.yml b/.circleci/config.yml index 5c8bef6f6..13bcbe069 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,30 @@ -version: 2 +version: 2.1 + +commands: + cibw_prepare_environment: + description: "Prepare the environment for testing." + steps: + - run: + name: Prepare the environment. + command: bash .circleci/prepare.sh + cibw_run_tests: + description: "Runs tests, with CIBW_ENABLE=all on the main branch" + steps: + - run: + name: Test + command: | + if [ "${CIRCLE_BRANCH}" == "main" ]; then + echo "INFO: Exporting CIBW_ENABLE=all for main branch test run." + export CIBW_ENABLE=all + else + echo "INFO: CIBW_ENABLE not set for this branch test run." + fi + + venv/bin/python ./bin/run_tests.py + no_output_timeout: 30m jobs: - osx-python3.12: + osx-python312: macos: xcode: 15.4.0 resource_class: macos.m1.medium.gen1 @@ -9,16 +32,10 @@ jobs: PYTHON: python3 steps: - checkout + - cibw_prepare_environment + - cibw_run_tests - - run: - name: Prepare the environment. - command: bash .circleci/prepare.sh - - run: - name: Test. - command: venv/bin/python ./bin/run_tests.py - no_output_timeout: 30m - - linux-python3.12: + linux-python312: docker: - image: cimg/python:3.12 environment: @@ -29,14 +46,8 @@ jobs: steps: - checkout - setup_remote_docker - - - run: - name: Prepare the environment. - command: bash .circleci/prepare.sh - - run: - name: Test. - command: venv/bin/python ./bin/run_tests.py - no_output_timeout: 30m + - cibw_prepare_environment + - cibw_run_tests linux-aarch64: machine: @@ -49,19 +60,13 @@ jobs: PYTEST_ADDOPTS: -k "unit_test or main_tests or test_0_basic or test_docker_images" steps: - checkout - - - run: - name: Prepare the environment. - command: bash .circleci/prepare.sh - - run: - name: Test. - command: venv/bin/python ./bin/run_tests.py - no_output_timeout: 30m + - cibw_prepare_environment + - cibw_run_tests workflows: version: 2 all-tests: jobs: - - osx-python3.12 - - linux-python3.12 + - osx-python312 + - linux-python312 - linux-aarch64 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ae094dcd..d860ef40b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,11 @@ on: - main - 2.x pull_request: + types: + - opened + - synchronize + - reopened + - labeled paths-ignore: - 'docs/**' - .pre-commit-config.yaml @@ -68,6 +73,26 @@ jobs: run: | uv sync --no-dev --group test + - uses: joerick/pr-labels-action@v1.0.9 + - name: Set CIBW_ENABLE + shell: bash + run: | + if [[ "${{ github.ref_name }}" == "main" ]]; then + CIBW_ENABLE=all + else + # get the default CIBW_ENABLE value from the test module + CIBW_ENABLE=$(uv run --no-sync python -c 'import sys, test.conftest as c; sys.stdout.write(c.DEFAULT_CIBW_ENABLE)') + + # if this is a PR, check for labels + if [[ -n "$GITHUB_PR_LABEL_CI_PYPY" ]]; then + CIBW_ENABLE+=" pypy" + fi + if [[ -n "$GITHUB_PR_LABEL_CI_GRAALPY" ]]; then + CIBW_ENABLE+=" graalpy" + fi + fi + echo "CIBW_ENABLE=${CIBW_ENABLE}" >> $GITHUB_ENV + - name: Generate a sample project run: | uv run --no-sync -m test.test_projects test.test_0_basic.basic_project sample_proj @@ -80,7 +105,6 @@ jobs: env: CIBW_ARCHS_MACOS: x86_64 universal2 arm64 CIBW_BUILD_FRONTEND: 'build[uv]' - CIBW_ENABLE: "cpython-prerelease cpython-freethreading pypy graalpy" - name: Run a sample build (GitHub Action, only) uses: ./ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ecf0361fb..5ee98dd54 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,10 @@ linux: # skip all but the basic tests # (comment the below line in a PR to debug a Gitlab-specific issue) PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + variables: + CIBW_ENABLE: "all" script: - curl -sSL https://get.docker.com/ | sh - docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all @@ -26,6 +30,10 @@ windows: PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code before_script: - choco install python -y --version 3.12.4 + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + variables: + CIBW_ENABLE: "all" script: - py -m pip install dependency-groups - py -m pip install -e. pytest-custom-exit-code $(py -m dependency_groups test) @@ -37,6 +45,10 @@ macos: image: macos-14-xcode-15 variables: PYTEST_ADDOPTS: -k "unit_test or test_0_basic" --suppress-no-test-exit-code + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + variables: + CIBW_ENABLE: "all" script: - python3 -m pip install dependency-groups - python3 -m dependency_groups test | xargs python3 -m pip install -e. pytest-custom-exit-code diff --git a/.travis.yml b/.travis.yml index ec2365a09..e4d1d72a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,13 +14,17 @@ jobs: - name: Linux | x86_64 + i686 | Python 3.12 python: 3.12 services: docker - env: PYTHON=python + env: + - PYTHON=python + - CIBW_ENABLE=all - name: Linux | arm64 | Python 3.12 python: 3.12 services: docker arch: arm64 - env: PYTHON=python + env: + - PYTHON=python + - CIBW_ENABLE=all - name: Linux | ppc64le | Python 3.12 python: 3.12 @@ -32,6 +36,7 @@ jobs: # skip test_manylinuxXXXX_only, it uses too much disk space # c.f. https://travis-ci.community/t/running-out-of-disk-space-quota-when-using-docker-on-ppc64le/11634 - PYTEST_ADDOPTS='-k "not test_manylinuxXXXX_only"' + - CIBW_ENABLE=all - name: Windows | x86_64 | Python 3.12 os: windows @@ -40,13 +45,16 @@ jobs: - choco upgrade python3 -y --version 3.12.8 --limit-output --params "/InstallDir:C:\\Python312" env: - PYTHON=C:\\Python312\\python + - CIBW_ENABLE=all - name: Linux | s390x | Python 3.12 python: 3.12 services: docker arch: s390x allow_failure: True - env: PYTHON=python + env: + - PYTHON=python + - CIBW_ENABLE=all install: - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all; fi diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1cc707fd9..ddc9f18fa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,6 +16,12 @@ jobs: docker run --rm --privileged docker.io/tonistiigi/binfmt:latest --install all python -m pip install dependency-groups python -m dependency_groups test | xargs python -m pip install -e. + if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then + echo "INFO: Exporting CIBW_ENABLE=all for main branch test run." + export CIBW_ENABLE=all + else + echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))." + fi python ./bin/run_tests.py - job: macos_311 @@ -28,6 +34,12 @@ jobs: - bash: | python -m pip install dependency-groups python -m dependency_groups test | xargs python -m pip install -e. + if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then + echo "INFO: Exporting CIBW_ENABLE=all for main branch test run." + export CIBW_ENABLE=all + else + echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))." + fi python ./bin/run_tests.py - job: windows_311 @@ -40,4 +52,10 @@ jobs: - bash: | python -m pip install dependency-groups python -m dependency_groups test | xargs python -m pip install -e. + if [ "$(Build.SourceBranch)" = "refs/heads/main" ]; then + echo "INFO: Exporting CIBW_ENABLE=all for main branch test run." + export CIBW_ENABLE=all + else + echo "INFO: CIBW_ENABLE not set for this branch ($(Build.SourceBranch))." + fi python ./bin/run_tests.py diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 283f48f7c..fa1d1c046 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -634,8 +634,10 @@ def globals(self) -> GlobalOptions: "enable", env_plat=False, option_format=ListFormat(sep=" "), env_rule=InheritRule.APPEND ) try: - enable = {EnableGroup(group) for group in enable_groups.split()} - enable.update(EnableGroup(command_line_group) for command_line_group in args.enable) + enable = { + *EnableGroup.parse_option_value(enable_groups), + *EnableGroup.parse_option_value(" ".join(args.enable)), + } except ValueError as e: msg = f"Failed to parse enable group. {e}. Valid group names are: {', '.join(g.value for g in EnableGroup)}" raise errors.ConfigurationError(msg) from e diff --git a/cibuildwheel/selector.py b/cibuildwheel/selector.py index 48578712d..2d78aea0e 100644 --- a/cibuildwheel/selector.py +++ b/cibuildwheel/selector.py @@ -39,6 +39,23 @@ class EnableGroup(StrEnum): def all_groups(cls) -> frozenset["EnableGroup"]: return frozenset(cls) + @classmethod + def parse_option_value(cls, value: str) -> frozenset["EnableGroup"]: + """ + Parses a string of space-separated values into a set of EnableGroup + members. The string may contain group names or "all". + """ + result = set() + for group in value.strip().split(): + if group == "all": + return cls.all_groups() + try: + result.add(cls(group)) + except ValueError: + msg = f"Unknown enable group: {group}" + raise ValueError(msg) from None + return frozenset(result) + @dataclass(frozen=True, kw_only=True) class BuildSelector: diff --git a/docs/contributing.md b/docs/contributing.md index 4eea79599..86cd49bdf 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -82,6 +82,8 @@ A few notes- - Running the macOS integration tests requires _system installs_ of Python from python.org for all the versions that are tested. We won't attempt to install these when running locally, but you can do so manually using the URL in the error message that is printed when the install is not found. +- The 'enable groups' run by default are just 'cpython-prerelease' and 'cpython-freethreading'. You can add other groups like pypy or graalpy by setting the [CIBW_ENABLE](options.md#enable) environment variable. On GitHub PRs, you can add a label to the PR to enable these groups. + #### Running pytest directly More advanced users might prefer to invoke pytest directly. Set up a [dev environment](#setting-up-a-dev-environment), then, diff --git a/docs/options.md b/docs/options.md index 5006d8e28..c36c423ab 100644 --- a/docs/options.md +++ b/docs/options.md @@ -324,7 +324,7 @@ values are: 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. - +- `all`: Enable all of the above. !!! caution `cpython-prerelease` is provided for testing purposes only. It is not diff --git a/test/conftest.py b/test/conftest.py index f2b575df0..8c3e15221 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,4 +1,5 @@ import json +import os import subprocess from collections.abc import Generator @@ -12,6 +13,9 @@ from .utils import EMULATED_ARCHS, platform +# default to just cpython +DEFAULT_CIBW_ENABLE = "cpython-freethreading cpython-prerelease cpython-experimental-riscv64" + def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption( @@ -29,6 +33,8 @@ def pytest_addoption(parser: pytest.Parser) -> None: help="macOS cp38 uses the universal2 installer", ) + os.environ.setdefault("CIBW_ENABLE", DEFAULT_CIBW_ENABLE) + def docker_warmup(request: pytest.FixtureRequest) -> None: machine = request.config.getoption("--run-emulation", default=None) diff --git a/test/test_0_basic.py b/test/test_0_basic.py index 6a7732031..d0503c105 100644 --- a/test/test_0_basic.py +++ b/test/test_0_basic.py @@ -1,8 +1,10 @@ +import os import textwrap import pytest from cibuildwheel.logger import Logger +from cibuildwheel.selector import EnableGroup from . import test_projects, utils @@ -38,11 +40,13 @@ def test(tmp_path, build_frontend_env, capfd): expected_wheels = utils.expected_wheels("spam", "0.1.0") assert set(actual_wheels) == set(expected_wheels) - # Verify pip warning not shown - captured = capfd.readouterr() - for stream in (captured.err, captured.out): - assert "WARNING: Running pip as the 'root' user can result" not in stream - assert "A new release of pip available" not in stream + enable_groups = EnableGroup.parse_option_value(os.environ.get("CIBW_ENABLE", "")) + if EnableGroup.GraalPy not in enable_groups: + # Verify pip warning not shown + captured = capfd.readouterr() + for stream in (captured.err, captured.out): + assert "WARNING: Running pip as the 'root' user can result" not in stream + assert "A new release of pip available" not in stream @pytest.mark.skip(reason="to keep test output clean") @@ -61,16 +65,19 @@ def test_sample_build(tmp_path, capfd): logger.step_end() -def test_build_identifiers(tmp_path): +@pytest.mark.parametrize( + "enable_setting", ["", "cpython-prerelease", "pypy", "cpython-freethreading"] +) +def test_build_identifiers(tmp_path, enable_setting, monkeypatch): project_dir = tmp_path / "project" basic_project.generate(project_dir) + monkeypatch.setenv("CIBW_ENABLE", enable_setting) + # check that the number of expected wheels matches the number of build # identifiers expected_wheels = utils.expected_wheels("spam", "0.1.0") - build_identifiers = utils.cibuildwheel_get_build_identifiers( - project_dir, prerelease_pythons=True - ) + build_identifiers = utils.cibuildwheel_get_build_identifiers(project_dir) assert len(expected_wheels) == len(build_identifiers), ( f"{expected_wheels} vs {build_identifiers}" ) diff --git a/test/test_abi_variants.py b/test/test_abi_variants.py index 5c7c27f54..62f03850f 100644 --- a/test/test_abi_variants.py +++ b/test/test_abi_variants.py @@ -35,36 +35,34 @@ def test_abi3(tmp_path): project_dir = tmp_path / "project" limited_api_project.generate(project_dir) - single_python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION) - # build the wheels actual_wheels = utils.cibuildwheel_run( project_dir, add_env={ # 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-* gp242-* {single_python_tag}-* cp313t-*" + "CIBW_BUILD": "cp39-* cp310-* pp310-* gp242-* cp312-* cp313t-*", + "CIBW_ENABLE": "all", }, ) # check that the expected wheels are produced - expected_wheels = utils.expected_wheels("spam", "0.1.0") if utils.platform == "pyodide": - # there's only 1 possible configuration for pyodide, the single_python_tag one - expected_wheels = [ - w.replace(f"{single_python_tag}-{single_python_tag}", "cp310-abi3") - for w in expected_wheels - ] + # there's only 1 possible configuration for pyodide, cp312 + expected_wheels = utils.expected_wheels("spam", "0.1.0", python_abi_tags=["cp310-abi3"]) else: - 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 "-graalpy242" in w - or "-cp313t" in w - ] + expected_wheels = utils.expected_wheels( + "spam", + "0.1.0", + python_abi_tags=[ + "cp39-cp39", + "cp310-abi3", # <-- ABI3, works with 3.10 and 3.12 + "cp313-cp313t", + "pp310-pypy310_pp73", + "graalpy311-graalpy242_311_native", + ], + ) + assert set(actual_wheels) == set(expected_wheels) @@ -187,6 +185,7 @@ def test_abi_none(tmp_path, capfd): "CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} ./test", # limit the number of builds for test performance reasons "CIBW_BUILD": "cp38-* cp{}{}-* cp313t-* pp310-*".format(*utils.SINGLE_PYTHON_VERSION), + "CIBW_ENABLE": "all", }, ) diff --git a/test/utils.py b/test/utils.py index 957ee7295..452d002fb 100644 --- a/test/utils.py +++ b/test/utils.py @@ -23,6 +23,9 @@ EMULATED_ARCHS: Final[list[str]] = sorted( arch.value for arch in (Architecture.all_archs("linux") - Architecture.auto_archs("linux")) ) +PYPY_ARCHS = ["x86_64", "i686", "AMD64", "aarch64", "arm64"] +GRAALPY_ARCHS = ["x86_64", "AMD64", "aarch64", "arm64"] + SINGLE_PYTHON_VERSION: Final[tuple[int, int]] = (3, 12) _AARCH64_CAN_RUN_ARMV7: Final[bool] = Architecture.aarch64.value not in EMULATED_ARCHS and { @@ -46,7 +49,8 @@ def cibuildwheel_get_build_identifiers( - project_path: Path, env: dict[str, str] | None = None, *, prerelease_pythons: bool = False + project_path: Path, + env: dict[str, str] | None = None, ) -> list[str]: """ Returns the list of build identifiers that cibuildwheel will try to build @@ -55,9 +59,6 @@ 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 graalpy" - if prerelease_pythons: - env["CIBW_ENABLE"] += " cpython-prerelease" cmd_output = subprocess.run( cmd, @@ -121,8 +122,6 @@ def cibuildwheel_run( _update_pip_cache_dir(env) - env["CIBW_ENABLE"] = " ".join(EnableGroup.all_groups()) - if single_python: env["CIBW_BUILD"] = "cp{}{}-*".format(*SINGLE_PYTHON_VERSION) @@ -222,6 +221,8 @@ def _expected_wheels( # {python tag} and {abi tag} are closely related to the python interpreter used to build the wheel # so we'll merge them below as python_abi_tag + enable_groups = EnableGroup.parse_option_value(os.environ.get("CIBW_ENABLE", "")) + if manylinux_versions is None: manylinux_versions = { "armv7l": ["manylinux_2_17", "manylinux2014", "manylinux_2_31"], @@ -243,25 +244,36 @@ def _expected_wheels( "cp311-cp311", "cp312-cp312", "cp313-cp313", - "cp313-cp313t", ] - if machine_arch == "ARM64": - # no CPython 3.8 on Windows ARM64 - python_abi_tags.pop(0) + if EnableGroup.CPythonFreeThreading in enable_groups: + python_abi_tags += [ + "cp313-cp313t", + ] - if machine_arch in ["x86_64", "i686", "AMD64", "aarch64", "arm64"]: + if EnableGroup.PyPy in enable_groups: python_abi_tags += [ "pp38-pypy38_pp73", "pp39-pypy39_pp73", "pp310-pypy310_pp73", "pp311-pypy311_pp73", ] - if machine_arch in ["x86_64", "AMD64", "aarch64", "arm64"]: + + if EnableGroup.GraalPy in enable_groups: python_abi_tags += [ "graalpy311-graalpy242_311_native", ] + if machine_arch == "ARM64" and platform == "windows": + # no CPython 3.8 on Windows ARM64 + python_abi_tags = [t for t in python_abi_tags if not t.startswith("cp38")] + + if machine_arch not in PYPY_ARCHS: + python_abi_tags = [tag for tag in python_abi_tags if not tag.startswith("pp")] + + if machine_arch not in GRAALPY_ARCHS: + python_abi_tags = [tag for tag in python_abi_tags if not tag.startswith("graalpy")] + if single_python: python_tag = "cp{}{}-".format(*SINGLE_PYTHON_VERSION) python_abi_tags = [ @@ -272,13 +284,6 @@ def _expected_wheels( ) ] - if platform == "pyodide": - assert len(python_abi_tags) == 1 - python_abi_tag = python_abi_tags[0] - platform_tag = "pyodide_2024_0_wasm32" - yield f"{package_name}-{package_version}-{python_abi_tag}-{platform_tag}.whl" - return - for python_abi_tag in python_abi_tags: platform_tags = [] @@ -327,6 +332,10 @@ def _expected_wheels( if include_universal2: platform_tags.append(f"macosx_{min_macosx.replace('.', '_')}_universal2") + + elif platform == "pyodide": + platform_tags = ["pyodide_2024_0_wasm32"] + else: msg = f"Unsupported platform {platform!r}" raise Exception(msg) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index 66ee4c952..731d10adc 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -329,6 +329,7 @@ def test_config_settings(platform_specific, platform, intercepted_build_args, mo @pytest.mark.usefixtures("platform", "intercepted_build_args", "allow_empty") def test_build_selector_deprecated_error(monkeypatch, selector, pattern, capsys): monkeypatch.setenv(selector, pattern) + monkeypatch.delenv("CIBW_ENABLE", raising=False) if selector == "CIBW_BUILD": with pytest.raises(SystemExit) as ex: @@ -422,6 +423,8 @@ def test_debug_traceback(monkeypatch, method, capfd): @pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) def test_enable(method, intercepted_build_args, monkeypatch): + monkeypatch.delenv("CIBW_ENABLE", raising=False) + if method == "command_line": monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "pypy", "--enable", "graalpy"]) elif method == "env_var": @@ -437,6 +440,15 @@ def test_enable(method, intercepted_build_args, monkeypatch): assert enable_groups == frozenset([EnableGroup.PyPy, EnableGroup.GraalPy]) +def test_enable_all(intercepted_build_args, monkeypatch): + monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "all"]) + + main() + + enable_groups = intercepted_build_args.args[0].globals.build_selector.enable + assert enable_groups == EnableGroup.all_groups() + + def test_enable_arg_inherits(intercepted_build_args, monkeypatch): monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy") monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "cpython-prerelease"]) diff --git a/unit_test/option_prepare_test.py b/unit_test/option_prepare_test.py index d5dc7d191..3356c523a 100644 --- a/unit_test/option_prepare_test.py +++ b/unit_test/option_prepare_test.py @@ -51,6 +51,7 @@ def ignore_context_call(*args, **kwargs): @pytest.mark.usefixtures("mock_build_container", "fake_package_dir") def test_build_default_launches(monkeypatch): monkeypatch.setattr(sys, "argv", [*sys.argv, "--platform=linux"]) + monkeypatch.delenv("CIBW_ENABLE", raising=False) main()