From 7933323184fde6831350c034f509faf357f15a68 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:12:15 +0530 Subject: [PATCH] Add pyodide-build as a separate build frontend and update its build verbosity handling (#2609) --- bin/generate_schema.py | 19 +++++ cibuildwheel/frontend.py | 7 +- cibuildwheel/options.py | 7 ++ cibuildwheel/platforms/ios.py | 6 ++ cibuildwheel/platforms/linux.py | 3 + cibuildwheel/platforms/macos.py | 6 ++ cibuildwheel/platforms/pyodide.py | 4 - cibuildwheel/platforms/windows.py | 6 ++ .../resources/cibuildwheel.schema.json | 35 ++++++++- cibuildwheel/resources/defaults.toml | 1 + docs/options.md | 22 +++--- docs/platforms.md | 5 ++ test/conftest.py | 10 +-- test/test_before_build.py | 2 +- test/test_build_frontend_args.py | 16 +++- unit_test/options_test.py | 76 ++++++++++++++++++- 16 files changed, 200 insertions(+), 25 deletions(-) diff --git a/bin/generate_schema.py b/bin/generate_schema.py index ad830496..46d39e4d 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -393,6 +393,25 @@ for os_name, command in [ del oses["linux"]["properties"]["dependency-versions"] +oses["pyodide"]["properties"]["build-frontend"] = { + **schema["properties"]["build-frontend"], + "default": "pyodide-build", + "description": 'On the pyodide platform, the build frontend must be "pyodide-build"', + "oneOf": [ + {"enum": ["pyodide-build"]}, + {"type": "string", "pattern": "^pyodide-build; ?args:"}, + { + "type": "object", + "additionalProperties": False, + "required": ["name"], + "properties": { + "name": {"enum": ["pyodide-build"]}, + "args": {"type": "array", "items": {"type": "string"}}, + }, + }, + ], +} + schema["properties"]["overrides"] = overrides schema["properties"] |= oses diff --git a/cibuildwheel/frontend.py b/cibuildwheel/frontend.py index 41c53b34..b3dc58ab 100644 --- a/cibuildwheel/frontend.py +++ b/cibuildwheel/frontend.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from cibuildwheel.typing import PathOrStr -BuildFrontendName = Literal["pip", "build", "build[uv]", "uv"] +BuildFrontendName = Literal["pip", "build", "build[uv]", "uv", "pyodide-build"] @dataclasses.dataclass(frozen=True) @@ -46,6 +46,11 @@ class BuildFrontendConfig: def _get_verbosity_flags(level: int, frontend: BuildFrontendName) -> list[str]: + if frontend == "pyodide-build": + if level > 0: + return ["-" + min(level, 2) * "v"] + return [] + if level < 0: return ["-" + -level * "q"] diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 8dd21a1a..6ca6820e 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -860,6 +860,13 @@ class Options: msg = f"Failed to parse build frontend. {e}" raise errors.ConfigurationError(msg) from e + if self.platform == "pyodide" and build_frontend.name != "pyodide-build": + msg = "The pyodide platform requires the 'pyodide-build' build frontend" + raise errors.ConfigurationError(msg) + if self.platform != "pyodide" and build_frontend.name == "pyodide-build": + msg = "The 'pyodide-build' build frontend is only supported on the pyodide platform" + raise errors.ConfigurationError(msg) + try: environment = parse_environment(environment_config) except (EnvironmentParseError, ValueError) as e: diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 60ee0daa..f8e895b5 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -463,6 +463,9 @@ def setup_python( *constraint_flags(dependency_constraint), env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) @@ -588,6 +591,9 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) diff --git a/cibuildwheel/platforms/linux.py b/cibuildwheel/platforms/linux.py index 54f29e4e..537d4327 100644 --- a/cibuildwheel/platforms/linux.py +++ b/cibuildwheel/platforms/linux.py @@ -360,6 +360,9 @@ def build_in_container( ], env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index 5ef4a3e7..61024271 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -449,6 +449,9 @@ def setup_python( *constraint_flags(dependency_constraint), env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) @@ -585,6 +588,9 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=build_env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index 2466d59d..01f490a8 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -426,10 +426,6 @@ def build(options: Options, tmp_path: Path) -> None: build_options = options.build_options(config.identifier) build_frontend = build_options.build_frontend - if build_frontend.name == "pip": - msg = "The pyodide platform doesn't support pip frontend" - raise errors.FatalError(msg) - log.build_start(config.identifier) identifier_tmp_dir = tmp_path / config.identifier diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index bd6cb3a5..409f4681 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -401,6 +401,9 @@ def setup_python( *constraint_flags(dependency_constraint), env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) @@ -539,6 +542,9 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=env, ) + case "pyodide-build": + msg = "The 'pyodide-build' build frontend is not supported on this platform" + raise errors.FatalError(msg) case _: assert_never(build_frontend) diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 5795aa2b..216461f8 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -1203,7 +1203,40 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "default": "pyodide-build", + "description": "On the pyodide platform, the build frontend must be \"pyodide-build\"", + "oneOf": [ + { + "enum": [ + "pyodide-build" + ] + }, + { + "type": "string", + "pattern": "^pyodide-build; ?args:" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "pyodide-build" + ] + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ], + "title": "CIBW_BUILD_FRONTEND" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index 0a46e945..4024697a 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -79,3 +79,4 @@ repair-wheel-command = "auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {whe [tool.cibuildwheel.pyodide] audit-command = "" +build-frontend = "pyodide-build" diff --git a/docs/options.md b/docs/options.md index 36d8fc55..fd4d18ce 100644 --- a/docs/options.md +++ b/docs/options.md @@ -481,7 +481,11 @@ files (from workspaces) are not supported. are building GraalPy wheels, use `build` or `pip` as the build frontend instead of `build[uv]` or `uv`. -On Android and Pyodide, the "pip" frontend is not supported. +On Android, the "pip" frontend is not supported. + +On Pyodide, `build-frontend` must be `pyodide-build`, which is the default for that +platform. See [Pyodide build frontend support](platforms.md#pyodide-build-frontend) for +details. You can specify extra arguments to pass to the build frontend using the optional `args` option. @@ -1910,14 +1914,14 @@ will not produce more logging about the build itself. Other levels only affect the build frontend output, which is usually things like resolving and downloading dependencies. The settings are: -| | build | pip | uv | desc | -|-------------|-------|--------|-------|----------------------------------------| -| -2 | `-qq` | `-qq` | `-qq` | even more quiet, where supported | -| -1 | `-q` | `-q` | `-q` | quiet mode, where supported | -| 0 (default) | | | | default for build tool | -| 1 | | `-v` | | print backend output | -| 2 | `-v` | `-vv` | `-v` | print log messages e.g. resolving info | -| 3 | `-vv` | `-vvv` | `-vv` | print even more debug info | +| | build | pip | uv | pyodide-build | desc | +|-------------|-------|--------|-------|---------------|----------------------------------------| +| -2 | `-qq` | `-qq` | `-qq` | | even more quiet, where supported | +| -1 | `-q` | `-q` | `-q` | | quiet mode, where supported | +| 0 (default) | | | | | default for build tool | +| 1 | | `-v` | | `-v` | print backend output | +| 2 | `-v` | `-vv` | `-v` | `-vv` | print log messages e.g. resolving info | +| 3 | `-vv` | `-vvv` | `-vv` | | print even more debug info | Settings that are not supported for a specific frontend will log a warning. The default build frontend is `build`, which does show build backend output by diff --git a/docs/platforms.md b/docs/platforms.md index cfae13d5..035b7ec3 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -188,6 +188,11 @@ repair-wheel-command = "pyodide auditwheel repair --libdir /path/to/libraries -- The `--libdir` option specifies the directory containing cross-compiled shared libraries for WASM. You should not use the system library directories (e.g. `/usr/lib`), as those libraries are not built for WebAssembly. +### Build frontend support {: #pyodide-build-frontend} + +The pyodide platform builds wheels by shelling out to `pyodide build`, via the `pyodide-build` [`build-frontend`](options.md#build-frontend), which itself is a meta build frontend and passes through commands to pypa/build with specialised handling. This is the only supported frontend for this platform, and is used by default. + +[`build-verbosity`](options.md#build-verbosity) is passed through to `pyodide build` as `-v`/`-vv`. It is capped at `-vv`. `pyodide build` has no `-vvv` flag. ## Android {: android} diff --git a/test/conftest.py b/test/conftest.py index 15d2f57f..6a9925e5 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -157,8 +157,8 @@ def build_frontend_env_nouv(request: pytest.FixtureRequest) -> dict[str, str]: marks = {m.name for m in request.node.iter_markers()} platform = "pyodide" if "pyodide" in marks else get_platform() - if platform == "pyodide" and frontend == "pip": - pytest.skip("Can't use pip as build frontend for pyodide platform") + if platform == "pyodide": + pytest.skip("pyodide only supports the pyodide-build frontend") return {"CIBW_BUILD_FRONTEND": frontend} @@ -176,10 +176,10 @@ def build_frontend_env(request: pytest.FixtureRequest) -> Generator[dict[str, st else: platform = get_platform() - if platform in {"pyodide", "ios", "android"} and frontend == "pip": + if platform == "pyodide": + pytest.skip("pyodide only supports the pyodide-build frontend") + if platform in {"ios", "android"} and frontend == "pip": pytest.skip(f"Can't use pip as build frontend for {platform}") - if platform == "pyodide" and frontend in {"build[uv]", "uv"}: - pytest.skip("Can't use uv with pyodide yet") uv_path = find_uv() if uv_path is None and frontend in {"build[uv]", "uv"}: pytest.skip("Can't find uv, so skipping uv tests") diff --git a/test/test_before_build.py b/test/test_before_build.py index 6986591c..ca120ad4 100644 --- a/test/test_before_build.py +++ b/test/test_before_build.py @@ -50,7 +50,7 @@ def test(tmp_path: Path) -> None: """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" + frontend = "pyodide-build" if utils.get_platform() == "pyodide" else "build" if utils.get_platform() != "pyodide": before_build = f"python -m pip install setuptools && {before_build}" frontend = f"{frontend};args: --no-isolation" diff --git a/test/test_build_frontend_args.py b/test/test_build_frontend_args.py index 3c1aa6a2..1e3c4988 100644 --- a/test/test_build_frontend_args.py +++ b/test/test_build_frontend_args.py @@ -15,7 +15,17 @@ if TYPE_CHECKING: "frontend_name", [ pytest.param("pip", marks=utils.skip_if_pyodide("No pip for pyodide")), - "build", + pytest.param( + "build", + marks=utils.skip_if_pyodide("pyodide only supports the pyodide-build frontend"), + ), + pytest.param( + "pyodide-build", + marks=pytest.mark.skipif( + utils.get_platform() != "pyodide", + reason="pyodide-build frontend is only valid on pyodide", + ), + ), ], ) def test_build_frontend_args( @@ -27,7 +37,7 @@ def test_build_frontend_args( # the build will fail because the frontend is called with '-h' - it prints the help message add_env = {"CIBW_BUILD_FRONTEND": f"{frontend_name}; args: -h"} - if utils.get_platform() == "pyodide": + if frontend_name == "pyodide-build": add_env["TERM"] = "dumb" # disable color / style add_env["NO_COLOR"] = "1" with pytest.raises(subprocess.CalledProcessError): @@ -40,7 +50,7 @@ def test_build_frontend_args( if frontend_name == "pip": assert "Usage:" in captured.out assert "Wheel Options:" in captured.out - elif utils.get_platform() == "pyodide": + elif frontend_name == "pyodide-build": assert "Usage: pyodide build" in captured.out else: assert "usage:" in captured.out diff --git a/unit_test/options_test.py b/unit_test/options_test.py index f5f8d7a0..f8c7b643 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -39,6 +39,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Callable, Sequence + from cibuildwheel.typing import PlatformName + PYPROJECT_1 = """ [tool.cibuildwheel] build = ["cp38-*", "cp313-*"] @@ -480,6 +482,73 @@ def test_build_frontend_option( assert parsed_build_frontend.args == () +def test_pyodide_build_frontend_default(tmp_path: Path) -> None: + args = CommandLineArguments.defaults() + args.package_dir = tmp_path + + tmp_path.joinpath("pyproject.toml").write_text("[tool.cibuildwheel]\n") + + options = Options(platform="pyodide", command_line_arguments=args, env={}) + build_frontend = options.build_options(identifier=None).build_frontend + + assert build_frontend.name == "pyodide-build" + assert build_frontend.args == [] + + +def test_pyodide_build_frontend_args(tmp_path: Path) -> None: + args = CommandLineArguments.defaults() + args.package_dir = tmp_path + + tmp_path.joinpath("pyproject.toml").write_text( + textwrap.dedent( + """\ + [tool.cibuildwheel.pyodide] + build-frontend = {name = "pyodide-build", args = ["--exports=whole_archive"]} + """ + ) + ) + + options = Options(platform="pyodide", command_line_arguments=args, env={}) + build_frontend = options.build_options(identifier=None).build_frontend + + assert build_frontend.name == "pyodide-build" + assert build_frontend.args == ["--exports=whole_archive"] + + +@pytest.mark.parametrize( + ("platform", "build_frontend_str"), + [ + ("pyodide", "pip"), + ("pyodide", "build"), + ("pyodide", "build[uv]"), + ("pyodide", "uv"), + ("linux", "pyodide-build"), + ("macos", "pyodide-build"), + ("windows", "pyodide-build"), + ("android", "pyodide-build"), + ("ios", "pyodide-build"), + ], +) +def test_build_frontend_platform_mismatch( + tmp_path: Path, platform: PlatformName, build_frontend_str: str +) -> None: + args = CommandLineArguments.defaults() + args.package_dir = tmp_path + + tmp_path.joinpath("pyproject.toml").write_text( + textwrap.dedent( + f"""\ + [tool.cibuildwheel] + build-frontend = "{build_frontend_str}" + """ + ) + ) + + options = Options(platform=platform, command_line_arguments=args, env={}) + with pytest.raises(errors.ConfigurationError): + options.build_options(identifier=None) + + def test_override_inherit_environment(tmp_path: Path) -> None: args = CommandLineArguments.defaults() args.package_dir = tmp_path @@ -663,10 +732,15 @@ def test_deprecated_image( ("build", 3, ["-Ca", "-Cb", "-1", "-vv"]), ("build[uv]", 3, ["-Ca", "-Cb", "-1", "-vv"]), ("uv", 3, ["-Ca", "-Cb", "-1", "-vv"]), + ("pyodide-build", -1, ["-Ca", "-Cb", "-1"]), + ("pyodide-build", 0, ["-Ca", "-Cb", "-1"]), + ("pyodide-build", 1, ["-Ca", "-Cb", "-1", "-v"]), + ("pyodide-build", 2, ["-Ca", "-Cb", "-1", "-vv"]), + ("pyodide-build", 3, ["-Ca", "-Cb", "-1", "-vv"]), ], ) def test_get_build_frontend_extra_flags( - frontend: Literal["pip", "build", "build[uv]"], + frontend: Literal["pip", "build", "build[uv]", "uv", "pyodide-build"], verbosity: int, result: list[str], monkeypatch: pytest.MonkeyPatch,