.abicheck.yml Project Targets Reference¶
.abicheck.yml's targets:/bundles:/profiles:/baseline:/aggregate:
block is the
portable, project-owned surface that declares a project's CI-integration
topology: which libraries/consumers/plugin-contracts exist, how they group
into release bundles, which build profiles are ABI contracts, which baseline
channels exist, and exactly which {channel, depth, required, gate_mode}
checks run against each target (G30).
Status. This page documents the schema and the
abicheck project validatecommand shipped in G30 P1.5. The run-plan generator that reads a validated block to fan out CI checks (abicheck project plan,check-single.yml/check-project.yml) is G30 P1.4 — see the run-plan schema and the reusable workflows reference. A project not using G30's CI-integration primitives sees no behavior change at all from adding (or omitting) this block: nothing indump/comparereads it today.
Example¶
# .abicheck.yml (excerpt)
targets:
libpvxs:
kind: library # default
binary_pattern: "lib/libpvxs.so*"
public_headers: ["headers/pvxs"]
bundle: pvxs-release
bundle_only: false # run libpvxs both standalone AND as a bundle member
checks:
- channel: accepted-main
depth: headers
required: true
gate_mode: local
libpvxsIoc:
kind: library
binary_pattern: "lib/libpvxsIoc.so*"
public_headers: ["headers/pvxsIoc"]
bundle: pvxs-release
myapp-consumer:
kind: app-consumer # compare --used-by
consumer_binary_pattern: "bin/myapp"
library: libpvxs
ioc-plugin-contract:
kind: plugin-contract # compare --required-symbol @FILE
contract_file: "contracts/ioc-plugin.syms"
library: libpvxsIoc
bundles:
pvxs-release:
targets: [libpvxs, libpvxsIoc]
profiles:
linux-x86_64-gcc13-release:
contract: true # this lane IS an ABI contract — gets a baseline, gates CI
os: linux
arch: x86_64
ubuntu-latest-clang-debug-sanitizer:
contract: false # test-only CI lane — never gets a baseline
baseline:
channels:
release-contract: {source: github-release, asset_pattern: "abicheck-baseline-*.tar.zst"}
accepted-main: {source: actions-cache, key_prefix: "abicheck-baseline-main"}
abicheck project validate — like the rest of .abicheck.yml —
loads this via PyYAML's safe_load,
so no custom YAML tags are ever evaluated.
targets:¶
A mapping of target id → target entry. Every id must match
^[A-Za-z0-9][A-Za-z0-9._-]*$ — the same charset the report-identity
envelope requires for check_id's
target@profile#baseline_channel@depth components, so a valid id here can
never produce an ambiguous identifier downstream.
kind (default library) is a discriminator; the remaining fields it
accepts/requires depend on it:
kind |
Required fields | Forbidden fields | Meaning |
|---|---|---|---|
library (default) |
binary_pattern |
consumer_binary_pattern, contract_file |
An ordinary shared-library ABI contract (S1–S17, S26). |
app-consumer |
consumer_binary_pattern, library |
binary_pattern, contract_file |
An application compatibility check (S22, compare --used-by). |
plugin-contract |
contract_file, library |
binary_pattern, consumer_binary_pattern |
A plugin/dlopen entrypoint contract (S23, compare --required-symbol @FILE). |
Common optional fields for kind: library:
| Field | Type | Meaning |
|---|---|---|
public_headers |
list of string | Public header roots for this target, forwarded as check-target's own header input for this target's generated cells (newline-joined, so a header root containing whitespace survives action/run.sh's add_flag() intact) — see run-plan-schema.md's header field and reusable-workflows.md's per-cell-override exception. Not yet honored for a kind: bundle member (no per-bundle-member header staging exists). |
bundle |
string | The bundles: entry this target belongs to. Must be declared under bundles:, and that bundle's own targets: list must include this target back (the two must agree). |
bundle_only |
boolean, default false |
When true, this target is checked only as a bundle member, never standalone. Requires bundle to be set, and must not declare its own checks: — a bundle_only target's own checks would never run standalone, so declare the policy under bundles:<id>.checks instead. |
checks |
list of check tuple | See checks: below. |
app-consumer/plugin-contract fields:
| Field | Type | Meaning |
|---|---|---|
consumer_binary_pattern |
string | (app-consumer only) Path pattern to the consumer binary under test. |
contract_file |
string | (plugin-contract only) A .syms file — one required linker symbol per line, # comments allowed. This is --required-symbol @FILE's actual on-disk format (abicheck/cli_helpers_compare.py's load_required_symbols), not YAML. |
library |
string | The kind: library target this entry resolves its baseline and candidate-artifact lookup through . Must name a real, declared kind: library target — never another app-consumer/plugin-contract entry. The check's own reporting identity (check_id/target_id) stays this entry's own name; only the lookup redirects to library. |
checks:¶
Each targets:<id>.checks[] entry is a {channel, depth, required,
gate_mode, profiles, allow_new_target, id, analysis} tuple — the assignment missing from the plain targets:/baseline:
channels: excerpt: declaring which channels exist doesn't say which
channel/depth/policy a given target actually runs.
| Field | Type | Default | Meaning |
|---|---|---|---|
channel |
string | — (required) | A baseline.channels id, or the literal "none" for a no-baseline audit check (scenario S5 — check-target must skip resolve-baseline entirely for this sentinel, never look it up as a declared channel). channel: "none" is only supported for a kind: library target — rejected at validation time for app-consumer/plugin-contract (no --used-by/--required-symbol equivalent for a one-build audit) and for any bundles: check (a bundle's candidate is always a staged directory of member binaries, which compare's audit-only shape rejects outright). |
depth |
string | — (required) | One of binary, headers, build, source — the same four rungs --depth/the report envelope's requested_depth accept. |
required |
boolean | true |
Whether this check gates aggregate's coverage requirement. |
gate_mode |
string | local (advisory when channel: "none") |
One of local, deferred, advisory. A channel: "none" no-baseline audit check defaults to advisory, not local — it has no baseline-drift verdict to gate CI on, so a minimal {channel: none, depth: ...} entry must not unexpectedly block CI (scenario S5: "Advisory by default"). Set gate_mode explicitly to override either default. |
profiles |
list of string | (unset) | An explicit profile-id selector — see Profile scoping below. A profile with contract: false may only be named here by a channel: "none" audit check — a real-channel check can never resolve a baseline on a lane that's documented to never get one (S17). |
allow_new_target |
boolean | false |
Forwarded as check-target's allow-new-target input — true turns a target genuinely absent from this check's otherwise-resolved baseline-set into the advisory new_target outcome instead of ambiguous (e.g. a new library's first release). Pair with required: false, or a required-coverage gate would still block on the target's first appearance. Rejected at validation time for any bundles: check — a bundle comparison needs one coherent release where every member already coexisted, so there is no well-defined old side for a member that's new. See Baseline Management → A new library's first release. |
id |
string | (unset) | (G42) An explicit, project-owned logical id for this check, appended to the generated check_id as a ~<id> tail. Two checks[] entries that would otherwise generate the identical target@profile#channel@depth string (most commonly two entries differing only in analysis:) must each declare a distinct id: — abicheck project plan rejects an unresolved collision outright, naming which entries collide and pointing at id: as the fix. |
analysis |
mapping | (unset) | (G42) {evidence, policy, assurance}, each an optional identifier string. evidence names which extraction/comparison method produced this check's facts (e.g. replay vs. clang-plugin); policy/assurance reference the already-existing policy-profile and assurance mechanisms. At this phase these are distinguishing/reporting labels only — carried through to run-plan.json and (via id's ~<id> tail) into the report's check_id, but nothing yet selects a different extraction pipeline or policy based on them. |
Profile scoping for checks:¶
There is an open gap: naively crossing every checks: entry with
every contract: true profile produces impossible cells for a target that
doesn't exist on every profile (a Windows-only library, a Linux-only .so).
This schema resolves it with two complementary mechanisms:
- An explicit
profiles:selector on achecks:entry restricts that check to the listed profile ids (each validated againstprofiles:). Use this when a check is genuinely profile-specific. - When
profiles:is omitted, this schema does not itself resolve a profile list — G30 P1.4's run-plan generator is responsible for deriving the actual(target, profile)cells from each profile's ownbuild-output.jsontargets[]list (only generating a cell where the target actually appears in that profile's declared targets), never from a blind cross-product.abicheck project validatecannot check that downstream behavior — it only validates that an explicit selector, when given, names real profile ids.
bundles:¶
A mapping of bundle id → {targets: [...], checks: [...]}. Every listed
target must be a declared kind: library target, and if that target itself
sets a bundle: field, it must name this same bundle back — the validator
flags a mismatch (e.g. a target claims bundle: bundle-a but only
bundle-b lists it as a member) as an integrity error, not a silent
inconsistency.
checks: on a bundle uses the exact same {channel, depth, required,
gate_mode, profiles} shape described above for a target — the run plan emits a kind: "bundle" check entry alongside
per-target ones (S14 bundle-scoped analysis, e.g. soname/provider-set
checks across the whole release), and that cell needs its own
baseline-channel/depth/gate policy independent of its member targets'.
Two restrictions that don't apply to a target check: depth must be
binary (never headers/build/source — a bundle check always
compares directories, which the CLI's per-library release fan-out never
collects inline build/source evidence for; headers is additionally
unsafe because a bundle's baseline is always raw binaries with no
historical header snapshot, so both sides would be parsed against the
same current checkout's headers, silently missing a header-only change),
and channel may not be "none" (a bundle's candidate is always a staged
directory of member binaries, which compare's audit-only shape — the
no-baseline routing — rejects outright). Both are rejected at validation
time.
A bundle spanning multiple mandatory build-toolchain variants (e.g. a
CPU build and a DPC++ build of the same release) must give its check an
explicit profiles: selector naming every required profile —
checks: [{channel: release, depth: binary, profiles: [cpu, dpc]}], not a
bare checks: [{channel: release, depth: binary}]. Profile scoping's
implicit sweep (profiles: omitted) is for "run this bundle wherever it
happens to apply," and treats a profile that doesn't build every member as
a silent, valid skip for that profile — correct for a genuinely
profile-specific bundle, wrong for one where every listed variant is
mandatory. With an explicit selector, a profile missing any member (or
missing a build-output.json entirely) is a hard abicheck project plan
error instead, which is what makes "the DPC++ variant went missing between
releases" a loud CI failure rather than a silently-incomplete run. G30
(run_plan.py's cell generation, one independent RunPlanCheck per
(bundle, profile) pair) is what already makes this true — see
tests/test_run_plan_bundle_multi_profile.py for the worked example.
profiles:¶
A mapping of profile id → {contract, os, arch, dependency_source, compile,
consumer_compile}. contract
(default true) decides whether this build lane is an ABI contract (gets a
baseline, gates CI) or a test-only CI lane that never gets one — "not every
CI lane gets a baseline" is the whole point of this field (S17). The map
key is the same profile.id string used throughout build-output.json,
run-plan.json, and the report envelope's profile_id field.
The optional compile: sub-block (P1 toolchain-profile audit) declares the
compiler/dialect/ABI-macro axes this profile pins — additive over the root
compile: block: compiler_family, compiler_version (a version
constraint string), target (a target triple), standard, stdlib,
binding (see below), abi_macros (a string→string mapping), and args
(a list of normalized extra compiler-flag atoms). Every string value must
be a single whitespace-free atom — a .abicheck.yml found by auto-discovery
is untrusted, and whitespace would let one YAML scalar smuggle multiple
argv tokens. standard/stdlib/target/abi_macros/args reach abicheck
project plan (P1 toolchain-profile audit, closing this gap) as each
resolved cell's composed compile_gcc_options; binding additionally
reaches compile_gcc_path, but only when project plan
--toolchain-bindings <path> resolves it (see below) — see
run-plan-schema.md's RunPlanCheck fields
for the exact composition rule and reusable-workflows.md's "Shared
analysis options" for how check-project.yml forwards them per cell.
compiler_family/compiler_version are validated here but not yet
projected into any invocation — see that same section for why.
profiles:
linux-x86_64-gcc14-libstdcxx-gnu17-default:
contract: true
os: linux
arch: x86_64
compile:
compiler_family: gcc
compiler_version: ">=14.0,<15"
standard: gnu++17
stdlib: libstdc++
binding: gcc14
consumer_compile: — a separate client-toolchain overlay (G34 Phase 0)¶
The optional consumer_compile: sub-block accepts the identical shape as
compile: (same fields, same validation), but declares a different
axis: compile: is the producer/artifact toolchain the library binary
was actually built with (mangling, layout, vtables, calling convention,
linked standard-library ABI); consumer_compile: is a client toolchain a
user of the library compiles their own code with against the public
headers, when it differs from the producer (which #ifdef __GNUC__/
__clang__/_MSC_VER branch, which standard-library ABI, which template
instantiation the client actually sees). A profile with no
consumer_compile: behaves exactly as today — its compile: block doubles
as the consumer's, so existing single-toolchain projects need no edits:
profiles:
linux-gcc14-build-clang20-client:
contract: true
os: linux
compile:
binding: gcc14
standard: gnu++17
consumer_compile:
binding: clang20
standard: gnu++20
stdlib: libc++
consumer_compile:'s fields reach abicheck project plan the same way
compile:'s do, into their own separate pair —
consumer_compile_gcc_path/consumer_compile_gcc_options
— never falling back to the producer overlay's own resolved values when
absent. Applied, for the candidate side only: check-project.yml runs
a separate dump of the (unchanged) candidate binary under this overlay's
frontend/binding/options, and the comparison consumes that materialized
snapshot as the candidate's entire new side — see the
compile.frontend/consumer_compile.frontend section immediately below
for how that separate dump is wired, and its "Known gap" note for what
still isn't covered (the baseline/old side). This is a real, working
extraction pass, not the L0/L1-producer-plus-L2-consumer merge originally
scoped in
docs/contribute/plans/g34-producer-consumer-compiler-profile-separation.md's
Phase 0 — see that plan for the design this superseded.
compile.frontend / consumer_compile.frontend — per-profile AST frontend (G34 Phase B)¶
Either overlay may set frontend: to one of the same four values the
global --ast-frontend flag accepts (auto/castxml/clang/hybrid),
overriding the global default for that profile's cell only:
profiles:
linux-gcc14-build-clang20-client:
contract: true
compile:
binding: gcc14
frontend: castxml
consumer_compile:
binding: clang20
frontend: clang
Reaches abicheck project plan as
compile_ast_frontend/consumer_compile_ast_frontend,
resolved independently for each overlay (a profile with no frontend: set
on an overlay leaves that field empty, deferring to a caller's own global
--ast-frontend/default — it never falls back to the other overlay's
frontend: value).
compile.frontend is applied end to end: check-project.yml's check job
forwards the projected compile_ast_frontend as that cell's own
--ast-frontend, preferring it over the workflow-level input the same way
gcc-path/gcc-options already prefer their per-cell overlay. So the
example above genuinely runs its producer pass under castxml. The one
exception is a kind: bundle check, whose operand is a staging directory
— the root Action rejects any non-auto frontend there, so such a cell
keeps resolving the workflow-global value.
consumer_compile.frontend drives a separate candidate dump in the native
check-project pipeline. The dump reads the same producer binary but parses
its public headers with the consumer overlay's frontend, compiler binding,
and options; the comparison then consumes that materialized snapshot instead
of parsing the candidate headers again under the producer context.
Known gap: only the candidate side is dumped under the consumer
context today. The baseline (old) side of a real (non-none)
baseline-channel comparison is produced hours or days earlier by
publish-baseline.yml/update-main-baseline.yml, which read only
build-output.json and have no way to apply a consumer_compile: overlay
to that dump. Comparing a consumer-context candidate against a
producer-context baseline usually differs enough in extraction-profile
fingerprint that compare's own comparability gate refuses the pair as
NOT_COMPARABLE/ProfileMismatchError rather than silently comparing
mismatched contexts — so consumer_compile: combined with a real baseline
channel does not yet work end to end. It is unaffected when
baseline-channel: none (an audit-only compare --no-baseline run has no
baseline snapshot to mismatch against). See abicheck/buildsource/run_plan.py's own docstring
and the G34 plan doc's Phase 0 for what closing this needs.
os: and dependency_source: — how a profile schedules its own check cell (G34 Phase C)¶
These two decide where a profile's check-project.yml check cell runs and
how it provisions its own system dependencies. Before this phase both were
fixed for the whole run: every cell ran on a hardcoded ubuntu-latest, and
dependency installation came from one workflow-level install-deps boolean —
so an os: windows profile could not be checked natively, and a GCC-profile
cell and a Clang-profile cell in the same run could not each get a matching
toolchain.
profiles:
linux-gcc14:
contract: true
os: linux # → runs-on: ubuntu-latest
dependency_source: conda-forge-gcc14
windows-msvc:
contract: true
os: windows # → runs-on: windows-latest
linux-clang20:
contract: true
os: linux
dependency_source: conda-forge-clang20
os: accepts linux, windows, macos (or darwin), case-insensitively,
and additionally passes a GitHub-hosted runner label through verbatim
(ubuntu-24.04, windows-2022, macos-14) so a project that already wrote
an image there keeps working. It reaches abicheck project plan as each
cell's runs_on. A profile with
no os: resolves to ubuntu-latest — that is every profile written before
this phase, so their scheduling is unchanged. A value naming no schedulable
platform (os: freebsd) is a validation error rather than a silent
fallback to Linux: a cell scheduled on the wrong platform reports success
having gated the wrong thing.
dependency_source: accepts the same five values as the Action's own
dependency-source input — conda-forge,
conda-forge-gcc14, conda-forge-clang20, system, none — and reaches
the cell as dependency_source,
forwarded to check-target. It is optional: an undeclared value leaves the
caller's workflow-level dependency-source input standing, and with both
unset the legacy install-deps boolean still decides, exactly as before.
compile.binding — resolving a logical toolchain id¶
binding is a logical identifier (e.g. "gcc14"), never a raw
executable path or command — the same untrusted-config trust boundary as
every other compile: field. Resolving it to an exact executable requires
a separately trusted bindings file (schema
abicheck.toolchain-bindings/v1):
# bindings.yml — operator/CI-managed, never auto-discovered
schema: abicheck.toolchain-bindings/v1
bindings:
gcc14: /opt/gcc-14.2.0/bin/g++
castxml07: /opt/conda/bin/castxml
abicheck project validate --toolchain-bindings bindings.yml checks
that every declared profiles.<id>.compile.binding resolves against it,
in addition to the ordinary validation checks below — a config author can
catch a typo'd or undeclared binding id before CI runs. Omitting
--toolchain-bindings skips this check entirely (a profile declaring a
binding with no bindings file given is not itself a validation error);
loading a bindings file with the wrong schema or a malformed document is
a usage error (exit 64), matching the rest of this command's strict-parsing
convention.
baseline:¶
Currently one recognized sub-key, channels: — a mapping of channel id →
{source, asset_pattern, key_prefix}:
source |
Requires | Backend |
|---|---|---|
github-release |
asset_pattern |
A GitHub Release asset — atomic single-tarball upload. |
actions-cache |
key_prefix |
GitHub Actions cache — cheap, no push, naturally ages out. |
git |
(neither) | Committed to the repo — S1's minimal case only, must go through a PR. |
An external object store (a fourth possible backend) is out of
scope for P0/P1 and not a valid source value here.
aggregate:¶
The durable, project-owned home for abicheck aggregate's
missing-required/unexpected-target gate policy (CLI cleanup phase two, PR 2
follow-up), stated once instead of re-typed on every project plan
invocation. abicheck project plan stamps this block's values onto the
generated run-plan.json's own top-level gate block (see the
run-plan schema)
— the same gate shape a hand-authored aggregate --manifest carries
directly (see Aggregate Reports).
One recognized sub-key, gate:, with two independently optional fields:
aggregate:
gate:
missing_required: fail # fail | warn (default: fail)
unexpected_target: include # include | warn | fail | ignore (default: include)
Omitting aggregate: entirely, or either gate: sub-key, leaves that field
unset on the generated plan — aggregate's own hard-coded defaults apply,
unchanged from before this block existed. There is no per-invocation CLI
override: project plan's former --gate-missing-required/
--gate-unexpected-target flags were removed (no CLI alias, same "no
deprecation window" stance as the rest of this cleanup) — set the policy
here instead.
Known limitations of the declarative topology¶
The declarative targets:/bundles:/profiles:/baseline: path is not
yet a drop-in replacement for every project. abicheck compare on
directory/package inputs, driven directly from your own workflow, remains
the path every project can use today (see
Multi-Binary Releases; note that its
cross-library findings — removed dependencies, provider changes — are
ELF/Linux-only, a Windows/macOS release gets per-library results with
bundle analysis skipped). Before adopting the
declarative topology, confirm none of these apply to you:
bundles:checks run at one depth only — see thebundles:section above for the allowed depth and whatproject validaterejects. If a bundle-level check needs header-scope evidence, it can't run through abundles:entry today.- Per-target
public_headers:is not honored for akind: bundlemember — see thepublic_headersrow of thetargets:field table above for the one authoritative statement of that gap. - Stored-facts bundle comparison (
BundleFacts) has no run-plan / composite-Action /check-project.ymlwiring — it is reachable from the Python API and fromcompare(OLD_INPUT auto-detected as a storedBundleFactsdocument), not from the declarative CI surface. publish-baseline.ymlexpects onebuild-output.jsonper contract profile (G30 P1.1). A build system that doesn't emit a per-profile manifest in that shape needs to add one first — see thebuild-output.jsonreference.profiles:describes a build lane (compiler/flags), not a library — one profile'stargets[]can list several libraries built under it, but a project needs one profile per distinct build configuration: e.g. a release with a SYCL-built subset needs one profile for that subset and another for the rest, not one profile per library.
None of these block compare-based CI — they are gaps in the declarative
topology specifically. The design history for the bundle-depth restriction
and the build-output.json contract lives in the
G30 GitHub Actions integration plan,
and for stored-facts bundle comparison in the
G38 bundle-facts plan.
Neither lists these as scheduled work, so check the code itself before
relying on a gap having closed.
Validation¶
abicheck project validate [CONFIG] (CONFIG defaults to
.abicheck.yml in the current directory) checks:
- Every target's
kind-specific required fields are set, and no kind-inappropriate field is (see the table above). app-consumer/plugin-contracttargets'libraryresolves to a real, declaredkind: librarytarget — never to anotherapp-consumer/plugin-contractentry, and never to an undeclared name.bundle_only: truerequiresbundleto be set, and forbids the target from declaring its ownchecks:(it's checked only as a bundle member; a standalone check on it would never run).- Every
bundle:reference resolves to a declaredbundles:entry, and everybundles:<id>.targets[]member resolves to a declaredkind: librarytarget whose ownbundle:field (if set) agrees. - Every
checks[].channelresolves to a declaredbaseline.channelsid, or is the"none"no-baseline sentinel. checks[].depthis one of the four valid rungs;checks[].gate_modeis one oflocal/deferred/advisory.- Every
checks[].profilesentry resolves to a declaredprofiles:id, and acontract: falseprofile may only be named by achannel: "none"audit check. - Every target/bundle/profile/channel id matches the
check_id-safe identifier charset. - Rules 5-7 apply identically to a bundle's own
checks[], not just a target's. checks[].allow_new_target: trueis rejected on any of a bundle's ownchecks[]— a bundle comparison needs one coherent release where every member already coexisted, so there is no well-defined old side for a member that's new.
Structural/type errors in the YAML itself (an unknown key at any level —
including a misspelled top-level block like tagrets:, checked against the
full .abicheck.yml key set, not just this block's five keys — or a
value of the wrong type, e.g. contract: "yes" instead of a boolean) fail
immediately, as a usage error, matching .abicheck.yml's existing
strict-parsing convention — the validation report above only
covers cross-reference/semantic issues on an already-well-formed block.
CLI¶
$ abicheck project validate .abicheck.yml
project-targets validation: .abicheck.yml
OK — no errors.
$ abicheck project validate .abicheck.yml -o json=-
{
"ok": true,
"errors": [],
"warnings": []
}
Exit codes: 0 valid (warnings may still be present), 1 one or more
validation errors, 64 usage error (CONFIG is not readable YAML, or its
targets:/bundles:/profiles:/baseline:/aggregate: block fails
strict parsing).