Since #2609, the pyodide platform required the 'pyodide-build' frontend, but a global build-frontend setting (TOML or CIBW_BUILD_FRONTEND) overrides the platform default, so previously-working configs like build-frontend = "build" failed with a ConfigurationError. Even "default" failed, since it was mapped to "build" before the check. Resolve "default" to the platform default, and warn and use pyodide-build when another frontend is set on pyodide, matching pre-#2609 behavior where the frontend name only affected verbosity flags. Assisted-by: ClaudeCode:claude-opus-4.8
954 lines
30 KiB
Python
954 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import platform as platform_module
|
|
import textwrap
|
|
import unittest.mock
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
import pytest
|
|
|
|
from cibuildwheel import errors
|
|
from cibuildwheel.bashlex_eval import local_environment_executor
|
|
from cibuildwheel.frontend import (
|
|
BuildFrontendConfig,
|
|
_split_config_settings,
|
|
get_build_frontend_extra_flags,
|
|
parse_config_settings,
|
|
prepare_config_settings,
|
|
)
|
|
from cibuildwheel.logger import Logger
|
|
from cibuildwheel.options import (
|
|
CommandLineArguments,
|
|
Options,
|
|
_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 Callable, Sequence
|
|
|
|
from cibuildwheel.typing import PlatformName
|
|
|
|
PYPROJECT_1 = """
|
|
[tool.cibuildwheel]
|
|
build = ["cp38-*", "cp313-*"]
|
|
skip = ["*musllinux*"]
|
|
archs = ["auto64", "auto32"]
|
|
environment = {FOO="BAR"}
|
|
|
|
test-command = "pyproject"
|
|
test-sources = ["test", "other dir"]
|
|
|
|
manylinux-x86_64-image = "manylinux_2_28"
|
|
|
|
environment-pass = ["EXAMPLE_ENV"]
|
|
|
|
pyodide-version = "0.29.4"
|
|
|
|
[tool.cibuildwheel.macos]
|
|
test-requires = "else"
|
|
|
|
[[tool.cibuildwheel.overrides]]
|
|
select = "cp313-*"
|
|
test-command = "pyproject-override"
|
|
manylinux-x86_64-image = "manylinux_2_34"
|
|
"""
|
|
|
|
|
|
def test_options_1(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
with tmp_path.joinpath("pyproject.toml").open("w") as f:
|
|
f.write(PYPROJECT_1)
|
|
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
|
|
module = ALL_PLATFORM_MODULES["linux"]
|
|
identifiers = get_build_identifiers(
|
|
platform_module=module,
|
|
build_selector=options.globals.build_selector,
|
|
architectures=options.globals.architectures,
|
|
)
|
|
|
|
override_display = """\
|
|
*: pyproject
|
|
cp313-manylinux_x86_64, cp313-manylinux_i686: pyproject-override"""
|
|
print(options.summary(identifiers))
|
|
|
|
assert override_display in options.summary(identifiers)
|
|
|
|
default_build_options = options.build_options(identifier=None)
|
|
|
|
assert default_build_options.environment.as_dictionary(prev_environment={}) == {"FOO": "BAR"}
|
|
|
|
all_pinned_container_images = _get_pinned_container_images()
|
|
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
|
|
|
|
local = options.build_options("cp38-manylinux_x86_64")
|
|
assert local.manylinux_images is not None
|
|
assert local.test_command == "pyproject"
|
|
assert local.test_sources == ["test", "other dir"]
|
|
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux_2_28"]
|
|
|
|
local = options.build_options("cp313-manylinux_x86_64")
|
|
assert local.manylinux_images is not None
|
|
assert local.test_command == "pyproject-override"
|
|
assert local.test_sources == ["test", "other dir"]
|
|
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux_2_34"]
|
|
|
|
local = options.build_options("cp312-pyodide_wasm32")
|
|
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"
|
|
pyproject_toml.write_text(
|
|
"""
|
|
[tool.cibuildwheel]
|
|
test-requires = [
|
|
"pytest",
|
|
"pyzstd; python_version >= '3.14'",
|
|
"zarr>=3",
|
|
]
|
|
audit-requires = ["abi3audit; python_version >= '3.9'"]
|
|
"""
|
|
)
|
|
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
build_options = options.build_options(identifier=None)
|
|
|
|
assert build_options.test_requires == [
|
|
"pytest",
|
|
"pyzstd; python_version >= '3.14'",
|
|
"zarr>=3",
|
|
]
|
|
assert build_options.audit_requires == ["abi3audit; python_version >= '3.9'"]
|
|
|
|
|
|
def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
with tmp_path.joinpath("pyproject.toml").open("w") as f:
|
|
f.write(PYPROJECT_1)
|
|
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={"EXAMPLE_ENV": "ONE"})
|
|
|
|
default_build_options = options.build_options(identifier=None)
|
|
|
|
assert default_build_options.environment.as_dictionary(prev_environment={}) == {
|
|
"FOO": "BAR",
|
|
"EXAMPLE_ENV": "ONE",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"env_var_value",
|
|
[
|
|
"normal value",
|
|
'"value wrapped in quotes"',
|
|
"an unclosed single-quote: '",
|
|
'an unclosed double-quote: "',
|
|
"string\nwith\ncarriage\nreturns\n",
|
|
"a trailing backslash \\",
|
|
],
|
|
)
|
|
def test_passthrough_evil(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, env_var_value: str
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
|
|
options = Options(
|
|
platform="linux",
|
|
command_line_arguments=args,
|
|
env={"CIBW_ENVIRONMENT_PASS_LINUX": "ENV_VAR", "ENV_VAR": env_var_value},
|
|
)
|
|
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
assert parsed_environment.as_dictionary(prev_environment={}) == {"ENV_VAR": env_var_value}
|
|
|
|
|
|
xfail_env_parse = pytest.mark.xfail(
|
|
raises=errors.ConfigurationError,
|
|
reason="until we can figure out the right way to quote these values",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"env_var_value",
|
|
[
|
|
"normal value",
|
|
pytest.param('"value wrapped in quotes"', marks=[xfail_env_parse]),
|
|
pytest.param('an unclosed double-quote: "', marks=[xfail_env_parse]),
|
|
"string\nwith\ncarriage\nreturns\n",
|
|
pytest.param("a trailing backslash \\", marks=[xfail_env_parse]),
|
|
],
|
|
)
|
|
def test_toml_environment_evil(tmp_path: Path, env_var_value: str) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
tmp_path.joinpath("pyproject.toml").write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel.environment]
|
|
EXAMPLE='''{env_var_value}'''
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
assert parsed_environment.as_dictionary(prev_environment={}) == {"EXAMPLE": env_var_value}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("toml_assignment", "result_value"),
|
|
[
|
|
('TEST_VAR="simple_value"', "simple_value"),
|
|
# spaces
|
|
('TEST_VAR="simple value"', "simple value"),
|
|
# env var
|
|
('TEST_VAR="$PARAM"', "spam"),
|
|
('TEST_VAR="$PARAM $PARAM"', "spam spam"),
|
|
# env var extension
|
|
('TEST_VAR="before:$PARAM:after"', "before:spam:after"),
|
|
# env var extension with spaces
|
|
('TEST_VAR="before $PARAM after"', "before spam after"),
|
|
# literal $ - this test is just for reference, I'm not sure if this
|
|
# syntax will work if we change the TOML quoting behaviour
|
|
(r'TEST_VAR="before\\$after"', "before$after"),
|
|
],
|
|
)
|
|
def test_toml_environment_quoting(tmp_path: Path, toml_assignment: str, result_value: str) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
tmp_path.joinpath("pyproject.toml").write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel.environment]
|
|
{toml_assignment}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
environment_values = parsed_environment.as_dictionary(
|
|
prev_environment={**os.environ, "PARAM": "spam"},
|
|
executor=local_environment_executor,
|
|
)
|
|
|
|
assert environment_values["TEST_VAR"] == result_value
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("toml_assignment", "result_name", "result_create_args", "result_disable_host_mount"),
|
|
[
|
|
(
|
|
'container-engine = "podman"',
|
|
"podman",
|
|
(),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = {name = "podman"}',
|
|
"podman",
|
|
(),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = "docker; create_args: --some-option"',
|
|
"docker",
|
|
("--some-option",),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = {name = "docker", create-args = ["--some-option"]}',
|
|
"docker",
|
|
("--some-option",),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = {name = "docker", create-args = ["--some-option", "value that contains spaces"]}',
|
|
"docker",
|
|
("--some-option", "value that contains spaces"),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = {name = "docker", create-args = ["--some-option", "value;that;contains;semicolons"]}',
|
|
"docker",
|
|
("--some-option", "value;that;contains;semicolons"),
|
|
False,
|
|
),
|
|
(
|
|
'container-engine = {name = "docker", disable-host-mount = true}',
|
|
"docker",
|
|
(),
|
|
True,
|
|
),
|
|
(
|
|
'container-engine = {name = "docker", disable_host_mount = true}',
|
|
"docker",
|
|
(),
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
def test_container_engine_option(
|
|
tmp_path: Path,
|
|
toml_assignment: str,
|
|
result_name: str,
|
|
result_create_args: tuple[str, ...],
|
|
result_disable_host_mount: bool,
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
tmp_path.joinpath("pyproject.toml").write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel]
|
|
{toml_assignment}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
parsed_container_engine = options.build_options(None).container_engine
|
|
|
|
assert parsed_container_engine.name == result_name
|
|
assert parsed_container_engine.create_args == result_create_args
|
|
assert parsed_container_engine.disable_host_mount == result_disable_host_mount
|
|
|
|
|
|
def test_environment_pass_references() -> None:
|
|
options = Options(
|
|
platform="linux",
|
|
command_line_arguments=CommandLineArguments.defaults(),
|
|
env={
|
|
"CIBW_ENVIRONMENT_PASS_LINUX": "STARTER MAIN_COURSE",
|
|
"STARTER": "green eggs",
|
|
"MAIN_COURSE": "ham",
|
|
"CIBW_ENVIRONMENT": 'MEAL="$STARTER and $MAIN_COURSE"',
|
|
},
|
|
)
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
assert parsed_environment.as_dictionary(prev_environment={}) == {
|
|
"MEAL": "green eggs and ham",
|
|
"STARTER": "green eggs",
|
|
"MAIN_COURSE": "ham",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("toml_assignment", "result_name", "result_args"),
|
|
[
|
|
(
|
|
"",
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
'build-frontend = "build"',
|
|
"build",
|
|
[],
|
|
),
|
|
(
|
|
'build-frontend = "uv"',
|
|
"uv",
|
|
[],
|
|
),
|
|
(
|
|
'build-frontend = {name = "build"}',
|
|
"build",
|
|
[],
|
|
),
|
|
(
|
|
'build-frontend = "pip; args: --some-option"',
|
|
"pip",
|
|
["--some-option"],
|
|
),
|
|
(
|
|
'build-frontend = {name = "pip", args = ["--some-option"]}',
|
|
"pip",
|
|
["--some-option"],
|
|
),
|
|
],
|
|
)
|
|
def test_build_frontend_option(
|
|
tmp_path: Path, toml_assignment: str, result_name: str, result_args: list[str]
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
tmp_path.joinpath("pyproject.toml").write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel]
|
|
{toml_assignment}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
parsed_build_frontend = options.build_options(identifier=None).build_frontend
|
|
|
|
if toml_assignment:
|
|
assert parsed_build_frontend.name == result_name
|
|
assert parsed_build_frontend.args == result_args
|
|
else:
|
|
assert parsed_build_frontend.name == "build"
|
|
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(
|
|
"build_frontend_str",
|
|
["default", "pip", "build", "build[uv]", "uv"],
|
|
)
|
|
def test_pyodide_global_build_frontend_coerced(tmp_path: Path, build_frontend_str: str) -> None:
|
|
"""A global non-pyodide frontend setting is coerced to pyodide-build, not an error."""
|
|
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="pyodide", command_line_arguments=args, env={})
|
|
build_frontend = options.build_options(identifier=None).build_frontend
|
|
|
|
assert build_frontend.name == "pyodide-build"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("platform", "build_frontend_str"),
|
|
[
|
|
("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
|
|
|
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
|
pyproject_toml.write_text(
|
|
textwrap.dedent(
|
|
"""\
|
|
[tool.cibuildwheel]
|
|
environment = {FOO="BAR", "HAM"="EGGS"}
|
|
|
|
[[tool.cibuildwheel.overrides]]
|
|
select = "cp37*"
|
|
inherit.environment = "append"
|
|
environment = {FOO="BAZ", "PYTHON"="MONTY"}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
assert parsed_environment.as_dictionary(prev_environment={}) == {
|
|
"FOO": "BAR",
|
|
"HAM": "EGGS",
|
|
}
|
|
|
|
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
|
|
prev_environment={}
|
|
) == {
|
|
"FOO": "BAZ",
|
|
"HAM": "EGGS",
|
|
"PYTHON": "MONTY",
|
|
}
|
|
|
|
|
|
def test_override_inherit_environment_with_references(tmp_path: Path) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
|
pyproject_toml.write_text(
|
|
textwrap.dedent(
|
|
"""\
|
|
[tool.cibuildwheel]
|
|
environment = {PATH="/opt/bin:$PATH"}
|
|
|
|
[[tool.cibuildwheel.overrides]]
|
|
select = "cp37*"
|
|
inherit.environment = "append"
|
|
environment = {PATH="/opt/local/bin:$PATH"}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="linux", command_line_arguments=args, env={"MONTY": "PYTHON"})
|
|
parsed_environment = options.build_options(identifier=None).environment
|
|
prev_environment = {"PATH": "/usr/bin:/bin"}
|
|
assert parsed_environment.as_dictionary(prev_environment=prev_environment) == {
|
|
"PATH": "/opt/bin:/usr/bin:/bin",
|
|
}
|
|
|
|
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
|
|
prev_environment=prev_environment
|
|
) == {
|
|
"PATH": "/opt/local/bin:/opt/bin:/usr/bin:/bin",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("toml_assignment", "base_file_path", "packages"),
|
|
[
|
|
("", resources.CONSTRAINTS, []),
|
|
("dependency-versions = 'pinned'", resources.CONSTRAINTS, []),
|
|
("dependency-versions = 'latest'", None, []),
|
|
("dependency-versions = 'constraints file.txt'", Path("constraints file.txt"), []),
|
|
(
|
|
"dependency-versions = \"file:'constraints file.txt'\"",
|
|
Path("constraints file.txt"),
|
|
[],
|
|
),
|
|
(
|
|
"dependency-versions = {file = 'constraints file.txt'}",
|
|
Path("constraints file.txt"),
|
|
[],
|
|
),
|
|
(
|
|
"dependency-versions = 'packages: foo==1.2.3 bar==4.5.6'",
|
|
None,
|
|
["foo==1.2.3", "bar==4.5.6"],
|
|
),
|
|
],
|
|
)
|
|
def test_dependency_versions_toml(
|
|
tmp_path: Path,
|
|
toml_assignment: str,
|
|
base_file_path: Path | None,
|
|
packages: list[str] | None,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
(tmp_path / "constraints file.txt").write_text("")
|
|
monkeypatch.chdir(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={})
|
|
parsed_dependency_constraints = options.build_options(None).dependency_constraints
|
|
if base_file_path is None and packages is None:
|
|
assert parsed_dependency_constraints == DependencyConstraints.latest()
|
|
else:
|
|
if parsed_dependency_constraints.base_file_path and base_file_path:
|
|
assert parsed_dependency_constraints.base_file_path.samefile(base_file_path)
|
|
else:
|
|
assert parsed_dependency_constraints.base_file_path == base_file_path
|
|
assert parsed_dependency_constraints.packages == packages
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("image", "deprecated", "raises"),
|
|
[
|
|
("manylinux1", True, True),
|
|
("manylinux2010", True, True),
|
|
("manylinux2014", False, False),
|
|
("manylinux_2_24", True, True),
|
|
("manylinux_2_28", False, False),
|
|
("manylinux_2_34", False, False),
|
|
("musllinux_1_1", True, True),
|
|
("musllinux_1_2", False, False),
|
|
],
|
|
)
|
|
def test_deprecated_image(
|
|
image: str, deprecated: bool, raises: bool, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
assert not raises or deprecated
|
|
args = CommandLineArguments.defaults()
|
|
env = {
|
|
"CIBW_ARCHS": "x86_64",
|
|
"CIBW_MANYLINUX_X86_64_IMAGE": image if image.startswith("manylinux") else "",
|
|
"CIBW_MUSLLINUX_X86_64_IMAGE": image if image.startswith("musllinux") else "",
|
|
}
|
|
options = Options(platform="linux", command_line_arguments=args, env=env)
|
|
try:
|
|
bo = options.build_options(None)
|
|
assert not raises
|
|
except errors.DeprecationError:
|
|
assert raises
|
|
return
|
|
images = bo.manylinux_images if image.startswith("manylinux") else bo.musllinux_images
|
|
assert images is not None
|
|
resolved_image = images["x86_64"]
|
|
captured = capsys.readouterr()
|
|
if deprecated:
|
|
assert f"Deprecated image {image!r}" in captured.err
|
|
assert f"{resolved_image!r}" in captured.err
|
|
else:
|
|
assert "Deprecated image" not in captured.err
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("frontend", "verbosity", "result"),
|
|
[
|
|
("pip", 3, ["-Ca", "-Cb", "-1", "-vvv"]),
|
|
("pip", 2, ["-Ca", "-Cb", "-1", "-vv"]),
|
|
("pip", -1, ["-Ca", "-Cb", "-1", "-q"]),
|
|
("uv", -1, ["-Ca", "-Cb", "-1", "-q"]),
|
|
("build", -1, ["-Ca", "-Cb", "-1", "-q"]),
|
|
("build[uv]", -1, ["-Ca", "-Cb", "-1", "-q"]),
|
|
("build", 0, ["-Ca", "-Cb", "-1"]),
|
|
("build", 1, ["-Ca", "-Cb", "-1"]),
|
|
("build", 2, ["-Ca", "-Cb", "-1", "-v"]),
|
|
("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]", "uv", "pyodide-build"],
|
|
verbosity: int,
|
|
result: list[str],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
mock_warning = unittest.mock.MagicMock()
|
|
monkeypatch.setattr(Logger, "warning", mock_warning)
|
|
build_frontend = BuildFrontendConfig(frontend, ["-1"])
|
|
args = get_build_frontend_extra_flags(
|
|
build_frontend=build_frontend, verbosity_level=verbosity, config_settings="a b"
|
|
)
|
|
|
|
assert args == result
|
|
mock_warning.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config_settings", "expected"),
|
|
[
|
|
(
|
|
"setup-args=--cross-file={project}/meson_cross_files/windows-386.ini",
|
|
"setup-args=--cross-file=C:/project/meson_cross_files/windows-386.ini",
|
|
),
|
|
(
|
|
"setup-args=--cross-file={package}/meson_cross_files/windows-386.ini",
|
|
"setup-args=--cross-file=C:/project/pkg/meson_cross_files/windows-386.ini",
|
|
),
|
|
],
|
|
)
|
|
def test_prepare_config_settings(config_settings: str, expected: str) -> None:
|
|
assert (
|
|
prepare_config_settings(config_settings, project="C:/project", package="C:/project/pkg")
|
|
== expected
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("project", "package"),
|
|
[
|
|
(R"C:\Users\John Doe\project", R"C:\Users\John Doe\project\pkg"),
|
|
("/home/me/my project", "/home/me/my project/pkg"),
|
|
],
|
|
)
|
|
def test_prepare_config_settings_special_chars(project: str, package: str) -> None:
|
|
# Paths with spaces or backslashes must survive the round-trip through the
|
|
# downstream shlex.split intact (regression test for the {package}/{project}
|
|
# substitution being mangled by re-parsing).
|
|
config_settings = "setup-args=--cross-file={package}/cross.ini other-setting={project}"
|
|
prepared = prepare_config_settings(config_settings, project=project, package=package)
|
|
assert _split_config_settings(prepared) == [
|
|
f"-Csetup-args=--cross-file={package}/cross.ini",
|
|
f"-Cother-setting={project}",
|
|
]
|
|
assert parse_config_settings(prepared) == {
|
|
"setup-args": f"--cross-file={package}/cross.ini",
|
|
"other-setting": project,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("definition", "expected_args"),
|
|
[
|
|
("", ()),
|
|
('test-runtime = ""', ()),
|
|
("test-runtime = {}", ()),
|
|
('test-runtime = {args = ""}', []),
|
|
('test-runtime = "args: --simulator foo"', ["--simulator", "foo"]),
|
|
('test-runtime = {args = ["--simulator", "foo"]}', ["--simulator", "foo"]),
|
|
],
|
|
)
|
|
def test_test_runtime_handling(
|
|
tmp_path: Path, definition: str, expected_args: Sequence[str] | None
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
|
pyproject_toml.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel]
|
|
{definition}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="ios", command_line_arguments=args, env={})
|
|
|
|
local = options.build_options("cp313-ios_13_0_arm64_iphoneos")
|
|
assert local.test_runtime.args == expected_args
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("platform", "platform_envvar"),
|
|
[
|
|
("android", "CIBW_TEST_RUNTIME_ANDROID"),
|
|
("ios", "CIBW_TEST_RUNTIME_IOS"),
|
|
],
|
|
)
|
|
def test_test_runtime_platform_environment(
|
|
tmp_path: Path, platform: PlatformName, platform_envvar: str
|
|
) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
options = Options(
|
|
platform=platform,
|
|
command_line_arguments=args,
|
|
env={
|
|
"CIBW_TEST_RUNTIME": "args: --global",
|
|
platform_envvar: "args: --platform-specific",
|
|
},
|
|
)
|
|
|
|
assert options.build_options(None).test_runtime.args == ["--platform-specific"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("definition", "expected"),
|
|
[
|
|
("", None),
|
|
("xbuild-tools = []", []),
|
|
('xbuild-tools = ["cmake", "rustc"]', ["cmake", "rustc"]),
|
|
],
|
|
)
|
|
def test_xbuild_tools_handling(tmp_path: Path, definition: str, expected: list[str] | None) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
|
pyproject_toml.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel]
|
|
{definition}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="ios", command_line_arguments=args, env={})
|
|
|
|
local = options.build_options("cp313-ios_13_0_arm64_iphoneos")
|
|
assert local.xbuild_tools == expected
|
|
|
|
|
|
DEFAULT_XBUILD_FILES = {
|
|
"numpy": [
|
|
"numpy/_core/include/numpy/_numpyconfig.h",
|
|
"numpy/_core/include/numpy/numpyconfig.h",
|
|
"numpy/_core/lib/libnpymath.a",
|
|
"numpy/random/lib/libnpyrandom.a",
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("definition", "expected"),
|
|
[
|
|
("", DEFAULT_XBUILD_FILES),
|
|
('xbuild-files = ""', {}),
|
|
('xbuild-files = "package1: file1"', {"package1": ["file1"]}),
|
|
(
|
|
"xbuild-files = \"package1: 'file1 with space'; package2: file2a file2b\"",
|
|
{"package1": ["file1 with space"], "package2": ["file2a", "file2b"]},
|
|
),
|
|
],
|
|
)
|
|
def test_xbuild_files(tmp_path: Path, definition: str, expected: dict[str, list[str]]) -> None:
|
|
args = CommandLineArguments.defaults()
|
|
args.package_dir = tmp_path
|
|
|
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
|
pyproject_toml.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
[tool.cibuildwheel]
|
|
{definition}
|
|
"""
|
|
)
|
|
)
|
|
|
|
options = Options(platform="ios", command_line_arguments=args, env={})
|
|
assert options.build_options(None).xbuild_files == expected
|
|
|
|
pyproject_toml.write_text(
|
|
"[tool.cibuildwheel.xbuild-files]\n"
|
|
+ "\n".join(f"{package} = {files}" for package, files in expected.items())
|
|
)
|
|
|
|
options = Options(platform="ios", command_line_arguments=args, env={})
|
|
assert options.build_options(None).xbuild_files == expected
|