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):