Add pyodide-build as a separate build frontend and update its build verbosity handling (#2609)

This commit is contained in:
Agriya Khetarpal
2026-06-22 00:12:15 +05:30
committed by GitHub
parent 291be6a345
commit 7933323184
16 changed files with 200 additions and 25 deletions
+19
View File
@@ -393,6 +393,25 @@ for os_name, command in [
del oses["linux"]["properties"]["dependency-versions"] 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"]["overrides"] = overrides
schema["properties"] |= oses schema["properties"] |= oses
+6 -1
View File
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
from cibuildwheel.typing import PathOrStr 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) @dataclasses.dataclass(frozen=True)
@@ -46,6 +46,11 @@ class BuildFrontendConfig:
def _get_verbosity_flags(level: int, frontend: BuildFrontendName) -> list[str]: 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: if level < 0:
return ["-" + -level * "q"] return ["-" + -level * "q"]
+7
View File
@@ -860,6 +860,13 @@ class Options:
msg = f"Failed to parse build frontend. {e}" msg = f"Failed to parse build frontend. {e}"
raise errors.ConfigurationError(msg) from 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: try:
environment = parse_environment(environment_config) environment = parse_environment(environment_config)
except (EnvironmentParseError, ValueError) as e: except (EnvironmentParseError, ValueError) as e:
+6
View File
@@ -463,6 +463,9 @@ def setup_python(
*constraint_flags(dependency_constraint), *constraint_flags(dependency_constraint),
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
@@ -588,6 +591,9 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
+3
View File
@@ -360,6 +360,9 @@ def build_in_container(
], ],
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
+6
View File
@@ -449,6 +449,9 @@ def setup_python(
*constraint_flags(dependency_constraint), *constraint_flags(dependency_constraint),
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
@@ -585,6 +588,9 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=build_env, env=build_env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
-4
View File
@@ -426,10 +426,6 @@ def build(options: Options, tmp_path: Path) -> None:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend 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) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
+6
View File
@@ -401,6 +401,9 @@ def setup_python(
*constraint_flags(dependency_constraint), *constraint_flags(dependency_constraint),
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
@@ -539,6 +542,9 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=env, env=env,
) )
case "pyodide-build":
msg = "The 'pyodide-build' build frontend is not supported on this platform"
raise errors.FatalError(msg)
case _: case _:
assert_never(build_frontend) assert_never(build_frontend)
@@ -1203,7 +1203,40 @@
"$ref": "#/properties/before-test" "$ref": "#/properties/before-test"
}, },
"build-frontend": { "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": { "build-verbosity": {
"$ref": "#/properties/build-verbosity" "$ref": "#/properties/build-verbosity"
+1
View File
@@ -79,3 +79,4 @@ repair-wheel-command = "auditwheel repair --ldpaths {ldpaths} -w {dest_dir} {whe
[tool.cibuildwheel.pyodide] [tool.cibuildwheel.pyodide]
audit-command = "" audit-command = ""
build-frontend = "pyodide-build"
+13 -9
View File
@@ -481,7 +481,11 @@ files (from workspaces) are not supported.
are building GraalPy wheels, use `build` or `pip` as the build frontend are building GraalPy wheels, use `build` or `pip` as the build frontend
instead of `build[uv]` or `uv`. 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 You can specify extra arguments to pass to the build frontend using the
optional `args` option. 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 the build frontend output, which is usually things like resolving and
downloading dependencies. The settings are: downloading dependencies. The settings are:
| | build | pip | uv | desc | | | build | pip | uv | pyodide-build | desc |
|-------------|-------|--------|-------|----------------------------------------| |-------------|-------|--------|-------|---------------|----------------------------------------|
| -2 | `-qq` | `-qq` | `-qq` | even more quiet, where supported | | -2 | `-qq` | `-qq` | `-qq` | | even more quiet, where supported |
| -1 | `-q` | `-q` | `-q` | quiet mode, where supported | | -1 | `-q` | `-q` | `-q` | | quiet mode, where supported |
| 0 (default) | | | | default for build tool | | 0 (default) | | | | | default for build tool |
| 1 | | `-v` | | print backend output | | 1 | | `-v` | | `-v` | print backend output |
| 2 | `-v` | `-vv` | `-v` | print log messages e.g. resolving info | | 2 | `-v` | `-vv` | `-v` | `-vv` | print log messages e.g. resolving info |
| 3 | `-vv` | `-vvv` | `-vv` | print even more debug info | | 3 | `-vv` | `-vvv` | `-vv` | | print even more debug info |
Settings that are not supported for a specific frontend will log a warning. 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 The default build frontend is `build`, which does show build backend output by
+5
View File
@@ -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. 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} ## Android {: android}
+5 -5
View File
@@ -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()} marks = {m.name for m in request.node.iter_markers()}
platform = "pyodide" if "pyodide" in marks else get_platform() platform = "pyodide" if "pyodide" in marks else get_platform()
if platform == "pyodide" and frontend == "pip": if platform == "pyodide":
pytest.skip("Can't use pip as build frontend for pyodide platform") pytest.skip("pyodide only supports the pyodide-build frontend")
return {"CIBW_BUILD_FRONTEND": frontend} return {"CIBW_BUILD_FRONTEND": frontend}
@@ -176,10 +176,10 @@ def build_frontend_env(request: pytest.FixtureRequest) -> Generator[dict[str, st
else: else:
platform = get_platform() 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}") 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() uv_path = find_uv()
if uv_path is None and frontend in {"build[uv]", "uv"}: if uv_path is None and frontend in {"build[uv]", "uv"}:
pytest.skip("Can't find uv, so skipping uv tests") pytest.skip("Can't find uv, so skipping uv tests")
+1 -1
View File
@@ -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)" && """ """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})"''' 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": if utils.get_platform() != "pyodide":
before_build = f"python -m pip install setuptools && {before_build}" before_build = f"python -m pip install setuptools && {before_build}"
frontend = f"{frontend};args: --no-isolation" frontend = f"{frontend};args: --no-isolation"
+13 -3
View File
@@ -15,7 +15,17 @@ if TYPE_CHECKING:
"frontend_name", "frontend_name",
[ [
pytest.param("pip", marks=utils.skip_if_pyodide("No pip for pyodide")), 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( 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 # 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"} 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["TERM"] = "dumb" # disable color / style
add_env["NO_COLOR"] = "1" add_env["NO_COLOR"] = "1"
with pytest.raises(subprocess.CalledProcessError): with pytest.raises(subprocess.CalledProcessError):
@@ -40,7 +50,7 @@ def test_build_frontend_args(
if frontend_name == "pip": if frontend_name == "pip":
assert "Usage:" in captured.out assert "Usage:" in captured.out
assert "Wheel Options:" 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 assert "Usage: pyodide build" in captured.out
else: else:
assert "usage:" in captured.out assert "usage:" in captured.out
+75 -1
View File
@@ -39,6 +39,8 @@ TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from cibuildwheel.typing import PlatformName
PYPROJECT_1 = """ PYPROJECT_1 = """
[tool.cibuildwheel] [tool.cibuildwheel]
build = ["cp38-*", "cp313-*"] build = ["cp38-*", "cp313-*"]
@@ -480,6 +482,73 @@ def test_build_frontend_option(
assert parsed_build_frontend.args == () 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: def test_override_inherit_environment(tmp_path: Path) -> None:
args = CommandLineArguments.defaults() args = CommandLineArguments.defaults()
args.package_dir = tmp_path args.package_dir = tmp_path
@@ -663,10 +732,15 @@ def test_deprecated_image(
("build", 3, ["-Ca", "-Cb", "-1", "-vv"]), ("build", 3, ["-Ca", "-Cb", "-1", "-vv"]),
("build[uv]", 3, ["-Ca", "-Cb", "-1", "-vv"]), ("build[uv]", 3, ["-Ca", "-Cb", "-1", "-vv"]),
("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( def test_get_build_frontend_extra_flags(
frontend: Literal["pip", "build", "build[uv]"], frontend: Literal["pip", "build", "build[uv]", "uv", "pyodide-build"],
verbosity: int, verbosity: int,
result: list[str], result: list[str],
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,