Skip to content

Troubleshooting

Use this page when a run fails to start (setup/environment) or when results look surprising (false positive, false negative, or unexpected verdict).


0) Setup & environment failures

"castxml not found in PATH"

Header AST analysis requires castxml. pip install abicheck does not install it, so any command that passes headers (--header / -H) fails with this error until castxml is on your PATH.

# conda (any OS) — bundles castxml + compiler automatically
conda install -c conda-forge abicheck
# macOS
brew install castxml
# Windows (PowerShell, admin)
choco install castxml

On Ubuntu CI, prefer a checksum-pinned CastXML Superbuild over apt install castxml. Ubuntu 24.04 currently packages CastXML with bundled Clang 17, which is too old for some GCC 13 libstdc++ headers. The abicheck GitHub Action installs the pinned v2026.01.30 Superbuild automatically.

No castxml and can't install it? Run binary-only mode by omitting the header flags — abicheck falls back to DWARF/symbols analysis (weaker, but catches symbol- and layout-level breaks):

abicheck compare old.so new.so   # no -H / --*-header → binary-only fallback

"command not found: abicheck" or wrong tool runs

Some distros ship unrelated tools with similar names (abi-compliance-checker wrappers in Debian devscripts, or abicheck in Fedora's libabigail-tools). Confirm you're running this project:

abicheck --version   # should print: abicheck X.Y.Z (abicheck/abicheck)

If a different tool shadows it, invoke via the module form: python -m abicheck.

Header parsing fails or finds nothing

If castxml runs but reports parse errors or an empty surface, the inputs usually don't match the build environment of the analyzed .so:

  • Pass the same include dirs the library was built with: -I include/ -I deps/include/.
  • Pass the same preprocessor macros with -D/--define (repeatable, and applied to both sides of a compare): -DFEATURE_X=1 -DNDEBUG. For a stable project/CI contract, record them in .abicheck.yml instead — see Headers that require a macro.
  • Best option: feed the real build flags from compile_commands.json with -p build/ (see CLI Usage → Build-context capture).
  • For pure C libraries, set compile.lang: c in .abicheck.yml (the default is c++).

Headers that require a macro before inclusion

Some libraries gate an opt-in public surface — or refuse to compile at all — behind a feature macro:

#ifndef PVXS_ENABLE_EXPERT_API
#error Define PVXS_ENABLE_EXPERT_API before including this header
#endif

Without the macro those declarations are absent, not merely undetailed, so a signature break inside that surface reads as compatible. Two supported ways to supply it, for two different situations:

# One-off run, experiment, or an integration that builds the compile context
# at invocation time. Repeatable; applies to both sides of a compare.
abicheck dump libpvxs.so -H include/ -DPVXS_ENABLE_EXPERT_API -o pvxs.json
abicheck dump libpcre2.so -H include/ -DPCRE2_CODE_UNIT_WIDTH=8 -o pcre2.json
# .abicheck.yml -- preferred for stable CI and baseline generation: the macro
# set is part of the project's reviewed contract, not a per-run choice.
compile:
  defines:
    - PVXS_ENABLE_EXPERT_API
    - PCRE2_CODE_UNIT_WIDTH=8

The two compose: a CLI -D overrides the config entry for that macro name only, leaving every other compile.defines entry in force.

-D/--define takes a macro definition (NAME or NAME=VALUE), not a compiler flag. A value containing whitespace, a function-like definition (F(x)=...), and anything that is not a bare macro name are rejected with a message naming compile.options, which is where general compiler flags live. Quoting is the shell's job and abicheck never re-splits the operand, so a string value is written -DNAME=\"text\" (the quotes reach the compiler); a value with a space in it has no portable spelling and belongs in compile.options.

Because the macro set changes what was extracted, it is part of extraction identity: comparing a snapshot dumped without the macro against one dumped with it is refused as profile_mismatch, not silently diffed. Re-dump both sides under the same macro context.

castxml aborts in system headers (_Float32, __assume__)

castxml drives an internal Clang while emulating your host GCC. If that bundled Clang is older than your host gcc/glibc, parsing your library's headers can fail inside the system headers — before abicheck compares anything — with errors like:

  • unknown type name '_Float32' (also _Float64 / _Float128) — glibc's sized-float types, understood by Clang ≥ 16.
  • a parse failure on the GCC 13+ libstdc++ __assume__ attribute — understood by Clang ≥ 18.

The fix is a castxml built against a newer Clang — the recommended floor is bundled Clang ≥ 18. Ubuntu 24.04's castxml package currently bundles Clang 17 and is therefore not suitable for GCC 13 C++ header analysis. Use either the conda-forge package or a release- and checksum-pinned official Superbuild:

conda install -c conda-forge castxml

# Reproducible CI: download the matching asset from this pinned release,
# verify its published SHA256, extract it, then prepend its bin/ to PATH.
# https://github.com/CastXML/CastXMLSuperbuild/releases/tag/v2026.01.30

The pinned abicheck Linux CI release bundles Clang 21.1.8. Always inspect the full castxml --version output: the CastXML version alone does not identify the bundled Clang frontend.

abicheck detects this case and appends your detected castxml --version plus the recommended floor to the error. As an alternative, point abicheck at a clang-parsable toolchain/sysroot with --compiler / --sysroot. A #ifdef __cplusplus extern "C" C header that fails only under --lang c should be scanned without --lang c (castxml always parses in a C++-aware mode).

"CastXML \<version> ... is not a supported default scanner setup"

An authoritative L2 scan runs a version gate (castxml_policy.py) before parsing any header: it rejects a resolved castxml build outside the supported range (currently >=0.6.11,<0.8.0, bundled/linked Clang >=18). This most commonly fires against the legacy PyPI castxml distribution (pip install castxml), which is not a supported default scanner setup — pip install abicheck deliberately never installs CastXML for you, and the PyPI castxml package's own release line predates this floor.

Fix: install a supported CastXML from conda-forge (recommended) or a pinned Superbuild release, same as the two sections above:

conda install -c conda-forge castxml

Only for deliberate legacy reproduction (never as a normal workflow), the gate can be overridden explicitly with ABICHECK_ALLOW_UNSUPPORTED_CASTXML=1. The resulting snapshot records ast_toolchain_supported: false and the specific ast_toolchain_unsupported_reasons, so it is never silently indistinguishable from a normal, policy-compliant scan — treat it as a degraded, non-baseline result.


castxml: __has_cpp_attribute not defined on macOS (Xcode 16.4+)

Status: Open — upstream castxml issue to be filed.

Affected platforms: macOS with Xcode 16.4+ (Apple Clang headers).

Symptom: When castxml processes a C header that includes <stddef.h>, the macOS SDK resolves this through the libc++ __config header, which uses the __has_cpp_attribute preprocessor macro. castxml does not define this macro, causing parse failures:

.../MacOSX.sdk/usr/include/c++/v1/__config:1009:7: error:
  function-like macro '__has_cpp_attribute' is not defined

Multiple lines in __config trigger the same error wherever __has_cpp_attribute(...) appears in #if / #elif directives.

Root cause: Per the C++ standard, __has_cpp_attribute should be a built-in macro that evaluates to 0 for unknown attributes. castxml's internal preprocessor does not predefine it, so the preprocessor treats the bare identifier as an error rather than defaulting to 0.

Workaround: In castxml-specific shim headers (not general project headers), replace #include <stddef.h> with typedef __SIZE_TYPE__ size_t; to avoid the libc++ header chain entirely. __SIZE_TYPE__ is a GCC/Clang built-in that castxml supports.

Caution: This typedef only supplies size_t — other <stddef.h> definitions (NULL, ptrdiff_t, offsetof, max_align_t) are not available. Do not use this substitution in normal build headers as it will break compilation that depends on those definitions. Safer alternatives: create an isolated shim header used only by castxml invocations, or provide a minimal custom header that supplies all needed type definitions.


1) "Why did I get API_BREAK/BREAKING unexpectedly?"

Check header/binary mismatch first

  • Are these the exact headers used to build the analyzed .so?
  • Are required -D macros the same as build time?
  • Is include search path the same as build environment?

If not, fix input parity and rerun.


2) "Why is verdict COMPATIBLE, but I expected NO_CHANGE?"

COMPATIBLE means real differences exist (new symbols, policy changes) but no binary break.

Run JSON output for detail:

abicheck compare old.json new.json -o json=result.json
python3 -c "import json; r=json.load(open('result.json')); print(r['verdict']); print(len(r['changes']))"

"old and new snapshots keep dependency declarations differently" / "were narrowed by different frontend prefilters"

Two refusals from the extraction-scope check. Each side records the rules its declarations were classified under; a pair that recorded a different scope.dependency_evidence, or a different frontend prefilter, did not keep the same declarations, so a dependency declaration present on one side only would read as an addition or a removal. Re-dump both sides under the same .abicheck.yml. The same refusal names an unrecorded side (a pre-v52 snapshot) when the other side was narrowed, since nothing proves what the old one kept.

Differing ownership rules alone are not refused while both sides keep everything (dependency_evidence: full): the comparison runs and the report lists the declarations that changed owner or contract. See Target ownership.


3) "Why are deep type changes not detected?"

Check if the binary has DWARF debug info:

# Check for embedded DWARF sections
readelf -S libfoo.so | grep -E "\.debug_info|\.zdebug_info" || echo "No DWARF sections"

# Check for externally linked split-debug files
readelf --debug-dump=links libfoo.so   # shows .gnu_debuglink / .gnu_debugaltlink references
readelf --debug-dump=follow-links libfoo.so  # follows the link and inspects linked debug-info

Without DWARF, the layout-level checks that depend on debug info (L1) are limited — abicheck falls back toward symbol-only (L0) analysis. Use debug builds (-g) for deeper analysis. If the binary uses split debug (separate .debug file), the linked debug info is still analysed automatically when --debug-dump=follow-links can resolve the path.


4) CI script says success but report shows changes

Remember: compare exit code 0 includes both NO_CHANGE and COMPATIBLE. If you need exact policy, parse JSON verdict instead of checking $? == 0.


5) Still unsure?

Open an issue with: - command line used - tool version (abicheck --version) - minimal header + .so pair - JSON output (-o json=-)