feet: general approach to auditing wheels with abi3audit default (#2805)

* WIP - initial punt at audit command

* Add `abi3audit` as a dependency

* Add helper functions to check stable ABI wheels

* Run `abi3audit` for macOS and Windows wheels

* Copy out of container for repairing?

* Add some notes that `cibuildwheel` runs `abi3audit`

* Add basic unit tests

* Add a basic C extension with `Py_LIMITED_API`

* Add a test project that violates Stable ABI

* Fix linux test

* Skip abi3 wheel tests for Pyodide

* Patch the correct subprocess module

* wrap cleanup of abi3audit dir

* Write the docs for the new options

* Move to above testing in docs

* Implement audit-requires and audit-command

* Some cleanups after self-review

* Add default value

* fix type errors

* the key is `audit-command`, not `audit`

* Add a variety of tests for audit requires options

* Add `test_audit_requires` similar to `test_test_requires`

* Add some configurability-related audit tests

* Fix parsing error with options docs leaving out commands

* Better way to extract version (maybe helps Pyodide?)

* Fix a case of unbound `use_uv`

* Standardise: rename to `abi3_wheel`

* Fix audit command run message

* Simplify custom audit command a bit

* Remove unnecessary skip for Pyodide

* Pyodide should have no default audit command

* More accurate skip messages for Pyodide skips

* Wheels are audited after they are repaired

* Regenerate constraints to include `abi3audit`

* Fix typos

* Some attempts for Windows fixes

* Check `pyvenv.cfg` instead of directory existence

* Add validation for lack of wheel placeholders

* Try yet another Windows `uv` fix

* Regenerate diagram and re-trigger Azure CI

* Add missing `import sys` for abi3 C extension tests

* Remove audit-command at the global level

* Clarify `abi3audit` pinning a little bit

* Regen constraints

* Discard changes to cibuildwheel/resources/constraints-pyodide312.txt

* Discard changes to cibuildwheel/resources/constraints-pyodide313.txt

* try opt-in uv again

* fix issue on windows on Python 3.13 related to nested venvs

On win / python 3.13, virtualenv creates a venv where the 'home'
points back to the venv that sys.executable was running in, rather
than the root install. that seemingly leads to problems with package
resolution, where pip.exe couldn't find the pip python package.
this appears to fix it!

* Update constraints

* chore: revert python-discovery bump

Assisted-by: OpenCode:glm-5.1
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: restore workaround for graalpy

Assisted-by: OpenCode:glm-5.1
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

---------

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com>
Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
This commit is contained in:
Joe Rickerby
2026-05-14 07:41:14 -07:00
committed by GitHub
co-authored by Agriya Khetarpal Henry Schreiner
parent e04baff444
commit 79244d366c
37 changed files with 1425 additions and 112 deletions
+132
View File
@@ -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
+6
View File
@@ -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
+13
View File
@@ -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:
+2
View File
@@ -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)
+5 -2
View File
@@ -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")
+14
View File
@@ -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...")
+3
View File
@@ -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"]]
+3
View File
@@ -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...")
+3
View File
@@ -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(
@@ -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"
},
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+1
View File
@@ -3,3 +3,4 @@ build
delocate
delvewheel
virtualenv
abi3audit
+53 -6
View File
@@ -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
+3
View File
@@ -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 = ""
+6
View File
@@ -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)
+15 -4
View File
@@ -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):