Useful Cargo Plugins: nextest, watch, audit, deny, expand, and More
22 min read
Quick Overview
Section titled “Quick Overview”In the Node world you assemble a working developer toolbox out of dozens of small packages: vitest/jest for tests, nodemon/tsx --watch for the dev loop, npm audit for vulnerabilities, license-checker for legal hygiene, depcheck for unused dependencies, a bundle analyzer for “why is my build so big.” Some you install globally, some you run once with npx, some live in devDependencies.
Cargo has the same culture, but the mechanism is cleaner: a cargo plugin is just a binary on your PATH named cargo-<thing>, which Cargo then exposes as the subcommand cargo <thing>. You install one with cargo install <crate> (the rough equivalent of npm install -g), and from then on cargo nextest, cargo audit, cargo deny, etc. behave as if they were built in. There is no plugin registry, no manifest entry, no config dance — Cargo discovers them by name.
This page tours the plugins worth installing for almost any serious project: cargo-nextest (a faster, nicer test runner), cargo-watch (re-run on file change), cargo-audit (RUSTSEC vulnerability scan), cargo-deny (license / advisory / ban policy), cargo-expand (see what macros expand to), cargo-outdated (find stale dependencies), and cargo-bloat (what is taking up binary space), plus quick notes on cargo-llvm-cov (coverage), cargo-machete (unused deps), and cargo-edit (now mostly built in).
Note: The current stable toolchain is Rust 1.96.0 on the 2024 edition. Every plugin here installs as a normal stable binary except
cargo-expand, which needs a nightly toolchain at runtime (explained below). Versions cited are the latest at the time of writing; always letcargo installresolve the current release rather than pinning from memory.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”A typical Node project wires its developer tooling through devDependencies and package.json scripts, mixing in a few npx one-offs:
// package.json — the JS "tooling" surface, scattered across dev deps + scripts{ "name": "billing-api", "scripts": { "test": "vitest run", "test:watch": "vitest", "dev": "tsx watch src/index.ts", "audit": "npm audit --audit-level=high", "licenses": "license-checker --production --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause'", "deadcode": "depcheck", "outdated": "npm outdated", "analyze": "esbuild src/index.ts --bundle --analyze --metafile=meta.json" }, "devDependencies": { "vitest": "^2.1.0", "tsx": "^4.19.0", "depcheck": "^1.4.7", "license-checker": "^25.0.1", "typescript": "^5.7.0" }}And the one-offs you reach for occasionally without installing anything:
# Run a tool once without adding it as a dependency:npx depcheck # find unused dependenciesnpx license-checker --summarynpm outdated # list dependencies behind their latest versionnpm audit # scan the lockfile against the advisory DBNotice the split: the dev loop tools (vitest, tsx watch) live in devDependencies so every contributor gets them on npm install, while occasional audits (depcheck, license-checker) are often npx-only. Cargo collapses all of this into one model — cargo install a binary once, run it as a cargo subcommand — and every plugin below maps onto one of these JS tools.
Rust Equivalent
Section titled “Rust Equivalent”Install the toolbox once. Each cargo install drops a cargo-<name> binary into ~/.cargo/bin (already on your PATH after rustup), and Cargo immediately exposes it as a subcommand:
# Test + dev loop (your vitest / tsx watch):cargo install cargo-nextest --lockedcargo install cargo-watch --locked
# Supply-chain + policy (your npm audit / license-checker):cargo install cargo-audit --lockedcargo install cargo-deny --locked
# Inspection + maintenance (your depcheck / npm outdated / bundle analyzer):cargo install cargo-expand --lockedcargo install cargo-outdated --lockedcargo install cargo-bloat --lockedcargo install cargo-machete --lockedcargo install cargo-llvm-cov --lockedTip: Pass
--lockedtocargo installso the tool builds against its own committedCargo.lock— reproducible installs, fewer surprises. It is thenpm ciofcargo install.
You can confirm what is installed at any time — the closest thing to npm ls -g:
cargo install --listcargo-audit v0.22.1: cargo-auditcargo-bloat v0.12.1: cargo-bloatcargo-deny v0.19.8: cargo-denycargo-expand v1.0.122: cargo-expandcargo-llvm-cov v0.6.21: cargo-llvm-covcargo-nextest v0.9.128: cargo-nextestcargo-outdated v0.19.0: cargo-outdatedcargo-watch v8.5.3: cargo-watchFrom here, each tool is a cargo subcommand. Running the faster test runner in a small project looks like this (real captured output):
$ cargo nextest run Starting 3 tests across 2 binaries PASS [ 0.008s] (1/3) demo tests::adds PASS [ 0.011s] (2/3) demo tests::doubles PASS [ 0.011s] (3/3) demo tests::negatives──────────── Summary [ 0.012s] 3 tests run: 3 passed, 0 skippedDetailed Explanation
Section titled “Detailed Explanation”cargo-nextest — the vitest/jest upgrade
Section titled “cargo-nextest — the vitest/jest upgrade”The built-in cargo test runs all the tests inside one process per test binary, sharing state and printing results serially. cargo-nextest is a drop-in replacement that runs each test in its own process, in parallel, with a much clearer per-test report, flaky-test retries, and machine-readable output for CI.
cargo nextest run # run everything (your `vitest run`)cargo nextest run billing:: # filter by path substringcargo nextest run -E 'test(parse)' # filter with the nextest filter DSLcargo nextest run --retries 2 # auto-retry flaky tests (CI lifesaver)cargo nextest run --no-fail-fast # don't stop at first failureWhy teams switch:
- Faster on suites with many small tests, because process-level parallelism scales better than the default in-binary threads.
- Clearer output — one line per test with timing, and failures grouped at the end instead of interleaved.
- CI-friendly —
--message-format libtest-jsonand JUnit output (cargo nextest run --profile ci) integrate with test reporters.
The one gap: nextest does not run doctests (the /// examples in your docs), because those have no separate binary. The standard pattern is cargo nextest run && cargo test --doc.
cargo-watch — the nodemon/tsx watch of Rust
Section titled “cargo-watch — the nodemon/tsx watch of Rust”cargo-watch watches your source tree and re-runs a Cargo command on every change — the tight feedback loop Node developers expect from nodemon or tsx watch:
cargo watch -x check # re-run `cargo check` on save (fastest loop)cargo watch -x test # re-run tests on savecargo watch -x 'nextest run' # combine with nextestcargo watch -x clippy -x test # chain: clippy, then testcargo watch -x run # rebuild + restart your binary (like nodemon)cargo watch -s 'cargo run -- --port 8080' # arbitrary shell command via -s-x takes a Cargo subcommand; -s takes an arbitrary shell command (use it for anything that is not a bare cargo call). For the fastest edit loop, watch cargo check rather than cargo build — check does type-checking without codegen, so it returns in a fraction of the time.
Note: Cargo is gaining a built-in
cargo watch-style mode, but the standalonecargo-watchplugin remains the ubiquitous, battle-tested choice today. The flags above are stable.
cargo-audit — npm audit for Rust
Section titled “cargo-audit — npm audit for Rust”cargo-audit scans your Cargo.lock against the RUSTSEC advisory database and reports any dependency with a known vulnerability — exactly what npm audit does against npm’s advisory feed:
cargo audit # scan Cargo.lock against RUSTSECcargo audit --deny warnings # treat unmaintained/yanked warnings as errors (CI)cargo audit fix # attempt to bump vulnerable deps (like `npm audit fix`)On a clean project it walks the lockfile and reports nothing wrong (real captured output):
$ cargo audit Fetching advisory database from `https://github.com/RustSec/advisory-db.git` Loaded 1100 security advisories (from ~/.cargo/advisory-db) Updating crates.io index Scanning Cargo.lock for vulnerabilities (2 crate dependencies)When it does find something, it prints the advisory ID (RUSTSEC-YYYY-NNNN), the affected crate and version, the dependency path that pulls it in, and the patched version range to upgrade to. RUSTSEC also tracks unmaintained and yanked crates, which surface as warnings.
cargo-deny — policy enforcement (license-checker + audit + bans)
Section titled “cargo-deny — policy enforcement (license-checker + audit + bans)”Where cargo-audit answers “any known vulnerabilities?”, cargo-deny enforces policy: which licenses are allowed, which crates are banned, whether duplicate versions are tolerated, and which advisories block the build. It is configured with a deny.toml and run as one gate:
cargo deny init # generate a starter deny.tomlcargo deny check # run all checkscargo deny check licenses # just the license policy (your license-checker)cargo deny check advisories # RUSTSEC, overlapping with cargo-auditcargo deny check bans # banned crates + duplicate-version policy# deny.toml — license allowlist, banned crate, and advisory policy[licenses]# Only these SPDX licenses are permitted anywhere in the graph:allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "Unicode-3.0"]
[bans]# Fail if a copyleft or known-problem crate sneaks in transitively:deny = [{ crate = "openssl-sys", reason = "prefer rustls; avoid system OpenSSL" }]# Warn when two versions of the same crate end up in the tree:multiple-versions = "warn"
[advisories]# Block the build on any RUSTSEC advisory (and on yanked crates):yanked = "deny"This is the tool you put in CI as a single quality gate; it subsumes both the license check and the advisory scan a Node project would run as separate steps.
cargo-expand — see what the macros actually generated
Section titled “cargo-expand — see what the macros actually generated”Rust’s #[derive(...)], println!, #[tokio::main], and other macros generate code you never see. cargo-expand runs the compiler’s macro-expansion pass and prints the resulting source — invaluable for understanding or debugging a macro. It is the one plugin here that needs nightly, because macro expansion is exposed only through an unstable compiler flag:
rustup toolchain install nightly # one-time: cargo-expand drives nightly rustccargo install cargo-expand --lockedcargo expand # expand the whole cratecargo expand --bin greeting # expand one targetcargo expand path::to::module # expand a single moduleFor a tiny program that derives Debug and uses println!, the expansion makes the generated impl visible (real captured output, trimmed):
struct Point { x: i32, y: i32,}#[automatically_derived]impl ::core::fmt::Debug for Point { #[inline] fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_struct_field2_finish( f, "Point", "x", &self.x, "y", &&self.y, ) }}fn main() { let p = Point { x: 1, y: 2 }; { ::std::io::_print(format_args!("{0:?}\n", p)); };}You can see the Debug impl the derive wrote and that println! lowered to a format_args! + _print call. There is no JS equivalent that is this clean — the closest is reading Babel/SWC transform output.
cargo-outdated — npm outdated for Cargo
Section titled “cargo-outdated — npm outdated for Cargo”cargo update bumps dependencies within their declared semver ranges, but it will not tell you when a newer major exists beyond your range. cargo-outdated does, listing each dependency’s current, latest-compatible, and latest-overall versions:
cargo outdated # full table (your `npm outdated`)cargo outdated --root-deps-only # only your direct deps, ignore transitivecargo outdated --workspace # across every workspace memberOn a fully up-to-date project it simply says so (real captured output):
$ cargo outdatedAll dependencies are up to date, yay!When something is behind, it prints a table with Name, Project (your pinned version), Compat (latest in-range), and Latest (newest published) so you can see at a glance whether a bump is a safe patch or a breaking major.
cargo-bloat — “why is my binary this big?”
Section titled “cargo-bloat — “why is my binary this big?””A Node bundle analyzer tells you which packages dominate your JS bundle. cargo-bloat does the same for a compiled Rust binary, attributing .text (code) size to functions and crates:
cargo bloat --release # biggest functions in the release binarycargo bloat --release --crates # roll the sizes up per cratecargo bloat --release -n 20 # top 20 entriesPer-function and per-crate views (real captured output, trimmed):
$ cargo bloat --release --crates File .text Size Crate51.6% 104.0% 222.2KiB std 0.6% 1.3% 2.7KiB [Unknown] 0.0% 0.0% 60B demo49.7% 100.0% 213.7KiB .text section size, the file size is 430.2KiB
Note: numbers above are a result of guesswork. They are not 100% correct and never will be.Read it as a guide, not gospel (the tool says so itself). It pairs naturally with the release-profile tuning (strip, lto, opt-level = "z") from ./cargo-deep-dive.md — measure with cargo bloat, then shrink with profile settings, then re-measure.
Honorable mentions
Section titled “Honorable mentions”- cargo-llvm-cov — code coverage built on LLVM source-based instrumentation;
cargo llvm-cov(text summary) orcargo llvm-cov --html/--lcovfor reports and CI upload. It also drives nextest:cargo llvm-cov nextest. This is the recommended modern coverage path; see ../13-testing/10_coverage.md. - cargo-machete — finds dependencies declared in
Cargo.tomlbut never actually used (yourdepcheck). Fast, scans source forusepaths:cargo macheteto report,cargo machete --fixto remove them. (The more thorough but slowercargo-udepsdoes the same via real compilation, but needs nightly.) - cargo-edit — historically provided
cargo add/cargo rm/cargo upgrade.cargo addandcargo removehave been built into Cargo since 1.62, so you rarely installcargo-editanymore — its one remaining draw iscargo upgrade, which bumps the version requirements inCargo.toml(not just the lockfile).
Key Differences
Section titled “Key Differences”| Concern | Node / npm | Cargo plugin |
|---|---|---|
| Install mechanism | npm i -g <pkg> or npx <pkg> | cargo install <crate> → cargo-<name> binary on PATH |
| Discovery | explicit binary name / npx | Cargo auto-exposes any cargo-* on PATH as a subcommand |
| Test runner | vitest / jest | cargo-nextest (process-per-test, retries, JUnit) |
| Dev watch loop | nodemon, tsx watch | cargo-watch -x check / -x 'nextest run' |
| Vulnerability scan | npm audit (npm advisory DB) | cargo-audit (RUSTSEC advisory DB) |
| License / policy gate | license-checker, manual | cargo-deny (licenses + advisories + bans in one deny.toml) |
| Unused deps | depcheck | cargo-machete (cargo-udeps for the thorough check) |
| Outdated deps | npm outdated | cargo-outdated |
| Bundle/binary analysis | esbuild --analyze, source-map-explorer | cargo-bloat (per-function/per-crate .text size) |
| Coverage | c8, nyc, vitest --coverage | cargo-llvm-cov |
| Macro/transform output | Babel/SWC transform dump | cargo-expand (needs nightly) |
Three points where the model genuinely differs from npm:
- Plugins are global binaries, not project dependencies. Unlike
devDependencies, a cargo plugin is not recorded inCargo.toml; it lives in~/.cargo/bin. To make a plugin reproducible for the whole team you pin it in CI (install at a known version) rather than in the manifest. Some teams record desired tool versions in arust-toolchain.tomlcomment or anxtask/Makefile. - Discovery is convention, not configuration. Any executable named
cargo-fooon yourPATHbecomescargo foo. There is no plugin registry or opt-in list — which is why you can write your own (cargo-xtaskis exactly this trick). - One tool, nightly; the rest, stable.
cargo-expandis the lone outlier that needs a nightly toolchain at run time (it installs on stable but invokes nightlyrustc). Everything else here is pure stable.
Common Pitfalls
Section titled “Common Pitfalls”Forgetting that cargo install compiles from source
Section titled “Forgetting that cargo install compiles from source”Unlike npm i -g, which downloads prebuilt JS, cargo install compiles the tool from source, which can take a minute or two per plugin. On CI this is wasted time on every run. Use prebuilt-binary installers instead: cargo-binstall (cargo binstall cargo-nextest) downloads a release binary when one exists, and most plugins ship GitHub Actions (e.g. taiki-e/install-action) that fetch a binary in seconds. Reserve cargo install --locked for local dev machines.
Assuming cargo-expand works on stable
Section titled “Assuming cargo-expand works on stable”cargo expand on a stable-only machine fails because it needs the nightly compiler’s expansion flag:
error: the option `Z` is only accepted on the nightly compilerFix: rustup toolchain install nightly. You do not have to switch your project to nightly — cargo-expand invokes nightly rustc for the expansion pass only, while your normal builds stay on stable.
Expecting cargo nextest to run doctests
Section titled “Expecting cargo nextest to run doctests”Nextest deliberately does not run doctests (they have no standalone test binary). If your /// examples contain assert!s you care about, a green cargo nextest run is not enough:
cargo nextest run && cargo test --doc # nextest for unit/integration, cargo for doctestsConfusing cargo audit with cargo deny check advisories
Section titled “Confusing cargo audit with cargo deny check advisories”They overlap — both read RUSTSEC — but they are not interchangeable. cargo-audit is laser-focused on vulnerabilities and offers cargo audit fix. cargo-deny rolls advisories into a broader policy gate (licenses, bans, duplicate versions) configured by deny.toml. Most teams run cargo-deny in CI as the single gate and keep cargo-audit for quick interactive checks and its fix subcommand.
Reading cargo bloat numbers as exact
Section titled “Reading cargo bloat numbers as exact”cargo-bloat itself prints “numbers above are a result of guesswork.” Symbol attribution after inlining and dead-code elimination is approximate. Use it to find the relatively largest contributors and to compare before/after a change — not to report a precise byte budget.
Treating cargo outdated’s “Latest” as “safe to upgrade”
Section titled “Treating cargo outdated’s “Latest” as “safe to upgrade””A newer major version (the Latest column) is by definition outside your semver range and may be a breaking change. cargo update only moves within range (the Compat column). Bumping to Latest means editing the requirement in Cargo.toml (or cargo upgrade from cargo-edit) and dealing with any API breakage — treat it like an npm install pkg@latest across a major.
Best Practices
Section titled “Best Practices”-
Standardize the team’s toolbox in CI, not in
Cargo.toml. Plugins are global binaries, so pin their versions in your workflow (and prefer prebuilt-binary installers likecargo-binstall/taiki-e/install-actionover compiling from source on every run). -
Make nextest the default test runner. Add an alias so
cargo truns it, and remember the doctest companion:.cargo/config.toml [alias]t = "nextest run"Then run
cargo t && cargo test --doclocally and in CI. -
Use
cargo watch -x checkas your inner loop.checkis dramatically faster thanbuild, so save-to-feedback stays sub-second. Switch tocargo watch -x 'nextest run'when you want tests on every save. -
Gate every PR with
cargo deny checkandcargo audit. Licenses and known vulnerabilities should fail the build, not get discovered in production. Commit a revieweddeny.toml. -
Run
cargo macheteperiodically (andcargo-outdatedbefore dependency-bump PRs) to keep the graph lean — fewer deps means faster builds, smaller binaries, and less audit surface. -
Profile binary size with
cargo bloatbefore reaching for exotic tricks, then shrink with release-profile settings (strip,lto,opt-level = "z",panic = "abort") from ./cargo-deep-dive.md. -
Don’t switch your whole project to nightly just for
cargo-expand. Install nightly alongside stable and let the plugin reach for it on demand.
Real-World Example
Section titled “Real-World Example”Here is a realistic two-part setup: a fast local dev loop, and a CI quality gate built from these plugins. Both mirror what a well-run Node project does with npm run dev and a CI workflow — but consolidated through Cargo subcommands.
Local: a save-triggered loop. Aliases in .cargo/config.toml keep commands short, and cargo-watch re-runs them on every change:
# .cargo/config.toml — short aliases for the daily loop[alias]t = "nextest run"lint = "clippy --all-targets --all-features -- -D warnings"# One terminal: type-check on every save (fastest feedback)cargo watch -x check
# Another terminal: lint, then run the full (fast) test suite on every savecargo watch -x lint -x 'nextest run'CI: a single quality gate. A Makefile-style sequence (or xtask) that any contributor and the CI runner execute identically — formatting, linting, the audit/policy gate, the fast test run, and doctests:
# scripts/ci.sh — the gate, runnable locally and in CIset -euo pipefail
cargo fmt --all -- --check # formatting (see ./formatting.md)cargo clippy --all-targets --all-features -- -D warnings # lints (./linting.md)cargo deny check # licenses + advisories + bans (deny.toml)cargo audit --deny warnings # RUSTSEC vulnerabilities, warnings fatalcargo nextest run --profile ci # fast parallel tests, JUnit output for the reportercargo test --doc # doctests (nextest skips these)cargo llvm-cov nextest --lcov --output-path lcov.info # coverage for uploadIn a GitHub Actions workflow you would install the plugins with a prebuilt-binary action (so the gate stays fast) and then run that script. The full workflow YAML — caching, the install step, matrix, and artifact upload — lives in ./github-actions.md; the broader CI design (when to fail vs. warn, gating strategy) is in ./ci-cd.md.
The payoff is the same as the Node version of this story, but tighter: every tool is a cargo subcommand, the audit and license checks are one binary (cargo deny), and the test runner doubles as the coverage harness (cargo llvm-cov nextest). One install step, one script, identical locally and in CI.
Further Reading
Section titled “Further Reading”- The Cargo Book — Third-party subcommands — how Cargo discovers
cargo-*binaries on yourPATH. - cargo-nextest documentation — filter DSL, CI profiles, JUnit output, retries.
- cargo-watch — flags,
-xvs-s, and watch behavior. - RustSec / cargo-audit and the advisory database.
- cargo-deny book —
deny.tomlreference for licenses, bans, advisories, and sources. - cargo-expand, cargo-outdated, cargo-bloat, cargo-machete, and cargo-llvm-cov.
- Sibling pages in this section:
- ./cargo-deep-dive.md — profiles, aliases, workspaces, and
cargo metadatathese tools build on. - ./ci-cd.md and ./github-actions.md — wiring these plugins into a CI gate.
- ./linting.md and ./formatting.md — Clippy and rustfmt, the other half of the gate.
- ./cargo-deep-dive.md — profiles, aliases, workspaces, and
- Related sections: ../13-testing/10_coverage.md (coverage with
cargo-llvm-cov) and ../27-security/08_security-audit.md (the full supply-chain story behindcargo-audit/cargo-deny).
Exercises
Section titled “Exercises”Exercise 1: Swap in cargo-nextest
Section titled “Exercise 1: Swap in cargo-nextest”Difficulty: Beginner
Objective: Install cargo-nextest, run a small test suite with it, and add an alias so it becomes your default test command.
Instructions:
- Create a project with a library that has two or three
#[test]functions. - Install nextest (
cargo install cargo-nextest --locked) and run the suite withcargo nextest run. - Add an
[alias] t = "nextest run"to.cargo/config.tomland confirmcargo truns the same suite. - Explain in one sentence why a green
cargo nextest rundoes not guarantee your doctests pass.
Solution
pub fn add(a: i64, b: i64) -> i64 { a + b }pub fn double(x: i64) -> i64 { x * 2 }
#[cfg(test)]mod tests { use super::*; #[test] fn adds() { assert_eq!(add(2, 3), 5); } #[test] fn doubles() { assert_eq!(double(21), 42); } #[test] fn negatives() { assert_eq!(add(-1, -1), -2); }}cargo install cargo-nextest --lockedcargo nextest run Starting 3 tests across 2 binaries PASS [ 0.008s] (1/3) demo tests::adds PASS [ 0.011s] (2/3) demo tests::doubles PASS [ 0.011s] (3/3) demo tests::negatives──────────── Summary [ 0.012s] 3 tests run: 3 passed, 0 skipped[alias]t = "nextest run"cargo t # now runs `cargo nextest run`Why doctests aren’t covered: nextest runs compiled test binaries in separate processes, but doctests (the /// examples) are compiled and executed by rustc/cargo test directly and have no standalone binary — so nextest skips them entirely. Run cargo test --doc alongside nextest.
Exercise 2: A supply-chain gate with cargo-deny
Section titled “Exercise 2: A supply-chain gate with cargo-deny”Difficulty: Intermediate
Objective: Configure cargo-deny to enforce a license allowlist and a banned crate, and observe it pass on a clean project.
Instructions:
- In a project with a couple of dependencies, install
cargo-denyand runcargo deny initto generate a starterdeny.toml. - Edit
deny.tomlto allow onlyMIT,Apache-2.0, andBSD-3-Clauselicenses, and todenythe crateopenssl-sysunder[bans]. - Run
cargo deny checkand confirm it passes (or reports exactly which crate violates the policy). - Explain how this single command replaces two separate Node tools.
Solution
cargo install cargo-deny --lockedcargo deny init # writes a starter deny.toml# deny.toml — trimmed to the parts this exercise changes[licenses]allow = ["MIT", "Apache-2.0", "BSD-3-Clause"]
[bans]deny = [{ crate = "openssl-sys", reason = "prefer rustls" }]multiple-versions = "warn"
[advisories]yanked = "deny"cargo deny check# `licenses ... ok`, `bans ... ok`, `advisories ... ok` (or a clear error naming# the crate whose license isn't on the allowlist, or that hit the ban)What it replaces: cargo deny check rolls together what a Node project would run as two separate tools — license-checker (the [licenses] allowlist) and npm audit (the [advisories] RUSTSEC scan) — plus a dependency-ban policy npm has no direct equivalent for. One binary, one deny.toml, one CI gate.
Exercise 3: Find and shrink binary bloat
Section titled “Exercise 3: Find and shrink binary bloat”Difficulty: Advanced
Objective: Use cargo-bloat to identify what dominates a release binary, then apply release-profile settings and measure the difference.
Instructions:
- Build a small binary in release mode and run
cargo bloat --release --cratesto see the per-crate.textbreakdown. - Add a size-tuned
[profile.release]toCargo.toml(strip,lto,opt-level = "z"). - Rebuild and re-run
cargo bloat --release --crates; note how the totals change. - Explain why
cargo-bloat’s numbers should be read as relative, not exact.
Solution
cargo install cargo-bloat --lockedcargo bloat --release --crates File .text Size Crate51.6% 104.0% 222.2KiB std 0.6% 1.3% 2.7KiB [Unknown] 0.0% 0.0% 60B demo49.7% 100.0% 213.7KiB .text section size, the file size is 430.2KiB
Note: numbers above are a result of guesswork. They are not 100% correct and never will be.# Cargo.toml — size-tuned release profile[profile.release]strip = true # drop symbolslto = true # link-time optimizationopt-level = "z" # optimize aggressively for sizecodegen-units = 1 # better cross-function optimizationpanic = "abort" # no unwinding tablescargo bloat --release --crates # rebuild + re-measure; file size shrinks noticeablyWhy numbers are relative: after inlining, monomorphization, and dead-code elimination, the linker no longer maps cleanly back to source symbols, so cargo-bloat estimates attribution (it says so itself). Trust it to rank the largest contributors and to compare before/after a change — not to assert an exact byte count.