diff --git a/README.md b/README.md
index c70d40f9b..272815885 100644
--- a/README.md
+++ b/README.md
@@ -139,6 +139,7 @@ Options
| | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.pypa.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. |
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.pypa.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
+| | [`CIBW_XBUILD_TOOLS`](https://cibuildwheel.pypa.io/en/stable/options/#xbuild-tools) | Binaries on the path that should be included in an isolated cross-build environment. |
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
| | [`CIBW_MANYLINUX_*_IMAGE`
`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
diff --git a/bin/generate_schema.py b/bin/generate_schema.py
index b82a91136..8ca6a1a76 100755
--- a/bin/generate_schema.py
+++ b/bin/generate_schema.py
@@ -181,9 +181,12 @@
musllinux-x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
- repair-wheel-command:
+ xbuild-tools:
+ description: Binaries on the path that should be included in an isolated cross-build environment
type: string_array
+ repair-wheel-command:
description: Execute a shell command to repair each built wheel.
+ type: string_array
skip:
description: Choose the Python versions to skip.
type: string_array
@@ -273,6 +276,7 @@
properties:
before-all: {"$ref": "#/$defs/inherit"}
before-build: {"$ref": "#/$defs/inherit"}
+ xbuild-tools: {"$ref": "#/$defs/inherit"}
before-test: {"$ref": "#/$defs/inherit"}
config-settings: {"$ref": "#/$defs/inherit"}
container-engine: {"$ref": "#/$defs/inherit"}
diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py
index 78cbaf519..1e07a3f97 100644
--- a/cibuildwheel/options.py
+++ b/cibuildwheel/options.py
@@ -94,6 +94,7 @@ class BuildOptions:
environment: ParsedEnvironment
before_all: str
before_build: str | None
+ xbuild_tools: list[str] | None
repair_command: str
manylinux_images: dict[str, str] | None
musllinux_images: dict[str, str] | None
@@ -718,6 +719,18 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions:
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
+ xbuild_tools: list[str] | None = shlex.split(
+ self.reader.get(
+ "xbuild-tools", option_format=ListFormat(sep=" ", quote=shlex.quote)
+ )
+ )
+ # ["\u0000"] is a sentinel value used as a default, because TOML
+ # doesn't have an explicit NULL value. If xbuild-tools is set to the
+ # sentinel, it indicates that the user hasn't defined xbuild-tools
+ # *at all* (not even an `xbuild-tools = []` definition).
+ if xbuild_tools == ["\u0000"]:
+ xbuild_tools = None
+
test_sources = shlex.split(
self.reader.get(
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
@@ -835,6 +848,7 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions:
before_build=before_build,
before_all=before_all,
build_verbosity=build_verbosity,
+ xbuild_tools=xbuild_tools,
repair_command=repair_command,
environment=environment,
dependency_constraints=dependency_constraints,
diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py
index 25dc2a461..06fce61fa 100644
--- a/cibuildwheel/platforms/ios.py
+++ b/cibuildwheel/platforms/ios.py
@@ -5,6 +5,7 @@
import shutil
import subprocess
import sys
+import textwrap
from collections.abc import Sequence, Set
from dataclasses import dataclass
from pathlib import Path
@@ -151,6 +152,7 @@ def cross_virtualenv(
build_python: Path,
venv_path: Path,
dependency_constraint_flags: Sequence[PathOrStr],
+ xbuild_tools: Sequence[str] | None,
) -> dict[str, str]:
"""Create a cross-compilation virtual environment.
@@ -178,6 +180,8 @@ def cross_virtualenv(
created.
:param dependency_constraint_flags: Any flags that should be used when
constraining dependencies in the environment.
+ :param xbuild_tools: A list of executable names (without paths) that are
+ on the path, but must be preserved in the cross environment.
"""
# Create an initial macOS virtual environment
env = virtualenv(
@@ -210,14 +214,52 @@ def cross_virtualenv(
#
# To prevent problems, set the PATH to isolate the build environment from
# sources that could introduce incompatible binaries.
+ #
+ # However, there may be some tools on the path that are needed for the
+ # build. Find their location on the path, and link the underlying binaries
+ # (fully resolving symlinks) to a "safe" location that will *only* contain
+ # those tools. This avoids needing to add *all* of Homebrew to the path just
+ # to get access to (for example) cmake for build purposes. A value of None
+ # means the user hasn't provided a list of xbuild tools.
+ xbuild_tools_path = venv_path / "cibw_xbuild_tools"
+ xbuild_tools_path.mkdir()
+ if xbuild_tools is None:
+ log.warning(
+ textwrap.dedent(
+ """
+ Your project configuration does not define any cross-build tools.
+
+ iOS builds use an isolated build environment; if your build process requires any
+ third-party tools (such as cmake, ninja, or rustc), you must explicitly declare
+ that those tools are required using xbuild-tools/CIBW_XBUILD_TOOLS. This will
+ likely manifest as a "somebuildtool: command not found" error.
+
+ If the build succeeds, you can silence this warning by setting adding
+ `xbuild-tools = []` to your pyproject.toml configuration, or exporting
+ CIBW_XBUILD_TOOLS as an empty string into your environment.
+ """
+ )
+ )
+ else:
+ for tool in xbuild_tools:
+ tool_path = shutil.which(tool)
+ if tool_path is None:
+ msg = f"Could not find a {tool!r} executable on the path."
+ raise errors.FatalError(msg)
+
+ # Link the binary into the safe tools directory
+ original = Path(tool_path).resolve()
+ print(f"{tool!r} will be included in the cross-build environment (using {original})")
+ (xbuild_tools_path / tool).symlink_to(original)
+
env["PATH"] = os.pathsep.join(
[
# The target python's binary directory
str(target_python.parent),
- # The cross-platform environments binary directory
+ # The cross-platform environment's binary directory
str(venv_path / "bin"),
- # Cargo's binary directory (to allow for Rust compilation)
- str(Path.home() / ".cargo" / "bin"),
+ # The directory of cross-build tools
+ str(xbuild_tools_path),
# The bare minimum Apple system paths.
"/usr/bin",
"/bin",
@@ -235,10 +277,12 @@ def cross_virtualenv(
def setup_python(
tmp: Path,
+ *,
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontendName,
+ xbuild_tools: Sequence[str] | None,
) -> tuple[Path, dict[str, str]]:
if build_frontend == "build[uv]":
msg = "uv doesn't support iOS"
@@ -291,6 +335,7 @@ def setup_python(
build_python=build_python,
venv_path=venv_path,
dependency_constraint_flags=dependency_constraint_flags,
+ xbuild_tools=xbuild_tools,
)
venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists()
@@ -414,10 +459,11 @@ def build(options: Options, tmp_path: Path) -> None:
target_install_path, env = setup_python(
identifier_tmp_dir / "build",
- config,
- dependency_constraint_flags,
- build_options.environment,
- build_frontend.name,
+ python_configuration=config,
+ dependency_constraint_flags=dependency_constraint_flags,
+ environment=build_options.environment,
+ build_frontend=build_frontend.name,
+ xbuild_tools=build_options.xbuild_tools,
)
pip_version = get_pip_version(env)
diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json
index 4a84606cf..10e07cc15 100644
--- a/cibuildwheel/resources/cibuildwheel.schema.json
+++ b/cibuildwheel/resources/cibuildwheel.schema.json
@@ -397,6 +397,21 @@
"description": "Specify alternative manylinux / musllinux container images",
"title": "CIBW_MUSLLINUX_X86_64_IMAGE"
},
+ "xbuild-tools": {
+ "description": "Binaries on the path that should be included in an isolated cross-build environment",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "title": "CIBW_XBUILD_TOOLS"
+ },
"repair-wheel-command": {
"description": "Execute a shell command to repair each built wheel.",
"oneOf": [
@@ -566,6 +581,9 @@
"environment-pass": {
"$ref": "#/$defs/inherit"
},
+ "xbuild-tools": {
+ "$ref": "#/$defs/inherit"
+ },
"repair-wheel-command": {
"$ref": "#/$defs/inherit"
},
@@ -991,6 +1009,9 @@
"repair-wheel-command": {
"$ref": "#/properties/repair-wheel-command"
},
+ "xbuild-tools": {
+ "$ref": "#/properties/xbuild-tools"
+ },
"test-command": {
"$ref": "#/properties/test-command"
},
diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml
index bd17245b4..8ca424556 100644
--- a/cibuildwheel/resources/defaults.toml
+++ b/cibuildwheel/resources/defaults.toml
@@ -14,6 +14,8 @@ build-verbosity = 0
before-all = ""
before-build = ""
+# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
+xbuild-tools = ["\u0000"]
repair-wheel-command = ""
test-command = ""
diff --git a/docs/options.md b/docs/options.md
index fe2adad84..cee3b6d7c 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -1043,6 +1043,49 @@ Platform-specific environment variables are also available:
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
[PEP 518]: https://www.python.org/dev/peps/pep-0517/
+### `CIBW_XBUILD_TOOLS` {: #xbuild-tools}
+> Binaries on the path that should be included in an isolated cross-build environment.
+
+When building in a cross-platform environment, it is sometimes necessary to isolate the ``PATH`` so that binaries from the build machine don't accidentally get linked into the cross-platform binary. However, this isolation process will also hide tools that might be required to build your wheel.
+
+If there are binaries present on the `PATH` when you invoke cibuildwheel, and those binaries are required to build your wheels, those binaries can be explicitly included in the isolated cross-build environment using `CIBW_XBUILD_TOOLS`. The binaries listed in this setting will be linked into an isolated location, and that isolated location will be put on the `PATH` of the isolated environment. You do not need to provide the full path to the binary - only the executable name that would be found by the shell.
+
+If you declare a tool as a cross-build tool, and that tool cannot be found in the runtime environment, an error will be raised.
+
+If you do not define `CIBW_XBUILD_TOOLS`, and you build for a platform that uses a cross-platform environment, a warning will be raised. If your project does not require any cross-build tools, you can set `CIBW_XBUILD_TOOLS` to an empty list to silence this warning.
+
+*Any* tool used by the build process must be included in the `CIBW_XBUILD_TOOLS` list, not just tools that cibuildwheel will invoke directly. For example, if your build invokes `cmake`, and the `cmake` script invokes `magick` to perform some image transformations, both `cmake` and `magick` must be included in your safe tools list.
+
+Platform-specific environment variables are also available on platforms that use cross-platform environment isolation:
+ `CIBW_XBUILD_TOOLS_IOS`
+
+#### Examples
+
+!!! tab examples "Environment variables"
+
+ ```yaml
+ # Allow access to the cmake and rustc binaries in the isolated cross-build environment.
+ CIBW_XBUILD_TOOLS: cmake rustc
+ ```
+
+ ```yaml
+ # No cross-build tools are required
+ CIBW_XBUILD_TOOLS:
+ ```
+
+!!! tab examples "pyproject.toml"
+
+ ```toml
+ [tool.cibuildwheel]
+ # Allow access to the cmake and rustc binaries in the isolated cross-build environment.
+ xbuild-tools = ["cmake", "rustc"]
+ ```
+
+ ```toml
+ [tool.cibuildwheel]
+ # No cross-build tools are required
+ xbuild-tools = []
+ ```
### `CIBW_REPAIR_WHEEL_COMMAND` {: #repair-wheel-command}
> Execute a shell command to repair each built wheel
diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md
index a58c0132b..0c93ba052 100644
--- a/docs/platforms/ios.md
+++ b/docs/platforms/ios.md
@@ -57,7 +57,9 @@ iOS builds support both the `pip` and `build` build frontends. In principle, sup
## Build environment
-The environment used to run builds does not inherit the full user environment - in particular, `PATH` is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it unusable on iOS. To prevent this, iOS builds always force `PATH` to a "known minimal" path, that includes only the bare system utilities, plus the current user's cargo folder (to facilitate Rust builds).
+The environment used to run builds does not inherit the full user environment - in particular, `PATH` is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it unusable on iOS. To prevent this, iOS builds always force `PATH` to a "known minimal" path, that includes only the bare system utilities, and the iOS compiler toolchain.
+
+If your project requires additional tools to build (such as `cmake`, `ninja`, or `rustc`), those tools must be explicitly declared as cross-build tools using [`CIBW_XBUILD_TOOLS`](../../options#xbuild-tools). *Any* tool used by the build process must be included in the `CIBW_XBUILD_TOOLS` list, not just tools that cibuildwheel will invoke directly. For example, if your build script invokes `cmake`, and the `cmake` script invokes `magick` to perform some image transformations, both `cmake` and `magick` must be included in your cross-build tools list.
## Tests
diff --git a/test/test_ios.py b/test/test_ios.py
index ca6bc7ab2..c3cfa61af 100644
--- a/test/test_ios.py
+++ b/test/test_ios.py
@@ -2,14 +2,15 @@
import os
import platform
+import shutil
import subprocess
import pytest
from . import test_projects, utils
-basic_project = test_projects.new_c_project()
-basic_project.files["tests/test_platform.py"] = f"""
+basic_project_files = {
+ "tests/test_platform.py": f"""
import platform
from unittest import TestCase
@@ -18,6 +19,7 @@ def test_platform(self):
self.assertEqual(platform.machine(), "{platform.machine()}")
"""
+}
# iOS tests shouldn't be run in parallel, because they're dependent on calling
@@ -35,19 +37,41 @@ def test_platform(self):
{"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"},
],
)
-def test_ios_platforms(tmp_path, build_config):
+def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
if utils.platform != "macos":
pytest.skip("this test can only run on macOS")
if utils.get_xcode_version() < (13, 0):
pytest.skip("this test only works with Xcode 13.0 or greater")
+ # Create a temporary "bin" directory, symlink a tool that we know eixsts
+ # (/usr/bin/true) into that location under a name that should be unique,
+ # and add the temp bin directory to the PATH.
+ tools_dir = tmp_path / "bin"
+ tools_dir.mkdir()
+ tools_dir.joinpath("does-exist").symlink_to(shutil.which("true"))
+
+ monkeypatch.setenv("PATH", str(tools_dir), prepend=os.pathsep)
+
+ # Generate a test project that has an additional before-build step using the
+ # known-to-exist tool.
project_dir = tmp_path / "project"
+ setup_py_add = "import subprocess\nsubprocess.run('does-exist', check=True)\n"
+ basic_project = test_projects.new_c_project(setup_py_add=setup_py_add)
+ basic_project.files.update(basic_project_files)
basic_project.generate(project_dir)
+ # Build and test the wheels. Mark the "does-exist" tool as a cross-build
+ # tool, and invoke it during a `before-build` step. It will also be invoked
+ # when `setup.py` is invoked.
+ #
+ # Tests are only executed on simulator. The test suite passes if it's
+ # running on the same architecture as the current platform.
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
+ "CIBW_BEFORE_BUILD": "does-exist",
"CIBW_BUILD": "cp313-*",
+ "CIBW_XBUILD_TOOLS": "does-exist",
"CIBW_TEST_SOURCES": "tests",
"CIBW_TEST_COMMAND": "unittest discover tests test_platform.py",
"CIBW_BUILD_VERBOSITY": "1",
@@ -55,11 +79,10 @@ def test_ios_platforms(tmp_path, build_config):
},
)
+ # The expected wheels were produced.
ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
platform_machine = platform.machine()
- # Tests are only executed on simulator. The test suite passes if it's
- # running on the same architecture as the current platform.
if platform_machine == "x86_64":
expected_wheels = {
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
@@ -73,15 +96,21 @@ def test_ios_platforms(tmp_path, build_config):
assert set(actual_wheels) == expected_wheels
+ # The user was notified that the cross-build tool was found.
+ captured = capfd.readouterr()
+ assert "'does-exist' will be included in the cross-build environment" in captured.out
+
-@pytest.mark.serial
def test_no_test_sources(tmp_path, capfd):
+ """Build will fail if test-sources isn't defined."""
if utils.platform != "macos":
pytest.skip("this test can only run on macOS")
if utils.get_xcode_version() < (13, 0):
pytest.skip("this test only works with Xcode 13.0 or greater")
project_dir = tmp_path / "project"
+ basic_project = test_projects.new_c_project()
+ basic_project.files.update(basic_project_files)
basic_project.generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
@@ -94,5 +123,125 @@ def test_no_test_sources(tmp_path, capfd):
},
)
+ # The error message indicates the configuration issue.
captured = capfd.readouterr()
assert "Testing on iOS requires a definition of test-sources." in captured.err
+
+
+def test_missing_xbuild_tool(tmp_path, capfd):
+ """Build will fail if xbuild-tools references a non-existent tool."""
+ if utils.platform != "macos":
+ pytest.skip("this test can only run on macOS")
+ if utils.get_xcode_version() < (13, 0):
+ pytest.skip("this test only works with Xcode 13.0 or greater")
+
+ project_dir = tmp_path / "project"
+ basic_project = test_projects.new_c_project()
+ basic_project.files.update(basic_project_files)
+ basic_project.generate(project_dir)
+
+ with pytest.raises(subprocess.CalledProcessError):
+ utils.cibuildwheel_run(
+ project_dir,
+ add_env={
+ "CIBW_PLATFORM": "ios",
+ "CIBW_BUILD": "cp313-*",
+ "CIBW_TEST_COMMAND": "tests",
+ "CIBW_XBUILD_TOOLS": "does-not-exist",
+ },
+ )
+
+ # The error message indicates the problem tool.
+ captured = capfd.readouterr()
+ assert "Could not find a 'does-not-exist' executable on the path." in captured.err
+
+
+def test_no_xbuild_tool_definition(tmp_path, capfd):
+ """Build will succeed with a warning if there is no xbuild-tools definition."""
+ if utils.platform != "macos":
+ pytest.skip("this test can only run on macOS")
+ if utils.get_xcode_version() < (13, 0):
+ pytest.skip("this test only works with Xcode 13.0 or greater")
+
+ project_dir = tmp_path / "project"
+ basic_project = test_projects.new_c_project()
+ basic_project.files.update(basic_project_files)
+ basic_project.generate(project_dir)
+
+ # Build, but don't test the wheels; we're only checking that the right
+ # warning was raised.
+ actual_wheels = utils.cibuildwheel_run(
+ project_dir,
+ add_env={
+ "CIBW_PLATFORM": "ios",
+ "CIBW_BUILD": "cp313-*",
+ "CIBW_TEST_SKIP": "*",
+ },
+ )
+
+ # The expected wheels were produced.
+ ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
+ platform_machine = platform.machine()
+
+ if platform_machine == "x86_64":
+ expected_wheels = {
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
+ }
+
+ elif platform_machine == "arm64":
+ expected_wheels = {
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl",
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl",
+ }
+
+ assert set(actual_wheels) == expected_wheels
+
+ # The user was notified that there was no cross-build tool definition.
+ captured = capfd.readouterr()
+ assert "Your project configuration does not define any cross-build tools." in captured.err
+
+
+def test_empty_xbuild_tool_definition(tmp_path, capfd):
+ """Build will succeed with no warning if there is an empty xbuild-tools definition."""
+ if utils.platform != "macos":
+ pytest.skip("this test can only run on macOS")
+ if utils.get_xcode_version() < (13, 0):
+ pytest.skip("this test only works with Xcode 13.0 or greater")
+
+ project_dir = tmp_path / "project"
+ basic_project = test_projects.new_c_project()
+ basic_project.files.update(basic_project_files)
+ basic_project.generate(project_dir)
+
+ # Build, but don't test the wheels; we're only checking that a warning
+ # wasn't raised.
+ actual_wheels = utils.cibuildwheel_run(
+ project_dir,
+ add_env={
+ "CIBW_PLATFORM": "ios",
+ "CIBW_BUILD": "cp313-*",
+ "CIBW_TEST_SKIP": "*",
+ "CIBW_XBUILD_TOOLS": "",
+ },
+ )
+
+ # The expected wheels were produced.
+ ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
+ platform_machine = platform.machine()
+
+ if platform_machine == "x86_64":
+ expected_wheels = {
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
+ }
+
+ elif platform_machine == "arm64":
+ expected_wheels = {
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl",
+ f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl",
+ }
+
+ assert set(actual_wheels) == expected_wheels
+
+ # The warnings about cross-build notifications were silenced.
+ captured = capfd.readouterr()
+ assert "Your project configuration does not define any cross-build tools." not in captured.err
diff --git a/unit_test/options_test.py b/unit_test/options_test.py
index bcf242916..fbf181754 100644
--- a/unit_test/options_test.py
+++ b/unit_test/options_test.py
@@ -618,3 +618,31 @@ def test_get_build_frontend_extra_flags_warning(
)
assert args == ["-Ca", "-Cb", "-1"]
mock_warning.assert_called_once()
+
+
+@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
diff --git a/unit_test/options_toml_test.py b/unit_test/options_toml_test.py
index e5c8d5593..6ec634152 100644
--- a/unit_test/options_toml_test.py
+++ b/unit_test/options_toml_test.py
@@ -17,6 +17,7 @@
[tool.cibuildwheel]
build = "cp39*"
environment = {THING = "OTHER", FOO="BAR"}
+xbuild-tools = ["first"]
test-command = "pyproject"
test-requires = "something"
@@ -89,6 +90,7 @@ def test_envvar_override(tmp_path, platform):
env={
"CIBW_BUILD": "cp38*",
"CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24",
+ "CIBW_XBUILD_TOOLS": "cmake rustc",
"CIBW_TEST_COMMAND": "mytest",
"CIBW_TEST_REQUIRES": "docs",
"CIBW_TEST_GROUPS": "mgroup two",
@@ -104,6 +106,10 @@ def test_envvar_override(tmp_path, platform):
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
+ assert (
+ options_reader.get("xbuild-tools", option_format=ListFormat(" ", quote=shlex.quote))
+ == "cmake rustc"
+ )
assert (
options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote))
== 'first "second third"'
@@ -269,6 +275,7 @@ def test_environment_override_empty(tmp_path):
env={
"CIBW_MANYLINUX_I686_IMAGE": "",
"CIBW_MANYLINUX_AARCH64_IMAGE": "manylinux1",
+ "CIBW_XBUILD_TOOLS": "",
},
)
@@ -280,6 +287,10 @@ def test_environment_override_empty(tmp_path):
assert options_reader.get("manylinux-i686-image", ignore_empty=True) == "manylinux1"
assert options_reader.get("manylinux-aarch64-image", ignore_empty=True) == "manylinux1"
+ assert (
+ options_reader.get("xbuild-tools", option_format=ListFormat(" ", quote=shlex.quote)) == ""
+ )
+
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
def test_resolve_cascade(ignore_empty):