Output Formats¶
abicheck supports multiple output formats for different use cases:
| Format | Flag | Best for |
|---|---|---|
| Markdown | --format markdown (default) |
Human review, PRs, terminals |
| JSON | --format json |
CI pipelines, machine processing |
| SARIF | --format sarif |
GitHub Code Scanning, SAST platforms |
| HTML | --format html |
Standalone reports, ABICC migration |
| JUnit XML | --format junit |
GitLab CI, Jenkins, Azure DevOps test dashboards |
All five formats support the report filtering options described below.
The ABICC-compatible XML output (via abicheck compat check) includes
redundancy annotations but does not support --show-only filtering.
In addition to report formats, abicheck can emit GitHub Actions workflow
command annotations (--annotate) that appear as inline comments on PR
diffs. See GitHub PR Annotations for details.
Redundancy filtering¶
When a root type change (e.g. struct size change) causes many derived changes
(e.g. 30 FUNC_PARAMS_CHANGED entries for functions using that struct),
abicheck automatically collapses the derived changes. The root type change is
annotated with:
caused_count— number of derived changes collapsedaffected_symbols— list of affected interface names
This keeps reports focused on root causes. Use --show-redundant to disable
filtering and see all changes.
How it appears in each format¶
Markdown: An info note at the bottom:
> ℹ️ 12 redundant change(s) hidden (derived from root type changes).
> Use `--show-redundant` to show all.
JSON: A top-level redundant_count field, and per-change caused_by_type
and caused_count annotations on root type changes.
SARIF: caused_by_type and caused_count in result properties;
redundant_count in run-level properties.
HTML: A highlighted banner showing the redundant count.
XML (ABICC compat): <redundant_changes> element in <problem_summary>,
<caused_by> and <caused_count> elements on individual problems. Both binary
and source sections include their own redundant counts.
JUnit XML: Redundant changes are filtered upstream before the formatter receives them, so derived changes do not appear as test cases. No JUnit-specific redundancy metadata is emitted.
Public-header surface scoping¶
Public-header surface scoping (ADR-024) restricts findings to the public ABI surface — the symbols exported and declared in the public headers you supplied, plus the types reachable from them. Changes that fall outside that surface (e.g. a layout change to an internal struct no public API references) are not dropped: they are moved to an audit ledger so the "why was this excluded" trail stays inspectable. Internal-type leaks are never filtered.
Scoping is on by default (ADR-024 Phase 5). When no public-header surface
can be resolved — e.g. comparing two stripped .so files with no header or
DWARF provenance — scoping is automatically a no-op and every finding is
reported, so the default never hides anything it cannot place. Pass
--no-scope-public-headers to force the unscoped report (every finding,
regardless of surface).
Use --show-filtered to print the ledger on the terminal.
Widening the surface (--public-symbol)¶
Some symbols you do guarantee as public can't be seen by header provenance —
hand-written asm stubs, .def exports, extern "C" shims, or symbols whose
MSVC mangling castxml can't match. The widening overlay (ADR-024 §D6) forces
such symbols back into the public surface so their changes are reported rather
than demoted:
# Force individual symbols (repeatable), à la abi-compliance-checker -symbols-list
abicheck compare old.so new.so --scope-public-headers \
--public-symbol my_asm_stub --public-symbol _ZN3foo3barEv
# Or from a file (one symbol per line; '#' comments and blank lines ignored)
abicheck compare old.so new.so --scope-public-headers \
--public-symbols-list public.syms
Matching is on the symbol as recorded on the finding (mangled or demangled),
plus the trailing :: segment of a qualified name. Widening only ever keeps a
finding — it can never hide a break — and only takes effect together with
--scope-public-headers. It is the counterpart to suppression, which narrows
the surface; the two remain separate, auditable inputs.
How it appears in each format¶
Each demoted finding carries a reason code explaining why it was excluded:
not-exported— the symbol is known but not in the public export set.non-public-type— the type is reachable from no public API root.private-header— the declaration originates in a project header outside the public-header set.system-header— the declaration originates in a toolchain/system header (/usr/include, MSVC, Xcode SDK, …).no-provenance— a type demoted by reachability while provenance was available for the snapshot but not for this type, so the demotion is reachability-based rather than provenance-confirmed (reduced confidence).
The private-header / system-header reasons are provenance-derived: they
only appear when the snapshots were produced with --public-header /
--public-header-dir (ADR-015 schema v6). --public-header is supported for
ELF, PE (provenance from PDB LF_UDT_SRC_LINE), and Mach-O inputs. Without a
public-header set, every declaration's origin is unknown and only the
linkage/reachability reasons above are emitted.
Scope-resolution confidence¶
The ledger also carries a structured confidence in the surface resolution itself (ADR-024 §D5.3), distinct from the overall verdict confidence:
confidence:"high"(a clean header-scoped run) or"reduced".notes: structured codes explaining any reduction —mangling-fallback/header-backend-unavailable(header scoping was requested on a PE/Mach-O binary but fell back to the export table; recorded on the snapshot asscope_fallback), orno-provenance(the surface resolved without any declaration provenance).
Text: With --show-filtered, an audit block on stderr (the reason is shown
in parentheses):
Filtered as non-public ABI surface (1 finding, --scope-public-headers):
- type_size_changed: InternalCache (non-public-type)
JSON: A top-level surface_scope object (present only when scoping is
active):
"surface_scope": {
"enabled": true,
"confidence": "high",
"notes": [],
"out_of_surface_count": 1,
"out_of_surface_changes": [
{"kind": "type_size_changed", "symbol": "InternalCache",
"description": "Size changed: InternalCache (64 → 128 bits)",
"source_location": null, "reason": "non-public-type"}
]
}
SARIF: A surfaceScope object in run-level properties with
confidence, notes, outOfSurfaceCount, and outOfSurfaceChanges (same
per-finding fields, camelCased; reason included when known), present only
when scoping is active.
--show-only filter¶
Limit displayed changes by severity, element, or action (AND across dimensions, OR within each). Does not affect the verdict or exit codes.
Markdown / JSON / HTML: Changes are filtered before rendering. A note shows
how many changes matched: > Filtered by: --show-only ... (5 of 42 changes shown).
SARIF: The show_only parameter filters which results appear in the SARIF
output.
JUnit XML: The show_only parameter filters which test cases appear in the
output. Filtered-out changes are omitted entirely.
--stat mode¶
One-line summary for CI gates:
$ abicheck compare old.json new.json --stat
BREAKING: 3 breaking, 1 risk (42 total) [12 redundant hidden]
$ abicheck compare old.json new.json --stat --format json
{"library": "libfoo", "verdict": "BREAKING", "summary": {...}}
--report-mode leaf¶
Groups output by root type changes with affected interface lists, instead of listing every change individually. Available in Markdown and JSON formats.
--show-impact¶
Appends an impact summary table to the report, showing root changes and how many interfaces each affects. Available in Markdown and HTML formats.
Analysis confidence and evidence tier¶
Every comparison reports how much evidence backed the verdict, so consumers can calibrate trust. Three related fields appear in the Markdown "Analysis Confidence" section and the JSON report:
| Field | Type | Meaning |
|---|---|---|
confidence |
high / medium / low |
Overall trust level (does the available evidence corroborate the verdict, and were any detectors disabled). |
evidence_tier |
elf_only / dwarf_aware / header_aware |
Canonical, ordered analysis depth. Key trust decisions off this scalar. |
evidence_tiers |
list of strings | Raw data sources that were available (elf, dwarf, dwarf_advanced, header, pe, macho). Retained for backward compatibility. |
The evidence_tier scalar collapses the raw sources into a single ordered label
(shallow → deep):
elf_only— symbol-table-only. Binary export tables (ELF/PE/Mach-O) are present, but there is no DWARF debug info and no header/AST surface. Only symbol add/remove and version changes are observable; struct layout, enum values, and type changes are not.dwarf_aware— DWARF (or equivalent debug info) is present, enabling struct layout, enum, and calling-convention analysis, but no header/AST surface is available to cross-check declared API intent.header_aware— a parsed header/AST surface (functions/types/enums) is present. The richest tier, and the only one that can reason about declared-but-not-emitted API, inline/template changes, and macro contracts.
These three values correspond to the artifact evidence layers L0–L2. The higher layers do not promote this scalar, and they differ in what they produce:
dump -p build/only bakes the build context into how the headers are parsed and recordsparsed_with_build_contexton the snapshot. On its own it adds no L3 findings and no evidence-coverage table — a plaincompare old.json new.jsonof two-p-dumped snapshots still reports only the L0–L2 artifact verdict.- Build/source build/source packs (L3/L4) are what add build-diff/source-diff
findings and the
layer_coveragetable, and only when you pass them at compare time via--old-build-info/--new-build-info(or a deeper--depthover--old/new-sources). These findings follow the authority rule — L3/L4 never overrides an artifact-proven verdict.
See Evidence & Detectability for the full L0–L4 model.
{
"verdict": "BREAKING",
"confidence": "high",
"evidence_tier": "header_aware",
"evidence_tiers": ["elf", "dwarf", "header"]
}
JSON schema and stability guarantees¶
The compare --format json document is a stable, machine-readable contract.
It is described by a versioned JSON Schema (draft
2020-12) that ships inside the package at
abicheck/schemas/compare_report.schema.json and is importable:
from abicheck.schemas import (
REPORT_SCHEMA_VERSION, # e.g. "1.0"
COMPARE_REPORT_SCHEMA_PATH, # pathlib.Path to the .schema.json
load_compare_report_schema, # -> dict
)
Every JSON report carries a top-level report_schema_version field
(MAJOR.MINOR) so consumers can detect the contract version they are reading:
Stability policy:
- Additive changes — new optional keys, new enum members, relaxing a constraint — bump the MINOR component. Existing consumers keep working.
- Breaking changes — removing or renaming a key, tightening a type, or removing an enum member — bump the MAJOR component.
Consumers should accept any report whose report_schema_version shares their
expected MAJOR component and ignore unknown keys (the schema sets
additionalProperties: true precisely so that MINOR additions never break
validation). Validating with the bundled schema requires the optional
jsonschema package:
import json, jsonschema
from abicheck.schemas import load_compare_report_schema
report = json.loads(open("report.json").read())
jsonschema.validate(report, load_compare_report_schema())
Release recommendation (--recommend)¶
Translates the verdict into the maintainer's actual question — what version do
I release, and do I need to bump the SONAME? — as a recommended semantic-version
bump (major/minor/patch/none) plus a SONAME action.
The recommendation is policy-aware (it honours --policy and
--policy-file):
| Verdict | Bump | SONAME |
|---|---|---|
NO_CHANGE |
none | no bump needed |
BREAKING |
major | bump required (or bump_missing/bump_performed if abicheck observed the soname) |
API_BREAK |
major | no bump needed (binary stays loadable) |
COMPATIBLE_WITH_RISK |
minor/patch | no bump needed |
COMPATIBLE (additions) |
minor | no bump needed |
COMPATIBLE (quality only) |
patch | no bump needed |
In JSON output the recommendation is always present (no flag needed) under
the release_recommendation key, so CI and agents can gate on it directly:
abicheck compare old.so new.so -H include/ --format json \
| jq -r '.release_recommendation | "\(.version_bump) (\(.soname_action))"'
# major (bump_required)
SARIF Output¶
abicheck supports SARIF 2.1.0 output for integration with GitHub Code Scanning and other SAST platforms.
Usage¶
GitHub Code Scanning integration¶
# .github/workflows/abi-check.yml
name: ABI Check
on: [pull_request]
jobs:
abi-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install abicheck
run: |
sudo apt-get update && sudo apt-get install -y castxml g++
pip install abicheck
- name: Dump ABI (baseline)
run: |
abicheck dump lib/libfoo.so.1 -H include/foo.h \
--version ${{ github.base_ref }} -o old.json
- name: Dump ABI (PR)
run: |
abicheck dump lib/libfoo.so.2 -H include/foo.h \
--version ${{ github.head_ref }} -o new.json
- name: Compare ABI
run: |
abicheck compare old.json new.json --format sarif -o abi.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: abi.sarif
Severity mapping¶
| ABI Change | SARIF Level |
|---|---|
| Function/variable removed | error |
| Type size/layout changed | error |
| Return/parameter type changed | error |
| Function/variable added | warning |
SARIF document structure¶
{
"$schema": "https://raw.githubusercontent.com/.../sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": { "driver": { "name": "abicheck", "rules": [...] } },
"results": [{
"ruleId": "func_removed",
"level": "error",
"message": { "text": "Function foo() removed" },
"locations": [{
"physicalLocation": { "artifactLocation": { "uri": "libfoo.so.1" } },
"logicalLocations": [{ "name": "_Z3foov" }]
}],
"properties": {
"caused_by_type": null,
"caused_count": 0
}
}]
}]
}
JUnit XML Output¶
abicheck can produce JUnit XML reports for CI systems that display test results in their standard dashboards — GitLab CI, Jenkins, Azure DevOps, CircleCI, and others.
Usage¶
abicheck compare old.json new.json --format junit -o results.xml
abicheck compare release-1.0/ release-2.0/ --format junit -o abi-tests.xml
How it works¶
ABI changes are mapped to JUnit test cases:
- Each library in a bundle
compare(directory/package inputs) becomes a<testsuite> - Each exported symbol or type that was checked becomes a
<testcase> - BREAKING and API_BREAK changes produce
<failure>elements - COMPATIBLE changes (additions, no-change) are passing test cases
- COMPATIBLE_WITH_RISK changes pass by default (unless their per-kind
severity is overridden to
"error") - Unchanged symbols from the old library also appear as passing test cases, so the pass-rate is meaningful
- When a symbol has multiple breaking changes, the
<testcase>contains multiple<failure>children (one per change)
Severity mapping¶
| ABI Verdict | JUnit Outcome |
|---|---|
| BREAKING | <failure type="BREAKING"> |
| API_BREAK | <failure type="API_BREAK"> |
| COMPATIBLE_WITH_RISK (severity=warning) | Pass |
| COMPATIBLE | Pass |
Classname groups¶
Test cases are grouped by classname for CI dashboards that support
hierarchical display:
| Element | classname |
|---|---|
| Functions | functions |
| Variables | variables |
| Types (struct/class/union) | types |
| Enums | enums |
| ELF metadata (SONAME, etc.) | metadata |
JUnit XML structure¶
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="abicheck" tests="47" failures="3" errors="0">
<testsuite name="libfoo.so.1" tests="47" failures="3" errors="0">
<!-- Passing: no ABI change detected -->
<testcase name="_ZN3foo3barEv" classname="functions" />
<!-- Failure: binary-incompatible change -->
<testcase name="_ZN3foo3bazEi" classname="functions">
<failure message="func_param_type_changed: parameter 1 type changed from int to long"
type="BREAKING">
parameter 1 type changed from int to long
(int → long)
Source: include/foo.h:42
</failure>
</testcase>
<!-- Failure: removed symbol -->
<testcase name="_ZN3foo6legacyEv" classname="functions">
<failure message="func_removed: Function foo::legacy() was removed"
type="BREAKING">
Function foo::legacy() was removed
</failure>
</testcase>
<!-- Passing: addition is compatible -->
<testcase name="_ZN3foo9new_thingEv" classname="functions" />
</testsuite>
</testsuites>
CI integration examples¶
GitLab CI¶
abi-check:
script:
- abicheck compare old.so new.so -H include/ --format junit -o abi-results.xml || true
artifacts:
when: always
reports:
junit: abi-results.xml
Jenkins (JUnit plugin)¶
stage('ABI Check') {
steps {
sh 'abicheck compare old.so new.so -H include/ --format junit -o abi-results.xml'
}
post {
always {
junit 'abi-results.xml'
}
}
}