.abicheck.yml Project Targets Reference¶
.abicheck.yml's targets:/bundles:/profiles:/baseline: 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/ADR-047 §3).
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/compare/scanreads 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-symbols
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 (ADR-047 §7) 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-symbols). |
Common optional fields for kind: library:
| Field | Type | Meaning |
|---|---|---|
public_headers |
list of string | Public header roots for this target. |
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-symbols's actual on-disk format (abicheck/cli_compare_helpers.py's _load_required_symbols), not YAML. |
library |
string | The kind: library target this entry resolves its baseline and candidate-artifact lookup through (ADR-047 §3's "unstated rule" correction). 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} tuple — the assignment ADR-047 §3 itself identifies as
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 (ADR-047 §6 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-symbols equivalent for a one-build audit) and for any bundles: check (a bundle's candidate is always a staged directory of member binaries, which the root Action's scan mode 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 (ADR-047 §4/§7). 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 (ADR-047 §8's S5 row: "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). |
Profile scoping for checks:¶
ADR-047 §3 flags 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
ADR-047 §5 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 the root Action's scan mode — the
no-baseline routing — rejects outright). Both are rejected at validation
time.
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. Not yet wired: actually applying consumer_compile: to a
separate header-AST (L2) extraction pass and merging it with the producer
toolchain's binary (L0/L1) facts — this schema slice only projects the
config axis into run-plan.json; see
docs/contribute/plans/g34-producer-consumer-compiler-profile-separation.md's
Phase 0 for the remaining extraction/merge integration.
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, by contrast, is still projection only — it
describes the header-AST pass of the two-pass producer/consumer extraction
that consumer_compile: itself has not built yet, so nothing forwards it;
see the G34 plan doc's Phase B and Phase 0 for what remains.
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 (ADR-047 §10) |
|---|---|---|
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 backend ADR-047 §10 lists) is out of
scope for P0/P1 and not a valid source value here.
Validation¶
abicheck project validate [CONFIG] (CONFIG defaults to
.abicheck.yml in the current directory) checks, per ADR-047 §3:
- 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.
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 four 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 (ADR-043) — 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 --format 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: block fails strict parsing).