diff --git a/README.md b/README.md index 2caeeaa76..31e046fb3 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ The following diagram summarises the steps that cibuildwheel takes on each platf | | [`container-engine`](https://cibuildwheel.pypa.io/en/stable/options/#container-engine) | Specify the container engine to use when building Linux wheels | | | [`dependency-versions`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Control the versions of the tools cibuildwheel uses | | | [`pyodide-version`](https://cibuildwheel.pypa.io/en/stable/options/#pyodide-version) | Specify the Pyodide version to use for `pyodide` platform builds | +| **Auditing** | [`audit-requires`](https://cibuildwheel.pypa.io/en/stable/options/#audit-requires) | Install Python dependencies for the audit step | +| | [`audit-command`](https://cibuildwheel.pypa.io/en/stable/options/#audit-command) | Use a tool to check wheels before the end of the run | | **Testing** | [`test-command`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | The command to test each built wheel | | | [`before-test`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel | | | [`test-sources`](https://cibuildwheel.pypa.io/en/stable/options/#test-sources) | Paths that are copied into the working directory of the tests | @@ -170,7 +172,7 @@ The following diagram summarises the steps that cibuildwheel takes on each platf | | [`build-verbosity`](https://cibuildwheel.pypa.io/en/stable/options/#build-verbosity) | Increase/decrease the output of the build | - + These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/). diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 022ffe356..52e44109a 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -39,6 +39,12 @@ description: cibuildwheel's settings. type: object properties: + audit-command: + description: Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels. + type: string_array + audit-requires: + description: Install Python dependencies for the audit step. + type: string_array archs: description: Change the architectures built on your machine by default. type: string_array @@ -309,6 +315,8 @@ type: object additionalProperties: false properties: + audit-command: {"$ref": "#/$defs/inherit"} + audit-requires: {"$ref": "#/$defs/inherit"} before-all: {"$ref": "#/$defs/inherit"} before-build: {"$ref": "#/$defs/inherit"} xbuild-tools: {"$ref": "#/$defs/inherit"} diff --git a/cibuildwheel/audit.py b/cibuildwheel/audit.py new file mode 100644 index 000000000..f91b01558 --- /dev/null +++ b/cibuildwheel/audit.py @@ -0,0 +1,132 @@ +import subprocess +import sys +from pathlib import Path + +from cibuildwheel import errors +from cibuildwheel.logger import log +from cibuildwheel.options import BuildOptions +from cibuildwheel.util.cmd import call, shell +from cibuildwheel.util.helpers import prepare_command +from cibuildwheel.util.packaging import is_abi3_wheel +from cibuildwheel.venv import activate_virtualenv, find_uv, virtualenv + + +def run_audit( + *, + tmp_dir: Path, + build_options: BuildOptions, + wheel: Path, +) -> None: + """ + Run the audit commands on a single wheel. + + Creates a virtualenv (or reuses an existing one) and installs any + audit requirements, then runs each audit command template against + the wheel. Commands containing {abi3_wheel} are skipped for + non-abi3 wheels. + """ + + if not needs_audit(build_options.audit_command, wheel.name): + return + + log.step("Auditing wheel...") + + use_uv = build_options.build_frontend.name in {"build[uv]", "uv"} + version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + dependency_constraint = build_options.dependency_constraints.get_for_python_version( + version=version, tmp_dir=tmp_dir + ) + + # Use the base interpreter, not the venv python, to avoid nested-venv + # issues where pip can't be found (seen on Windows + Python 3.13). + host_python = Path(getattr(sys, "_base_executable", sys.executable)) + + audit_venv_dir = tmp_dir / "audit_venv" + if not (audit_venv_dir / "pyvenv.cfg").exists(): + env = virtualenv( + version, + host_python, + audit_venv_dir, + dependency_constraint=dependency_constraint, + use_uv=use_uv, + ) + else: + env = activate_virtualenv(audit_venv_dir) + + # install audit requirements. This is run every time in case the user has + # defined overrides. + audit_requires = build_options.audit_requires + if audit_requires: + print(f"Installing audit dependencies: {', '.join(audit_requires)}") + + pip: list[str] + if use_uv: + uv_path = find_uv() + assert uv_path is not None + pip = [str(uv_path), "pip"] + else: + pip = ["pip"] + # we pin if the audit-requires is left as the default "abi3audit" + should_pin = audit_requires == ["abi3audit"] and dependency_constraint + + call( + *pip, + "install", + *(["--constraint", str(dependency_constraint)] if should_pin else []), + *audit_requires, + env=env, + ) + + audit_command = build_options.audit_command + + for command_template in audit_command: + if "{abi3_wheel}" in command_template and "{wheel}" in command_template: + msg = ( + f"Invalid audit command {command_template!r}: cannot contain both {{abi3_wheel}} " + "and {{wheel}} placeholders" + ) + raise errors.ConfigurationError(msg) + + if "{abi3_wheel}" in command_template and not is_abi3_wheel(wheel.name): + continue + + prepared_command = prepare_command( + command_template, + abi3_wheel=wheel, + wheel=wheel, + project=".", + package=build_options.package_dir, + ) + + print(f"Running audit command: {prepared_command}") + try: + shell(prepared_command, env=env) + except subprocess.CalledProcessError as e: + print(f"Audit command failed with exit code {e.returncode}") + msg = f"Audit command failed: {prepared_command}" + raise errors.AuditCommandFailedError(msg) from e + + +def needs_audit(audit_commands: list[str], wheel_name: str) -> bool: + saw_abi3_placeholder = False + for audit_command in audit_commands: + if "{abi3_wheel}" not in audit_command and "{wheel}" not in audit_command: + msg = ( + f"Invalid audit command {audit_command!r}: must contain either " + "{{abi3_wheel}} or {{wheel}} placeholder" + ) + raise errors.ConfigurationError(msg) + + if "{abi3_wheel}" in audit_command: + saw_abi3_placeholder = True + if is_abi3_wheel(wheel_name): + return True + elif "{wheel}" in audit_command: + return True + + if saw_abi3_placeholder: + print("No audit required for this wheel, as it is not abi3") + else: + print("No audit configured") + + return False diff --git a/cibuildwheel/errors.py b/cibuildwheel/errors.py index 9f0a71f15..c03b0cd0c 100644 --- a/cibuildwheel/errors.py +++ b/cibuildwheel/errors.py @@ -103,3 +103,9 @@ def __init__(self, wheels: list[str]) -> None: ) super().__init__(message) self.return_code = 8 + + +class AuditCommandFailedError(FatalError): + def __init__(self, message: str) -> None: + super().__init__(message) + self.return_code = 9 diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 972c7b2c6..ea57b51ce 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -125,6 +125,8 @@ class BuildOptions: test_groups: list[str] test_environment: ParsedEnvironment test_runtime: TestRuntimeConfig + audit_requires: list[str] + audit_command: list[str] build_verbosity: int build_frontend: BuildFrontendConfig config_settings: str @@ -894,6 +896,15 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions: pyodide_version = self.reader.get("pyodide-version", env_plat=False) + audit_command_str = self.reader.get( + "audit-command", option_format=ListFormat(sep=" && ") + ) + audit_command = audit_command_str.split(" && ") if audit_command_str else [] + + audit_requires = self.reader.get( + "audit-requires", option_format=ListFormat(sep=" ") + ).split() + return BuildOptions( globals=self.globals, test_command=test_command, @@ -917,6 +928,8 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions: config_settings=config_settings, container_engine=container_engine, pyodide_version=pyodide_version or None, + audit_command=audit_command, + audit_requires=audit_requires, ) def check_for_invalid_configuration(self, identifiers: Iterable[str]) -> None: diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index 50b07884c..7913c5556 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -24,6 +24,7 @@ from cibuildwheel import errors, platforms # pylint: disable=cyclic-import from cibuildwheel.architecture import Architecture, arch_synonym +from cibuildwheel.audit import run_audit from cibuildwheel.frontend import get_build_frontend_extra_flags, parse_config_settings from cibuildwheel.logger import log from cibuildwheel.options import BuildOptions, Options @@ -150,6 +151,7 @@ def build(options: Options, tmp_path: Path) -> None: before_build(state) built_wheel = build_wheel(state) repaired_wheel = repair_wheel(state, built_wheel) + run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) test_wheel(state, repaired_wheel, build_frontend=build_options.build_frontend.name) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 9e2474483..54092b065 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -14,6 +14,7 @@ from cibuildwheel import errors from cibuildwheel.architecture import Architecture +from cibuildwheel.audit import run_audit from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import BuildFrontendName, get_build_frontend_extra_flags from cibuildwheel.logger import log @@ -539,10 +540,12 @@ def build(options: Options, tmp_path: Path) -> None: if repaired_wheel.name in {wheel.name for wheel in built_wheels}: raise errors.AlreadyBuiltWheelError(repaired_wheel.name) - test_wheel = repaired_wheel - log.step_end() + run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) + + test_wheel = repaired_wheel + if build_options.test_command and build_options.test_selector(config.identifier): if not config.is_simulator: log.step("Skipping tests on non-simulator SDK") diff --git a/cibuildwheel/platforms/linux.py b/cibuildwheel/platforms/linux.py index d5d9366b7..6dbde2046 100644 --- a/cibuildwheel/platforms/linux.py +++ b/cibuildwheel/platforms/linux.py @@ -1,5 +1,6 @@ import contextlib import dataclasses +import shutil import subprocess import sys import textwrap @@ -10,6 +11,7 @@ from cibuildwheel import errors from cibuildwheel.architecture import Architecture +from cibuildwheel.audit import needs_audit, run_audit from cibuildwheel.frontend import get_build_frontend_extra_flags from cibuildwheel.logger import log from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform @@ -359,6 +361,18 @@ def build_in_container( if repaired_wheel.name in {wheel.name for wheel in built_wheels}: raise errors.AlreadyBuiltWheelError(repaired_wheel.name) + log.step_end() + + if needs_audit(build_options.audit_command, repaired_wheel.name): + local_abi3audit_dir = local_identifier_tmp_dir / "audit" + local_abi3audit_dir.mkdir(parents=True, exist_ok=True) + try: + container.copy_out(repaired_wheel_dir, local_abi3audit_dir) + local_wheel = local_abi3audit_dir / repaired_wheel.name + run_audit(tmp_dir=local_tmp_dir, build_options=build_options, wheel=local_wheel) + finally: + shutil.rmtree(local_abi3audit_dir, ignore_errors=True) + if build_options.test_command and build_options.test_selector(config.identifier): log.step("Testing wheel...") diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index 37c3d799c..f47b3a821 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -17,6 +17,7 @@ from cibuildwheel import errors from cibuildwheel.architecture import Architecture +from cibuildwheel.audit import run_audit from cibuildwheel.ci import detect_ci_provider from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import BuildFrontendName, get_build_frontend_extra_flags @@ -569,6 +570,8 @@ def build(options: Options, tmp_path: Path) -> None: log.step_end() + run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) + if build_options.test_command and build_options.test_selector(config.identifier): machine_arch = platform.machine() python_arch = call( diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index e0560b4a0..1cb52673b 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -16,6 +16,7 @@ from cibuildwheel import errors from cibuildwheel.architecture import Architecture +from cibuildwheel.audit import run_audit from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import get_build_frontend_extra_flags from cibuildwheel.logger import log @@ -461,6 +462,8 @@ def build(options: Options, tmp_path: Path) -> None: if repaired_wheel.name in {wheel.name for wheel in built_wheels}: raise errors.AlreadyBuiltWheelError(repaired_wheel.name) + run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) + if build_options.test_command and build_options.test_selector(config.identifier): log.step("Testing wheel...") diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index 3bb9e86f7..fdbd3cf82 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -14,6 +14,7 @@ from cibuildwheel import errors from cibuildwheel.architecture import Architecture +from cibuildwheel.audit import run_audit from cibuildwheel.environment import ParsedEnvironment from cibuildwheel.frontend import BuildFrontendName, get_build_frontend_extra_flags from cibuildwheel.logger import log @@ -559,6 +560,8 @@ def build(options: Options, tmp_path: Path) -> None: if repaired_wheel.name in {wheel.name for wheel in built_wheels}: raise errors.AlreadyBuiltWheelError(repaired_wheel.name) + run_audit(tmp_dir=tmp_path, build_options=build_options, wheel=repaired_wheel) + test_selected = options.globals.test_selector(config.identifier) if test_selected and config.arch == "ARM64" != platform_module.machine(): log.warning( diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index a82518f4b..022faf129 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -27,6 +27,36 @@ "description": "cibuildwheel's settings.", "type": "object", "properties": { + "audit-command": { + "description": "Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "title": "CIBW_AUDIT_COMMAND" + }, + "audit-requires": { + "description": "Install Python dependencies for the audit step.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "title": "CIBW_AUDIT_REQUIRES" + }, "archs": { "description": "Change the architectures built on your machine by default.", "oneOf": [ @@ -635,6 +665,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, "before-all": { "$ref": "#/$defs/inherit" }, @@ -682,6 +718,12 @@ } } }, + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "before-all": { "$ref": "#/properties/before-all" }, @@ -800,6 +842,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -930,6 +978,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -993,6 +1047,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1069,6 +1129,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1132,6 +1198,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1195,6 +1267,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, diff --git a/cibuildwheel/resources/constraints-python310.txt b/cibuildwheel/resources/constraints-python310.txt index 3f218e356..1c6c3fe05 100644 --- a/cibuildwheel/resources/constraints-python310.txt +++ b/cibuildwheel/resources/constraints-python310.txt @@ -1,42 +1,92 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +exceptiongroup==1.3.1 + # via cattrs +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize importlib-metadata==9.0.0 # via build +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit tomli==2.4.1 # via build typing-extensions==4.15.0 # via + # cattrs # delocate + # exceptiongroup # virtualenv -virtualenv==21.2.1 +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata diff --git a/cibuildwheel/resources/constraints-python311.txt b/cibuildwheel/resources/constraints-python311.txt index 636dfbb8c..f9b23d07b 100644 --- a/cibuildwheel/resources/constraints-python311.txt +++ b/cibuildwheel/resources/constraints-python311.txt @@ -1,34 +1,82 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.2.1 + # via + # cattrs + # delocate +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python312.txt b/cibuildwheel/resources/constraints-python312.txt index 636dfbb8c..f9b23d07b 100644 --- a/cibuildwheel/resources/constraints-python312.txt +++ b/cibuildwheel/resources/constraints-python312.txt @@ -1,34 +1,82 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.2.1 + # via + # cattrs + # delocate +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python313.txt b/cibuildwheel/resources/constraints-python313.txt index 636dfbb8c..f9b23d07b 100644 --- a/cibuildwheel/resources/constraints-python313.txt +++ b/cibuildwheel/resources/constraints-python313.txt @@ -1,34 +1,82 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.2.1 + # via + # cattrs + # delocate +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python314.txt b/cibuildwheel/resources/constraints-python314.txt index 636dfbb8c..f9b23d07b 100644 --- a/cibuildwheel/resources/constraints-python314.txt +++ b/cibuildwheel/resources/constraints-python314.txt @@ -1,34 +1,82 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.2.1 + # via + # cattrs + # delocate +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python38.txt b/cibuildwheel/resources/constraints-python38.txt index 84c6ceae5..bd18367f9 100644 --- a/cibuildwheel/resources/constraints-python38.txt +++ b/cibuildwheel/resources/constraints-python38.txt @@ -1,42 +1,93 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.17 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.4.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==25.3.0 + # via + # cattrs + # requests-cache build==1.2.2.post1 # via -r cibuildwheel/resources/constraints.in +cattrs==24.1.3 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.12.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv +exceptiongroup==1.3.1 + # via cattrs filelock==3.16.1 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize importlib-metadata==8.5.0 # via build +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==24.2 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==25.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.3.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.19.2 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.32.4 + # via + # abi3audit + # requests-cache +requests-cache==1.2.1 + # via abi3audit +rich==13.8.1 + # via abi3audit tomli==2.4.1 # via build typing-extensions==4.13.2 # via + # cattrs # delocate + # exceptiongroup + # rich # virtualenv -virtualenv==21.2.1 +url-normalize==2.2.1 + # via requests-cache +urllib3==2.2.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in zipp==3.20.2 # via importlib-metadata diff --git a/cibuildwheel/resources/constraints-python39.txt b/cibuildwheel/resources/constraints-python39.txt index 441105022..0838442a5 100644 --- a/cibuildwheel/resources/constraints-python39.txt +++ b/cibuildwheel/resources/constraints-python39.txt @@ -1,42 +1,92 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.25 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.4.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==25.3.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv +exceptiongroup==1.3.1 + # via cattrs filelock==3.19.1 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize importlib-metadata==8.7.1 # via build +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==25.0 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.4.0 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.32.5 + # via + # abi3audit + # requests-cache +requests-cache==1.2.1 + # via abi3audit +rich==14.2.0 + # via abi3audit tomli==2.4.1 # via build typing-extensions==4.15.0 # via + # cattrs # delocate + # exceptiongroup # virtualenv -virtualenv==21.2.1 +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in -zipp==3.23.0 +zipp==3.23.1 # via importlib-metadata diff --git a/cibuildwheel/resources/constraints.in b/cibuildwheel/resources/constraints.in index 50bfabb6e..1f7841f01 100644 --- a/cibuildwheel/resources/constraints.in +++ b/cibuildwheel/resources/constraints.in @@ -2,3 +2,4 @@ pip build delocate virtualenv +abi3audit diff --git a/cibuildwheel/resources/constraints.txt b/cibuildwheel/resources/constraints.txt index 636dfbb8c..f9b23d07b 100644 --- a/cibuildwheel/resources/constraints.txt +++ b/cibuildwheel/resources/constraints.txt @@ -1,34 +1,82 @@ # This file was autogenerated by uv via the following command: # nox -s update_constraints +abi3audit==0.0.26 + # via -r cibuildwheel/resources/constraints.in +abi3info==2025.11.29 + # via abi3audit altgraph==0.17.5 # via macholib +attrs==26.1.0 + # via + # cattrs + # requests-cache build==1.4.3 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.2.25 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv -filelock==3.25.2 +filelock==3.28.0 # via # python-discovery # virtualenv +idna==3.11 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.0 +markdown-it-py==4.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 # via + # abi3audit # build # delocate +pefile==2024.8.26 + # via abi3audit pip==26.0.1 # via -r cibuildwheel/resources/constraints.in platformdirs==4.9.6 # via # python-discovery + # requests-cache # virtualenv +pyelftools==0.32 + # via abi3audit +pygments==2.20.0 + # via rich pyproject-hooks==1.2.0 # via build python-discovery==1.2.2 # via virtualenv +requests==2.33.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.1 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.2.1 + # via + # cattrs + # delocate +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.2.4 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index 78895bf9a..ab7d31905 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -5,6 +5,8 @@ test-skip = "" enable = [] archs = ["auto"] +audit-requires = ["abi3audit"] +audit-command = "abi3audit --strict --report {abi3_wheel}" build-frontend = "default" config-settings = {} dependency-versions = "pinned" @@ -64,3 +66,4 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest [tool.cibuildwheel.ios] [tool.cibuildwheel.pyodide] +audit-command = "" diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index 82e3494c4..1dc71eaf0 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -177,3 +177,9 @@ def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None: return wheel return None + + +def is_abi3_wheel(wheel_name: str) -> bool: + """Check if a wheel uses the abi3 stable ABI based on its filename.""" + _, _, _, tags = parse_wheel_filename(wheel_name) + return any(tag.abi == "abi3" for tag in tags) diff --git a/cibuildwheel/venv.py b/cibuildwheel/venv.py index dd4fe90aa..5e68da9b8 100644 --- a/cibuildwheel/venv.py +++ b/cibuildwheel/venv.py @@ -139,10 +139,7 @@ def virtualenv( python, venv_path, ) - paths = [str(venv_path), str(venv_path / "Scripts")] if _IS_WIN else [str(venv_path / "bin")] - venv_env = os.environ.copy() if env is None else env.copy() - venv_env["PATH"] = os.pathsep.join([*paths, venv_env["PATH"]]) - venv_env["VIRTUAL_ENV"] = str(venv_path) + venv_env = activate_virtualenv(venv_path, env=env) if not use_uv and pip_version == "embed": call( "python", @@ -158,6 +155,20 @@ def virtualenv( return venv_env +def activate_virtualenv( + venv_path: Path, + env: dict[str, str] | None = None, +) -> dict[str, str]: + """ + Return a copy of the environment with the virtualenv at `venv_path` activated. + """ + paths = [str(venv_path), str(venv_path / "Scripts")] if _IS_WIN else [str(venv_path / "bin")] + venv_env = os.environ.copy() if env is None else env.copy() + venv_env["PATH"] = os.pathsep.join([*paths, venv_env["PATH"]]) + venv_env["VIRTUAL_ENV"] = str(venv_path) + return venv_env + + def find_uv() -> Path | None: # Prefer uv in our environment with contextlib.suppress(ImportError, FileNotFoundError): diff --git a/docs/data/how-it-works.png b/docs/data/how-it-works.png index bbbc2a301..e066fcdd7 100644 Binary files a/docs/data/how-it-works.png and b/docs/data/how-it-works.png differ diff --git a/docs/diagram.html b/docs/diagram.html index 8d9785e2f..07e62c028 100644 --- a/docs/diagram.html +++ b/docs/diagram.html @@ -38,7 +38,7 @@