Skip to content

Commit 830e79e

Browse files
feat: add the ability to declare safe tools in a cross-build environment. (#2317)
* Add the ability to declare safe tools in a cross-build environment. * Add an xfail if cmake isn't available on the test machine. * Placate linter regarding positional args. * Rework test to provide more robust confirmation of safe tools. * Remove a test skip condition that is no longer needed. Co-authored-by: Joe Rickerby <[email protected]> * Rename the setting to xbuild-tools. * Add docs to clarify that xbuild-tools is transitive. * Raise a warning if xbuild-tools isn't defined. * Correct a bad copy-paste in the schema generator. * .. and now fix the indentation. * Move sentinel handling earlier into the parsing process. * Remove serialization from tests that won't start a test suite. --------- Co-authored-by: Joe Rickerby <[email protected]>
1 parent 2aaa489 commit 830e79e

File tree

11 files changed

+336
-15
lines changed

11 files changed

+336
-15
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Options
139139
| | [`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. |
140140
| | [`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. |
141141
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.pypa.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
142+
| | [`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. |
142143
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel |
143144
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
144145
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |

bin/generate_schema.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,12 @@
181181
musllinux-x86_64-image:
182182
type: string
183183
description: Specify alternative manylinux / musllinux container images
184-
repair-wheel-command:
184+
xbuild-tools:
185+
description: Binaries on the path that should be included in an isolated cross-build environment
185186
type: string_array
187+
repair-wheel-command:
186188
description: Execute a shell command to repair each built wheel.
189+
type: string_array
187190
skip:
188191
description: Choose the Python versions to skip.
189192
type: string_array
@@ -273,6 +276,7 @@
273276
properties:
274277
before-all: {"$ref": "#/$defs/inherit"}
275278
before-build: {"$ref": "#/$defs/inherit"}
279+
xbuild-tools: {"$ref": "#/$defs/inherit"}
276280
before-test: {"$ref": "#/$defs/inherit"}
277281
config-settings: {"$ref": "#/$defs/inherit"}
278282
container-engine: {"$ref": "#/$defs/inherit"}

cibuildwheel/options.py

+14
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class BuildOptions:
9494
environment: ParsedEnvironment
9595
before_all: str
9696
before_build: str | None
97+
xbuild_tools: list[str] | None
9798
repair_command: str
9899
manylinux_images: dict[str, str] | None
99100
musllinux_images: dict[str, str] | None
@@ -718,6 +719,18 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions:
718719

719720
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
720721
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
722+
xbuild_tools: list[str] | None = shlex.split(
723+
self.reader.get(
724+
"xbuild-tools", option_format=ListFormat(sep=" ", quote=shlex.quote)
725+
)
726+
)
727+
# ["\u0000"] is a sentinel value used as a default, because TOML
728+
# doesn't have an explicit NULL value. If xbuild-tools is set to the
729+
# sentinel, it indicates that the user hasn't defined xbuild-tools
730+
# *at all* (not even an `xbuild-tools = []` definition).
731+
if xbuild_tools == ["\u0000"]:
732+
xbuild_tools = None
733+
721734
test_sources = shlex.split(
722735
self.reader.get(
723736
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
@@ -835,6 +848,7 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions:
835848
before_build=before_build,
836849
before_all=before_all,
837850
build_verbosity=build_verbosity,
851+
xbuild_tools=xbuild_tools,
838852
repair_command=repair_command,
839853
environment=environment,
840854
dependency_constraints=dependency_constraints,

cibuildwheel/platforms/ios.py

+53-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import shutil
66
import subprocess
77
import sys
8+
import textwrap
89
from collections.abc import Sequence, Set
910
from dataclasses import dataclass
1011
from pathlib import Path
@@ -151,6 +152,7 @@ def cross_virtualenv(
151152
build_python: Path,
152153
venv_path: Path,
153154
dependency_constraint_flags: Sequence[PathOrStr],
155+
xbuild_tools: Sequence[str] | None,
154156
) -> dict[str, str]:
155157
"""Create a cross-compilation virtual environment.
156158
@@ -178,6 +180,8 @@ def cross_virtualenv(
178180
created.
179181
:param dependency_constraint_flags: Any flags that should be used when
180182
constraining dependencies in the environment.
183+
:param xbuild_tools: A list of executable names (without paths) that are
184+
on the path, but must be preserved in the cross environment.
181185
"""
182186
# Create an initial macOS virtual environment
183187
env = virtualenv(
@@ -210,14 +214,52 @@ def cross_virtualenv(
210214
#
211215
# To prevent problems, set the PATH to isolate the build environment from
212216
# sources that could introduce incompatible binaries.
217+
#
218+
# However, there may be some tools on the path that are needed for the
219+
# build. Find their location on the path, and link the underlying binaries
220+
# (fully resolving symlinks) to a "safe" location that will *only* contain
221+
# those tools. This avoids needing to add *all* of Homebrew to the path just
222+
# to get access to (for example) cmake for build purposes. A value of None
223+
# means the user hasn't provided a list of xbuild tools.
224+
xbuild_tools_path = venv_path / "cibw_xbuild_tools"
225+
xbuild_tools_path.mkdir()
226+
if xbuild_tools is None:
227+
log.warning(
228+
textwrap.dedent(
229+
"""
230+
Your project configuration does not define any cross-build tools.
231+
232+
iOS builds use an isolated build environment; if your build process requires any
233+
third-party tools (such as cmake, ninja, or rustc), you must explicitly declare
234+
that those tools are required using xbuild-tools/CIBW_XBUILD_TOOLS. This will
235+
likely manifest as a "somebuildtool: command not found" error.
236+
237+
If the build succeeds, you can silence this warning by setting adding
238+
`xbuild-tools = []` to your pyproject.toml configuration, or exporting
239+
CIBW_XBUILD_TOOLS as an empty string into your environment.
240+
"""
241+
)
242+
)
243+
else:
244+
for tool in xbuild_tools:
245+
tool_path = shutil.which(tool)
246+
if tool_path is None:
247+
msg = f"Could not find a {tool!r} executable on the path."
248+
raise errors.FatalError(msg)
249+
250+
# Link the binary into the safe tools directory
251+
original = Path(tool_path).resolve()
252+
print(f"{tool!r} will be included in the cross-build environment (using {original})")
253+
(xbuild_tools_path / tool).symlink_to(original)
254+
213255
env["PATH"] = os.pathsep.join(
214256
[
215257
# The target python's binary directory
216258
str(target_python.parent),
217-
# The cross-platform environments binary directory
259+
# The cross-platform environment's binary directory
218260
str(venv_path / "bin"),
219-
# Cargo's binary directory (to allow for Rust compilation)
220-
str(Path.home() / ".cargo" / "bin"),
261+
# The directory of cross-build tools
262+
str(xbuild_tools_path),
221263
# The bare minimum Apple system paths.
222264
"/usr/bin",
223265
"/bin",
@@ -235,10 +277,12 @@ def cross_virtualenv(
235277

236278
def setup_python(
237279
tmp: Path,
280+
*,
238281
python_configuration: PythonConfiguration,
239282
dependency_constraint_flags: Sequence[PathOrStr],
240283
environment: ParsedEnvironment,
241284
build_frontend: BuildFrontendName,
285+
xbuild_tools: Sequence[str] | None,
242286
) -> tuple[Path, dict[str, str]]:
243287
if build_frontend == "build[uv]":
244288
msg = "uv doesn't support iOS"
@@ -291,6 +335,7 @@ def setup_python(
291335
build_python=build_python,
292336
venv_path=venv_path,
293337
dependency_constraint_flags=dependency_constraint_flags,
338+
xbuild_tools=xbuild_tools,
294339
)
295340
venv_bin_path = venv_path / "bin"
296341
assert venv_bin_path.exists()
@@ -414,10 +459,11 @@ def build(options: Options, tmp_path: Path) -> None:
414459

415460
target_install_path, env = setup_python(
416461
identifier_tmp_dir / "build",
417-
config,
418-
dependency_constraint_flags,
419-
build_options.environment,
420-
build_frontend.name,
462+
python_configuration=config,
463+
dependency_constraint_flags=dependency_constraint_flags,
464+
environment=build_options.environment,
465+
build_frontend=build_frontend.name,
466+
xbuild_tools=build_options.xbuild_tools,
421467
)
422468
pip_version = get_pip_version(env)
423469

cibuildwheel/resources/cibuildwheel.schema.json

+21
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,21 @@
397397
"description": "Specify alternative manylinux / musllinux container images",
398398
"title": "CIBW_MUSLLINUX_X86_64_IMAGE"
399399
},
400+
"xbuild-tools": {
401+
"description": "Binaries on the path that should be included in an isolated cross-build environment",
402+
"oneOf": [
403+
{
404+
"type": "string"
405+
},
406+
{
407+
"type": "array",
408+
"items": {
409+
"type": "string"
410+
}
411+
}
412+
],
413+
"title": "CIBW_XBUILD_TOOLS"
414+
},
400415
"repair-wheel-command": {
401416
"description": "Execute a shell command to repair each built wheel.",
402417
"oneOf": [
@@ -566,6 +581,9 @@
566581
"environment-pass": {
567582
"$ref": "#/$defs/inherit"
568583
},
584+
"xbuild-tools": {
585+
"$ref": "#/$defs/inherit"
586+
},
569587
"repair-wheel-command": {
570588
"$ref": "#/$defs/inherit"
571589
},
@@ -991,6 +1009,9 @@
9911009
"repair-wheel-command": {
9921010
"$ref": "#/properties/repair-wheel-command"
9931011
},
1012+
"xbuild-tools": {
1013+
"$ref": "#/properties/xbuild-tools"
1014+
},
9941015
"test-command": {
9951016
"$ref": "#/properties/test-command"
9961017
},

cibuildwheel/resources/defaults.toml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ build-verbosity = 0
1414

1515
before-all = ""
1616
before-build = ""
17+
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
18+
xbuild-tools = ["\u0000"]
1719
repair-wheel-command = ""
1820

1921
test-command = ""

docs/options.md

+43
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,49 @@ Platform-specific environment variables are also available:<br/>
10431043
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
10441044
[PEP 518]: https://www.python.org/dev/peps/pep-0517/
10451045

1046+
### `CIBW_XBUILD_TOOLS` {: #xbuild-tools}
1047+
> Binaries on the path that should be included in an isolated cross-build environment.
1048+
1049+
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.
1050+
1051+
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.
1052+
1053+
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.
1054+
1055+
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.
1056+
1057+
*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.
1058+
1059+
Platform-specific environment variables are also available on platforms that use cross-platform environment isolation:<br/>
1060+
`CIBW_XBUILD_TOOLS_IOS`
1061+
1062+
#### Examples
1063+
1064+
!!! tab examples "Environment variables"
1065+
1066+
```yaml
1067+
# Allow access to the cmake and rustc binaries in the isolated cross-build environment.
1068+
CIBW_XBUILD_TOOLS: cmake rustc
1069+
```
1070+
1071+
```yaml
1072+
# No cross-build tools are required
1073+
CIBW_XBUILD_TOOLS:
1074+
```
1075+
1076+
!!! tab examples "pyproject.toml"
1077+
1078+
```toml
1079+
[tool.cibuildwheel]
1080+
# Allow access to the cmake and rustc binaries in the isolated cross-build environment.
1081+
xbuild-tools = ["cmake", "rustc"]
1082+
```
1083+
1084+
```toml
1085+
[tool.cibuildwheel]
1086+
# No cross-build tools are required
1087+
xbuild-tools = []
1088+
```
10461089

10471090
### `CIBW_REPAIR_WHEEL_COMMAND` {: #repair-wheel-command}
10481091
> Execute a shell command to repair each built wheel

docs/platforms/ios.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ iOS builds support both the `pip` and `build` build frontends. In principle, sup
5757

5858
## Build environment
5959

60-
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).
60+
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.
61+
62+
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.
6163

6264
## Tests
6365

0 commit comments

Comments
 (0)