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
+1
View File
@@ -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`<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 |
+5 -1
View File
@@ -181,9 +181,12 @@ properties:
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 @@ items:
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"}
+14
View File
@@ -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 @@ class Options:
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 @@ class Options:
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,
+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)
@@ -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"
},
+2
View File
@@ -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 = ""
+43
View File
@@ -1043,6 +1043,49 @@ Platform-specific environment variables are also available:<br/>
[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:<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}
> Execute a shell command to repair each built wheel
+3 -1
View File
@@ -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
+155 -6
View File
@@ -2,14 +2,15 @@ from __future__ import annotations
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 @@ class TestPlatform(TestCase):
self.assertEqual(platform.machine(), "{platform.machine()}")
"""
}
# 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"},
],
)
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
+28
View File
@@ -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
+11
View File
@@ -17,6 +17,7 @@ PYPROJECT_1 = """
[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 @@ manylinux-x86_64-image = ""
env={
"CIBW_MANYLINUX_I686_IMAGE": "",
"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-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):