cargo-copter [](https://github.com/imazen/cargo-copter/actions/workflows/ci.yml) [](https://crates.io/crates/cargo-copter) [](#license)
June 28, 2026 · View on GitHub
Test the downstream impact of a Rust crate change before you publish it — catch regressions and API breakages in the crates that depend on yours.
cargo-copter cross-tests any versions of your crate (or your uncommitted local WIP) against any versions of any dependents (default: the top 5 by download count). For each (version × dependent) pair it runs install → check → test and tells you whether your change broke them, or whether they were already broken before you touched anything.
- Simulate publishing to crates.io with natural version resolution:
--test-versions "0.8.50 0.8.51". - Simulate a downstream upgrade (a hand-edit to their
Cargo.toml) with--force-versions "0.8.52 0.8.53".
⚠️ Security: cargo-copter executes arbitrary code from crates.io (build scripts, tests). Run it in a sandbox. The
--dockerflag adds basic isolation on Linux/WSL via a small wrapper script — convenient, but not a hardened boundary, so YMMV.
Why "cargo-copter"? To make it absolutely impossible to find via Google.
Quick start
cargo binstall cargo-copter # prebuilt binary
# or
cargo install cargo-copter # build from source
Run it from your crate's directory, invoking the binary as cargo-copter (with the hyphen):
cd my-crate
cargo-copter --top-dependents 2
Note: invoke
cargo-copter(hyphen), notcargo copter(space). Thecargo <subcommand>dispatch form is not wired up in this release, socargo copter ...fails withunexpected argument 'copter' found. Usecargo-copter ...directly.
Test your local work-in-progress version
There is no --wip flag. Running cargo-copter from your crate's directory (or with
--path <DIR>) and without any of --crate, --test-versions, or --force-versions
is what offers your local WIP version. In that default mode each dependent is tested twice:
- baseline — the latest version of your crate currently published on crates.io
- WIP — your uncommitted local source (the version in your local
Cargo.toml)
so you see exactly what your unpublished changes do to each dependent versus what they already ship against.
cd my-crate
cargo-copter --top-dependents 2 # baseline (published) + your local WIP, vs top 2 dependents
Precondition: default dependent discovery needs your crate published
When you don't pass any --dependent* flag, dependents are discovered via the crates.io
reverse-dependencies API for your crate. A brand-new crate that has never been published
(or one that nothing depends on yet) therefore finds zero dependents and exits with
Configuration error: No dependents to test.
For an unpublished crate, point cargo-copter at local dependents instead — these paths are read straight from disk and never touch crates.io:
# Unpublished crate: name the local dependents explicitly
cargo-copter --path . --dependent-paths ~/work/dep-a ~/work/dep-b
# ...or auto-discover local dependents (only crates that actually depend on yours are kept)
cargo-copter --path . --dependent-dir ~/work/ ~/work/zen/
cargo-copter --path . --dependent-glob "~/work/*/Cargo.toml"
Example output
Testing 2 reverse dependencies of rgb
Dependents: ansi_colours, resvg
rgb versions: baseline, 0.8.91-alpha.3 [!], 0.8.52
2 × 3 = 6 tests
this = 0.8.52 bd35c97* (your work-in-progress version)
┌─────────────────────────────┬────────────┬──────────────────┬──────────────────────────────┬─────────────────────────┐
│ Offered │ Spec │ Resolved │ Dependent │ Result Time │
├─────────────────────────────┼────────────┼──────────────────┼──────────────────────────────┼─────────────────────────┤
│ - baseline │ 0.8 │ 0.8.52 📦 │ ansi_colours 1.2.3 │ passed ✓✓✓ 8.3s │
│ ✗ ≠0.8.91-alpha.3 [≠→!] │ → =0.8.... │ 0.8.91-alpha.... │ ansi_colours 1.2.3 │ test failed ✓✓✗ 1.7s │
│ ┌────────────────────────┴────────────┘ └──────────────────────────────┘ │
│ │ cargo test failed on ansi_colours │
│ │ error[E0277]: the trait bound `Gray<u8>: ToLab` is not satisfied │
│ │ --> src/test.rs:45:47 │
│ └────────────────────────┬────────────┬──────────────────┬──────────────────────────────┬─────────────────────────┤
│ ✓ =0.8.52 │ 0.8 │ 0.8.52 📦 │ ansi_colours 1.2.3 │ passed ✓✓✓ 3.1s │
├─────────────────────────────┼────────────┼──────────────────┼──────────────────────────────┼─────────────────────────┤
│ - baseline │ 0.8 │ 0.8.52 📦 │ resvg 0.45.1 │ passed ✓✓✓ 8.4s │
│ ✓ ≠0.8.91-alpha.3 [≠→!] │ → =0.8.... │ 0.8.91-alpha.... │ resvg 0.45.1 │ passed ✓✓✓ 4.9s │
│ ✓ =0.8.52 │ 0.8 │ 0.8.52 📦 │ resvg 0.45.1 │ passed ✓✓✓ 2.1s │
└─────────────────────────────┴────────────┴──────────────────┴──────────────────────────────┴─────────────────────────┘
Version Comparison:
Default 0.8.52 0.8.91-alpha.3
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total tested 2 2 2
Already broken 0 - -
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Passed fetch 2 2 2
Passed check 2 2 2
Passed test 2 2 -1 → 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Fully passing 2 2 -1 → 1
Markdown report: copter-report/report.md
Detailed failure logs: copter-report/failures.log
💡 To analyze API changes that may have caused regressions:
cargo install cargo-public-api
cargo public-api diff rgb@0.8.50 rgb@0.8.91 # compare two crates.io versions
cd .copter/staging/rgb-0.8.91 && cargo public-api diff 0.8.50 # compare local against crates.io
Docker
# Run in Docker (for security isolation)
cargo-copter --docker --top-dependents 5
# Or directly with Docker
docker run --rm -v $(pwd):/workspace ghcr.io/imazen/cargo-copter:latest \
--path /workspace --top-dependents 5
Common usage
# Test top dependents
cargo-copter --top-dependents 10
# Test specific dependents
cargo-copter --dependents image serde tokio
# Test specific dependent versions
cargo-copter --dependents image:0.25.8 serde:1.0.200
# Test multiple versions of your crate (baseline is included automatically)
cargo-copter --test-versions "0.8.50 0.8.51"
# Force incompatible versions (bypasses semver)
cargo-copter --force-versions "0.9.0 1.0.0-rc.1"
# Test a published crate without local source
cargo-copter --crate rgb --force-versions "0.8.51"
# Clean the cache and retest
cargo-copter --clean --top-dependents 5
# Test local unpublished dependents (works without crates.io)
cargo-copter --path . --dependent-paths ~/work/my-dep1 ~/work/my-dep2
# Auto-discover local dependents in directories
cargo-copter --path . --dependent-dir ~/work/ ~/work/zen/
# Auto-discover via glob (filters to crates that depend on yours)
cargo-copter --path . --dependent-glob "~/work/*/Cargo.toml"
# Breadth + depth: test top dependents, then add popular older versions
cargo-copter --top-dependents 5 --top-versions 50
CLI options
-p, --path <PATH> Path to the crate under test (directory or Cargo.toml)
-c, --crate <NAME> Test a published crate by name (no local source needed)
--top-dependents <N> Test the top N dependents by download count [default: 5]
--top-versions <Q> Budget of extra dependent-version slots, ranked by downloads
--dependents <CRATE[:VER]> Test specific crates from crates.io (space-separated)
--dependent-paths <PATH> Test local crate paths (works with unpublished crates)
--dependent-glob <GLOB> Discover local dependents via glob patterns
--dependent-dir <DIR> Discover local dependents in directories (one level deep)
--test-versions <VER>... Test multiple versions in semver-respecting patch mode
--force-versions <VER>... Force versions, bypassing semver
--skip-normal-testing Skip the auto-added patch-mode test for forced versions
--only-fetch Only fetch dependencies (skip check and test)
--only-check Only fetch and check (skip tests)
--clean Clean the staging cache before running
--staging-dir <PATH> Staging/cache directory [default: ~/.cache/cargo-copter/staging]
--error-lines <N> Number of error lines to show per failure [default: 10]
--output-html <PATH> HTML report output path [default: copter-report.html]
--json Emit results as JSON
--simple Verbal output format (good for AI parsing / large dep counts)
--docker Run inside a Docker container for isolation (Linux/WSL)
--console-width <COLS> Override the detected console width
How it works
- Baseline test: tests each dependent against the currently published version of your crate.
- Offered-version tests: tests against the versions you specify (
--test-versions,--force-versions, or your local WIP). - Three-step ICT: Install (fetch) → Check → Test, stopping early on the first failure.
- Classification:
- ✓ passed: baseline and offered both passed
- ✗ regressed: baseline passed, offered failed — your change is implicated
- ✗ broken: baseline check/fetch already failed (not your problem)
- ⊘ skipped: a version was offered but cargo didn't actually resolve to it
- Robust to inapplicable cells: a reverse-dep with no resolvable published version (yanked, unpublished, or path-only) and a historical dependent version that predates the dependency on your crate are logged and skipped — they no longer abort the whole run.
- End-of-run report separates "your fault" from "not your problem", categorizing baseline failures by root cause (yanked deps, system libs, build.rs, nightly, version conflicts, platform-specific).
Version testing modes
Patch mode (default with --test-versions)
- Uses
[patch.crates-io]inCargo.toml - Respects semver requirements
- Cargo can ignore an offered version if it doesn't satisfy the dependent's spec
Force mode (--force-versions)
- Directly rewrites the dependency in the dependent's
Cargo.toml - Bypasses semver requirements
- Always tests the exact version specified
- Auto-adds a normal patch-mode test too, unless
--skip-normal-testing
Transitive unification (automatic)
When a forced version produces a "multiple versions of crate X" error — because a dependent
pulls in your crate both directly and transitively (e.g. testing rgb against image, which
depends on ravif, which also uses rgb) — cargo-copter automatically retries with
[patch.crates-io] applied to unify all copies of your crate across the dependency tree. No
flag is required. Retried rows are tagged in the output:
[!!]= auto-patched (needed[patch.crates-io]to unify transitive versions)[!!!]= deep conflict (still failed even after[patch.crates-io]; see the blocking deps)
Unification also covers workspace siblings of a local WIP. When you test a --path crate
that is a workspace member (e.g. magetypes path-depending on archmage) and a dependent
also pulls in those siblings, cargo-copter emits --config patch.crates-io.<sibling>.path=
at the build root for the base crate and every local path-dep sibling — unifying transitive
copies that a member-level [patch] can't reach (cargo only honors [patch] in the workspace
root). So a WIP member no longer collides with the crates.io copy a dependent resolves.
The old
--patch-transitiveflag is deprecated and hidden — it is now effectively a no-op, since auto-retry handles transitive unification on its own. It is kept only for backwards compatibility and prints a deprecation notice if you pass it.
Caching
Cache location (platform-specific):
- Linux:
~/.cache/cargo-copter/staging/{crate}-{version}/ - macOS:
~/Library/Caches/cargo-copter/staging/{crate}-{version}/ - Windows:
%LOCALAPPDATA%/cargo-copter/staging/{crate}-{version}/
Contains:
- Unpacked sources
- Build artifacts (
target/) - ~10x speedup on subsequent runs
Downloaded .crate files live in ~/.cache/cargo-copter/crate-cache/ (or the platform equivalent).
Reports
All reports are written to ./copter-report/:
- Markdown:
report.md— optimized for LLM analysis - JSON:
report.json— structured data for CI/automation - Consolidated failures:
failures.log - Per-failure logs:
{dependent}-{version}_{base-version}.txt— full compiler output for each failure
An HTML report is also written to the --output-html path (default copter-report.html).
Per-failure logs include the full path to the staged source code for easy navigation:
# Failure Log: image 0.25.9 with base crate version 0.8.52
# Generated: 2025-12-15 10:30:00
# Source: /home/user/.cache/cargo-copter/staging/image-0.25.9
=== CHECK (cargo check) ===
Status: FAILED (1.7s)
error[E0277]: the trait bound `Rgb<u8>: From<...>` is not satisfied
--> src/lib.rs:42:15
...
The copter-report/ directory is automatically appended to .gitignore if one exists.
Table symbols
Offered column:
-= baseline row✓= test passed✗= test failed⊘= version skipped== exact version match↑= upgraded to a newer version≠= version mismatch[≠→!]= forced version
Resolved column:
📦= published from crates.io📁= local path
Result column:
✓✓✓= Install + Check + Test passed✓✓✗= Install + Check passed, Test failed✓✗-= Install passed, Check failed, Test skipped
Build from source
git clone https://github.com/imazen/cargo-copter
cd cargo-copter
cargo install --path .
# Or build and test in place
cargo build --release
cargo test
# Run with debug logging
RUST_LOG=debug ./target/release/cargo-copter --top-dependents 1
Links
- GitHub: https://github.com/imazen/cargo-copter
- Rust API Evolution RFC: https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md
- Inspired by cargo-crusader
AI
This was made with Claude Code and around 300 prompts to keep it on track. It wasn't a net savings in time vs. writing it myself, but at least I could do it from my phone. You can probably tell that it needs a lot of refactoring and improved test coverage. However, this is the kind of tool that doesn't need to be perfect — it just needs to be good enough to be useful. It's not a library.
License
cargo-copter is dual-licensed under either the MIT license or the Apache License, Version 2.0, at your option.
Image tech I maintain
| Codecs ¹ | zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr |
| Codec internals | zenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize |
| Compression | zenflate · zenzop · zenzstd |
| Processing | zenresize · zenquant · zenblend · zenfilters · zensally · zentone |
| Pixels & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline & framework | zenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract |
| Metrics | zensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs |
| Pickers & ML | zenanalyze · zenpredict · zenpicker |
| Products | Imageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#) |
¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026
General Rust awesomeness
zenbench · archmage · magetypes · enough · whereat · cargo-copter