Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

LaneInWhat it runs
manifest-lintgatethe static rules below, over cargo metadata and the toolchain pin
gategateper crate: host clippy in the fixed lanes, the browser-wasm cargo check, and the lane-matched test runs
doctestsgatecargo test --doc in the documented lane — defaults plus async and the matching harness lane
fmt--fullcargo fmt --all --check
fixtures--fullthe fixture attestation and format gate
clippy--fullper-crate cargo hack clippy --feature-powerset, host and browser wasm, plus the --all-targets passes for the default and async harness lanes
tests--fullper-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--fullcargo test --target wasm32-unknown-unknown --no-run — a compile check of the wasm test binaries
bare--fullclippy powersets on thumbv7em-none-eabihf and riscv32imac-unknown-none-elf with --cfg getrandom_backend="custom"; std/js and platform-bound features excluded
loom--fullmodel checks under --cfg capi_loom, in an isolated target/loom
docsrs--fullcargo 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--fullthe 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--fullcargo 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, or single-threaded must 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 local js feature included in its std.
  • E — no retired wasm-js feature may exist. Not allowlistable.
  • F — the async and single-threaded axes 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.toml pins exactly the highest rust-version its 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.