Evidence, Build-Context, and Debug Flags¶
This page is the flag reference for everything that widens dump/compare
beyond the basic .so + headers case covered in CLI Usage: C
vs C++ mode, cross-compilation, feeding in the exact build flags (evidence
layer L3), embedding build/source evidence packs (L3/L4), and
resolving debug info that isn't in the binary itself.
Split out of CLI Usage to keep that page to the everyday compare/dump flow. See Choose Your Workflow for which of these you actually need for your situation, and Evidence & Detectability for what each layer buys you conceptually.
Language mode¶
By default castxml uses C++ mode. For pure C libraries, set compile.lang
in .abicheck.yml. There is no --lang flag on any command:
abicheck dump libfoo.so -H foo.h -o snap.json --config .abicheck.yml
abicheck compare libv1.so libv2.so -H foo.h --config .abicheck.yml
Dependency exclusion in dump¶
By default, dump drops any declaration whose own defining header is a
toolchain/system header (/usr/include, MSVC VC/Tools, the Xcode/macOS
SDK, ...) from the written snapshot. This is a header-origin filter, not a
public-API-visibility one: your library's own private/internal declarations
are always kept, exactly like its public ones — only declarations belonging
to a dependency (the C++ standard library, SYCL runtime headers, etc.) are
excluded. For a library with a large or heavily-templated dependency stack,
a full header-AST dump can otherwise put the transitive dependency surface
in the hundreds-of-MB range, most of which the library itself never declares.
# api.h declares your own public API and pulls in <string>/<vector> etc.
# transitively; the written snapshot keeps every declaration from api.h and
# any of your own project headers it includes, and drops the std:: ones.
abicheck dump libfoo.so -H api.h -o snap.json
# Opt out to get the old, unfiltered full dump (every declaration the
# header AST parser saw, including the full dependency surface):
abicheck dump libfoo.so -H api.h --include-system-declarations -o snap.json
Since this changes what a scoped snapshot can see, compare two snapshots
dumped the same way — don't mix a default-scoped snapshot with one dumped
via --include-system-declarations.
Cross-compilation¶
When analysing libraries built for a different architecture, declare the
cross-toolchain in .abicheck.yml's compile: block. Phase 7
(one-comparison-product.md §4.1/§4.2) moved this whole family
off dump/compare's CLI entirely — a stable project/toolchain property,
not a per-run choice — so there is **no --compiler/--compiler-prefix/
--compiler-option/--sysroot/--nostdinc/--ast-frontend/--lang flag
left on any command at all. The two per-run inputs that remain are the
operand-shaped ones: -I/--include (below) and -D/--define (covered under Preprocessor macros):
# .abicheck.yml
compile:
frontend: castxml # auto | castxml | clang | hybrid
compiler: aarch64-linux-gnu- # a trailing "-" is a toolchain prefix,
# anything else a path to the compiler
# binary (merges the former --compiler/
# --compiler-prefix pair into one key)
options: [-march=armv8-a] # was the repeatable --compiler-option
std: c++20 # synthesizes -std=c++20
defines: [FOO=1, NDEBUG] # synthesizes -DFOO=1 -DNDEBUG; also settable
# per run with -D/--define, merged by macro name
include_dirs: [include, third_party/inc] # appended after -I roots
sysroot: /opt/sysroots/aarch64
nostdinc: false
lang: c++ # was --lang; defaults to c++ when unset
# dump (single artifact) -- --config loads the compile: block above; without
# it (or a --sources tree that auto-discovers .abicheck.yml), none of these
# cross-toolchain settings would actually be used.
abicheck dump libfoo.so -H include/foo.h -o snap.json --config .abicheck.yml
# compare (two artifacts) -- applies to BOTH sides
abicheck compare libv1.so libv2.so -H include/foo.h --config .abicheck.yml
The full compile: field list (with each field's former CLI spelling on
dump/compare, where one existed) is documented under
compile: in the Config File reference; the two
env-var-toggle fields (ast_frontend_fallback:/allow_unsupported_castxml:)
and frontend_context: are there too — they replace --allow-ast-frontend-
fallback/--allow-unsupported-castxml/--frontend-context the same way.
--compiler, meaning a path to the cross-compiler binary specifically (not
a bare prefix): for the clang frontend, this is honored only when the
binary is clang-family (basename contains clang, or is a known
non-clang-named clang-based fork — currently Intel's
icx/icpx/dpcpp/dpcpp-cl); a path to a real GCC binary is ignored
here and the frontend falls back to plain clang on PATH instead
(castxml can't take clang-only flags, so this guards against a GCC path
being misread as a clang toolchain).
compare reads the block from --config or the nearest .abicheck.yml found from
the current directory upward; dump from --config or the one auto-discovered
at the --sources tree root (a plain dump SO_PATH with no --sources and no
--config reads no project config for this block at all — name --config
explicitly if you need it there). It is applied on every header-scoping path — ELF and
the PE/Mach-O header parse alike. A malformed explicit --config fails loudly
rather than silently dropping the settings; an auto-discovered one warns and falls
back.
There is no surviving CLI override for any key in this block, on any
command, and no per-side spelling: the sided --ast-frontend old=/new=
override is gone too, since there is no CLI spelling left to be sided. A
script still passing one of these flags exits 64 — see
Upgrading from 0.5 to 0.6 → compiler/frontend.
Preprocessor macros: -D / --define¶
The one L2 compile-context setting that kept a CLI spelling. It takes
a logical macro definition, NAME or NAME=VALUE, never a compiler flag,
and is repeatable on both dump and compare:
# One-off experiment: an opt-in public surface that only exists under a macro.
abicheck dump libfoo.so -H include/ -DFEATURE_API=1 -o foo.json
# Both sides of a compare are parsed under the same macro context -- always.
abicheck compare old/libfoo.so new/libfoo.so -H include/ -DFEATURE_API
Why this one and not --compiler/--sysroot/--compiler-option: those name
toolchain identity, which is stable per project and belongs in reviewed
configuration. A feature macro selects which public surface is being analysed
— the same question -H and -I answer, which is why those kept their CLI
spelling too. General compiler-option pass-through stays config-only under
compile.options.
Precedence. A CLI -D merges with compile.defines by macro name: the
macro you name on the command line wins, every other config define stays in
force, and exactly one -D per macro reaches the frontend. --dry-run prints
the resulting effective set.
Both sides, never one. There is deliberately no -D old=/new= form,
unlike -H/-I/--version: two sides parsed under different macro contexts
are two different public surfaces, so every finding between them would be an
artifact of the flags. (-Dold=FOO therefore defines a macro literally named
old — it is not a side selector.)
Extraction identity. The macro set is recorded in the snapshot and in its
extraction contract, so a snapshot dumped without a macro and one dumped with it
are refused as profile_mismatch rather than silently diffed.
Accepted and rejected.
| Spelling | Result |
|---|---|
-DNAME, -D NAME, --define NAME, --define=NAME |
accepted |
-DNAME=VALUE, --define=NAME=A=B (split on the first = only) |
accepted |
-DNAME= (defined to an empty token sequence) |
accepted, and distinct from -DNAME |
-DNAME="text" (shell quoting; abicheck never re-splits the operand) |
accepted |
-D"NAME=a b" (whitespace in the value) |
rejected — no portable GNU/MSVC spelling; use compile.options |
-DF(x)=x+1 (function-like) |
rejected — same reason |
--define=-DFOO, --define=-Xclang, --define=@resp.txt |
rejected with a targeted hint |
-U/undefine |
no CLI spelling; remove the entry from compile.defines |
Build-context capture (compile_commands.json) — evidence layer L3¶
This is evidence layer L3 in abicheck's five-source evidence
model: on top of the binary (L0),
debug info (L1), and headers (L2), it feeds abicheck the flags the library was
actually built with. Modern build systems (CMake, Meson, Ninja) generate a
compile_commands.json file that captures the exact compiler flags for every
source file. abicheck can ingest this file directly, eliminating manual flag
specification:
# Generate compile_commands.json during build
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
cmake --build build
# Dump ABI with exact build flags derived automatically
abicheck dump build/libfoo.so -H include/ -p build/
The -p build/ flag tells abicheck to look for build/compile_commands.json
and derive all flags automatically: defines, include paths, language standard,
target triple, sysroot, and ABI-affecting options like -fvisibility=hidden.
| Flag | Description |
|---|---|
--build-info <path> |
A build directory containing compile_commands.json, the database file itself, or a pre-captured pack |
--build-info is the one flag that takes this operand: with -H/--header
also given, the database it resolves to parameterizes the header parse with
the build's exact flags, and it is the L3 build source either way. To filter
entries by source file pattern (e.g., src/libfoo/**), set build:
compile_db_filter: <glob> in .abicheck.yml beside compile_db: — a
project-scoped setting, not a per-invocation flag (one-comparison-product.md
Phase 7i).
When both --build-info and .abicheck.yml's compile: block (options:,
sysroot:, ...) are given, the config values take precedence.
# .abicheck.yml -- override a single flag while inheriting the rest from
# compile_commands.json
compile:
options: [-DEXTRA_DEFINE=1]
Evidence packs — build & source context (L3 / L4)¶
The build context above (L3) and source evidence (L4) can also be bundled
into a reusable build/source pack — a post-build, opt-in artifact that abicheck
reads alongside your binaries. A pack never rebuilds your project or runs
arbitrary commands; it reads existing build outputs and build-system query
interfaces only. See Source & Build Evidence
Packs for the full model and
Build Evidence Setup for producing a pack
(abicheck-cc, the Clang plugin, and a full worked CMake example).
# 1. Point dump straight at a raw source checkout — it collects L3/L4/L5
# evidence inline itself (compile DB auto-inferred for cmake/make/bazel),
# no separate collection step needed. The resulting .abi.json is
# self-contained.
abicheck dump build/libfoo.so -H include/ \
--sources . -o libfoo.abi.json
# 2. Or feed in an out-of-band pack produced by the abicheck-cc wrapper or the
# Clang plugin (abicheck also auto-detects an abicheck_inputs/ pack
# alongside the binary with no flag at all).
abicheck dump build/libfoo.so -H include/ \
--build-info abicheck_inputs/ --sources abicheck_inputs/ -o libfoo.abi.json
# 3. Compare two snapshots — the embedded facts diff automatically, with no
# pack directories to carry around.
abicheck compare old.abi.json new.abi.json
Build/source data travels inside the snapshot
dump --build-info/--sources embed the normalized build + source
facts in the .abi.json, so compare old.json new.json carries them with
no out-of-band directories (single-artifact UX). For advanced use, the
--build-info and --sources
flags supply or override those facts per side from a pack directory; raw
provenance is never embedded — only the normalized facts that feed the
comparison.
| Flag | Command | Description |
|---|---|---|
--build-info <dir> |
dump |
Embed a pack's L3 build-info facts inline in the snapshot |
--sources <dir> |
dump |
Embed a pack's L4/L5 source facts (source ABI replay + graph) inline in the snapshot |
--build-info old=<dir> / --build-info new=<dir> |
compare |
Out-of-band L3 build-info pack per side (overrides embedded) |
--sources old=<dir> / --sources new=<dir> |
compare |
Out-of-band L4/L5 source pack per side (overrides embedded) |
--depth <rung> |
compare, dump |
Evidence-depth dial: binary (L0/L1 only), headers (+L2 AST, default), build (+L3 build context), source (+L4 replay & the L5 graph). On compare, depths past headers collect from an --sources tree (or read embedded facts); without a source tree the requested mode is reported in the coverage table only. |
To additionally capture L4 source ABI replay (macro/constexpr values,
default-argument values, uninstantiated templates), pass --sources at
--depth source on dump/compare (a raw source checkout is replayed
inline; a pre-built pack from the abicheck-cc wrapper or Clang plugin is
loaded as-is). L4 requires clang (or castxml for the declaration subset);
if it is missing, abicheck degrades gracefully — L4 is marked partial and
the artifact-backed tiers (L0–L2) remain fully authoritative. Build/source
evidence (L3/L4) explains, localizes, and scopes findings or raises its own
source-level findings, but it never silently deletes an artifact-proven
break (the authority rule).
Diagnosing which layers you have
Run abicheck dump libfoo.so --dry-run to classify the inputs and print
which data layers (L0–L5) are available, without producing a snapshot —
useful for confirming a stripped build really is missing its debug info
before you trust a symbols-only verdict.
Dry-run validation¶
Both dump and compare accept --dry-run: it resolves and validates the
invocation — classifying inputs, resolving depth/scope, discovering config,
and (on dump) reporting which data layers (L0–L5) are available — then
prints a report without producing a snapshot or running the diff. It writes
nothing, so it's incompatible with -o/--output, and its exit codes are
only 0 (ok) or 1 (blocked) or 64 (usage error) — never the verdict codes
2/4.
# Check what dump would see before spending time on a full extraction
abicheck dump libfoo.so -H include/ --sources . --dry-run
# Check what compare would resolve/collect before running the diff
abicheck compare old.so new.so -H include/ --depth source --sources . --dry-run
When the project's .abicheck.yml sets an ownership key
(scope.dependencies, private_headers, private_namespaces), dump
--dry-run also prints the ownership rules and the owner and contract of each
-H header, with a warning line for a -H header that is not public target
API. It classifies files without parsing them; the per-declaration answer is
recorded in the snapshot a real dump writes (extraction_scope). See
Target ownership.
Debug artifact resolution¶
abicheck achieves its highest accuracy with DWARF debug information, but in many deployments debug info is not embedded in the binary (stripped builds, split DWARF, distro debuginfo packages, dSYM bundles, PDB files). abicheck automatically searches for debug artifacts across multiple locations:
1. Split DWARF (.dwo files or .dwp package)
2. Embedded DWARF (binary itself has .debug_info)
3. Build-id tree (/usr/lib/debug/.build-id/<ab>/<cdef...>.debug)
4. Path mirror (/usr/lib/debug/usr/lib/libfoo.so.debug)
5. dSYM bundle (macOS: Foo.dylib.dSYM/Contents/Resources/DWARF/Foo.dylib)
6. PDB (Windows: adjacent .pdb or _NT_SYMBOL_PATH)
7. debuginfod (opt-in network: query by build-id)
| Flag / config key | Description |
|---|---|
--debug-info <path> |
The separate-debug-info input, over all of its transports: a directory containing separate debug files, a detached DWARF debug file (a .debug sidecar), or -- on compare with directory/package operands -- a debug package (RPM/Deb/tar). Which one a given operand is comes from its content, not its name. Naming a .pdb or a DWARF-package (.dwp) file directly is a usage error: no extraction path reads one, so it would be accepted and silently ignored -- pass the directory holding it (which the resolver searches), or set debug.pdb_path for a PDB. Can be repeated. A per-run evidence input -- stays a CLI flag on dump/compare. Spelled --debug-root before plan Phase 7n merged the two; the old spelling exits 64. |
--debug-info old=<path> |
Debug info for the old side only (compare command). |
--debug-info new=<path> |
Debug info for the new side only (compare command). |
dump --debug-info <pkg> |
A usage error: dump's operand is one binary, with no package-extraction stage. Pass the extracted directory, or use compare --debug-info on the release packages. |
.abicheck.yml debug.debuginfod |
Enable debuginfod network resolution (opt-in). Phase 7: no --debuginfod flag left on dump/compare (was demoted to this config key, then the override itself removed — .abicheck.yml is its only source now). |
.abicheck.yml debug.debuginfod_url |
Override debuginfod server URL. Was --debuginfod-url, same Phase 7 removal. |
# Locate + report separate debuginfo for stripped .so files
abicheck compare \
old/usr/lib64/libfoo.so.1 new/usr/lib64/libfoo.so.1 \
--debug-info old=old-debug/usr/lib/debug \
--debug-info new=new-debug/usr/lib/debug
# .abicheck.yml -- Fedora/RHEL: debug info located automatically by build-id
debug:
debuginfod: true
export DEBUGINFOD_URLS="https://debuginfod.fedoraproject.org/"
abicheck compare old-libfoo.so new-libfoo.so
What --debug-info/debug.debuginfod feed into the DWARF parse today
On dump and compare, a build-id-tree, path-mirror, or debuginfod-fetched
.debug file — a separate ELF file distinct from the input binary — is
parsed for DWARF instead of the (stripped) input itself: the commands above
correctly detect the libpng16 struct-layout example even though
old/usr/lib64/libfoo.so.1/new/usr/lib64/libfoo.so.1 carry no .debug_info
of their own (P1.1). Split DWARF (.dwo/.dwp, the first entry in the
resolver chain above) and dSYM bundles (macOS) are resolved and reported
(Debug info: <source>) but not yet threaded into the parse — a binary
whose only debug info takes one of those two shapes still analyzes
symbols-only. For those, package the debug info as its own artifact
(RPM/Deb/tar, or a dSYM bundle) and pass it on directory/package inputs with
the side-aware --debug-info flag, which is wired into the dump:
What these binary shapes actually look like (
nm/readelfoutput for a debug build vs. a fully stripped release vs. a split.debugfile) is shown concretely in Part 1 §2 of the ABI series.
Verbose output¶
Add -v / --verbose to any native command to enable debug logging: