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:
co-authored by
Joe Rickerby
parent
2aaa489371
commit
830e79e11c
@@ -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_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_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_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_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`<br/>`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.pypa.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images |
|
| | [`CIBW_MANYLINUX_*_IMAGE`<br/>`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 |
|
| | [`CIBW_CONTAINER_ENGINE`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify which container engine to use when building Linux wheels |
|
||||||
|
|||||||
@@ -181,9 +181,12 @@ properties:
|
|||||||
musllinux-x86_64-image:
|
musllinux-x86_64-image:
|
||||||
type: string
|
type: string
|
||||||
description: Specify alternative manylinux / musllinux container images
|
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
|
type: string_array
|
||||||
|
repair-wheel-command:
|
||||||
description: Execute a shell command to repair each built wheel.
|
description: Execute a shell command to repair each built wheel.
|
||||||
|
type: string_array
|
||||||
skip:
|
skip:
|
||||||
description: Choose the Python versions to skip.
|
description: Choose the Python versions to skip.
|
||||||
type: string_array
|
type: string_array
|
||||||
@@ -273,6 +276,7 @@ items:
|
|||||||
properties:
|
properties:
|
||||||
before-all: {"$ref": "#/$defs/inherit"}
|
before-all: {"$ref": "#/$defs/inherit"}
|
||||||
before-build: {"$ref": "#/$defs/inherit"}
|
before-build: {"$ref": "#/$defs/inherit"}
|
||||||
|
xbuild-tools: {"$ref": "#/$defs/inherit"}
|
||||||
before-test: {"$ref": "#/$defs/inherit"}
|
before-test: {"$ref": "#/$defs/inherit"}
|
||||||
config-settings: {"$ref": "#/$defs/inherit"}
|
config-settings: {"$ref": "#/$defs/inherit"}
|
||||||
container-engine: {"$ref": "#/$defs/inherit"}
|
container-engine: {"$ref": "#/$defs/inherit"}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class BuildOptions:
|
|||||||
environment: ParsedEnvironment
|
environment: ParsedEnvironment
|
||||||
before_all: str
|
before_all: str
|
||||||
before_build: str | None
|
before_build: str | None
|
||||||
|
xbuild_tools: list[str] | None
|
||||||
repair_command: str
|
repair_command: str
|
||||||
manylinux_images: dict[str, str] | None
|
manylinux_images: dict[str, str] | None
|
||||||
musllinux_images: dict[str, str] | None
|
musllinux_images: dict[str, str] | None
|
||||||
@@ -718,6 +719,18 @@ class Options:
|
|||||||
|
|
||||||
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
||||||
before_test = self.reader.get("before-test", 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(
|
test_sources = shlex.split(
|
||||||
self.reader.get(
|
self.reader.get(
|
||||||
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
|
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
|
||||||
@@ -835,6 +848,7 @@ class Options:
|
|||||||
before_build=before_build,
|
before_build=before_build,
|
||||||
before_all=before_all,
|
before_all=before_all,
|
||||||
build_verbosity=build_verbosity,
|
build_verbosity=build_verbosity,
|
||||||
|
xbuild_tools=xbuild_tools,
|
||||||
repair_command=repair_command,
|
repair_command=repair_command,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
dependency_constraints=dependency_constraints,
|
dependency_constraints=dependency_constraints,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import shlex
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
from collections.abc import Sequence, Set
|
from collections.abc import Sequence, Set
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -151,6 +152,7 @@ def cross_virtualenv(
|
|||||||
build_python: Path,
|
build_python: Path,
|
||||||
venv_path: Path,
|
venv_path: Path,
|
||||||
dependency_constraint_flags: Sequence[PathOrStr],
|
dependency_constraint_flags: Sequence[PathOrStr],
|
||||||
|
xbuild_tools: Sequence[str] | None,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Create a cross-compilation virtual environment.
|
"""Create a cross-compilation virtual environment.
|
||||||
|
|
||||||
@@ -178,6 +180,8 @@ def cross_virtualenv(
|
|||||||
created.
|
created.
|
||||||
:param dependency_constraint_flags: Any flags that should be used when
|
:param dependency_constraint_flags: Any flags that should be used when
|
||||||
constraining dependencies in the environment.
|
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
|
# Create an initial macOS virtual environment
|
||||||
env = virtualenv(
|
env = virtualenv(
|
||||||
@@ -210,14 +214,52 @@ def cross_virtualenv(
|
|||||||
#
|
#
|
||||||
# To prevent problems, set the PATH to isolate the build environment from
|
# To prevent problems, set the PATH to isolate the build environment from
|
||||||
# sources that could introduce incompatible binaries.
|
# 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(
|
env["PATH"] = os.pathsep.join(
|
||||||
[
|
[
|
||||||
# The target python's binary directory
|
# The target python's binary directory
|
||||||
str(target_python.parent),
|
str(target_python.parent),
|
||||||
# The cross-platform environments binary directory
|
# The cross-platform environment's binary directory
|
||||||
str(venv_path / "bin"),
|
str(venv_path / "bin"),
|
||||||
# Cargo's binary directory (to allow for Rust compilation)
|
# The directory of cross-build tools
|
||||||
str(Path.home() / ".cargo" / "bin"),
|
str(xbuild_tools_path),
|
||||||
# The bare minimum Apple system paths.
|
# The bare minimum Apple system paths.
|
||||||
"/usr/bin",
|
"/usr/bin",
|
||||||
"/bin",
|
"/bin",
|
||||||
@@ -235,10 +277,12 @@ def cross_virtualenv(
|
|||||||
|
|
||||||
def setup_python(
|
def setup_python(
|
||||||
tmp: Path,
|
tmp: Path,
|
||||||
|
*,
|
||||||
python_configuration: PythonConfiguration,
|
python_configuration: PythonConfiguration,
|
||||||
dependency_constraint_flags: Sequence[PathOrStr],
|
dependency_constraint_flags: Sequence[PathOrStr],
|
||||||
environment: ParsedEnvironment,
|
environment: ParsedEnvironment,
|
||||||
build_frontend: BuildFrontendName,
|
build_frontend: BuildFrontendName,
|
||||||
|
xbuild_tools: Sequence[str] | None,
|
||||||
) -> tuple[Path, dict[str, str]]:
|
) -> tuple[Path, dict[str, str]]:
|
||||||
if build_frontend == "build[uv]":
|
if build_frontend == "build[uv]":
|
||||||
msg = "uv doesn't support iOS"
|
msg = "uv doesn't support iOS"
|
||||||
@@ -291,6 +335,7 @@ def setup_python(
|
|||||||
build_python=build_python,
|
build_python=build_python,
|
||||||
venv_path=venv_path,
|
venv_path=venv_path,
|
||||||
dependency_constraint_flags=dependency_constraint_flags,
|
dependency_constraint_flags=dependency_constraint_flags,
|
||||||
|
xbuild_tools=xbuild_tools,
|
||||||
)
|
)
|
||||||
venv_bin_path = venv_path / "bin"
|
venv_bin_path = venv_path / "bin"
|
||||||
assert venv_bin_path.exists()
|
assert venv_bin_path.exists()
|
||||||
@@ -414,10 +459,11 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
|
|
||||||
target_install_path, env = setup_python(
|
target_install_path, env = setup_python(
|
||||||
identifier_tmp_dir / "build",
|
identifier_tmp_dir / "build",
|
||||||
config,
|
python_configuration=config,
|
||||||
dependency_constraint_flags,
|
dependency_constraint_flags=dependency_constraint_flags,
|
||||||
build_options.environment,
|
environment=build_options.environment,
|
||||||
build_frontend.name,
|
build_frontend=build_frontend.name,
|
||||||
|
xbuild_tools=build_options.xbuild_tools,
|
||||||
)
|
)
|
||||||
pip_version = get_pip_version(env)
|
pip_version = get_pip_version(env)
|
||||||
|
|
||||||
|
|||||||
@@ -397,6 +397,21 @@
|
|||||||
"description": "Specify alternative manylinux / musllinux container images",
|
"description": "Specify alternative manylinux / musllinux container images",
|
||||||
"title": "CIBW_MUSLLINUX_X86_64_IMAGE"
|
"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": {
|
"repair-wheel-command": {
|
||||||
"description": "Execute a shell command to repair each built wheel.",
|
"description": "Execute a shell command to repair each built wheel.",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@@ -566,6 +581,9 @@
|
|||||||
"environment-pass": {
|
"environment-pass": {
|
||||||
"$ref": "#/$defs/inherit"
|
"$ref": "#/$defs/inherit"
|
||||||
},
|
},
|
||||||
|
"xbuild-tools": {
|
||||||
|
"$ref": "#/$defs/inherit"
|
||||||
|
},
|
||||||
"repair-wheel-command": {
|
"repair-wheel-command": {
|
||||||
"$ref": "#/$defs/inherit"
|
"$ref": "#/$defs/inherit"
|
||||||
},
|
},
|
||||||
@@ -991,6 +1009,9 @@
|
|||||||
"repair-wheel-command": {
|
"repair-wheel-command": {
|
||||||
"$ref": "#/properties/repair-wheel-command"
|
"$ref": "#/properties/repair-wheel-command"
|
||||||
},
|
},
|
||||||
|
"xbuild-tools": {
|
||||||
|
"$ref": "#/properties/xbuild-tools"
|
||||||
|
},
|
||||||
"test-command": {
|
"test-command": {
|
||||||
"$ref": "#/properties/test-command"
|
"$ref": "#/properties/test-command"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ build-verbosity = 0
|
|||||||
|
|
||||||
before-all = ""
|
before-all = ""
|
||||||
before-build = ""
|
before-build = ""
|
||||||
|
# TOML doesn't support explicit NULLs; use ["\u0000"] as a sentinel value.
|
||||||
|
xbuild-tools = ["\u0000"]
|
||||||
repair-wheel-command = ""
|
repair-wheel-command = ""
|
||||||
|
|
||||||
test-command = ""
|
test-command = ""
|
||||||
|
|||||||
@@ -1043,6 +1043,49 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
||||||
[PEP 518]: 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:<br/>
|
||||||
|
`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}
|
### `CIBW_REPAIR_WHEEL_COMMAND` {: #repair-wheel-command}
|
||||||
> Execute a shell command to repair each built wheel
|
> Execute a shell command to repair each built wheel
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ iOS builds support both the `pip` and `build` build frontends. In principle, sup
|
|||||||
|
|
||||||
## Build environment
|
## 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
|
## Tests
|
||||||
|
|
||||||
|
|||||||
+155
-6
@@ -2,14 +2,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from . import test_projects, utils
|
from . import test_projects, utils
|
||||||
|
|
||||||
basic_project = test_projects.new_c_project()
|
basic_project_files = {
|
||||||
basic_project.files["tests/test_platform.py"] = f"""
|
"tests/test_platform.py": f"""
|
||||||
import platform
|
import platform
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ class TestPlatform(TestCase):
|
|||||||
self.assertEqual(platform.machine(), "{platform.machine()}")
|
self.assertEqual(platform.machine(), "{platform.machine()}")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# iOS tests shouldn't be run in parallel, because they're dependent on calling
|
# iOS tests shouldn't be run in parallel, because they're dependent on calling
|
||||||
@@ -35,19 +37,41 @@ class TestPlatform(TestCase):
|
|||||||
{"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"},
|
{"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":
|
if utils.platform != "macos":
|
||||||
pytest.skip("this test can only run on macOS")
|
pytest.skip("this test can only run on macOS")
|
||||||
if utils.get_xcode_version() < (13, 0):
|
if utils.get_xcode_version() < (13, 0):
|
||||||
pytest.skip("this test only works with Xcode 13.0 or greater")
|
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"
|
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)
|
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(
|
actual_wheels = utils.cibuildwheel_run(
|
||||||
project_dir,
|
project_dir,
|
||||||
add_env={
|
add_env={
|
||||||
|
"CIBW_BEFORE_BUILD": "does-exist",
|
||||||
"CIBW_BUILD": "cp313-*",
|
"CIBW_BUILD": "cp313-*",
|
||||||
|
"CIBW_XBUILD_TOOLS": "does-exist",
|
||||||
"CIBW_TEST_SOURCES": "tests",
|
"CIBW_TEST_SOURCES": "tests",
|
||||||
"CIBW_TEST_COMMAND": "unittest discover tests test_platform.py",
|
"CIBW_TEST_COMMAND": "unittest discover tests test_platform.py",
|
||||||
"CIBW_BUILD_VERBOSITY": "1",
|
"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(".", "_")
|
ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
|
||||||
platform_machine = platform.machine()
|
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":
|
if platform_machine == "x86_64":
|
||||||
expected_wheels = {
|
expected_wheels = {
|
||||||
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
|
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
|
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):
|
def test_no_test_sources(tmp_path, capfd):
|
||||||
|
"""Build will fail if test-sources isn't defined."""
|
||||||
if utils.platform != "macos":
|
if utils.platform != "macos":
|
||||||
pytest.skip("this test can only run on macOS")
|
pytest.skip("this test can only run on macOS")
|
||||||
if utils.get_xcode_version() < (13, 0):
|
if utils.get_xcode_version() < (13, 0):
|
||||||
pytest.skip("this test only works with Xcode 13.0 or greater")
|
pytest.skip("this test only works with Xcode 13.0 or greater")
|
||||||
|
|
||||||
project_dir = tmp_path / "project"
|
project_dir = tmp_path / "project"
|
||||||
|
basic_project = test_projects.new_c_project()
|
||||||
|
basic_project.files.update(basic_project_files)
|
||||||
basic_project.generate(project_dir)
|
basic_project.generate(project_dir)
|
||||||
|
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
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()
|
captured = capfd.readouterr()
|
||||||
assert "Testing on iOS requires a definition of test-sources." in captured.err
|
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
|
||||||
|
|||||||
@@ -618,3 +618,31 @@ def test_get_build_frontend_extra_flags_warning(
|
|||||||
)
|
)
|
||||||
assert args == ["-Ca", "-Cb", "-1"]
|
assert args == ["-Ca", "-Cb", "-1"]
|
||||||
mock_warning.assert_called_once()
|
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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ PYPROJECT_1 = """
|
|||||||
[tool.cibuildwheel]
|
[tool.cibuildwheel]
|
||||||
build = "cp39*"
|
build = "cp39*"
|
||||||
environment = {THING = "OTHER", FOO="BAR"}
|
environment = {THING = "OTHER", FOO="BAR"}
|
||||||
|
xbuild-tools = ["first"]
|
||||||
|
|
||||||
test-command = "pyproject"
|
test-command = "pyproject"
|
||||||
test-requires = "something"
|
test-requires = "something"
|
||||||
@@ -89,6 +90,7 @@ def test_envvar_override(tmp_path, platform):
|
|||||||
env={
|
env={
|
||||||
"CIBW_BUILD": "cp38*",
|
"CIBW_BUILD": "cp38*",
|
||||||
"CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24",
|
"CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24",
|
||||||
|
"CIBW_XBUILD_TOOLS": "cmake rustc",
|
||||||
"CIBW_TEST_COMMAND": "mytest",
|
"CIBW_TEST_COMMAND": "mytest",
|
||||||
"CIBW_TEST_REQUIRES": "docs",
|
"CIBW_TEST_REQUIRES": "docs",
|
||||||
"CIBW_TEST_GROUPS": "mgroup two",
|
"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-x86_64-image") == "manylinux_2_24"
|
||||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
options_reader.get("xbuild-tools", option_format=ListFormat(" ", quote=shlex.quote))
|
||||||
|
== "cmake rustc"
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote))
|
options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote))
|
||||||
== 'first "second third"'
|
== 'first "second third"'
|
||||||
@@ -269,6 +275,7 @@ manylinux-x86_64-image = ""
|
|||||||
env={
|
env={
|
||||||
"CIBW_MANYLINUX_I686_IMAGE": "",
|
"CIBW_MANYLINUX_I686_IMAGE": "",
|
||||||
"CIBW_MANYLINUX_AARCH64_IMAGE": "manylinux1",
|
"CIBW_MANYLINUX_AARCH64_IMAGE": "manylinux1",
|
||||||
|
"CIBW_XBUILD_TOOLS": "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -280,6 +287,10 @@ manylinux-x86_64-image = ""
|
|||||||
assert options_reader.get("manylinux-i686-image", ignore_empty=True) == "manylinux1"
|
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("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"])
|
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
|
||||||
def test_resolve_cascade(ignore_empty):
|
def test_resolve_cascade(ignore_empty):
|
||||||
|
|||||||
Reference in New Issue
Block a user