Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
619e1dc989 |
@@ -384,10 +384,23 @@ def build_in_directory(args: CommandLineArguments) -> None:
|
|||||||
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Snapshot wheels before build to detect newly built ones
|
||||||
|
wheels_before = {p.name for p in output_dir.glob("*.whl")}
|
||||||
|
|
||||||
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
|
||||||
try:
|
try:
|
||||||
with log.print_summary(options=options):
|
with log.print_summary(options=options):
|
||||||
platform_module.build(options, tmp_path)
|
platform_module.build(options, tmp_path)
|
||||||
|
|
||||||
|
# Run audit step after all builds complete
|
||||||
|
if options.globals.audit_command:
|
||||||
|
from cibuildwheel.audit import run_audit
|
||||||
|
|
||||||
|
run_audit(
|
||||||
|
audit_command=options.globals.audit_command,
|
||||||
|
output_dir=output_dir,
|
||||||
|
wheels_before=wheels_before,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
# avoid https://github.com/python/cpython/issues/86962 by performing
|
# avoid https://github.com/python/cpython/issues/86962 by performing
|
||||||
# cleanup manually
|
# cleanup manually
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
Audit step for wheels built by cibuildwheel.
|
||||||
|
|
||||||
|
This module provides functionality to run audit commands (like abi3audit)
|
||||||
|
on built wheels after all platform builds are complete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from packaging.utils import parse_wheel_filename
|
||||||
|
|
||||||
|
from .logger import log
|
||||||
|
from .util.helpers import format_safe
|
||||||
|
|
||||||
|
|
||||||
|
def is_abi3_wheel(wheel_path: Path) -> bool:
|
||||||
|
"""Check if a wheel is an abi3 wheel by parsing its filename."""
|
||||||
|
_, _, _, tags = parse_wheel_filename(wheel_path.name)
|
||||||
|
return any(t.abi == "abi3" for t in tags)
|
||||||
|
|
||||||
|
|
||||||
|
def run_audit(
|
||||||
|
audit_command: str,
|
||||||
|
output_dir: Path,
|
||||||
|
wheels_before: set[str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Run the audit command on wheels built in this run.
|
||||||
|
|
||||||
|
The audit command supports the following placeholders:
|
||||||
|
- {wheel}: expands to each wheel path, runs the command once per wheel
|
||||||
|
- {abi3_wheel}: same as {wheel}, but only for abi3 wheels
|
||||||
|
|
||||||
|
If the command contains {abi3_wheel} but no abi3 wheels were produced,
|
||||||
|
the audit step is skipped.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audit_command: The command template to run
|
||||||
|
output_dir: Directory where wheels were output
|
||||||
|
wheels_before: Set of wheel filenames that existed before the build
|
||||||
|
"""
|
||||||
|
if not audit_command:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find wheels built in this run (new wheels that weren't there before)
|
||||||
|
all_wheels = sorted(output_dir.glob("*.whl"))
|
||||||
|
just_built = [w for w in all_wheels if w.name not in wheels_before]
|
||||||
|
|
||||||
|
if not just_built:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine if we're auditing abi3 wheels only
|
||||||
|
abi3_only = "{abi3_wheel}" in audit_command
|
||||||
|
|
||||||
|
# Filter wheels if needed
|
||||||
|
if abi3_only:
|
||||||
|
wheels_to_audit = [w for w in just_built if is_abi3_wheel(w)]
|
||||||
|
if not wheels_to_audit:
|
||||||
|
log.step("Skipping audit step (no abi3 wheels produced)")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
wheels_to_audit = just_built
|
||||||
|
|
||||||
|
log.step("Running audit...")
|
||||||
|
|
||||||
|
for wheel in wheels_to_audit:
|
||||||
|
# Prepare command with placeholders
|
||||||
|
prepared = format_safe(
|
||||||
|
audit_command,
|
||||||
|
wheel=wheel,
|
||||||
|
abi3_wheel=wheel,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.step(f" Auditing {wheel.name}...")
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
prepared,
|
||||||
|
shell=True,
|
||||||
|
check=True,
|
||||||
|
env=env,
|
||||||
|
cwd=output_dir,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
log.error(f"Audit command failed for {wheel.name}")
|
||||||
|
raise SystemExit(e.returncode) from e
|
||||||
@@ -90,6 +90,7 @@ class GlobalOptions:
|
|||||||
test_selector: TestSelector
|
test_selector: TestSelector
|
||||||
architectures: set[Architecture]
|
architectures: set[Architecture]
|
||||||
allow_empty: bool
|
allow_empty: bool
|
||||||
|
audit_command: str
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
@@ -695,6 +696,8 @@ class Options:
|
|||||||
)
|
)
|
||||||
test_selector = TestSelector(skip_config=test_skip)
|
test_selector = TestSelector(skip_config=test_skip)
|
||||||
|
|
||||||
|
audit_command = self.reader.get("audit", option_format=ListFormat(sep=" && "))
|
||||||
|
|
||||||
return GlobalOptions(
|
return GlobalOptions(
|
||||||
package_dir=package_dir,
|
package_dir=package_dir,
|
||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
@@ -702,6 +705,7 @@ class Options:
|
|||||||
test_selector=test_selector,
|
test_selector=test_selector,
|
||||||
architectures=architectures,
|
architectures=architectures,
|
||||||
allow_empty=allow_empty,
|
allow_empty=allow_empty,
|
||||||
|
audit_command=audit_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_pinned_image(self, value: str, pinned_images: Mapping[str, str]) -> None:
|
def _check_pinned_image(self, value: str, pinned_images: Mapping[str, str]) -> None:
|
||||||
|
|||||||
@@ -27,6 +27,21 @@
|
|||||||
"description": "cibuildwheel's settings.",
|
"description": "cibuildwheel's settings.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"audit": {
|
||||||
|
"description": "Execute a shell command to audit each wheel after all builds complete. 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"
|
||||||
|
},
|
||||||
"archs": {
|
"archs": {
|
||||||
"description": "Change the architectures built on your machine by default.",
|
"description": "Change the architectures built on your machine by default.",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ test-skip = ""
|
|||||||
enable = []
|
enable = []
|
||||||
|
|
||||||
archs = ["auto"]
|
archs = ["auto"]
|
||||||
|
audit = ""
|
||||||
build-frontend = "default"
|
build-frontend = "default"
|
||||||
config-settings = {}
|
config-settings = {}
|
||||||
dependency-versions = "pinned"
|
dependency-versions = "pinned"
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cibuildwheel.audit import is_abi3_wheel, run_audit
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsAbi3Wheel:
|
||||||
|
def test_abi3_wheel(self) -> None:
|
||||||
|
assert is_abi3_wheel(Path("example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"))
|
||||||
|
|
||||||
|
def test_abi3_wheel_macos(self) -> None:
|
||||||
|
assert is_abi3_wheel(Path("example-1.0.0-cp39-abi3-macosx_10_9_x86_64.whl"))
|
||||||
|
|
||||||
|
def test_abi3_wheel_windows(self) -> None:
|
||||||
|
assert is_abi3_wheel(Path("example-1.0.0-cp310-abi3-win_amd64.whl"))
|
||||||
|
|
||||||
|
def test_non_abi3_wheel(self) -> None:
|
||||||
|
assert not is_abi3_wheel(Path("example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"))
|
||||||
|
|
||||||
|
def test_pure_python_wheel(self) -> None:
|
||||||
|
assert not is_abi3_wheel(Path("example-1.0.0-py3-none-any.whl"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunAudit:
|
||||||
|
def test_empty_command_does_nothing(self, tmp_path: Path) -> None:
|
||||||
|
# Create a wheel file
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Should not raise and should be a no-op
|
||||||
|
run_audit(
|
||||||
|
audit_command="",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_audit_runs_on_new_wheels(self, tmp_path: Path) -> None:
|
||||||
|
# Create a wheel file (simulating a build)
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Create a marker file to verify command ran
|
||||||
|
marker = tmp_path / "audit_ran.txt"
|
||||||
|
|
||||||
|
run_audit(
|
||||||
|
audit_command=f"touch {marker}",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert marker.exists()
|
||||||
|
|
||||||
|
def test_audit_skips_old_wheels(self, tmp_path: Path) -> None:
|
||||||
|
# Create a wheel file
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Create a marker file to verify command ran
|
||||||
|
marker = tmp_path / "audit_ran.txt"
|
||||||
|
|
||||||
|
# Pre-existing wheel should be skipped
|
||||||
|
run_audit(
|
||||||
|
audit_command=f"touch {marker}",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before={wheel_path.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
def test_abi3_only_mode_skips_non_abi3(self, tmp_path: Path) -> None:
|
||||||
|
# Create a non-abi3 wheel
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Create a marker file to verify command ran
|
||||||
|
marker = tmp_path / "audit_ran.txt"
|
||||||
|
|
||||||
|
run_audit(
|
||||||
|
audit_command=f"echo {{abi3_wheel}} && touch {marker}",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not run because no abi3 wheels
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
def test_abi3_only_mode_runs_on_abi3(self, tmp_path: Path) -> None:
|
||||||
|
# Create an abi3 wheel
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp38-abi3-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Create a marker file to verify command ran
|
||||||
|
marker = tmp_path / "audit_ran.txt"
|
||||||
|
|
||||||
|
run_audit(
|
||||||
|
audit_command=f"echo {{abi3_wheel}} && touch {marker}",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert marker.exists()
|
||||||
|
|
||||||
|
def test_wheel_placeholder_expanded(self, tmp_path: Path) -> None:
|
||||||
|
# Create a wheel file
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
# Write wheel path to a file to verify expansion
|
||||||
|
output_file = tmp_path / "wheel_path.txt"
|
||||||
|
|
||||||
|
run_audit(
|
||||||
|
audit_command=f"echo {{wheel}} > {output_file}",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output_file.exists()
|
||||||
|
content = output_file.read_text().strip()
|
||||||
|
assert content == str(wheel_path)
|
||||||
|
|
||||||
|
def test_audit_fails_on_error(self, tmp_path: Path) -> None:
|
||||||
|
# Create a wheel file
|
||||||
|
wheel_path = tmp_path / "example-1.0.0-cp310-cp310-manylinux_2_17_x86_64.whl"
|
||||||
|
wheel_path.touch()
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
run_audit(
|
||||||
|
audit_command="exit 1",
|
||||||
|
output_dir=tmp_path,
|
||||||
|
wheels_before=set(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.code == 1
|
||||||
@@ -541,3 +541,46 @@ before-all = ["override2"]
|
|||||||
options_reader.get("config-settings", option_format=ShlexTableFormat())
|
options_reader.get("config-settings", option_format=ShlexTableFormat())
|
||||||
== "key1=value1 key2=override2 empty='' key3=value3"
|
== "key1=value1 key2=override2 empty='' key3=value3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_option(tmp_path, platform):
|
||||||
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||||
|
pyproject_toml.write_text(
|
||||||
|
"""
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
audit = "abi3audit {abi3_wheel}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
options_reader = OptionsReader(pyproject_toml, platform=platform, env={})
|
||||||
|
assert options_reader.get("audit", option_format=ListFormat(" && ")) == "abi3audit {abi3_wheel}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_option_list(tmp_path, platform):
|
||||||
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||||
|
pyproject_toml.write_text(
|
||||||
|
"""
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
audit = ["first command", "second command"]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
options_reader = OptionsReader(pyproject_toml, platform=platform, env={})
|
||||||
|
assert (
|
||||||
|
options_reader.get("audit", option_format=ListFormat(" && "))
|
||||||
|
== "first command && second command"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_audit_option_env(tmp_path, platform):
|
||||||
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||||
|
pyproject_toml.write_text(
|
||||||
|
"""
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
options_reader = OptionsReader(
|
||||||
|
pyproject_toml, platform=platform, env={"CIBW_AUDIT": "my-audit-tool {wheel}"}
|
||||||
|
)
|
||||||
|
assert options_reader.get("audit", option_format=ListFormat(" && ")) == "my-audit-tool {wheel}"
|
||||||
|
|||||||
Reference in New Issue
Block a user