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 <joerick@mac.com>

* 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 <joerick@mac.com>
This commit is contained in:
Russell Keith-Magee
2025-04-07 08:20:20 +02:00
committed by GitHub
co-authored by Joe Rickerby
parent 2aaa489371
commit 830e79e11c
11 changed files with 336 additions and 15 deletions
+53 -7
View File
@@ -5,6 +5,7 @@ import shlex
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)