diff --git a/README.md b/README.md index 10f86513..e8c6874f 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,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 | @@ -169,7 +171,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 6d0826be..59654d70 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -38,6 +38,12 @@ additionalProperties: false 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 @@ -308,6 +314,8 @@ items: 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 00000000..f91b0155 --- /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 9f0a71f1..c03b0cd0 100644 --- a/cibuildwheel/errors.py +++ b/cibuildwheel/errors.py @@ -103,3 +103,9 @@ class RepairStepProducedMultipleWheelsError(FatalError): ) 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 705c2db0..1adbaf87 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 @@ -892,6 +894,15 @@ class Options: 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, @@ -915,6 +926,8 @@ class Options: 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 94a5de1c..ae2b2a79 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -24,6 +24,7 @@ from filelock import FileLock 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, @@ -154,6 +155,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 217b4ddf..40c66b87 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -14,6 +14,7 @@ from filelock import FileLock 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, @@ -546,10 +547,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 437e4dc7..af8c2208 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 typing import TYPE_CHECKING, assert_never 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, prepare_config_settings from cibuildwheel.logger import log from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform @@ -372,6 +374,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 7a5fd214..bb830a34 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -17,6 +17,7 @@ from packaging.version import Version 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 ( @@ -576,6 +577,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() testing_archs: list[Literal["x86_64", "arm64"]] diff --git a/cibuildwheel/platforms/pyodide.py b/cibuildwheel/platforms/pyodide.py index 2c2e9082..262eac2b 100644 --- a/cibuildwheel/platforms/pyodide.py +++ b/cibuildwheel/platforms/pyodide.py @@ -16,6 +16,7 @@ from filelock import FileLock 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, prepare_config_settings from cibuildwheel.logger import log @@ -463,6 +464,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 f0b12068..1a858c8f 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -14,6 +14,7 @@ from filelock import FileLock 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, @@ -585,6 +586,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 c5cefc82..7d2522a6 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -26,6 +26,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": [ @@ -634,6 +664,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, "before-all": { "$ref": "#/$defs/inherit" }, @@ -681,6 +717,12 @@ } } }, + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "before-all": { "$ref": "#/properties/before-all" }, @@ -799,6 +841,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -929,6 +977,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -992,6 +1046,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1068,6 +1128,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1131,6 +1197,12 @@ "type": "object", "additionalProperties": false, "properties": { + "audit-command": { + "$ref": "#/properties/audit-command" + }, + "audit-requires": { + "$ref": "#/properties/audit-requires" + }, "archs": { "$ref": "#/properties/archs" }, @@ -1194,6 +1266,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-pyodide312.txt b/cibuildwheel/resources/constraints-pyodide312.txt index 4a9e1323..f0c7ed52 100644 --- a/cibuildwheel/resources/constraints-pyodide312.txt +++ b/cibuildwheel/resources/constraints-pyodide312.txt @@ -23,7 +23,7 @@ filelock==3.29.0 # via # python-discovery # virtualenv -idna==3.14 +idna==3.15 # via requests leb128==1.0.9 # via auditwheel-emscripten @@ -37,8 +37,7 @@ packaging==26.2 # build # pyodide-build # wheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" +pip==26.1.1 # via -r .nox/update_constraints/tmp/constraints-pyodide.in platformdirs==4.9.6 # via @@ -63,9 +62,9 @@ pyodide-lock==0.1.3 # via pyodide-build pyproject-hooks==1.2.0 # via build -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv -requests==2.34.0 +requests==2.34.1 # via pyodide-build rich==15.0.0 # via @@ -82,7 +81,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via requests -virtualenv==21.3.1 +virtualenv==21.3.3 # via # build # pyodide-build diff --git a/cibuildwheel/resources/constraints-pyodide313.txt b/cibuildwheel/resources/constraints-pyodide313.txt index d74cb73f..f0c7ed52 100644 --- a/cibuildwheel/resources/constraints-pyodide313.txt +++ b/cibuildwheel/resources/constraints-pyodide313.txt @@ -23,7 +23,7 @@ filelock==3.29.0 # via # python-discovery # virtualenv -idna==3.14 +idna==3.15 # via requests leb128==1.0.9 # via auditwheel-emscripten @@ -62,9 +62,9 @@ pyodide-lock==0.1.3 # via pyodide-build pyproject-hooks==1.2.0 # via build -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv -requests==2.34.0 +requests==2.34.1 # via pyodide-build rich==15.0.0 # via @@ -81,7 +81,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via requests -virtualenv==21.3.1 +virtualenv==21.3.3 # via # build # pyodide-build diff --git a/cibuildwheel/resources/constraints-pyodide314.txt b/cibuildwheel/resources/constraints-pyodide314.txt index d74cb73f..f0c7ed52 100644 --- a/cibuildwheel/resources/constraints-pyodide314.txt +++ b/cibuildwheel/resources/constraints-pyodide314.txt @@ -23,7 +23,7 @@ filelock==3.29.0 # via # python-discovery # virtualenv -idna==3.14 +idna==3.15 # via requests leb128==1.0.9 # via auditwheel-emscripten @@ -62,9 +62,9 @@ pyodide-lock==0.1.3 # via pyodide-build pyproject-hooks==1.2.0 # via build -python-discovery==1.3.0 +python-discovery==1.3.1 # via virtualenv -requests==2.34.0 +requests==2.34.1 # via pyodide-build rich==15.0.0 # via @@ -81,7 +81,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.7.0 # via requests -virtualenv==21.3.1 +virtualenv==21.3.3 # via # build # pyodide-build diff --git a/cibuildwheel/resources/constraints-python310.txt b/cibuildwheel/resources/constraints-python310.txt index e36c1168..8d2dbc3c 100644 --- a/cibuildwheel/resources/constraints-python310.txt +++ b/cibuildwheel/resources/constraints-python310.txt @@ -1,47 +1,96 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 # via -r cibuildwheel/resources/constraints.in distlib==0.4.0 # via virtualenv +exceptiongroup==1.3.1 + # via cattrs filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize importlib-metadata==9.0.0 # via build +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" + # via + # abi3audit + # delvewheel +pip==26.1.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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # 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.3.1 +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in zipp==3.23.1 # via importlib-metadata diff --git a/cibuildwheel/resources/constraints-python311.txt b/cibuildwheel/resources/constraints-python311.txt index 043692b3..34bb22ce 100644 --- a/cibuildwheel/resources/constraints-python311.txt +++ b/cibuildwheel/resources/constraints-python311.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,60 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel + # via + # abi3audit + # delvewheel pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" # 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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python312.txt b/cibuildwheel/resources/constraints-python312.txt index 043692b3..34bb22ce 100644 --- a/cibuildwheel/resources/constraints-python312.txt +++ b/cibuildwheel/resources/constraints-python312.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,60 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel + # via + # abi3audit + # delvewheel pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" # 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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python313.txt b/cibuildwheel/resources/constraints-python313.txt index 043692b3..54ed30f2 100644 --- a/cibuildwheel/resources/constraints-python313.txt +++ b/cibuildwheel/resources/constraints-python313.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,59 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" + # via + # abi3audit + # delvewheel +pip==26.1.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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python314.txt b/cibuildwheel/resources/constraints-python314.txt index 043692b3..54ed30f2 100644 --- a/cibuildwheel/resources/constraints-python314.txt +++ b/cibuildwheel/resources/constraints-python314.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,59 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" + # via + # abi3audit + # delvewheel +pip==26.1.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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python315.txt b/cibuildwheel/resources/constraints-python315.txt index 043692b3..54ed30f2 100644 --- a/cibuildwheel/resources/constraints-python315.txt +++ b/cibuildwheel/resources/constraints-python315.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,59 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" + # via + # abi3audit + # delvewheel +pip==26.1.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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/constraints-python39.txt b/cibuildwheel/resources/constraints-python39.txt index e23de627..f96b1547 100644 --- a/cibuildwheel/resources/constraints-python39.txt +++ b/cibuildwheel/resources/constraints-python39.txt @@ -1,46 +1,96 @@ # 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.4 # via -r cibuildwheel/resources/constraints.in +cattrs==25.3.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 # 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.15 + # via + # requests + # url-normalize importlib-metadata==8.7.1 # via build +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate -packaging==26.2 +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 delvewheel + # via + # abi3audit + # delvewheel 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.3.0 +python-discovery==1.3.1 # 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.3.1 +url-normalize==2.2.1 + # via requests-cache +urllib3==2.6.3 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in zipp==3.23.1 # via importlib-metadata diff --git a/cibuildwheel/resources/constraints.in b/cibuildwheel/resources/constraints.in index 8f40eb61..72b0befd 100644 --- a/cibuildwheel/resources/constraints.in +++ b/cibuildwheel/resources/constraints.in @@ -3,3 +3,4 @@ build delocate delvewheel virtualenv +abi3audit diff --git a/cibuildwheel/resources/constraints.txt b/cibuildwheel/resources/constraints.txt index 043692b3..54ed30f2 100644 --- a/cibuildwheel/resources/constraints.txt +++ b/cibuildwheel/resources/constraints.txt @@ -1,9 +1,23 @@ # 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.5.0 # via -r cibuildwheel/resources/constraints.in +cattrs==26.1.0 + # via requests-cache +certifi==2026.4.22 + # via requests +charset-normalizer==3.4.7 + # via requests delocate==0.13.0 # via -r cibuildwheel/resources/constraints.in delvewheel==1.12.1 @@ -14,26 +28,59 @@ filelock==3.29.0 # via # python-discovery # virtualenv +idna==3.15 + # via + # requests + # url-normalize +kaitaistruct==0.11 + # via abi3audit macholib==1.16.4 # via delocate +markdown-it-py==4.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py packaging==26.2 # via + # abi3audit # build # delocate pefile==2024.8.26 - # via delvewheel -pip==26.1.1; implementation_name != "graalpy" or platform_system != "Windows" -pip==26.0.1; implementation_name == "graalpy" and platform_system == "Windows" + # via + # abi3audit + # delvewheel +pip==26.1.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.3.0 +python-discovery==1.3.1 # via virtualenv +requests==2.34.1 + # via + # abi3audit + # requests-cache +requests-cache==1.3.2 + # via abi3audit +rich==15.0.0 + # via abi3audit typing-extensions==4.15.0 - # via delocate -virtualenv==21.3.1 + # via + # cattrs + # delocate +url-normalize==3.0.0 + # via requests-cache +urllib3==2.7.0 + # via + # requests + # requests-cache +virtualenv==21.3.3 # via -r cibuildwheel/resources/constraints.in diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index 2807a1a5..e0937490 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" @@ -65,3 +67,4 @@ repair-wheel-command = "delvewheel repair -w {dest_dir} -v {wheel}" [tool.cibuildwheel.ios] [tool.cibuildwheel.pyodide] +audit-command = "" diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index 82e3494c..1dc71eaf 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 c03c2923..f38ab11e 100644 --- a/cibuildwheel/venv.py +++ b/cibuildwheel/venv.py @@ -166,10 +166,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", @@ -185,6 +182,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 bbbc2a30..e066fcdd 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 8d9785e2..07e62c02 100644 --- a/docs/diagram.html +++ b/docs/diagram.html @@ -38,7 +38,7 @@