Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
619e1dc
WIP - initial punt at audit command
joerick Feb 4, 2026
322f2c4
Add `abi3audit` as a dependency
agriyakhetarpal Feb 16, 2026
f917a24
Add helper functions to check stable ABI wheels
agriyakhetarpal Feb 16, 2026
f2fdc93
Run `abi3audit` for macOS and Windows wheels
agriyakhetarpal Feb 16, 2026
1054219
Copy out of container for repairing?
agriyakhetarpal Feb 16, 2026
03d1708
Add some notes that `cibuildwheel` runs `abi3audit`
agriyakhetarpal Feb 16, 2026
97cc3c2
Add basic unit tests
agriyakhetarpal Feb 16, 2026
97b6d51
Add a basic C extension with `Py_LIMITED_API`
agriyakhetarpal Feb 16, 2026
87ac303
Add a test project that violates Stable ABI
agriyakhetarpal Feb 16, 2026
babfb13
Fix linux test
agriyakhetarpal Feb 16, 2026
8d6988d
Skip abi3 wheel tests for Pyodide
agriyakhetarpal Feb 17, 2026
6e9c281
Patch the correct subprocess module
agriyakhetarpal Feb 17, 2026
597f061
wrap cleanup of abi3audit dir
agriyakhetarpal Feb 17, 2026
a8ee2e5
Merge branch 'audit' into audit2
joerick Apr 1, 2026
91c394f
Write the docs for the new options
joerick Apr 1, 2026
77f571d
Merge remote-tracking branch 'origin/main' into audit2
joerick Apr 2, 2026
e4c7164
Move to above testing in docs
joerick Apr 2, 2026
4f3250d
Implement audit-requires and audit-command
joerick Apr 2, 2026
46ce64a
Some cleanups after self-review
joerick Apr 2, 2026
678d8f5
Add default value
joerick Apr 2, 2026
08330a4
fix type errors
joerick Apr 2, 2026
d145df6
the key is `audit-command`, not `audit`
agriyakhetarpal Apr 13, 2026
ba25da0
Add a variety of tests for audit requires options
agriyakhetarpal Apr 13, 2026
80b6529
Add `test_audit_requires` similar to `test_test_requires`
agriyakhetarpal Apr 13, 2026
86d8f9a
Merge branch 'main' into audit2
agriyakhetarpal Apr 13, 2026
d65a33a
Add some configurability-related audit tests
agriyakhetarpal Apr 13, 2026
c53eb6b
Fix parsing error with options docs leaving out commands
agriyakhetarpal Apr 13, 2026
6c20ebc
Better way to extract version (maybe helps Pyodide?)
agriyakhetarpal Apr 13, 2026
7b1d688
Fix a case of unbound `use_uv`
agriyakhetarpal Apr 13, 2026
b95caa6
Standardise: rename to `abi3_wheel`
agriyakhetarpal Apr 13, 2026
7886e96
Fix audit command run message
agriyakhetarpal Apr 13, 2026
f7d9ccb
Simplify custom audit command a bit
agriyakhetarpal Apr 13, 2026
672299b
Remove unnecessary skip for Pyodide
agriyakhetarpal Apr 13, 2026
b791437
Pyodide should have no default audit command
agriyakhetarpal Apr 13, 2026
dc354e0
More accurate skip messages for Pyodide skips
agriyakhetarpal Apr 13, 2026
e745612
Wheels are audited after they are repaired
agriyakhetarpal Apr 13, 2026
ab87881
Regenerate constraints to include `abi3audit`
agriyakhetarpal Apr 13, 2026
389c580
Fix typos
agriyakhetarpal Apr 13, 2026
d80ed3b
Some attempts for Windows fixes
agriyakhetarpal Apr 13, 2026
5bcd54c
Check `pyvenv.cfg` instead of directory existence
agriyakhetarpal Apr 13, 2026
7168365
Add validation for lack of wheel placeholders
agriyakhetarpal Apr 13, 2026
45e8825
Try yet another Windows `uv` fix
agriyakhetarpal Apr 13, 2026
6bdb9d7
Regenerate diagram and re-trigger Azure CI
agriyakhetarpal Apr 13, 2026
55bea88
Add missing `import sys` for abi3 C extension tests
agriyakhetarpal Apr 13, 2026
854ee13
Remove audit-command at the global level
agriyakhetarpal Apr 13, 2026
e3c3b01
Clarify `abi3audit` pinning a little bit
agriyakhetarpal Apr 13, 2026
6283752
Merge main
agriyakhetarpal Apr 15, 2026
dcb4cbb
Regen constraints
agriyakhetarpal Apr 15, 2026
cb81bd4
Discard changes to cibuildwheel/resources/constraints-pyodide312.txt
agriyakhetarpal Apr 15, 2026
7cfec02
Discard changes to cibuildwheel/resources/constraints-pyodide313.txt
agriyakhetarpal Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |


<!--[[[end]]] (sum: dbfwOkj/k/) -->
<!--[[[end]]] (sum: b7YIjCyCkf) -->

These options can be specified in a pyproject.toml file, or as environment variables, see [configuration docs](https://cibuildwheel.pypa.io/en/latest/configuration/).

Expand Down
8 changes: 8 additions & 0 deletions bin/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"}
Expand Down
128 changes: 128 additions & 0 deletions cibuildwheel/audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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 = find_uv() is not None
version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
dependency_constraint = build_options.dependency_constraints.get_for_python_version(
Comment thread
agriyakhetarpal marked this conversation as resolved.
version=version, tmp_dir=tmp_dir
)

audit_venv_dir = tmp_dir / "audit_venv"
if not (audit_venv_dir / "pyvenv.cfg").exists():
env = virtualenv(
version,
Path(sys.executable),
audit_venv_dir,
dependency_constraint=dependency_constraint,
use_uv=use_uv,
)
else:
env = activate_virtualenv(audit_venv_dir)
Comment thread
agriyakhetarpal marked this conversation as resolved.

# 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
Comment thread
agriyakhetarpal marked this conversation as resolved.

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
Comment thread
agriyakhetarpal marked this conversation as resolved.
6 changes: 6 additions & 0 deletions cibuildwheel/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions cibuildwheel/platforms/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions cibuildwheel/platforms/ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions cibuildwheel/platforms/linux.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import dataclasses
import shutil
import subprocess
import sys
import textwrap
Expand All @@ -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
Expand Down Expand Up @@ -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...")

Expand Down
3 changes: 3 additions & 0 deletions cibuildwheel/platforms/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions cibuildwheel/platforms/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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...")

Expand Down
3 changes: 3 additions & 0 deletions cibuildwheel/platforms/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading