The .abicheck.yml config file¶
.abicheck.yml is the per-project configuration file. It holds
the stable, reviewed-in-a-PR properties of a project's ABI contract — build
system, header compile context, severity policy, public-surface scoping, and
suppression hygiene — as opposed to per-run invocation flags. See the
Config Keys Reference for the exhaustive,
generated list of every key/sub-key BuildConfig itself validates, and its
exact required type (other recognized top-level keys, parsed by a sibling
module, are also listed there but without a type); this page covers
effective defaults, precedence, and a worked example.
Every field is optional; an absent, empty, or non-mapping file yields the
all-defaults configuration. CLI flags always override the config, which in
turn overrides the built-in defaults (CLI > config > default).
- Loader (build/source blocks):
load_build_config()inabicheck/buildsource/inline.py; parsed into theBuildConfigdataclass. - Precedence resolver (
compareproject-contract blocks):resolve_compare_config()inabicheck/cli_helpers_compare.py.
File discovery¶
Within any one directory, three locations are recognized, checked in this order (first match wins):
.abicheck.yml— the original, project-root spelling..github/.abicheck.yml— alongside workflows/CODEOWNERS, for a project that keeps tool configuration out of its own root..github/abicheck/.abicheck.yml— a dedicated subdirectory, for a project that wants its abicheck config kept apart from other.githubcontent (or that already groups per-tool config under.github/<tool>/).
Only the file's location changes between these three — its content, schema,
and strictness rules are identical regardless of which one is used. A file
present at a higher-precedence location always wins over one at a
lower-precedence location in the same directory; see
abicheck/config_paths.py for the exact, shared candidate list every
discovery entry point below draws from.
Every command selects its config with one rule
(resolve_project_config() in abicheck/config_paths.py), first match wins:
- An explicit
--config <path>. - The
--sourcestree root (all three locations, no parent walk), when--sourcesis given -- a source tree carries its own contract. - The nearest config at or above the current directory, walking up to the filesystem root and checking all three locations in each directory.
So dump and compare run from inside a project checkout pick up the same
.abicheck.yml with or without --config. (dump used to consult only the
--sources root and silently ignore a config above the current directory.)
The typed Python API does not walk up from the process's working directory:
pass build_config explicitly there.
Note: an auto-discovered (untrusted)
.abicheck.ymlnever causes a build command inbuild.queryto run — it is skipped with a diagnostic. Abuild.queryruns only when the config is supplied explicitly with--config(which marks it trusted for subprocess execution). No separate opt-in flag is needed or exists any more (the old--allow-build-querywas always a no-op, and has since been removed outright).
Strict loading¶
Config loading is strict: an unknown top-level key, an unknown sub-key
inside a recognized block, a value of the wrong type, or a bad enum value are
all hard errors — not warnings. This is a behavior change from earlier
abicheck versions, which warned on unknown keys and kept going. A malformed
YAML file is also a hard error. On the CLI, any of these surfaces as a usage
error (exit 64; see Exit Codes) — the run never proceeds
on a config abicheck could not fully validate.
This "hard error" rule applies unconditionally to compare's own project
config — severity, scope, policy — whether the file came from an explicit
--config or was auto-discovered by walking up from the current directory:
a parse failure always raises a usage error (exit 64), never a
warn-and-continue. It only reaches as far as BuildConfig.from_dict()
itself validates, though: targets:/bundles:/profiles:/baseline: are
recognized top-level keys (so an unrecognized sibling key still errors) but
their contents are opaque to this loader — compare never inspects them
at all, so e.g. an invalid targets.foo.kind passes silently here. Only
abicheck project validate/abicheck project plan (project_targets.py)
actually validate that block's contents, deeply and independently of this
loader.
A separate load of the same .abicheck.yml — the compile: block shared
by compare/dump's L2 compile context (compiler, includes,
sysroot, …), resolved by merge_compile_config() in cli_options.py — does
distinguish explicit from auto-discovered: an explicit --config that
fails to parse still fails loudly, but an
auto-discovered file that fails only prints a warning and continues
with the CLI's own compile context, since a config the user never pointed
at explicitly could otherwise silently break an unrelated invocation just
by existing somewhere upward of the working directory. See
Build-context capture
for that path in detail. Don't assume this leniency extends to the rest of
the file's blocks — it's specific to that one loader, and only when it's
the only one invoked: a dump/compare run with --sources/--build-info
also goes through cli_buildsource.embed_build_source()'s own, separate
config resolution (L3/L4/L5 evidence collection, not the L2 compile
context), and that one raises a hard click.UsageError on a parse
failure unconditionally — explicit --config or auto-discovered alike,
same as the project-config rule above. Passing --sources therefore loses
the auto-discovered warn-and-continue leniency even for a run that would
otherwise get it through merge_compile_config() alone.
There is no longer an init/config scaffolding or diagnostic command
(abicheck init, config validate, config show-effective are all gone) — write .abicheck.yml by hand, using this page as the schema/key
reference. Since unknown keys are now a hard error rather than a silent
warning, a typo or a key from a newer abicheck release will fail loudly
instead of being ignored — set the top-level version: if you need to
signal a schema generation to tooling, though it does not by itself suppress
an unknown-key error.
Top-level keys¶
build:, sources:, severity:, scope:, suppression:, source:,
compile:, debug:, bundle:, python:, gate:, release:,
assurance:, deployment:, resource_limits:, performance:, policy:, version:,
risk_rules:, crosschecks:, targets:, bundles:, profiles:,
baseline:, bundle_variants:, and contract: are the recognized top-level keys.
See the
Config Keys Reference for the exhaustive,
generated key/type list (BuildConfig's own schema); the sections below
cover what each block does, its effective defaults, and behavior that isn't
visible from the type alone.
build:¶
Drives inline build/source collection: an advisory build-system hint
(system:, default auto), a build-query command (query:) to produce a
compile DB, and/or an explicit compile_db: path or glob. query runs
only when the config is passed explicitly with --config (trusted) —
never from an auto-discovered config; no separate opt-in flag exists. See
Producing source facts and
Build & source data.
sources:¶
Public-header roots/globs (public_headers:, default []) defining the
public surface, paths/globs excluded from source collection (exclude:,
default []), and the L5 source-graph detail cap (graph:, summary
(default, a cheap changed-scope CI graph) or full, a full replay scope).
severity:¶
Per-category severity map consumed by compare: a baseline preset
(default/strict/info-only) plus per-category overrides
(abi_breaking/potential_breaking/quality_issues/addition, each
error/warning/info) — per-category levels override the preset. When any
severity value is in effect, compare uses the severity-aware exit-code path.
See Severity and Exit codes.
scope:¶
Public-surface scoping — the main false-positive control. public: (default
effectively true) restricts analysis to the public exported surface. It has
no CLI flag; for one run, compare --contract all turns it off and
--contract public names it;
collapse_versioned_symbols: (default false) collapses symbol-versioned
duplicates before diffing; show_redundant: (default false) disables
redundancy filtering. public_symbols: is an explicit public-symbol overlay,
the only spelling for it — the per-run CLI duplicates that used to shadow
this key were removed, so a project states it once, here — entries match
exactly
(the raw symbol, or a qualified name's trailing :: segment, so foo also
matches ns::foo); globs/wildcards are not supported (mylib_* matches
nothing), list each symbol. See
API-surface intelligence.
exclude_headers: (default []) is the config spelling of
--exclude-header: fnmatch-style patterns naming headers to drop from the
parsed surface, so a header directory containing two headers that
cannot be parsed in one translation unit stays usable as a -H operand
(the reported case is Intel MKL's include/, which ships FFTW2 and FFTW3
headers declaring conflicting typedefs). A matching header is also scoped
out when it is only reached through another header's #include, exactly
like a toolchain header: anything only it declares is not observed (reported
as reduced evidence rather than as a removal), while a type the library's own
public API uses is still checked. This needs dependency scoping, so it does
not apply under --include-system-declarations. Both sides of a comparison are narrowed by the same
rules, and a pair whose two sides were narrowed differently is refused
rather than compared.
This is not sources.exclude, and the distinction is load-bearing.
sources.exclude narrows source collection — which files the L3–L5
build/source evidence layers gather. scope.exclude_headers narrows
header extraction — which declarations the L2 header-AST parse ever sees.
They act on different inputs at different layers, and neither implies the
other: a project that wants a vendored header out of the parse is not
asking for it to disappear from source collection, and the reverse.
A --exclude-header on the command line takes the whole decision: a run
that states the flag at all uses exactly the rules it stated and ignores
this key, rather than unioning the two. The rule set is part of the run's
scope identity (it feeds effective_config_fields["surface.exclude_headers"]
and therefore the configuration digest), so silently widening a stated set
would make the run narrower than the command line says it is. A rule that
matches no header warns once for the whole run — including a
directory/package comparison, where the rules are release-wide and are
stated once rather than repeated per library.
public_header_dirs: (default []) names directories (project-root-relative
or absolute) whose headers are this project's public/internal boundary for
the boundary-dependent cross-source checks (exported_not_public,
public_not_exported, rtti_for_internal_type,
public_to_internal_dependency). It is unrelated to public: above despite
the name: public: decides whether findings are scoped to the public
surface, public_header_dirs: says which headers draw that surface's
boundary. Entries are folded in beside any -H directory (never a -H
file). With neither, every declaration's header origin is unknown and those
four checks report NOT_EVALUATED rather than a finding.
Ownership keys (dependencies:, private_headers:,
private_namespaces:, dependency_evidence:) say who owns each
declaration a header parse sees: the target (this library), a named
dependency, or the toolchain. They classify, they do not filter: every
header dump records each declaration's owner and contract, and the rules it
was classified under, in the snapshot's extraction_scope
.
No declaration is kept or dropped because of them. A declaration whose
contract is private or external owes no export, so it produces no
public_not_exported finding. abicheck dump … --dry-run previews the
rules and the owner of every -H header. The task guide is
Target ownership; the plan behind the keys is
Target ownership and extraction scope.
scope:
public_header_dirs: [include/svs/] # the target's roots
dependencies: # named dependency roots
- name: fmt
header_roots: [include/svs/third-party/fmt/include/]
private_headers: [include/svs/*/detail/**] # owned, not promised
private_namespaces: [svs::detail]
dependency_evidence: full # the only accepted value today
Roots are relative to the project root: the directory holding the config
file, or the repository root for .github/.abicheck.yml. A -H directory
is also a target root; a -H file is not. dependency_evidence accepts
only full (keep every dependency declaration, today's behaviour);
referenced is rejected until retention by reference lands. The rules are
applied in this order:
- An explicit root beats the system-path heuristic: a target installed
under
/usr/include/svs/is still the target's. - The most specific root wins: a dependency vendored inside a target root belongs to the dependency. A root claimed by two owners is an error.
- A
-I(include) directory is compile context. It never makes anything target-owned. private_headers(fnmatch patterns, matched against the path relative to the project root and the absolute path) andprivate_namespacesnarrow only target-owned declarations tocontract=private.svs::detailcovers what is declared inside it (svs::detail::X), notsvs::detailed.- A file no root claims, outside the system directories, is
owner=unresolved. - A compiler builtin that castxml declares implicitly (
__atomic_*,__builtin_*,__sync_*) belongs to the toolchain, whichever file castxml attributes it to. - A declaration whose namespace disagrees with its file (a target file
declaring
fmt::formatter<svs::…>) keeps the file's owner and gets a diagnostic.
There is deliberately no namespace-based ownership key: a namespace filter lost owned declarations on every real target measured (the plan's M2).
Both sides of a compare are classified under the one project config, so
their rules agree. A stored baseline keeps the rules it was dumped under;
if they differ from the candidate's, the pair is still compared (presence
is unchanged under full) and the report names the declarations that moved
owner or contract. A baseline written before snapshot schema v52 has no
recorded rules; it is compared with a note saying so.
on_incomplete: (warn, the default, or block) is Phase 7d's
(one-comparison-product.md §4.1) CONFIG-only replacement for the former
compare --on-incomplete-scope on the directory/package release fan-out —
no CLI spelling exists any more. Governs what an incompletely checked
comparison scope does to the exit code: warn reports every
unchecked member and contributes 0; block contributes 1, folded with
max() like the contract-coverage axis. A run that completed no comparison
at all contributes 1 under either setting. See
Multi-binary § Comparison scope and completeness.
contract:¶
Contract overlays: concrete documents that decide part of the public
contract domain beside the headers. overlays: maps an overlay kind to a
document path; post_manifest is the only kind today — a POST Python export
manifest whose committed pp_*/ufunc-loop surface scopes the comparison
(private __pp_* kernel churn is demoted to the filtered ledger, which every
run discloses). See POST Python extensions.
A relative path resolves against the project root, like
compile.include_dirs. This key is the overlay's only spelling (the former
per-run flag was removed).
It applies only from a config named with --config. The overlay narrows
what gates, and an auto-discovered .abicheck.yml is one a pull request can
edit in the very checkout it is judged on, so a discovered value is noted on
stderr and not applied. This is the same trust boundary build.query and
compile.compiler sit behind. The composite Action likewise drops it from a
discovered config; set its build-config input to a reviewed config to opt
in. The overlay applies to a single-pair compare
only. A directory/package comparison and a --no-baseline audit do not
apply it and say so on stderr (an unapplied narrowing overlay can only add
findings, never hide one). A stored-bundle-facts baseline rejects the
contract: block like the other blocks it cannot honour.
acknowledgment:¶
Acknowledgment records and the additions-review gate. file: names the
records document (a relative path resolves against the project root) and
unacknowledged_additions: is allow (default), warn or block.
file applies only from a config named with --config; a discovered value
is noted and not loaded. unacknowledged_additions applies from any config.
A --policy document stating its own acknowledgment: block wins over this
one. See Change acknowledgment.
suppression:¶
Suppression hygiene policy (a project rule, distinct from the suppression
rules file — see Related files):
strict: (default false) treats suppression-file problems strictly;
require_justification: (default false) requires a justification on every
suppression entry. See Suppressions.
source:¶
method: pins the precise S-axis (evidence method, s0..s6) for power
users.
Use a concrete
s0..s6, notauto. Whencomparereadssource.methodfrom the config (i.e. no--depthon the command line), the value must resolve to a concrete method —comparerejectsautowith a usage error. Pin a specific level here, or leave the key unset and let--depth(binary/headers/build/source—--maxand the oldfulldepth no longer exist) drive the collection depth per run.
See Evidence depth and the
--depth dial.
(graph is not a valid source: sub-key — a config with source: {graph:
...} now fails with an unknown-key error. The L5 graph-detail knob is
sources.graph, in the plural sources: block above.)
compile:¶
The L2 header compile context. This is Phase 7's CONFIG surface (one-comparison-product.md §4.1/§4.2) — every field below has no CLI spelling at all, on any command, with no escape hatch and no per-side spelling. See Upgrading to 0.6 §C1 for the flag→key mapping.
frontend:— AST frontend (auto/castxml/clang/hybrid, case-insensitive —hybridruns castxml and clang together and merges them). Was--ast-frontendon compare/dump.std:— C/C++ standard, e.g.c++17.include_dirs:/defines:— lists.defines:is the stable half of a pair:dump/comparealso accept a per-invocation-D/--define NAME[=VALUE], which merges with this list by macro name — the CLI value wins for the macro it names and every other entry here stays in force. Prefer this key for CI and baseline generation; use-Dfor one-off runs and experiments. It is one of twocompile:fields with a per-run CLI counterpart — the other isinclude_dirs, whose-I/--includeroots are searched before the configured ones rather than merged by name.sysroot:— was--sysrooton compare/dump.nostdinc:— boolean; was--nostdinc/--no-nostdincon compare/dump.compiler:— path to the compiler binary, or a cross-toolchain prefix (e.g.aarch64-linux-gnu-) when the value ends in-. Merges the former--compiler/--compiler-prefixpair into one spelling ("not one-for-one" —--compiler-prefixdoes not become its owncompiler_prefix:key).options:— a list of raw compiler flags passed through verbatim, each a single whitespace-free atom likestd:/defines:below. Was the repeatable--compiler-optionon compare/dump.ast_frontend_fallback:— boolean; was--allow-ast-frontend-fallback(itself always a pureABICHECK_ALLOW_AST_FALLBACKenv-var toggle, so a configtruehas the identical effect).allow_unsupported_castxml:— boolean; was--allow-unsupported-castxml(same env-var-toggle shape asast_frontend_fallback:above, viaABICHECK_ALLOW_UNSUPPORTED_CASTXML).frontend_context:—host/device; was--frontend-context.lang:—c++/c; was--langon compare/dump. Defaults toc++when unset (no header/source-content language inference exists in this codebase to do better than that fixed default).
Values in
compile.std/compile.defines/compile.optionsmust each be a single whitespace-free compiler-option atom (a config scalar cannot expand into multiple compiler arguments).A relative
compile.include_dirsentry resolves against the project root — the directory containing the discovered config, or the directory containing.github/when the config was found under.github/or.github/abicheck/(see File discovery) — never against.github/itself.
debug:¶
Separate-debug-file resolution for ELF. On compare/dump this
is Phase 7's CONFIG surface (one-comparison-product.md §4.1/§4.2) — every
field below has no CLI spelling at all on those two commands (0.5.0 first demoted them to a config-key-with-override; Phase 7 removed
the override itself, since this repo runs no deprecation window). The
coarse per-run --debug-info stays a visible CLI flag on both commands —
it is a per-run evidence input, not a stable project
property. (It was spelled --debug-root until plan Phase 7n merged it into
--debug-info, which now carries the whole role: a directory to search, a
detached debug file, or — on compare — a debug package.)
format: (auto/dwarf/btf/ctf, case-insensitive, default auto-pick)
forces the ELF debug format for both sides (was --debug-format);
dwarf_only: (default false) uses DWARF as the primary source even when
headers are available (was --dwarf-only); debuginfod: (default false)
enables debuginfod network resolution (was --debuginfod); debuginfod_url:
overrides DEBUGINFOD_URLS (was --debuginfod-url); pdb_path: — explicit
path to a Windows PE PDB file, overriding automatic PDB discovery (was
dump --pdb-path). This key is the only PDB input that survives, on
any command, and it is a single value — not side-aware, so a two-sided PE
comparison cannot point old and new at different PDBs.
bundle:¶
Cross-library bundle-analysis topology (CLI cleanup phase two, PR J) — the
sole source for both settings now, replacing the removed
--bundle-system-providers/--bundle-cohort CLI flags: system_providers:
(a list of extra sonames to treat as system-provided, extending the built-in
libc/libstdc++/libgcc/libtbb allow-list) and cohorts: (a list of
co-versioned library name prefixes enabling the BUNDLE_SONAME_SKEW check).
Entries are stripped of surrounding whitespace and empty entries dropped at
parse time. system_providers: and cohorts: (the SONAME-skew check) both apply to
compare's directory/package fan-out. (system_providers: also reached
scan --artifact-set until 0.6 retired that mode.) Distinct from the
plural bundles: block below, which serves a different, unrelated purpose
(the project command family's target declarations). See
Multi-binary § The bundle-analysis flags.
python:¶
CPython extension-module properties. One key today: abi3_floor: — the
Py_LIMITED_API (stable ABI) version this project promises, e.g.
abi3_floor: "3.9". Quote it: a bare 3.9 is a YAML float and is rejected
rather than coerced.
Which floor a project targets is a stable, reviewed-in-a-PR property, not a
per-run decision, so it lives here — and compare --abi3
VERSION is the per-run override on top of it. With either in effect,
compare audits the candidate (NEW) side's imported CPython C-API
against that floor and reports python_stable_abi_violation findings, marked
as candidate-side enrichment, in the same report as the comparison itself.
The findings are advisory (RISK): gate them through --policy /
policy.overrides. A candidate that is not a recognisable CPython extension
module is an evidence-contract error (exit 7), since the audit that was
asked for cannot be performed at all. scan --abi3 accepts the identical
version spelling.
gate:¶
CI gate policy demoted off the CLI (Phase 7d, one-comparison-product.md
§4.1). One key today: fail_on_removed_library: (default false) — the
former compare --fail-on-removed-library/--no-fail-on-removed-library,
no CLI spelling any more. Exits 8 when a library present in OLD is
proven removed in NEW on compare's directory/package fan-out — NEW's
inventory must be proven complete; an unmatched library under
an unproven inventory is reported as an incomplete scope instead. Whether a
release enforces this gate is a stable project policy, not a per-run
choice. See
Multi-binary § Comparison scope and completeness.
release:¶
Directory/package release topology demoted off the CLI (Phase 7d,
one-comparison-product.md §4.1), applying only to compare's
directory/package fan-out — no CLI spelling exists for either key any
more:
dso_only:(defaultfalse) — the formercompare --dso-only: only compare shared objects, skip executables.include_private_dso:(defaultfalse) — the formercompare --include-private-dso: include private (non-public) shared objects from non-standard paths.
assurance:¶
P0.4's orthogonal analysis-assurance exit floor, demoted off the CLI
(rulings.py deferred-option followup) — no CLI spelling any more. One
key today: require_complete: (default false) — the former compare
--require-complete-analysis: fail the step when analysis_assurance.status
is not complete, independent of the compatibility verdict. Contributes
exit 1, folded with max the same way --contract's coverage axis is
: it raises a clean 0 to 1 and never lowers a 2/4.
Applies to a single-pair compare and to a directory/package (release)
fan-out alike (the release operand used to reject it). A release
has one analysis_assurance per compared member, not one for the run, so its
contribution is max over the members': over a one-member package that is
the identity (it gates exactly as the scalar path does for the same pair),
and any member whose analysis fell short floors the release no matter how
many complete siblings it has. The release JSON then carries an
analysis_assurance block naming the short members and why, plus a
per-libraries[] analysis_assurance_status; a stored BundleFacts operand
folds identically. See Exit codes § Analysis-assurance
contribution.
deployment:¶
The project's declared deployment constraints, demoted off the
CLI — no CLI spelling exists any more: the former compare
--env-matrix FILE is now this key, embedding
EnvironmentMatrix's own YAML shape
inline instead of a side file. When runtime_floors is set, a new
symbol-version requirement is judged against the declared floor: at or
below it → COMPATIBLE, above it → BREAKING, instead of the default
COMPATIBLE_WITH_RISK verdict. Unlike release:'s two keys above, this
one does apply to compare's directory/package fan-out — a project-wide
property, not a per-invocation one, so it reaches every library the fan-out
compares.
deployment:
target_os: linux
target_arch: x86_64
compilers: [gcc-13, clang-17]
runtime_floors:
GLIBC: "2.28" # we ship to RHEL 8 / Ubuntu 20.04
GLIBCXX: "3.4.28"
sycl:
implementation: dpcpp
backends: [level_zero, opencl]
Comparing wheels. When NEW is a .whl and this block declares no
runtime_floors, the wheel's own claims become them: its platform tag's
floor (GLIBC from a manylinux_X_Y tag, MUSLLINUX,
MACOS_DEPLOYMENT_TARGET), the single architecture it names (WHEEL_ARCH),
WHEEL_CONTEXT, and the numpy requirement from its METADATA
(NUMPY_REQUIREMENT). Each library in the wheel is then checked against
what the wheel promises: a binary needing a newer glibc than the tag allows
is platform_baseline_floor_raised, and a NumPy C-API target above the
declared numpy floor is numpy_metadata_understates_required_version
(plus numpy_abi_major_incompatible when the target needs NumPy 2 and the
declaration still admits 1.x). A declared runtime_floors wins whole; the
tag does not fill its gaps. NUMPY_REQUIREMENT can also be declared by hand
as a PEP 440 specifier set ("" for no floor).
See Environment & Toolchain Drift for the full worked example, including CI/GitHub Action usage.
bundle_variants:¶
Declares the build variants of one project — each a target
triple, a compiler family, and a set of feature toggles — for
abicheck project capture-variants, which captures
every declared variant into one ProjectSnapshot package, one
VariantRef per variant.
bundle_variants:
linux-x86_64-gcc: # variant name: [A-Za-z0-9._-], 1-100 chars
target_triple: x86_64-linux-gnu # required, non-empty string
compiler_family: gcc # required, non-empty string
feature_toggles: # optional mapping
simd: avx2 # value: string, boolean or integer
threads: true # (stored as "true"/"false"/"2")
required: true # optional boolean, default true
linux-aarch64-clang:
target_triple: aarch64-linux-gnu
compiler_family: clang
required: false
| Key | Type | Default | Meaning |
|---|---|---|---|
target_triple |
string | — (required) | the variant's target |
compiler_family |
string | — (required) | the variant's compiler family |
feature_toggles |
mapping of string → string/bool/int | {} |
build toggles that define the variant; may not reuse target_triple/compiler_family as a key |
required |
boolean | true |
whether the capture must produce this variant |
Validation is strict on every .abicheck.yml load, not only in
capture-variants: an unknown key, a wrong type, a missing coordinate, an
unsafe or case-colliding variant name is a usage error (exit 64) naming
every finding at once.
What the block drives:
declared— each variant'sVariantRef.declaredmap is{target_triple, compiler_family, **feature_toggles}, known from this block alone.captured— filled from what the capture actually observed (the DWARFDW_AT_producercompiler family/version/producer string, target architecture, binary format, ELF machine/class, header-parse standard and frontend). The two maps are stored side by side and never merged: if the config saysclangand the binary saysGCC 13.2.0, the package records both.required— a required variant with no--variantinput, a missing path, nothing capturable, or a failed capture is an error before anything is written (no partial package). An optional variant in any of those states is skipped and reported; the package carries noVariantReffor it (never an empty placeholder).
Comparing two such packages (abicheck compare OLD NEW --variant old=ID)
adds comparison_scope.variant_pairing to the JSON report: both packages'
variants paired by id, a change to a variant's declared coordinates reported
as a variant-boundary change (distinct from captured drift such as a
compiler-version bump), and a variant present on only one side reported as
unmatched — never as a removal — with its recorded required flag.
policy:¶
The documented project-config policy override mechanism. One key today: overrides:, a ChangeKind slug ->
severity mapping (break/warn/risk/ignore) — the identical vocabulary
and validation --policy <file>'s own overrides: block uses (an unknown
slug or severity spelling is a hard load error, not a silently-skipped
entry). Folds into the run's effective policy at the project_config
precedence tier: an explicit --policy <file> that also states an override
for the same kind always wins; a kind only the project config states is
otherwise applied as-is. A project with no --policy <file> given at all
still gets its policy.overrides applied.
performance:¶
The memory/speed trade-off a run executes under. One key today:
| Key | Values | Default |
|---|---|---|
profile |
balanced, low-memory |
balanced |
balanced— fastest. Both sides of a comparison may be resolved concurrently in one process (the typed API does; thecompareCLI resolves sides one after the other), and a directory/package comparison sizes its worker pool to the host.low-memory— lowest peak memory. The two sides are resolved one at a time, each in its own short-lived process on Linux, so the memory a side's header parse used goes back to the system before the next side starts. A directory/package comparison handles its libraries one at a time. On macOS and Windows the sides run one at a time in-process. Measured on a oneDAL comparison: peak 1.34 → 0.89 GiB for about 17% more wall time on that small input (see Memory).
A profile changes how a run executes, never what it reports: every profile produces the same snapshots, findings, verdict and exit code.
Precedence: compare --performance-profile › .abicheck.yml's
performance.profile › balanced. In the Python API, set
CompareRequest.performance_profile; left unset, it follows the ambient
profile, which is balanced unless the caller entered
abicheck.model.performance.performance_profile_scope(...).
An unknown value is a load-time error naming the valid ones (exit 64 from the CLI). The block is where further memory/speed settings will go; each maps onto the profile rather than exposing a separate mechanism switch.
resource_limits:¶
The calibrated JSON decode resource limit, demoted off the CLI (Phase 7g,
one-comparison-product.md §4.1/§3 #21) — no CLI spelling any more. One
key today: max_bundle_facts_decode_nodes: (default unset, applying
bundle_facts.DEFAULT_MAX_JSON_OBJECT_NODES,
1,000,000 — deliberately kept conservative rather than raised to cover
a real large-blob need, since raising the default would widen every
unconfigured/untrusted run's own decode-bomb ceiling) — the former
compare --max-json-object-nodes: overrides the
JSON container/scalar-token budget when decoding a stored BundleFacts
document (OLD_INPUT, from a prior compare --bundle-facts-out). A real
per-library facts blob for a large, template-heavy library (e.g.
SYCL/DPC++, or a release sized like oneDAL's own ~20k-25k-function public
header surface) can legitimately need well over the default to decode.
Only an explicitly-supplied --config may raise this budget past the
default — an auto-discovered .abicheck.yml (found by walking up from
the current directory) can come from the same checkout supplying the
content being decoded, so honoring a raise from it would let that
checkout pair a raised budget with a compact decode-bomb payload. An
auto-discovered config may still lower the budget below the default,
since narrowing a ceiling is never a decode-bomb risk (Codex review, PR
1174, second round).¶
Deliberately node-based, not a memory size — see
bundle_facts.DEFAULT_MAX_JSON_OBJECT_NODES's own docstring for the real
calibration measurement and why a memory-labelled dial would understate
the actual container-count defense this budget provides.
Through the composite GitHub Action, "an explicitly-supplied --config"
means the build-config input: a config named there is the operator's own
reviewed document and its raise is honored, while a config the Action
discovered on its own is folded into a synthesized overlay that caps this
one key back to the default (with a ::warning:: saying so). So a
large-toolkit stored-facts comparison in CI needs build-config pointing at
the .abicheck.yml carrying the raised value — not extra-args, and not a
bare .abicheck.yml left for autodiscovery. The decode failure itself names
this key and that requirement, so a run that hits the ceiling says how to fix
it rather than reading as an unfixable refusal.
Exit-code scheme (no config key — fully automatic)¶
There is no exit_code_scheme: key (CLI cleanup phase two PR G2 removed
it, along with the --exit-code-scheme CLI flag): the exit-code scheme is
severity when a severity map is in effect (this file's severity: block,
a --severity-preset, or a kind: gate pack's gate.severity.<category>),
otherwise legacy. There is no way to force one scheme regardless of
whether a severity setting is present.
See Exit codes.
version:¶
Top-level integer. Default 0 (unset). Declares the config schema version for
forward compatibility.
risk_rules: and crosschecks:¶
Both are recognized top-level keys (so they do not trigger the unknown-key
error), but they are handled outside the compare config merge:
risk_rules:— formerly a mapping of rule-name →{ paths: [...], weight: <int> }path-glob risk profile, parsed byRiskRules.from_dictinbuildsource/risk.py. Nothing loads it any more, and there is no replacement:scan --risk-rules, the one option that ever read arisk_rules:block, was retired ahead of the command itself, andbuildsource/risk.py— including the risk scorer, not only theauto-depth-escalation half — was deleted outright along with the rest ofscanin 0.6 (no alias, no deprecation window). There is now no risk score of any kind, reported or otherwise;compare's--depthis always an explicit pin. The key stays recognized (so an existing file does not trigger the unknown-key error), but is inert. See Evidence depth.crosschecks:— reserved. It has never actually been read: cross-checks (buildsource/cross_source_checks.py) run unconditionally as part of everycompareinvocation, with no per-check tuning surface — thescancommand's repeatable--crosscheck KEY=LEVELflag that once tuned them was deleted along withscanitself.
targets:, bundles:, profiles:, and baseline:¶
Recognized top-level keys (so they do not trigger the unknown-key error),
but — like risk_rules:/crosschecks: above — not parsed by BuildConfig
itself. dump/compare never read this block; it exists solely for
G30's GitHub Actions CI-integration primitives: abicheck project validate
validates it, and abicheck project plan consumes it to generate
run-plan.json. Parsed and validated by buildsource/project_targets.py;
see the Project Targets Schema reference
for the full field-by-field schema, the checks: list, and both
abicheck project subcommands.
Related files (not .abicheck.yml keys)¶
Some settings often discussed alongside the config live in separate YAML
files, not in .abicheck.yml:
| Concept | File / flag | Top-level schema | Docs |
|---|---|---|---|
| Policy profile | --policy <file> (PolicyFile.load, policy_file.py) — note --policy only takes the built-in names strict_abi/sdk_vendor/plugin_abi |
base_policy, overrides, reclassify, frozen_namespaces, evidence_policy |
Policies |
| Suppression rules | --suppress <file> (suppression.py) |
Suppression rule entries (YAML or ABICC format) | Suppressions |
The evidence_policy block is part of the policy file, not .abicheck.yml.
Complete example¶
A .abicheck.yml using only verified keys:
# Config schema version (forward-compat marker)
version: 1
# Build-system hint + where the compile DB lands
build:
system: cmake
compile_db: build/compile_commands.json
# Public surface definition for source collection
sources:
public_headers:
- include/**
exclude:
- include/**/detail/**
graph: summary
# Stable L2 header compile context
compile:
frontend: castxml
std: c++17
include_dirs:
- include
defines:
- MYLIB_STATIC=0
nostdinc: false
# Separate-debug-file resolution (coarse --debug-info stays a CLI flag)
debug:
format: auto
dwarf_only: false
debuginfod: false
# Severity policy consumed by `compare`
severity:
preset: default
abi_breaking: error
potential_breaking: warning
addition: info
# Public-surface scoping (false-positive control)
scope:
public: true
collapse_versioned_symbols: false
show_redundant: false
public_symbols:
- mylib_foo
- mylib_bar
# Suppression hygiene
suppression:
strict: true
require_justification: true
# Precise evidence method (optional; a concrete s0..s6, never `auto`)
source:
method: s6