Python API¶
abicheck's functionality is available as a Python library through the
abicheck.service module. This is the supported public entry point — the
same Tier-2 service layer the CLI calls. Front-ends should route through
service rather than importing the internal abicheck.checker core
directly. Agent and script integrations use this API (or the
CLI's structured JSON/SARIF output) directly — there is no separate
protocol server.
Install.
pip install abicheck. Native-binary header analysis also needscastxmland a C++ compiler; without them, binary-only mode still works. See Getting Started.
Compare two libraries¶
run_compare is the one-call entry point: it resolves both inputs to snapshots,
runs the comparison, and returns the classified result.
from pathlib import Path
from abicheck.service import run_compare
result = run_compare(
old_input=Path("libfoo.so.1"),
new_input=Path("libfoo.so.2"),
old_headers=[Path("include/v1/foo.h")],
new_headers=[Path("include/v2/foo.h")],
)
print(result.diff.verdict) # Verdict.BREAKING, Verdict.COMPATIBLE, ...
print(len(result.diff.changes)) # number of detected changes
for change in result.diff.changes:
print(change.kind, change.name)
run_compare returns a CompareResult — diff (the DiffResult),
old_snapshot, new_snapshot, and the resolved suppression list. It raises
SnapshotError if an input cannot be loaded and ValidationError for an
unrecognised input format (both from abicheck.errors).
Changed in 0.6
run_compare and run_compare_request returned a bare
tuple[DiffResult, AbiSnapshot, AbiSnapshot] before 0.6. A struct can gain
a field without breaking positional callers, which a tuple cannot — so the
typed result became the only shape rather than a second one alongside it
. To migrate a positional caller in one line:
```python
result, old_snapshot, new_snapshot = run_compare(...).as_tuple()
```
Common keyword arguments¶
run_compare is a keyword shim over a typed CompareRequest; the arguments you
will reach for most often:
| Argument | Type | Default | Purpose |
|---|---|---|---|
old_input / new_input |
Path |
— | Binary (.so/.dll/.dylib) or a .abi.json snapshot |
old_headers / new_headers |
list[Path] |
None |
Public headers for L2 API analysis (-H on the CLI) |
old_includes / new_includes |
list[Path] |
None |
Extra include dirs passed to the header parser (-I) |
old_version / new_version |
str |
"" |
Version labels recorded in the snapshots |
lang |
str |
"c++" |
Header language mode ("c++" or "c") |
frontend |
str |
"auto" |
Header-AST frontend: "auto", "castxml", "clang", or "hybrid" (runs castxml and clang together and merges them). (A fifth value, "android", is source-ABI-only — it needs source inputs and is rejected by run_compare, which has no source-input path.) |
policy |
str |
"strict_abi" |
Built-in policy profile (strict_abi, sdk_vendor, plugin_abi) |
policy_file_path |
Path |
None |
Custom YAML policy file |
suppress |
Path |
None |
Suppression file (YAML or ABICC format) |
scope_to_public_surface |
bool |
True |
Restrict findings to the public ABI surface |
enable_debuginfod |
bool |
False |
Resolve debug info via debuginfod |
The table above is the common subset, not the full surface. run_compare also
takes per-side PDB paths, debug roots, forced public symbols, and pattern
verdicts; for those, build a CompareRequest/InputSpec directly and call
run_compare_request. See the Python API Reference
for the complete, generated argument/field list of every name in service.__all__.
Work with snapshots directly¶
To produce a snapshot once and reuse it (for example, to build a baseline), use
resolve_input (auto-detects the input type) or run_dump (native binaries),
then compare_snapshots to classify two already-loaded snapshots.
from pathlib import Path
from abicheck.service import resolve_input, compare_snapshots
from abicheck.serialization import save_snapshot, load_snapshot
# Build and persist a baseline snapshot.
baseline = resolve_input(Path("libfoo.so.1"), headers=[Path("include/foo.h")], version="1.0")
save_snapshot(baseline, Path("baseline.abi.json"))
# Later — compare a fresh build against the saved baseline.
old = load_snapshot(Path("baseline.abi.json"))
new = resolve_input(Path("build/libfoo.so"), headers=[Path("include/foo.h")])
result = compare_snapshots(old, new, policy="strict_abi")
print(result.verdict)
compare_snapshots returns a DiffResult. Unlike run_compare, it works on
already-loaded objects, not file paths: policy is a built-in profile name,
but a custom policy file is passed as a loaded PolicyFile via policy_file=,
and suppressions as a loaded SuppressionList via the suppression= argument
(scoping keywords such as scope_to_public_surface match run_compare). Use
load_suppression_and_policy to turn paths into those objects:
from abicheck.service import load_suppression_and_policy, compare_snapshots
suppression, policy_file = load_suppression_and_policy(
suppress=Path("suppressions.yaml"),
policy_file_path=Path("policy.yaml"),
)
result = compare_snapshots(old, new, suppression, policy_file=policy_file)
If you only have file paths and don't want to pre-load them, call run_compare
(or run_compare_request) instead — it accepts suppress=/policy_file_path=
as paths and does the loading for you. Snapshots are serialised as .abi.json;
see Snapshot Format for the on-disk contract
and current schema_version, Output Formats for the
comparison-report shape, and Baseline Management for
the baseline workflow.
Render results¶
render_output turns a DiffResult into any of the supported report formats,
so you can reuse abicheck's exact reporter output from your own code.
from abicheck.service import render_output
report = render_output("sarif", result, old, new)
Path("report.sarif").write_text(report)
Supported fmt values: "markdown" (alias "md"), "json", "sarif",
"html", "junit", and "review" (the compact review digest). render_output
raises ValidationError for an unrecognised format.
Typed request API¶
run_compare/run_dump are convenience shims — keyword arguments in,
typed result out. Underneath, this Python API resolves through the same
typed request objects the native CLI does: DumpRequest and
CompareRequest. The native compare CLI resolves through
CompareRequest too (cli_resolve.py assembles it from compare's loose
arguments and hands it to resolve_compare_request); the native dump CLI
is the one exception — it still runs its own dump_cmd argument path
rather than building a DumpRequest (see G33 Phase 5's note in AGENTS.md
for what that migration still needs). Reaching for the typed request
directly buys you two things a keyword shim can't:
- The identical validation rules across every front end that
actually builds the typed request — the
compareCLI,run_compare_requestandrun_dump_request— reject a bad combination of fields the same way regardless of which one built the request. (The nativedumpCLI is the exception noted above: since it doesn't build aDumpRequest, this shared-validation guarantee doesn't cover it.) How a rejection surfaces still differs per transport: calling the typed API directly raisesValidationError; the CLI translates the equivalent failure into its own usage-error/exit-code behavior. The rule is shared, not the exception type — see the parity table below for how each transport represents the same failure. - Repeatable configuration — build one
CompareRequestonce (from a config file, a test fixture, a stored preset) and reuse it, rather than re-threading a dozen keyword arguments.
| Operation | Convenience API | Typed API | Result |
|---|---|---|---|
| Dump | run_dump(...) |
run_dump_request(DumpRequest(...)) |
AbiSnapshot |
| Compare | run_compare(...) |
run_compare_request(CompareRequest(...)) |
CompareResult |
scan has no typed request of its own. ScanRequest/ScanResult and
run_scan/run_scan_set were removed in 0.6 — CompareRequest
→ CompareResult is the one typed contract now. See
Scanning from Python below.
DumpRequest¶
from pathlib import Path
from abicheck.service import DumpRequest, InputSpec, run_dump_request
request = DumpRequest(
input=InputSpec(
path=Path("libfoo.so"),
headers=[Path("include/foo.h")],
version="1.0",
),
depth="headers", # a floor, not a target — see below
)
snapshot = run_dump_request(request)
Key DumpRequest fields, beyond the InputSpec it wraps (path,
headers, includes, version, pdb, debug_roots,
include_dependencies, sources, build_info, dump_manifest,
compile, public_header_dirs):
| Field | Meaning |
|---|---|
depth |
binary/headers/build/source — an explicit value is an enforced floor: run_dump_request raises ValidationError if the resolved snapshot's evidence doesn't actually reach it, the same guarantee the CLI's dump --depth gives via DumpDepthNotSatisfiedError (a different exception type, since Tier-2 has no ClickException concept — same guarantee, different vocabulary). |
frontend |
Header-AST frontend: auto/castxml/clang/hybrid, plus a fifth, source-ABI-only value android — rejected unless the request also carries source evidence (has_sources=True or sources/build_info set), since android has no header-AST extraction path of its own. |
dwarf_only / debug_format / enable_debuginfod / debuginfod_url |
Debug-info resolution knobs. |
follow_dependencies / dependency_search_paths |
Dependency-closure walk. |
has_sources |
Legacy flag consulted by the android frontend's source-evidence rule. |
The exhaustive, generated field/type/default table for DumpRequest (and
every other typed request/result dataclass) lives in the Python API
Reference; the table above is a
curated subset for the fields most callers actually reach for.
InputSpec.headers combines fine with sources/build_info — that's the
normal way to collect additive L2 (headers) plus L3/L4 (build/source)
evidence in one request. Only dump_manifest is mutually exclusive with
headers/includes/public_header_dirs — a request combining those fails
validation before any extraction runs (DumpRequest.validation_errors()),
since a manifest already declares the equivalent surface itself.
CompareRequest¶
from pathlib import Path
from abicheck.service import (
CompareRequest,
InputSpec,
classify_compare_pair,
resolve_compare_request,
run_compare_request,
)
request = CompareRequest(
old=InputSpec(path=Path("libfoo.so.1")),
new=InputSpec(path=Path("libfoo.so.2")),
contract_evaluation=True,
contract_mode="public",
)
# One call, the normal case:
result = run_compare_request(request) # -> CompareResult
# Or the same thing in two steps, e.g. to inspect the resolved snapshots
# before classifying:
pair = resolve_compare_request(request) # -> ResolvedComparePair (old/new snapshots)
result = classify_compare_pair(request, pair) # -> CompareResult
run_compare_request(request) does both steps in one call — the two-step
form exists because the native CLI runs its own Click-specific resolution
(--pack application, receipt recording) between them; a typed caller
normally just wants run_compare_request.
Scanning from Python¶
There is no typed scan request. ScanRequest, ScanResult and
run_scan/run_audit/run_scan_set were removed in 0.6: two request/result pairs is what made the "equivalent input,
equivalent answer, whichever front end" rule impossible to check, and
compare's own pair covers the capability. Importing any of those names now
raises ImportError.
A baseline comparison — what run_scan(ScanRequest(baseline=...)) did — is a
CompareRequest:
from pathlib import Path
from abicheck.service import CompareRequest, InputSpec
from abicheck.service import run_compare_request
result = run_compare_request(CompareRequest(
old=InputSpec.of(Path("baseline.json")),
new=InputSpec.of(Path("build/libfoo.so"), headers=[Path("include/")]),
depth="headers",
contract_evaluation=True,
contract_mode="exports",
))
The one-sided audit (compare --no-baseline, formerly the retired scan
with no --against) has no Python entry point yet. As of the fixes
recorded in docs/contribute/known-gaps.md's
"compare --no-baseline does not yet reproduce scan's audit-mode
findings" entry) it now reproduces scan's own candidate-side findings and,
opt-in via --severity-preset, its exit-code gating too. Concretely: all
eleven cross-source hygiene checks and the pattern/preprocessor pre-scan run
on the self-compared candidate, and everything they find lands in the
report's findings[] — changes stays empty, since an audit reports no
addition, removal or comparison verdict at all. A consumer
reading only changes therefore sees an empty audit; read findings. Until a typed
CompareRequest/CompareResult-shaped entry point exists for it, call the
CLI directly (subprocess, or abicheck.service's CLI-adjacent helpers) --
the abicheck scan CLI this migration replaced no longer exists.
estimate_scan — the dry-run per-layer cost projection — survives, but takes
an InputSpec plus the run-scoped level arguments rather than a request:
from abicheck.service import InputSpec
from abicheck.service import estimate_scan
rows = estimate_scan(
InputSpec.of(Path("build/libfoo.so"), headers=[Path("include/")]),
depth="source",
)
CLI / Python parity¶
The rules are shared across both front ends; the surface doesn't
always match field-for-field — dump still runs its own argument path
rather than building a DumpRequest end to end (see above). Read this
table as "where the capability is reachable today":
| Capability | CLI | Python (typed) |
|---|---|---|
| Depth floor | dump --depth → DumpDepthNotSatisfiedError |
DumpRequest.depth/CompareRequest.depth → ValidationError |
| Not comparable | exit code 16 |
raises ProfileMismatchError/ScopeMismatchError |
| Contract evaluation | --contract {public,exports,all,auto} |
CompareRequest.contract_evaluation/.contract_mode (the typed API still needs both, and has no auto) |
| Consumer scoping | compare --used-by |
abicheck.appcompat.scope_diff_to_app(...) — no CompareRequest field, a post-classification step |
One asymmetry worth knowing about, not a bug to work around:
- Consumer scoping has no
CompareRequestfield.--used-byis a post-classification scoping pass layered on top of an already-computedCompareResult(appcompat.scope_diff_to_app), not a resolution input — the CLI and a direct Python caller both call the same function afterward, rather than a field on the request itself.
Result types¶
DiffResult(abicheck.checker_types) — the comparison result. Key fields:verdict(aVerdict),changes(list[Change]), andsuppressed_changes(the suppression audit trail).Verdict(abicheck.change_registry_types) — one ofNO_CHANGE,COMPATIBLE,COMPATIBLE_WITH_RISK,API_BREAK,BREAKING. See Verdicts and, for the CLI mapping, Exit Codes.AbiSnapshot(abicheck.model) — the serialisable ABI surface produced byresolve_input/run_dump.
The complete list of exported names, with full signatures/dataclass fields, is
the generated Python API Reference.
Public types live in model.py, checker_types.py, and checker_policy.py;
treat changes to their surface as breaking changes to this API.