diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 41f4ad4cd..dae29c4f5 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -26,6 +26,13 @@ - append default: none description: How to inherit the parent's value. + enable: + enum: + - cpython-eol + - cpython-free-threaded + - cpython-prerelease + - pypy-eol + description: A Python version or flavor to enable. additionalProperties: false description: cibuildwheel's settings. type: object @@ -99,6 +106,13 @@ default: pinned description: Specify how cibuildwheel controls the versions of the tools it uses type: string + enable: + description: Enable or disable certain builds. + oneOf: + - $ref: "#/$defs/enable" + - type: array + items: + $ref: "#/$defs/enable" environment: description: Set environment variables needed during the build. type: string_table @@ -110,6 +124,7 @@ type: boolean default: false description: The project supports free-threaded builds of Python (PEP703) + deprecated: Use the `enable` option instead. manylinux-aarch64-image: type: string description: Specify alternative manylinux / musllinux container images @@ -258,6 +273,7 @@ del non_global_options["skip"] del non_global_options["test-skip"] del non_global_options["free-threaded-support"] +del non_global_options["enable"] overrides["items"]["properties"]["select"]["oneOf"] = string_array overrides["items"]["properties"] |= non_global_options.copy() diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 5e1e71d16..c2386d943 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -30,6 +30,7 @@ BuildFrontendConfig, BuildSelector, DependencyConstraints, + EnableGroups, TestSelector, format_safe, resources_dir, @@ -511,6 +512,7 @@ def get( env_plat: bool = True, option_format: OptionFormat | None = None, ignore_empty: bool = False, + env_rule: InheritRule = InheritRule.NONE, ) -> str: """ Get and return the value for the named option from environment, @@ -542,8 +544,8 @@ def get( (o.options.get(name), o.inherit.get(name, InheritRule.NONE)) for o in self.active_config_overrides ], - (self.env.get(envvar), InheritRule.NONE), - (self.env.get(plat_envvar) if env_plat else None, InheritRule.NONE), + (self.env.get(envvar), env_rule), + (self.env.get(plat_envvar) if env_plat else None, env_rule), ignore_empty=ignore_empty, option_format=option_format, ) @@ -599,16 +601,36 @@ def globals(self) -> GlobalOptions: skip_config = self.reader.get("skip", env_plat=False, option_format=ListFormat(sep=" ")) test_skip = self.reader.get("test-skip", env_plat=False, option_format=ListFormat(sep=" ")) + allow_empty = args.allow_empty or strtobool(self.env.get("CIBW_ALLOW_EMPTY", "0")) + + enable_groups = self.reader.get( + "enable", env_plat=False, option_format=ListFormat(sep=" "), env_rule=InheritRule.APPEND + ) + if enable_groups: + enable = {EnableGroups(group) for group in enable_groups.split()} + free_threaded_support = strtobool( self.reader.get("free-threaded-support", env_plat=False, ignore_empty=True) ) - allow_empty = args.allow_empty or strtobool(self.env.get("CIBW_ALLOW_EMPTY", "0")) - prerelease_pythons = args.prerelease_pythons or strtobool( self.env.get("CIBW_PRERELEASE_PYTHONS", "0") ) + if enable and (free_threaded_support or prerelease_pythons): + msg = ( + "free-threaded-support and prerelease-pythons should be specified by enable instead" + ) + raise OptionsReaderError(msg) + + if free_threaded_support: + enable.add(EnableGroups.CPythonFreeThreaded) + if prerelease_pythons: + enable.add(EnableGroups.CPythonPrerelease) + + # For backwards compatibility, we are adding EoL Python versions to the enable group for one more release. + enable |= {EnableGroups.CPythonEoL, EnableGroups.PyPyEoL} + # This is not supported in tool.cibuildwheel, as it comes from a standard location. # Passing this in as an environment variable will override pyproject.toml, setup.cfg, or setup.py requires_python_str: str | None = ( @@ -624,15 +646,13 @@ def globals(self) -> GlobalOptions: build_config = args.only skip_config = "" architectures = Architecture.all_archs(self.platform) - prerelease_pythons = True - free_threaded_support = True + enable = set(EnableGroups) build_selector = BuildSelector( build_config=build_config, skip_config=skip_config, requires_python=requires_python, - prerelease_pythons=prerelease_pythons, - free_threaded_support=free_threaded_support, + enable=frozenset(enable), ) test_selector = TestSelector(skip_config=test_skip) diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 8e2508bc9..a4394a713 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -10,7 +10,16 @@ ], "default": "none", "description": "How to inherit the parent's value." - } + }, + "enable": { + "enum": [ + "cpython-eol", + "cpython-free-threaded", + "cpython-prerelease", + "pypy-eol" + ] + }, + "description": "A Python version or flavor to enable." }, "additionalProperties": false, "description": "cibuildwheel's settings.", @@ -228,6 +237,21 @@ "type": "string", "title": "CIBW_DEPENDENCY_VERSIONS" }, + "enable": { + "description": "Enable or disable certain builds.", + "oneOf": [ + { + "$ref": "#/$defs/enable" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/enable" + } + } + ], + "title": "CIBW_ENABLE" + }, "environment": { "description": "Set environment variables needed during the build.", "oneOf": [ diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index bfbe50c7a..60c1d5c67 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -1,6 +1,7 @@ from __future__ import annotations import contextlib +import enum import fnmatch import itertools import os @@ -23,7 +24,7 @@ from pathlib import Path, PurePath from tempfile import TemporaryDirectory from time import sleep -from typing import Any, ClassVar, Final, Literal, TextIO, TypeVar +from typing import Any, Final, Literal, TextIO, TypeVar from zipfile import ZipFile import bracex @@ -41,6 +42,7 @@ __all__ = [ "MANYLINUX_ARCHS", + "EnableGroups", "call", "chdir", "combine_constraints", @@ -66,6 +68,17 @@ test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py" +class EnableGroups(enum.Enum): + """ + Groups of build selectors that are not enabled by default. + """ + + CPythonEoL = "cpython-eol" + CPythonFreeThreaded = "cpython-free-threaded" + CPythonPrerelease = "cpython-prerelease" + PyPyEoL = "pypy-eol" + + MANYLINUX_ARCHS: Final[tuple[str, ...]] = ( "x86_64", "i686", @@ -247,12 +260,7 @@ class BuildSelector: build_config: str skip_config: str requires_python: SpecifierSet | None = None - - # a pattern that skips prerelease versions, when include_prereleases is False. - PRERELEASE_SKIP: ClassVar[str] = "" - prerelease_pythons: bool = False - - free_threaded_support: bool = False + enable: frozenset[EnableGroups] = frozenset() def __call__(self, build_id: str) -> bool: # Filter build selectors by python_requires if set @@ -266,12 +274,20 @@ def __call__(self, build_id: str) -> bool: if not self.requires_python.contains(version): return False - # filter out the prerelease pythons if self.prerelease_pythons is False - if not self.prerelease_pythons and selector_matches(self.PRERELEASE_SKIP, build_id): + # filter out groups that are not enabled + if EnableGroups.CPythonEoL not in self.enable and selector_matches( + "cp3{6,7,8}-*", build_id + ): return False - - # filter out free threaded pythons if self.free_threaded_support is False - if not self.free_threaded_support and selector_matches("*t-*", build_id): + if EnableGroups.CPythonFreeThreaded not in self.enable and selector_matches( + "cp3??t-*", build_id + ): + return False + if EnableGroups.CPythonPrerelease not in self.enable and selector_matches( + "cp314*", build_id + ): + return False + if EnableGroups.PyPyEoL not in self.enable and selector_matches("pp3{7,8,9}-*", build_id): return False should_build = selector_matches(self.build_config, build_id) @@ -284,8 +300,7 @@ def options_summary(self) -> Any: "build_config": self.build_config, "skip_config": self.skip_config, "requires_python": str(self.requires_python), - "prerelease_pythons": self.prerelease_pythons, - "free_threaded_support": self.free_threaded_support, + "enable": sorted(group.value for group in self.enable), } diff --git a/unit_test/build_selector_test.py b/unit_test/build_selector_test.py index f89222461..278456296 100644 --- a/unit_test/build_selector_test.py +++ b/unit_test/build_selector_test.py @@ -2,7 +2,7 @@ from packaging.specifiers import SpecifierSet -from cibuildwheel.util import BuildSelector +from cibuildwheel.util import BuildSelector, EnableGroups def test_build(): @@ -43,7 +43,9 @@ def test_build_filter_pre(): build_selector = BuildSelector( build_config="cp3*-* *-manylinux*", skip_config="", - prerelease_pythons=True, + enable=frozenset( + [EnableGroups.CPythonPrerelease, EnableGroups.CPythonEoL, EnableGroups.PyPyEoL] + ), ) assert build_selector("cp37-manylinux_x86_64") @@ -146,9 +148,7 @@ def test_build_limited_python_patch(): def test_build_free_threaded_python(): - build_selector = BuildSelector( - build_config="*", skip_config="", prerelease_pythons=True, free_threaded_support=True - ) + build_selector = BuildSelector(build_config="*", skip_config="", enable=frozenset(EnableGroups)) assert build_selector("cp313t-manylinux_x86_64") diff --git a/unit_test/options_test.py b/unit_test/options_test.py index c19c6619f..41dc74dcb 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -15,6 +15,7 @@ Options, _get_pinned_container_images, ) +from cibuildwheel.util import EnableGroups PYPROJECT_1 = """ [tool.cibuildwheel] @@ -448,4 +449,7 @@ def test_free_threaded_support( ) ) options = Options(platform="linux", command_line_arguments=args, env=env) - assert options.globals.build_selector.free_threaded_support is expected_result + if expected_result: + assert EnableGroups.CPythonFreeThreaded in options.globals.build_selector.enable + else: + assert EnableGroups.CPythonFreeThreaded not in options.globals.build_selector.enable