diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index ad354d1e6..a409d725b 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -1,11 +1,13 @@ from __future__ import annotations import argparse +import dataclasses import os import shutil import sys import tarfile import textwrap +import traceback import typing from collections.abc import Iterable, Sequence, Set from pathlib import Path @@ -18,6 +20,7 @@ import cibuildwheel.pyodide import cibuildwheel.util import cibuildwheel.windows +from cibuildwheel import errors from cibuildwheel._compat.typing import assert_never from cibuildwheel.architecture import Architecture, allowed_architectures_check from cibuildwheel.logger import log @@ -31,10 +34,38 @@ chdir, detect_ci_provider, fix_ansi_codes_for_github_actions, + strtobool, ) +@dataclasses.dataclass +class GlobalOptions: + print_traceback_on_error: bool = True # decides what happens when errors are hit. + + def main() -> None: + global_options = GlobalOptions() + try: + main_inner(global_options) + except errors.FatalError as e: + message = e.args[0] + if log.step_active: + log.step_end_with_error(message) + else: + print(f"cibuildwheel: {message}", file=sys.stderr) + + if global_options.print_traceback_on_error: + traceback.print_exc(file=sys.stderr) + + sys.exit(e.return_code) + + +def main_inner(global_options: GlobalOptions) -> None: + """ + `main_inner` is the same as `main`, but it raises FatalError exceptions + rather than exiting directly. + """ + parser = argparse.ArgumentParser( description="Build wheels for all the platforms.", epilog=""" @@ -132,8 +163,17 @@ def main() -> None: help="Enable pre-release Python versions if available.", ) + parser.add_argument( + "--debug-traceback", + action="store_true", + default=strtobool(os.environ.get("CIBW_DEBUG_TRACEBACK", "0")), + help="Print a full traceback for all errors", + ) + args = CommandLineArguments(**vars(parser.parse_args())) + global_options.print_traceback_on_error = args.debug_traceback + args.package_dir = args.package_dir.resolve() # This are always relative to the base directory, even in SDist builds @@ -179,11 +219,8 @@ def _compute_platform_only(only: str) -> PlatformName: return "windows" if "pyodide_" in only: return "pyodide" - print( - f"Invalid --only='{only}', must be a build selector with a known platform", - file=sys.stderr, - ) - sys.exit(2) + msg = f"Invalid --only='{only}', must be a build selector with a known platform" + raise errors.ConfigurationError(msg) def _compute_platform_auto() -> PlatformName: @@ -194,34 +231,27 @@ def _compute_platform_auto() -> PlatformName: elif sys.platform == "win32": return "windows" else: - print( + msg = ( 'cibuildwheel: Unable to detect platform from "sys.platform". cibuildwheel doesn\'t ' "support building wheels for this platform. You might be able to build for a different " - "platform using the --platform argument. Check --help output for more information.", - file=sys.stderr, + "platform using the --platform argument. Check --help output for more information." ) - sys.exit(2) + raise errors.ConfigurationError(msg) def _compute_platform(args: CommandLineArguments) -> PlatformName: platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto") if args.only and args.platform is not None: - print( - "--platform cannot be specified with --only, it is computed from --only", - file=sys.stderr, - ) - sys.exit(2) + msg = "--platform cannot be specified with --only, it is computed from --only" + raise errors.ConfigurationError(msg) if args.only and args.archs is not None: - print( - "--arch cannot be specified with --only, it is computed from --only", - file=sys.stderr, - ) - sys.exit(2) + msg = "--arch cannot be specified with --only, it is computed from --only" + raise errors.ConfigurationError(msg) if platform_option_value not in PLATFORMS | {"auto"}: - print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr) - sys.exit(2) + msg = f"Unsupported platform: {platform_option_value}" + raise errors.ConfigurationError(msg) if args.only: return _compute_platform_only(args.only) @@ -268,9 +298,8 @@ def build_in_directory(args: CommandLineArguments) -> None: if not any(package_dir.joinpath(name).exists() for name in package_files): names = ", ".join(sorted(package_files, reverse=True)) - msg = f"cibuildwheel: Could not find any of {{{names}}} at root of package" - print(msg, file=sys.stderr) - sys.exit(2) + msg = f"Could not find any of {{{names}}} at root of package" + raise errors.ConfigurationError(msg) platform_module = get_platform_module(platform) identifiers = get_build_identifiers( @@ -301,16 +330,14 @@ def build_in_directory(args: CommandLineArguments) -> None: options.check_for_invalid_configuration(identifiers) allowed_architectures_check(platform, options.globals.architectures) except ValueError as err: - print("cibuildwheel:", *err.args, file=sys.stderr) - sys.exit(4) + raise errors.DeprecationError(*err.args) from err if not identifiers: - print( - f"cibuildwheel: No build identifiers selected: {options.globals.build_selector}", - file=sys.stderr, - ) - if not args.allow_empty: - sys.exit(3) + message = f"No build identifiers selected: {options.globals.build_selector}" + if args.allow_empty: + print(f"cibuildwheel: {message}", file=sys.stderr) + else: + raise errors.NothingToDoError(message) output_dir = options.globals.output_dir @@ -365,7 +392,9 @@ def print_preamble(platform: str, options: Options, identifiers: Sequence[str]) def get_build_identifiers( - platform_module: PlatformModule, build_selector: BuildSelector, architectures: Set[Architecture] + platform_module: PlatformModule, + build_selector: BuildSelector, + architectures: Set[Architecture], ) -> list[str]: python_configurations = platform_module.get_python_configurations(build_selector, architectures) return [config.identifier for config in python_configurations] diff --git a/cibuildwheel/errors.py b/cibuildwheel/errors.py new file mode 100644 index 000000000..717260e54 --- /dev/null +++ b/cibuildwheel/errors.py @@ -0,0 +1,27 @@ +""" +Errors that can cause the build to fail. Each subclass of FatalError has +a different return code, by defining them all here, we can ensure that they're +semantically clear and unique. +""" + + +class FatalError(BaseException): + """ + Raising an error of this type will cause the message to be printed to + stderr and the process to be terminated. Within cibuildwheel, raising this + exception produces a better error message, and optional traceback. + """ + + return_code: int = 1 + + +class ConfigurationError(FatalError): + return_code = 2 + + +class NothingToDoError(FatalError): + return_code = 3 + + +class DeprecationError(FatalError): + return_code = 4 diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 8e885cbce..329120d7b 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -10,6 +10,7 @@ from packaging.version import Version +from . import errors from ._compat.typing import assert_never from .architecture import Architecture from .logger import log @@ -147,11 +148,7 @@ def check_all_python_exist( exist = False if not exist: message = "\n".join(messages) - print( - f"cibuildwheel:\n{message}", - file=sys.stderr, - ) - sys.exit(1) + raise errors.FatalError(message) def build_in_container( @@ -225,28 +222,19 @@ def build_in_container( # check config python is still on PATH which_python = container.call(["which", "python"], env=env, capture_output=True).strip() if PurePosixPath(which_python) != python_bin / "python": - print( - "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it." + raise errors.FatalError(msg) if use_uv: which_uv = container.call(["which", "uv"], env=env, capture_output=True).strip() if not which_uv: - print( - "cibuildwheel: uv not found on PATH. You must use a supported manylinux or musllinux environment with uv.", - file=sys.stderr, - ) - sys.exit(1) + msg = "uv not found on PATH. You must use a supported manylinux or musllinux environment with uv." + raise errors.FatalError(msg) else: which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip() if PurePosixPath(which_pip) != python_bin / "pip": - print( - "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it." + raise errors.FatalError(msg) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) if compatible_wheel: @@ -446,21 +434,18 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001 check=True, stdout=subprocess.DEVNULL, ) - except subprocess.CalledProcessError: - print( - unwrap( - f""" - cibuildwheel: {build_step.container_engine.name} not found. An - OCI exe like Docker or Podman is required to run Linux builds. - If you're building on Travis CI, add `services: [docker]` to - your .travis.yml. If you're building on Circle CI in Linux, - add a `setup_remote_docker` step to your .circleci/config.yml. - If you're building on Cirrus CI, use `docker_builder` task. - """ - ), - file=sys.stderr, + except subprocess.CalledProcessError as error: + msg = unwrap( + f""" + cibuildwheel: {build_step.container_engine.name} not found. An + OCI exe like Docker or Podman is required to run Linux builds. + If you're building on Travis CI, add `services: [docker]` to + your .travis.yml. If you're building on Circle CI in Linux, + add a `setup_remote_docker` step to your .circleci/config.yml. + If you're building on Cirrus CI, use `docker_builder` task. + """ ) - sys.exit(2) + raise errors.ConfigurationError(msg) from error try: ids_to_build = [x.identifier for x in build_step.platform_configs] @@ -483,11 +468,9 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001 ) except subprocess.CalledProcessError as error: - log.step_end_with_error( - f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}" - ) troubleshoot(options, error) - sys.exit(1) + msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" + raise errors.FatalError(msg) from error def _matches_prepared_command(error_cmd: Sequence[str], command_template: str) -> bool: @@ -506,7 +489,6 @@ def troubleshoot(options: Options, error: Exception) -> None: ) # TODO allow matching of overrides too? ): # the wheel build step or the repair step failed - print("Checking for common errors...") so_files = list(options.globals.package_dir.glob("**/*.so")) if so_files: diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py index d679f57ae..312e2f559 100644 --- a/cibuildwheel/logger.py +++ b/cibuildwheel/logger.py @@ -152,6 +152,10 @@ def error(self, error: BaseException | str) -> None: c = self.colors print(f"{c.bright_red}Error{c.end}: {error}\n", file=sys.stderr) + @property + def step_active(self) -> bool: + return self.step_start_time is not None + def _start_fold_group(self, name: str) -> None: self._end_fold_group() self.active_fold_group_name = name diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 890ca89e9..c07574df2 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -16,6 +16,7 @@ from filelock import FileLock from packaging.version import Version +from . import errors from ._compat.typing import assert_never from .architecture import Architecture from .environment import ParsedEnvironment @@ -150,14 +151,13 @@ def install_cpython(tmp: Path, version: str, url: str, free_threading: bool) -> if detect_ci_provider() is None: # if running locally, we don't want to install CPython with sudo # let the user know & provide a link to the installer - print( + msg = ( f"Error: CPython {version} is not installed.\n" "cibuildwheel will not perform system-wide installs when running outside of CI.\n" f"To build locally, install CPython {version} on this machine, or, disable this version of Python using CIBW_SKIP=cp{version.replace('.', '')}-macosx_*\n" - f"\nDownload link: {url}", - file=sys.stderr, + f"\nDownload link: {url}" ) - raise SystemExit(1) + raise errors.FatalError(msg) pkg_path = tmp / "Python.pkg" # download the pkg download(url, pkg_path) @@ -279,22 +279,16 @@ def setup_python( call("pip", "--version", env=env) which_pip = call("which", "pip", env=env, capture_stdout=True).strip() if which_pip != str(venv_bin_path / "pip"): - print( - "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it." + raise errors.FatalError(msg) # check what Python version we're on call("which", "python", env=env) call("python", "--version", env=env) which_python = call("which", "python", env=env, capture_stdout=True).strip() if which_python != str(venv_bin_path / "python"): - print( - "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it." + raise errors.FatalError(msg) config_is_arm64 = python_configuration.identifier.endswith("arm64") config_is_universal2 = python_configuration.identifier.endswith("universal2") @@ -756,7 +750,5 @@ def build(options: Options, tmp_path: Path) -> None: log.build_end() except subprocess.CalledProcessError as error: - log.step_end_with_error( - f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}" - ) - sys.exit(1) + msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" + raise errors.FatalError(msg) from error diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index da2d68d65..a2b1b7a8b 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -8,15 +8,14 @@ import enum import functools import shlex -import sys import textwrap -import traceback from collections.abc import Callable, Generator, Iterable, Iterator, Set from pathlib import Path from typing import Any, Literal, Mapping, Sequence, TypedDict, Union # noqa: TID251 from packaging.specifiers import SpecifierSet +from . import errors from ._compat import tomllib from ._compat.typing import NotRequired, assert_never from .architecture import Architecture @@ -52,6 +51,7 @@ class CommandLineArguments: print_build_identifiers: bool allow_empty: bool prerelease_pythons: bool + debug_traceback: bool @staticmethod def defaults() -> CommandLineArguments: @@ -65,6 +65,7 @@ def defaults() -> CommandLineArguments: package_dir=Path("."), prerelease_pythons=False, print_build_identifiers=False, + debug_traceback=False, ) @@ -597,18 +598,14 @@ def build_options(self, identifier: str | None) -> BuildOptions: try: build_frontend = BuildFrontendConfig.from_config_string(build_frontend_str) except ValueError as e: - print(f"cibuildwheel: {e}", file=sys.stderr) - sys.exit(2) + msg = f"Failed to parse build frontend. {e}" + raise errors.ConfigurationError(msg) from e try: environment = parse_environment(environment_config) - except (EnvironmentParseError, ValueError): - print( - f"cibuildwheel: Malformed environment option {environment_config!r}", - file=sys.stderr, - ) - traceback.print_exc(None, sys.stderr) - sys.exit(2) + except (EnvironmentParseError, ValueError) as e: + msg = f"Malformed environment option {environment_config!r}" + raise errors.ConfigurationError(msg) from e # Pass through environment variables if self.platform == "linux": @@ -680,9 +677,8 @@ def build_options(self, identifier: str | None) -> BuildOptions: try: container_engine = OCIContainerEngineConfig.from_config_string(container_engine_str) except ValueError as e: - msg = f"cibuildwheel: Failed to parse container config. {e}" - print(msg, file=sys.stderr) - sys.exit(2) + msg = f"Failed to parse container config. {e}" + raise errors.ConfigurationError(msg) from e return BuildOptions( globals=self.globals, @@ -862,6 +858,6 @@ def _get_pinned_container_images() -> Mapping[str, Mapping[str, str]]: def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None: if "p2" in selector or "p35" in selector: msg = f"cibuildwheel 2.x no longer supports Python < 3.6. Please use the 1.x series or update {name}" - print(msg, file=sys.stderr) if error: - sys.exit(4) + raise errors.DeprecationError(msg) + log.warning(msg) diff --git a/cibuildwheel/pyodide.py b/cibuildwheel/pyodide.py index 780e43cee..67488edce 100644 --- a/cibuildwheel/pyodide.py +++ b/cibuildwheel/pyodide.py @@ -2,13 +2,13 @@ import os import shutil -import sys from collections.abc import Sequence, Set from dataclasses import dataclass from pathlib import Path from filelock import FileLock +from . import errors from .architecture import Architecture from .environment import ParsedEnvironment from .logger import log @@ -96,11 +96,8 @@ def get_base_python(identifier: str) -> Path: python_name = f"python{major_minor}" which_python = shutil.which(python_name) if which_python is None: - print( - f"Error: CPython {major_minor} is not installed.", - file=sys.stderr, - ) - raise SystemExit(1) + msg = f"CPython {major_minor} is not installed." + raise errors.FatalError(msg) return Path(which_python) @@ -141,22 +138,16 @@ def setup_python( call("pip", "--version", env=env) which_pip = call("which", "pip", env=env, capture_stdout=True).strip() if which_pip != str(venv_bin_path / "pip"): - print( - "cibuildwheel: pip available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "pip available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it." + raise errors.FatalError(msg) # check what Python version we're on call("which", "python", env=env) call("python", "--version", env=env) which_python = call("which", "python", env=env, capture_stdout=True).strip() if which_python != str(venv_bin_path / "python"): - print( - "cibuildwheel: python available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "python available on PATH doesn't match our venv instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it." + raise errors.FatalError(msg) log.step("Installing build tools...") call( @@ -219,8 +210,9 @@ def build(options: Options, tmp_path: Path) -> None: build_frontend = build_options.build_frontend or BuildFrontendConfig("build") if build_frontend.name == "pip": - print("The pyodide platform doesn't support pip frontend", file=sys.stderr) - sys.exit(1) + msg = "The pyodide platform doesn't support pip frontend" + raise errors.FatalError(msg) + log.build_start(config.identifier) identifier_tmp_dir = tmp_path / config.identifier diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index ce6e60a5f..efc0d0a78 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -448,7 +448,7 @@ def from_config_string(config_string: str) -> BuildFrontendConfig: config_dict = parse_key_value_string(config_string, ["name"], ["args"]) name = " ".join(config_dict["name"]) if name not in {"pip", "build", "build[uv]"}: - msg = f"Unrecognised build frontend {name}, only 'pip', 'build', and 'build[uv]' are supported" + msg = f"Unrecognised build frontend {name!r}, only 'pip', 'build', and 'build[uv]' are supported" raise ValueError(msg) name = typing.cast(BuildFrontendName, name) diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 263e8cf9a..010657e6e 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -4,7 +4,6 @@ import platform as platform_module import shutil import subprocess -import sys import textwrap from collections.abc import MutableMapping, Sequence, Set from dataclasses import dataclass @@ -14,6 +13,7 @@ from filelock import FileLock from packaging.version import Version +from . import errors from ._compat.typing import assert_never from .architecture import Architecture from .environment import ParsedEnvironment @@ -290,22 +290,16 @@ def setup_python( call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env) where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip() if where_python != str(venv_path / "Scripts" / "python.exe"): - print( - "cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it." + raise errors.FatalError(msg) # check what pip version we're on if not use_uv: assert (venv_path / "Scripts" / "pip.exe").exists() where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip() if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"): - print( - "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", - file=sys.stderr, - ) - sys.exit(1) + msg = "pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it." + raise errors.FatalError(msg) call("pip", "--version", env=env) @@ -583,7 +577,5 @@ def build(options: Options, tmp_path: Path) -> None: log.build_end() except subprocess.CalledProcessError as error: - log.step_end_with_error( - f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}" - ) - sys.exit(1) + msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" + raise errors.FatalError(msg) from error diff --git a/docs/options.md b/docs/options.md index b322317a6..052172543 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1612,6 +1612,20 @@ Default: Off (0). export CIBW_DEBUG_KEEP_CONTAINER=TRUE ``` +### `CIBW_DEBUG_TRACEBACK` +> Print full traceback when errors occur. + +Print a full traceback for the cibuildwheel process when errors occur. This +option is provided for debugging cibuildwheel. + +This option can also be set using the [command-line option](#command-line) `--debug-traceback`. + +#### Examples + +```shell +export CIBW_DEBUG_TRACEBACK=TRUE +``` + ### `CIBW_BUILD_VERBOSITY` {: #build-verbosity} > Increase/decrease the output of pip wheel @@ -1638,12 +1652,25 @@ Platform-specific environment variables are also available:
``` -## Command line options {: #command-line} +## Command line {: #command-line} + +### Options ```text « subprocess_run("cibuildwheel", "--help") » ``` +### Return codes + +cibuildwheel exits 0 on success, or >0 if an error occurs. + +Specific error codes are defined: + +- 2 means a configuration error +- 3 means no builds are selected (and --allow-empty wasn't passed) +- 4 means you specified an option that has been deprecated. + + ## Placeholders Some options support placeholders, like `{project}`, `{package}` or `{wheel}`, that are substituted by cibuildwheel before they are used. If, for some reason, you need to write the literal name of a placeholder, e.g. literally `{project}` in a command that would ordinarily substitute `{project}`, prefix it with a hash character - `#{project}`. This is only necessary in commands where the specific string between the curly brackets would be substituted - otherwise, strings not modified. diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index 5b85da73b..acd37c1bc 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -344,6 +344,28 @@ def test_before_all(before_all, platform_specific, platform, intercepted_build_a assert build_options.before_all == (before_all or "") +@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"]) +def test_debug_traceback(monkeypatch, method, capfd): + if method == "command_line": + monkeypatch.setattr(sys, "argv", [*sys.argv, "--debug-traceback"]) + elif method == "env_var": + monkeypatch.setenv("CIBW_DEBUG_TRACEBACK", "TRUE") + + # set an option that produces a configuration error + monkeypatch.setenv("CIBW_BUILD_FRONTEND", "invalid_value") + + with pytest.raises(SystemExit) as exit: + main() + assert exit.value.code == 2 + + _, err = capfd.readouterr() + + if method == "unset": + assert "Traceback (most recent call last)" not in err + else: + assert "Traceback (most recent call last)" in err + + def test_defaults(platform, intercepted_build_args): main() diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 1b482c752..5320c06d9 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -7,6 +7,7 @@ import pytest +from cibuildwheel import errors from cibuildwheel.__main__ import get_build_identifiers, get_platform_module from cibuildwheel.bashlex_eval import local_environment_executor from cibuildwheel.environment import parse_environment @@ -127,7 +128,8 @@ def test_passthrough_evil(tmp_path, monkeypatch, env_var_value): xfail_env_parse = pytest.mark.xfail( - raises=SystemExit, reason="until we can figure out the right way to quote these values" + raises=errors.ConfigurationError, + reason="until we can figure out the right way to quote these values", )