diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 41941897..231edd90 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -104,6 +104,10 @@ properties: description: Set environment variables on the host to pass-through to the container during the build. type: string_array + free-threaded-support: + type: boolean + default: false + description: The project supports free-threaded builds of Python (PEP703) manylinux-aarch64-image: type: string description: Specify alternative manylinux / musllinux container images @@ -248,6 +252,7 @@ non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properti del non_global_options["build"] del non_global_options["skip"] del non_global_options["test-skip"] +del non_global_options["free-threaded-support"] 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 e5b4458e..da2d68d6 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -117,7 +117,7 @@ class BuildOptions: return self.globals.architectures -Setting = Union[Mapping[str, str], Sequence[str], str, int] +Setting = Union[Mapping[str, str], Sequence[str], str, int, bool] @dataclasses.dataclass(frozen=True) @@ -196,7 +196,7 @@ def _resolve_cascade( if value is None: continue - if ignore_empty and not value: + if ignore_empty and not value and value is not False: continue value_string = _stringify_setting(value, list_sep, table_format) @@ -258,7 +258,7 @@ def _stringify_setting( raise ConfigOptionError(msg) return list_sep.join(setting) - if isinstance(setting, int): + if isinstance(setting, (bool, int)): return str(setting) return setting @@ -516,6 +516,10 @@ class Options: skip_config = self.reader.get("skip", env_plat=False, list_sep=" ") test_skip = self.reader.get("test-skip", env_plat=False, list_sep=" ") + free_threaded_support = strtobool( + self.reader.get("free-threaded-support", env_plat=False, ignore_empty=True) + ) + prerelease_pythons = args.prerelease_pythons or strtobool( self.env.get("CIBW_PRERELEASE_PYTHONS", "0") ) @@ -536,12 +540,14 @@ class Options: skip_config = "" architectures = Architecture.all_archs(self.platform) prerelease_pythons = True + free_threaded_support = True 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, ) test_selector = TestSelector(skip_config=test_skip) diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 2681aca5..d0ba3ff6 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -255,6 +255,12 @@ ], "title": "CIBW_ENVIRONMENT_PASS" }, + "free-threaded-support": { + "type": "boolean", + "default": false, + "description": "The project supports free-threaded builds of Python (PEP703)", + "title": "CIBW_FREE_THREADED_SUPPORT" + }, "manylinux-aarch64-image": { "type": "string", "description": "Specify alternative manylinux / musllinux container images", diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index b3ddeb95..85e82f2f 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -2,6 +2,7 @@ build = "*" skip = "" test-skip = "" +free-threaded-support = false archs = ["auto"] build-frontend = "default" diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index e704911c..72c937d7 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -241,6 +241,8 @@ class BuildSelector: PRERELEASE_SKIP: ClassVar[str] = "cp313-* cp313t-*" prerelease_pythons: bool = False + free_threaded_support: bool = False + def __call__(self, build_id: str) -> bool: # Filter build selectors by python_requires if set if self.requires_python is not None: @@ -257,6 +259,10 @@ class BuildSelector: if not self.prerelease_pythons and selector_matches(self.PRERELEASE_SKIP, 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): + return False + should_build = selector_matches(self.build_config, build_id) should_skip = selector_matches(self.skip_config, build_id) @@ -268,6 +274,7 @@ class BuildSelector: "skip_config": self.skip_config, "requires_python": str(self.requires_python), "prerelease_pythons": self.prerelease_pythons, + "free_threaded_support": self.free_threaded_support, } diff --git a/test/utils.py b/test/utils.py index 4bc02f47..08e2bc93 100644 --- a/test/utils.py +++ b/test/utils.py @@ -37,6 +37,9 @@ def cibuildwheel_get_build_identifiers(project_path, env=None, *, prerelease_pyt cmd = [sys.executable, "-m", "cibuildwheel", "--print-build-identifiers", str(project_path)] if prerelease_pythons: cmd.append("--prerelease-pythons") + if env is None: + env = os.environ.copy() + env.setdefault("CIBW_FREE_THREADED_SUPPORT", "1") cmd_output = subprocess.run( cmd, @@ -94,6 +97,8 @@ def cibuildwheel_run( _update_pip_cache_dir(env) + env.setdefault("CIBW_FREE_THREADED_SUPPORT", "1") + with TemporaryDirectory() as tmp_output_dir: subprocess.run( [ diff --git a/unit_test/build_selector_test.py b/unit_test/build_selector_test.py index 7d7eeb5f..6066c82e 100644 --- a/unit_test/build_selector_test.py +++ b/unit_test/build_selector_test.py @@ -50,6 +50,7 @@ def test_build_filter_pre(): assert build_selector("cp313-manylinux_x86_64") assert build_selector("cp37-win_amd64") assert build_selector("cp313-win_amd64") + assert not build_selector("cp313t-manylinux_x86_64") def test_skip(): @@ -144,6 +145,14 @@ def test_build_limited_python_patch(): assert build_selector("cp37-manylinux_x86_64") +def test_build_free_threaded_python(): + build_selector = BuildSelector( + build_config="*", skip_config="", prerelease_pythons=True, free_threaded_support=True + ) + + assert build_selector("cp313t-manylinux_x86_64") + + def test_testing_selector(): # local import to avoid pytest trying to collect this as a test class! from cibuildwheel.util import TestSelector diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 288f95ca..1b482c75 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -415,3 +415,36 @@ def test_override_inherit_environment_with_references(tmp_path: Path): ) == { "PATH": "/opt/local/bin:/opt/bin:/usr/bin:/bin", } + + +@pytest.mark.parametrize( + ("toml_assignment", "env", "expected_result"), + [ + ("", {}, False), + ("free-threaded-support = true", {}, True), + ("free-threaded-support = false", {}, False), + ("", {"CIBW_FREE_THREADED_SUPPORT": "0"}, False), + ("", {"CIBW_FREE_THREADED_SUPPORT": "1"}, True), + ("free-threaded-support = false", {"CIBW_FREE_THREADED_SUPPORT": "1"}, True), + ("free-threaded-support = true", {"CIBW_FREE_THREADED_SUPPORT": "0"}, False), + ("free-threaded-support = true", {"CIBW_FREE_THREADED_SUPPORT": ""}, True), + ("free-threaded-support = false", {"CIBW_FREE_THREADED_SUPPORT": ""}, False), + ], +) +def test_free_threaded_support( + tmp_path: Path, toml_assignment: str, env: dict[str, str], expected_result: bool +): + args = CommandLineArguments.defaults() + args.package_dir = tmp_path + + pyproject_toml: Path = tmp_path / "pyproject.toml" + pyproject_toml.write_text( + textwrap.dedent( + f"""\ + [tool.cibuildwheel] + {toml_assignment} + """ + ) + ) + options = Options(platform="linux", command_line_arguments=args, env=env) + assert options.globals.build_selector.free_threaded_support is expected_result