diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index 444c55a4..2466d59d 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -218,6 +218,36 @@ def validate_pyodide_build_version( raise errors.FatalError(msg) +def validate_pyodide_target_python( + xbuildenv_info: PyodideXBuildEnvInfo, python_configuration: PythonConfiguration +) -> None: + """ + Validate that the resolved Pyodide xbuildenv targets the same Python version + as the identifier being built. + + This is to catch the case where a global ``pyodide-version`` is applied to an + identifier whose Python version does not match, such as setting + ``pyodide-version = "3Y.0.0"`` (a Python 3.Y environment) while + building ``cp3X-pyodide_wasm32``. + """ + xbuildenv_version = xbuildenv_info["version"] + identifier = python_configuration.identifier + expected_version = python_configuration.version + default_version = python_configuration.default_pyodide_version + xbuildenv_python_minor = ".".join(xbuildenv_info["python"].split(".")[:2]) + if xbuildenv_python_minor != expected_version: + msg = unwrap_preserving_paragraphs(f""" + The `pyodide-version` option is set to {xbuildenv_version}, which + provides Python {xbuildenv_python_minor}, but the {identifier} build + needs Python {expected_version}. + + Either remove `pyodide-version` so {identifier} uses its default + ({default_version}), or stop building {identifier} so that + `pyodide-version` only applies to an identifier it matches. + """) + raise errors.ConfigurationError(msg) + + def install_xbuildenv(env: dict[str, str], xbuildenv_cache_path: Path, pyodide_version: str) -> str: """Install a particular Pyodide xbuildenv version and set a path to the Pyodide root.""" pyodide_root = xbuildenv_cache_path / pyodide_version / "xbuildenv" / "pyodide-root" @@ -337,6 +367,10 @@ def setup_python( xbuildenv_info=xbuildenv_info, pyodide_build_version=pyodide_build_version, ) + validate_pyodide_target_python( + xbuildenv_info=xbuildenv_info, + python_configuration=python_configuration, + ) xbuildenv_cache_path = CIBW_CACHE_PATH / f"pyodide-build-{pyodide_build_version}" diff --git a/unit_test/options_test.py b/unit_test/options_test.py index aafeb161..f5f8d7a0 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -25,12 +25,19 @@ from cibuildwheel.options import ( _get_pinned_container_images, ) from cibuildwheel.platforms import ALL_PLATFORM_MODULES, get_build_identifiers +from cibuildwheel.platforms.pyodide import ( + PyodideXBuildEnvInfo, + validate_pyodide_target_python, +) +from cibuildwheel.platforms.pyodide import ( + PythonConfiguration as PyodidePythonConfiguration, +) from cibuildwheel.util import resources from cibuildwheel.util.packaging import DependencyConstraints TYPE_CHECKING = False if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence PYPROJECT_1 = """ [tool.cibuildwheel] @@ -106,6 +113,62 @@ def test_options_1(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: assert local.pyodide_version == "0.29.4" +@pytest.fixture +def make_pyodide_xbuildenv_info() -> Callable[[str, str], PyodideXBuildEnvInfo]: + def _make(version: str, python: str) -> PyodideXBuildEnvInfo: + return { + "version": version, + "python": python, + "emscripten": "5.0.3", + "pyodide_build": {"min": None, "max": None}, + "compatible": True, + } + + return _make + + +@pytest.fixture +def make_pyodide_python_configuration() -> Callable[[str, str], PyodidePythonConfiguration]: + def _make(identifier: str, version: str) -> PyodidePythonConfiguration: + return PyodidePythonConfiguration( + version=version, + identifier=identifier, + default_pyodide_version="0.0.0", + node_version="v22", + ) + + return _make + + +def test_validate_pyodide_target_python_matching( + make_pyodide_xbuildenv_info: Callable[[str, str], PyodideXBuildEnvInfo], + make_pyodide_python_configuration: Callable[[str, str], PyodidePythonConfiguration], +) -> None: + xbuildenv_info = make_pyodide_xbuildenv_info("314.0.0", "3.14.2") + config = make_pyodide_python_configuration("cp314-pyodide_wasm32", "3.14") + # should not raise + validate_pyodide_target_python(xbuildenv_info, config) + + +def test_validate_pyodide_target_python_mismatch( + make_pyodide_xbuildenv_info: Callable[[str, str], PyodideXBuildEnvInfo], + make_pyodide_python_configuration: Callable[[str, str], PyodidePythonConfiguration], +) -> None: + # a Python 3.14 xbuildenv applied to a cp313-pyodide_wasm32 build, when a global + # pyodide-version is set across mismatched targets + xbuildenv_info = make_pyodide_xbuildenv_info("314.0.0", "3.14.2") + config = make_pyodide_python_configuration("cp313-pyodide_wasm32", "3.13") + + with pytest.raises(errors.ConfigurationError) as exc_info: + validate_pyodide_target_python(xbuildenv_info, config) + + message = str(exc_info.value) + assert "cp313-pyodide_wasm32" in message + assert "314.0.0" in message + assert "3.13" in message + assert "3.14" in message + + def test_test_and_audit_requires_with_dependency_specifiers(tmp_path: Path) -> None: """Regression test for https://github.com/pypa/cibuildwheel/issues/2912""" pyproject_toml = tmp_path / "pyproject.toml"