Skip to content
rusthacks.dev

Cargo Tree for Finding Why a Dependency Is in Your Build

cargo tree prints the dependency graph. Use -i to find who pulls in a crate, -d to find duplicates, -e features to trace features and --depth to keep it readable.

6 min read
Summarize in AI
Flat illustration for the post “Cargo Tree for Finding Why a Dependency Is in Your Build”, composed around cargo.

Someone adds a one-line dependency and your binary gains forty crates. Or a security advisory names a crate you have never heard of, and you need to know who brought it in. cargo tree answers both, and it has four flags that cover nearly every question: invert, duplicates, features and depth. This post runs each on a real dependency graph, shows how to read the output, and ends with the cases where the tree misleads.

The graph is a small program depending on serde_json 1 and regex 1, eleven crates in total, built with Cargo 1.99 and edition 2024. Paths are shortened.

The whole graph, then one level

cargo tree on its own prints everything, indented by who depends on whom, which is too much for a first look. Ask for one level:

Shell
cargo tree --depth 1
Text
pf v0.1.0 (/work/pf)
├── regex v1.13.1
└── serde_json v1.0.154

Those are the two crates named in Cargo.toml. Everything else is a transitive dependency, and the next question is usually about one of those.

Why is this crate here? Invert the tree

-i (--invert) takes a package and prints the paths to it, from the crate outward to your own:

Shell
cargo tree -i memchr
Text
memchr v2.8.3
├── aho-corasick v1.1.5
│   ├── regex v1.13.1
│   │   └── pf v0.1.0 (/work/pf)
│   └── regex-automata v0.4.18
│       └── regex v1.13.1 (*)
├── regex v1.13.1 (*)
├── regex-automata v0.4.18 (*)
└── serde_json v1.0.154
    └── pf v0.1.0 (/work/pf)

Read it from the top: memchr is wanted by aho-corasick, regex, regex-automata and serde_json, and each of those traces up to pf. An asterisk marks a subtree already printed above, so the listing stays finite. To remove memchr you would have to remove every branch, which here means dropping both direct dependencies, so the answer to "can I get rid of it" is no. That is the typical result, and knowing it in ten seconds beats an hour of editing manifests. When the inverted tree has a single branch, the crate that owns that branch is the one to question.

Which features pulled it in

A crate often arrives because a feature enabled an optional dependency. -e features adds feature edges, and combined with -i it shows who turned each one on:

Shell
cargo tree -e features -i regex-automata
Text
regex-automata v0.4.18
├── regex-automata feature "alloc"
│   └── regex v1.13.1
│       ├── regex feature "default"
│       │   └── pf v0.1.0 (/work/pf)
│       │       └── pf feature "default" (command-line)
│       ├── regex feature "perf"
│       │   └── regex feature "default" (*)

Now the answer is at feature level: the program asked for the default features of regex, which include perf, which turns on more of regex-automata. If you do not need that, regex = { version = "1", default-features = false, features = ["std"] } prunes it; see optional dependencies for the mechanics. In the earlier build-speed measurement that trim saved 7 percent of a clean build on this tree, and the same trick saves far more where the feature drags in a network stack.

Duplicates

cargo tree -d lists crates that appear at more than one version, the usual cause of a slow build, a large binary and a type mismatch between "two" copies of one library. On this graph it has nothing to say:

Text
$ cargo tree -d
warning: nothing to print.

To find dependencies that require specific target platforms, try to use option `--target all` first, and then narrow your search scope accordingly.

Treat that message as good news. When it does print something, each version appears with its own inverted tree, so you can see which dependent holds back the old copy. The fix is nearly always upstream: update the dependent that pins the older major, or allow both, because Cargo will build two incompatible majors side by side. See multiple versions in the graph.

Shaping the output for a script or a review

A few formatting flags make the output usable in a pull request or a pipeline:

Shell
cargo tree --prefix none --depth 1    # a flat list, no drawing characters
cargo tree --charset ascii            # for terminals and logs that mangle the box characters
cargo tree -e no-dev                  # leave out dev-dependencies
cargo tree --edges build,normal       # only build scripts and runtime
cargo tree --target all               # include dependencies for other platforms
cargo tree --format "{p} {l}"         # name, version and license
Text
$ cargo tree --prefix none --depth 1
pf v0.1.0 (/work/pf)
regex v1.13.1
serde_json v1.0.154

The license format is worth keeping: cargo tree --prefix none --format "{p} {l}" | sort -u is a one-line license inventory, and cargo deny turns it into a policy.

A failing question: asking about the wrong thing

Two mistakes appear constantly. Asking for a crate that is not in the graph gives an error naming the spec:

Text
$ cargo tree -i tokio
error: package ID specification `tokio` did not match any packages

That is a useful answer, not a failure: the crate is not there. The other mistake is trusting the output for something it does not model. cargo tree shows the resolved graph for the current target and feature set. A dependency under [target.'cfg(windows)'.dependencies] disappears on a Mac unless you pass --target all, which is also why a "nothing to print" duplicates report on one machine can differ from the same command on a Windows CI runner. Build scripts that download or vendor code at build time are invisible entirely.

Worked example: deciding whether to drop a dependency

Suppose a review asks whether serde_json is worth keeping, because the program only parses one tiny document. Run the three commands in order. cargo tree --depth 1 confirms it is a direct dependency. cargo tree -i serde_json prints one line plus pf, so nothing else needs it, and removing it removes only its own subtree. Then count the cost of that subtree:

Shell
cargo tree -p serde_json --prefix none | sort -u

On this graph that lists serde_json plus its four small dependencies, itoa, memchr, serde_core and zmij. But memchr is also used by regex, as the inverted tree showed, so removing serde_json sheds three crates, not four. The decision is then an informed one: three small crates against the code needed to parse the document by hand. For a document that never changes shape, a hand parser may win. For anything user-supplied, keep the library.

Notice what the example needed: not the full tree, but two targeted questions and a set difference. That is the habit worth building. A full cargo tree on a real application runs to thousands of lines, and nobody reads those; the flags exist so you never have to.

A short workflow for a dependency review

  1. cargo tree --depth 1 before and after adding a crate; the difference is the direct cost.
  2. cargo tree --prefix none | sort -u | wc -l counts unique crates (11 here); track the number over time.
  3. cargo tree -d after every update.
  4. cargo tree -i <crate> for any name in an advisory, then fix the branch that owns it.

When the tree is not the right tool

It tells you what is in the graph, not whether any of it is safe, maintained or necessary; that is the job of audit and deny and vet. For compile cost, cargo build --timings shows time per crate, which is a different question from "how many". The flag reference is in the cargo tree manual.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.