Environment Variables¶
abicheck is configured primarily through CLI flags and the
.abicheck.yml config file. A small set of environment
variables tune behaviour that is awkward to express as a per-run flag —
parallelism, memory budgets, the build-injection wrapper, and debug-info
resolution.
Conventions used below:
- Default is the value used when the variable is unset (or empty, where noted).
- Unless stated otherwise, an unparsable value falls back to the default rather than raising.
- "Module" is the code that reads the variable (the source of truth for its behaviour).
Boolean variables, one value domain. Every
ABICHECK_*boolean knob on this page is parsed by one shared reader (abicheck/env_flags.py, registryBOOLEAN_ENV_FLAGS):1/true/yes/onturn it on and0/false/no/offturn it off, case-insensitively and ignoring surrounding whitespace, whichever way the variable's own default points. Unset, empty, or any value outside those ten tokens means the documented default — never the opposite of it. SoABICHECK_CC_DISABLE=0leaves extraction on, which it did not before this reader existed (it treated any non-empty value,0included, as "disable").Note:
ABICHECK_BUILD_DIRandABICHECK_INPUTS_VERSIONappear in the code but are module constants, not environment variables — abicheck never reads them from the environment. They are not listed here.
AST / header parsing (L2)¶
These affect header parsing (the L2 backend), i.e. any command that dumps or
compares from C/C++ headers (dump, compare with --sources/headers,
scan).
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_AST_FRONTEND |
castxml, clang, hybrid (any other value is ignored) |
unset → resolves to castxml |
Pins the AST frontend when the request is auto (no explicit --ast-frontend). An explicit --ast-frontend castxml/clang/hybrid on the CLI is honoured verbatim and takes precedence over this variable. Does not suppress the automatic castxml→clang fallback on a castxml toolchain-version / direct-include error, which only happens when the frontend was auto-selected (no flag and no castxml/clang/hybrid pin here). hybrid (G28 Phase 3) runs both castxml and clang and merges them — needs both tools installed, never auto-selected by auto itself. |
dumper.py (_resolve_header_backend); flag in cli_options.py |
ABICHECK_AUTO_SYSTEM_INCLUDES |
truthy / falsey (see the boolean note above) | 1 (enabled) |
When enabled, abicheck probes the host compiler for its system include search paths and feeds them to the castxml/clang frontend. Set to a falsey value to suppress the probe (e.g. a hermetic build that supplies its own -isystem/--sysroot). |
dumper_sysinc.py (_auto_system_includes_enabled) |
ABICHECK_CLANG_LAYOUT_TOOL |
path to a compiled binary | unset → enrichment skipped | Opt-in path to the G28 Phase 4 companion tool (contrib/clang-layout-tool/, built separately with LibTooling — never a default/hard dependency). When set and the snapshot's L2 backend is clang or hybrid, enriches its RecordTypes with real field offsets/vtable-pointer placement clang's own -ast-dump=json never computes. Falls back to a bare abicheck-clang-layout-tool on PATH if unset. Any failure (missing binary, compile error, timeout) silently skips enrichment — never a hard error. |
clang_layout_tool.py (find_layout_tool_bin) |
ABICHECK_ALLOW_UNSUPPORTED_CASTXML |
truthy / falsey (see the boolean note above) | unset → gate enforced | The CastXML version gate (castxml_policy.py) rejects an authoritative L2 CastXML scan whose resolved castxml --version falls outside the supported range (>=0.6.11,<0.8.0, bundled/linked Clang >=18) — notably the legacy PyPI castxml distribution — before any header is parsed. --allow-unsupported-castxml (dump/compare/scan, same family as --compiler/--ast-frontend) is the CLI spelling of this opt-in, scoped to one invocation; the env var also works directly for scripting. This is an explicit, exploratory-mode-only opt-in to proceed with castxml anyway; the resulting snapshot's ast_toolchain_supported is false with ast_toolchain_unsupported_reasons recording why, so it is not silently indistinguishable from a normal supported scan. This is not the only way past the gate: when the frontend was auto-selected (no explicit --ast-frontend/ABICHECK_AST_FRONTEND pin) and ABICHECK_ALLOW_AST_FALLBACK=1 is also set, the gate failure instead triggers a graceful fallback to the clang backend rather than a hard error — see the ABICHECK_AST_FRONTEND row above and --allow-ast-frontend-fallback in CLI usage. With neither opt-in set, the gate fails closed. |
dumper_toolchain.py (_allow_unsupported_castxml_enabled), dumper.py (_castxml_dump), cli_options.py (_enable_unsupported_castxml_for_command) |
See the --ast-frontend flag in CLI usage.
L4 source-replay parallelism & memory¶
These affect the L4 source-ABI replay (dump --sources, compare/scan at a
source depth). See Producing source facts
and Build & source data.
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_L4_JOBS |
positive integer (1 forces serial) |
unset → auto: min(n_units, cpu, 8) |
Worker count for the parallel L4 extract phase. An explicit value is clamped to the oversubscription ceiling and to the available-memory cap; the auto default is also memory-capped. An unparsable value falls back to 1. Clamps are logged, never silent. |
buildsource/source_replay.py (_l4_jobs) |
ABICHECK_L4_EXECUTOR |
thread, process |
thread |
Selects the executor for the L4 extract phase. process uses a ProcessPoolExecutor to parallelize the GIL-bound clang-AST post-processing (opt-in; validate the win before relying on it). Any unrecognized value falls back to thread. |
buildsource/source_replay.py (_l4_use_process_pool) |
ABICHECK_L4_JOB_MEM_GIB |
float GiB (floored at 0.25) |
3.0 |
Per-worker RAM budget used to compute the memory cap on L4 (and L5 call-graph) workers, so a template-heavy TU's multi-GiB clang JSON AST cannot OOM-kill the replay. Available memory is min(/proc/meminfo MemAvailable, cgroup v2/v1 headroom) (Linux only). An unparsable value falls back to the default. |
buildsource/source_replay.py (_l4_job_mem_budget_gib) |
ABICHECK_L4_CACHE_DIR |
directory path | unset → no persistent cache dir | Persists the per-TU L4 source-ABI cache across runs (the CI-friendly knob — point it at a restored cache directory). No CLI flag exposes this directly (the pre-1.0 CLI reset kept collect's --source-abi-cache as a library-only capability — see Build evidence setup); a caller of buildsource.inline's Python functions can pass an explicit source_abi_cache_dir argument instead, which wins over this variable when both are present. |
buildsource/inline.py |
Other scan-phase parallelism¶
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_PATTERN_SCAN_JOBS |
auto, 0, 1, or a positive integer |
unset / auto → min(cpu, 8) above a 256-file floor, else serial |
Worker count for the lexical (compiler-free) ABI-risk pattern pre-scan. 0/1 force serial (CI/test determinism, constrained sandboxes); N caps at N (still serial below the file floor). Always serial inside a daemonic process. |
buildsource/pattern_facts.py (_resolve_scan_jobs) |
ABICHECK_CALL_GRAPH_JOBS |
positive integer | unset → min(n_units, cpu, 8) |
Overrides the CPU-derived worker count for the best-effort L5 clang call-graph pass. Capped by min(n_units, N, max(8, 2×cpu)) and by the shared L4 memory cap (ABICHECK_L4_JOB_MEM_GIB). An unparsable value falls back to 1. |
buildsource/call_graph.py (_call_graph_jobs) |
ABICHECK_INCLUDE_MAP_JOBS |
positive integer (1 forces serial), 0 → auto |
unset / 0 → auto: min(n_units, max(2, cpu)) |
Worker count for the per-compile-unit clang -M include-map probes (also what the L2 per-header include closure drives, one synthetic unit per header). Serial below 3 units regardless. An explicit value is clamped to the oversubscription ceiling and the available-memory cap; an unparsable value falls back to the auto default and records an extractor diagnostic. Concurrently spawned clang -M children are additionally capped process-wide, so two sides resolving at once cannot oversubscribe the host, and every caller shares one process-wide probe thread pool, so the thread count does not grow with the number of concurrent members. That pool has as many threads as an explicit value here (probes beyond it would only wait), max(4, cpu) otherwise, and draws them from ABICHECK_MAX_THREADS. |
buildsource/include_graph_workers.py (resolve_jobs) |
ABICHECK_MAX_THREADS |
positive integer; 0/unset/unparsable → unlimited |
unlimited | Process-wide cap on worker threads held by abicheck's thread pools at once — release members, the two sides of a comparison, include probes, per-TU header parses, and the L4/L5 build-source passes. The per-pool settings in this table still decide how many threads each pool asks for; this is the only setting that bounds their total, since pools nest. A pool created while the budget is spent gets fewer threads, down to none, in which case it runs its work in the calling thread: slower, never a deadlock and never a different result. Process pools (ABICHECK_L4_EXECUTOR=process, the pattern pre-scan) are not counted. |
process_resources.py (THREAD_BUDGET, BudgetedExecutor) |
ABICHECK_REFERENCE_MODE |
1/true/yes/on (any case) enables; anything else or unset → off |
off | Turns every optimization off: each cache built on the central wrapper (memoized functions and properties, in-memory and request-scoped memos, the whole-snapshot, header-AST, build-evidence and source-ABI disk caches) computes instead of reading or storing, and ABICHECK_MAX_THREADS reads as 1 with every pool granted no thread, so all work runs inline and a directory/package compare takes its sequential member path. Never changes a result -- that equivalence is what the H5 harness and the scheduled reference-mode.yml lane check. Much slower; for differential testing and for ruling a cache out when chasing a bug. |
model/execution_cache.py (reference_mode), process_resources.py (max_threads) |
ABICHECK_MEMBER_JOBS |
positive integer; 0/unset → auto |
auto: 2 under the GIL, the CPU count on a free-threaded (python3.xt) interpreter |
How many release fan-out members (a directory/package compare) run at once, and how many member threads exist. A member's cost is mostly Python (AST decoding, model building, comparison), which the GIL serializes, so running more members than the interpreter can execute only multiplies resident memory; the default follows the interpreter (sys._is_gil_enabled()). The memory admission gate (ABICHECK_RELEASE_JOB_MEM_GIB) can still narrow it further. An explicit value is clamped to the oversubscription ceiling; an unparsable value is ignored. |
process_resources.py (python_parallelism), workflows/release_jobs.py (plan_release_workers) |
ABICHECK_RELEASE_JOB_MEM_GIB |
float GiB (floored at 0.25) |
depth-dependent: 1.0 at binary depth, 4.0 at headers, 6.0 at build/source |
Per-worker RAM budget for the release fan-out's auto worker count (a directory/package compare). The default varies by the run's --depth because a worker's real footprint does: at binary depth it holds two snapshots, at header depth it also runs that member's own header-AST parse and holds two much larger snapshots (a measured six-member toolkit bundle peaked at 20.4 GiB, ~3.4 GiB per member). An explicit value here wins at every depth. Skipped entirely when RAM cannot be probed. |
workflows/release_jobs.py (release_job_mem_budget_gib) |
ABICHECK_INCLUDE_MAP_JOB_MEM_GIB |
float GiB (floored at 0.25) |
0.5 |
Per-worker RAM budget for the include-map pool's memory cap. Much smaller than the L4 default because clang -M is preprocess-only — it builds no AST. Same min(MemAvailable, cgroup headroom) probe as L4 (Linux only). |
buildsource/include_graph_workers.py (resolve_jobs) |
Build-injection wrapper (abicheck-cc, Flow 2)¶
Read by the abicheck-cc compile wrapper, which stays argv-transparent and is
therefore configured entirely by environment. See
Producing source facts.
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_INPUTS_DIR |
directory path | abicheck_inputs |
Output directory for the emitted abicheck_inputs/ facts pack. |
cc_wrapper.py |
ABICHECK_CC_EXTRACTOR |
auto, clang, castxml |
auto |
Source-ABI extractor the wrapper uses to capture per-TU facts. | cc_wrapper.py |
ABICHECK_CC_HEADERS |
os.pathsep-joined header roots |
"" (empty) |
Public-header roots used to classify which decls belong to the public surface. | cc_wrapper.py |
ABICHECK_CC_LIBRARY |
string | "" (empty) |
Library name stamped into the pack manifest / target id. | cc_wrapper.py |
ABICHECK_CC_VERSION |
string | "" (empty) |
Version stamped into the pack manifest. | cc_wrapper.py |
ABICHECK_CC_DISABLE |
truthy / falsey (see the boolean note above) | unset / falsey (extraction on) | When truthy, the wrapper is a pure pass-through: it runs the real compile and skips all fact extraction. A falsey value (0, false, no, off) leaves extraction on — it no longer disables capture the way any non-empty value once did. |
cc_wrapper.py |
Fact extraction is best-effort and never fails the build: a missing front-end or a parse error degrades to a warning on stderr and preserves the compiler's exit code.
Snapshot storage limits¶
Decompression-bomb ceilings applied when reading a stored snapshot envelope
(plain / gzip / zstd). Both are public knobs: a real bundle member's snapshot
can legitimately exceed the default, and the ceiling is a process-level
property of the read, so it is set in the environment rather than in
.abicheck.yml (the sibling node budget, resource_limits.max_bundle_facts_decode_nodes,
is a config key because it is resolved per comparison). The legacy
underscore-prefixed spellings (_ABICHECK_SNAPSHOT_MAX_DECODED_BYTES,
_ABICHECK_SNAPSHOT_MAX_STORED_BYTES) are still honoured and take precedence
when both are set.
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_SNAPSHOT_MAX_DECODED_BYTES |
positive integer (bytes); a malformed or non-positive value is ignored | DEFAULT_MAX_DECODED_BYTES |
Ceiling on the decoded size of a snapshot envelope, enforced incrementally during decompression — a stream that exceeds it is rejected rather than buffered. Raise it to read a legitimately large snapshot (e.g. a big bundle member at header depth). | snapshot_io.py (_max_decoded_bytes) |
ABICHECK_SNAPSHOT_MAX_STORED_BYTES |
positive integer (bytes); same parsing rule | DEFAULT_MAX_STORED_BYTES |
Ceiling on the stored (on-disk) size abicheck will buffer before decoding at all, applied to a gzip or zstd envelope only — a plain (uncompressed) file's stored size equals its decoded size, so it is checked against the decoded ceiling above instead. Deliberately independent of that ceiling: a valid multi-member gzip stream's overhead scales with member count, not payload size, so raising the decoded ceiling must not widen this one. | snapshot_io.py (_max_stored_bytes, read_snapshot_bytes) |
Debug-info resolution¶
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
DEBUGINFOD_URLS |
space-separated server URLs | "" (no servers) |
The standard debuginfod server list. abicheck consults it only when network resolution is enabled with .abicheck.yml's debug.debuginfod: true and debug.debuginfod_url is not given. debug.debuginfod_url overrides DEBUGINFOD_URLS. Only http/https URLs are used. |
debug_resolver.py (DebuginfodResolver._default_urls) |
Progress output¶
| Variable | Values | Default | Effect | Module |
|---|---|---|---|---|
ABICHECK_PROGRESS |
0/false/no/off disables; anything else (or unset) enables |
enabled | Progress lines for the long phases of a CLI run -- the header-AST parse, DWARF, build/source (L3-L5) evidence, and i/N counters for the per-TU header parse, L4 source replay, include map and call graph -- written to stderr as abicheck: ..., so stdout (a snapshot or report) is unchanged. Counters are throttled to one line per 5 s. -v always enables them. A Python caller that configures no logging hears nothing. |
extract/progress.py, frontends/cli/runtime.py (_setup_verbosity) |
Other environment variables abicheck honours¶
These are standard / third-party variables read for caching, Windows symbol resolution, reproducibility, and CI annotations. abicheck does not define them.
| Variable | Read for | Module |
|---|---|---|
XDG_CACHE_HOME |
Base directory for the snapshot cache, header-AST cache, and debuginfod cache (falls back to ~/.cache). |
snapshot_cache.py, dumper_cache.py, debug_resolver.py |
LOCALAPPDATA |
Windows cache base directory for the header-AST cache. | dumper_cache.py |
_NT_SYMBOL_PATH |
Windows symbol search path used when resolving PDB / debug info. | debug_resolver.py |
SOURCE_DATE_EPOCH |
Reproducible snapshot created_at provenance timestamp. |
cli.py |
GITHUB_ACTIONS |
When == "true", enables GitHub Actions-specific output (job summary / annotations). |
annotations.py, cli_compare_release.py |
GITHUB_STEP_SUMMARY |
Path abicheck writes a Markdown job summary to, when present. | annotations.py, cli.py |