Verifying a Repo: cargo capi verify
A crate built on Capi compiles sync, async, and for browser wasm from
one definition, which is exactly why a default-lane cargo test proves almost
nothing about it. Whole modules sit behind async; the single-threaded lane
changes every bound in the graph; the browser target picks a different platform
half. A green default lane says nothing about any of them. capi_verify is the
lane vocabulary that does, and cargo-capi is the command over it — the gate
in any repo built on the framework, run from the repo you are working in:
cargo install cargo-capi
cargo capi verify
cargo capi verify --full
The gate and the full form
cargo capi verify is the push gate, cheap and linear in the number of
features: the manifest lint; clippy in the fixed lanes — default, no-default,
std,async, and the single-threaded lane where the crate declares it; the
lane-matched test runs, sync and async; doctests; and the browser-wasm check
(--all-targets and no-default) where the crate reaches it.
--full is the exhaustive form, for a nightly run or the commit before a
release. It adds clippy and test feature powersets on the host and on browser
wasm, the wasm test build, the bare-metal targets, loom, the docs.rs renders
(async and sync), the whole-workspace check, the fixture gate, and formatting:
| Lane | In | What it runs |
|---|---|---|
manifest-lint | gate | the static rules below, over cargo metadata and the toolchain pin |
gate | gate | per crate: host clippy in the fixed lanes, the browser-wasm cargo check, and the lane-matched test runs |
doctests | gate | cargo test --doc in the documented lane — defaults plus async and the matching harness lane |
fmt | --full | cargo fmt --all --check |
fixtures | --full | the fixture attestation and format gate |
clippy | --full | per-crate cargo hack clippy --feature-powerset, host and browser wasm, plus the --all-targets passes for the default and async harness lanes |
tests | --full | per-crate --lib --tests in the sync and async lanes, each over a depth-2 powerset of the crate’s extra features; pinned-test-lanes add one plain run each |
wasm-tests | --full | cargo test --target wasm32-unknown-unknown --no-run — a compile check of the wasm test binaries |
bare | --full | clippy powersets on thumbv7em-none-eabihf and riscv32imac-unknown-none-elf with --cfg getrandom_backend="custom"; std/js and platform-bound features excluded |
loom | --full | model checks under --cfg capi_loom, in an isolated target/loom |
docsrs | --full | cargo doc --no-deps under the crate’s real docs.rs feature set with RUSTDOCFLAGS=-D warnings; a wasm doc build too when the docs.rs metadata lists the target |
docsrs-sync | --full | the same render with the async axis off, for a crate whose docs.rs feature set names async — the sync half of a cfg(not(feature = "async")) twin is rendered nowhere else |
workspace-check | --full | cargo check --workspace --all-targets, host and browser wasm |
--only and --skip select lanes by name, -p narrows the per-package lanes,
and --list prints the plan without running it. Every task announces itself
before it spawns, captures its output, and prints it only on failure — with a
reproduce: line that runs the same command from a shell, environment
included. --log writes one JSON record per task, and --resume <log> skips
what a previous run proved green at the same commit, so an interrupted full
run continues rather than restarts. The clippy, tests, and bare lanes
need cargo-hack; the browser and bare lanes need their rustup targets.
Policy in the manifest
How a repo verifies is declared in the repo, the way [package.metadata.docs.rs]
already is: [workspace.metadata.capi] for repo-wide settings and
[package.metadata.capi] on the package a setting names. A repo with a root
package and no workspace table puts its repo-wide settings in that package’s
table, and an empty table is a complete declaration — the defaults, browser
wasm on and every lane, suit a dual-shape add-on or a client library:
[package.metadata.capi]
# The gRPC upload test drives the reqwest adapter as a second transport, and
# its grpc lane implies its async lane, so the async test lane names it.
extra-async-test-features = ["capic_reqwest/grpc"]
[package.metadata.docs.rs]
all-features = true
Repo-wide keys: browser (whether the packages build for browser wasm; default
true), browser-only, bare (join the bare-metal lane), bare-self-configured,
skip-lanes, workspace-check-features, aliases (extra cargo aliases for the
generated block, as { name = "command" }), and wasm-incompatible-dev-deps
(extends Rule B’s list). Per-package keys: browser = false (a native-only
member), host-only (a build-time library outside the platform model),
bare = false, powerset = false (pinned lanes instead of feature powersets),
pinned-test-lanes, bare-pinned-lanes, platform-std-features,
host-only-features, mutually-exclusive-features (as [["a", "b"]]),
bare-pinned-features, bare-exclude-features, sync-docs-features,
loom-tests, extra-async-test-features, and lint-allow.
Every key is validated against the metadata it describes: an unknown key, a key in the wrong position, or a feature or dependency name the package does not have is an error, never a silently ignored string.
all-features = true is the docs.rs convention: features are additive per
target, so the full surface renders in one pass, and the docsrs lane renders
under exactly that set. The sync render has nothing to subtract from, so a
framework crate that documents with all-features and declares async names
its sync doc feature set in sync-docs-features; a client crate, whose async
only forwards, is skipped there at no cost in coverage.
The manifest lint
The platform model makes a handful of manifest mistakes easy, and each one
surfaces far from its cause — as an unresolved import three crates away, or a
lane that cannot compile at all. manifest-lint catches them statically:
- A — no
[features]entry may forward into a dev-only dependency. Cargo propagates the feature name but not the optional-dependency activation it gates, so the failure is a far-away unresolved import. - B — a dev-dependency that cannot compile for browser wasm (a curated
default list, extended with
wasm-incompatible-dev-deps) must live in a dev-dependency table whose target excludes browser wasm, whenever the crate builds for it. - D — forward symmetry over the feature axes: a crate declaring
std,async, orsingle-threadedmust forward it into every path dependency that declares the same axis. A crate with JS host bindings as browser-wasm target-table optionals routes them through a localjsfeature included in itsstd. - E — no retired
wasm-jsfeature may exist. Not allowlistable. - F — the
asyncandsingle-threadedaxes must close over the entire family dependency chain, lane-selected dev harnesses included. Not allowlistable, because the resulting lane cannot compile. - I — every package declares
rust-version. - J — the repo’s
rust-toolchain.tomlpins exactly the highestrust-versionits packages declare, so the declared floor is what compiles them. A floating channel and a missing file are both violations.
An intentional exception to A, B, or D goes in the package’s own table, as
lint-allow = ["rule_a:<feature>:<dep>", "rule_b:<dev-dep>", "rule_d:<dep>:<axis>", "rule_d:js"].
An entry that suppresses nothing is an error, so the list cannot go stale.
The alias block
The async axis reshapes traits graph-wide and cannot be forwarded into a
dev-only test harness from a lib feature, so the harness lane has to be named
at the command line — cargo test --features async,capi_test_framework/async —
and that is the spelling nobody should retype. cargo capi aliases derives it
from the manifest and writes it into the repo’s committed .cargo/config.toml:
# >>> generated by `cargo capi aliases` — do not edit
[alias]
test-sync = "test"
test-async = "test --features async,capi_test_framework/async,capic_reqwest/async,capic_reqwest/grpc"
check-wasm = "check --target wasm32-unknown-unknown --all-targets"
# <<< end generated
Only the fenced block belongs to the writer; durable per-repo cargo settings
live outside the fences in the same file and survive regeneration. --check
fails when the committed block has drifted from what the metadata implies, and
a full-scope verify runs it. No [patch.crates-io] table lives here,
deliberately: a patch entry names a filesystem path, and this file is
committed — the table that redirects family crates at local checkouts is
cargo capi patch’s,
written at a parent directory and inside no repo.
The fixture gate
cargo capi fixtures is the commit gate on recorded test data: it walks every
tests/data directory in the repo, reads what each fixture attests, and refuses
an unmasked or unattested file, a file in a shape the format gate does not
accept, and any working artifact carrying a capture= marker — the
attestation model
in one command. --stamp synthetic writes that disposition into every sidecar
that attests none, a claim made on your behalf for a corpus you know never held
a real secret; --list-synthetic prints the attested set for review. The
fixtures lane of a full verify runs the same check, and a suite runs it as
one of its own tests through FixtureAudit.
What “done” means
Per-crate lanes while iterating, cargo capi verify before a push, --full
before a merge or a release — and, whichever you ran, name it when you report.
A repo’s CI calls the same command, so local verification and the pipeline
agree on what green means; and because the framework’s own crates verify with
the same tool, a client library’s gate is the one the framework’s authors run.
This closes the Testing part — and the journey from an empty project to a verified, shippable client. What follows is reference material: the internals of one query, the full error model, the design rationale, and the appendix.