Skip to content

Instantly share code, notes, and snippets.

@mkatychev
Created May 28, 2026 18:02
Show Gist options
  • Select an option

  • Save mkatychev/f0c7c560ed7564f016f4aef29e87807f to your computer and use it in GitHub Desktop.

Select an option

Save mkatychev/f0c7c560ed7564f016f4aef29e87807f to your computer and use it in GitHub Desktop.
cargo versioning cheatsheet

Cargo Dependency Resolution & Versioning

Condensed reference for Cargo's resolver.

How the Resolver Works

Cargo builds a dependency graph using a backtracking algorithm with three core operations:

  1. Walk dependencies: processes deps in order; ordering influences conflict resolution.
  2. Unify versions: reuses a single version across the graph whenever requirements overlap, reducing build time and keeping types compatible.
  3. Pick versions: prefers the highest version satisfying all constraints; backtracks on conflict.

Cargo.lock has the highest priority: once a version is locked, it stays locked until the requirement in Cargo.toml no longer allows it (or cargo update is run).

SemVer Symbol Reference

Symbol Example Meaning Equivalent Range
^ ^1.2.3 Compatible (default if no symbol) >=1.2.3, <2.0.0
~ ~1.2.3 Tilde: patch-level only >=1.2.3, <1.3.0
* 1.* Wildcard >=1.0.0, <2.0.0
= =1.2.3 Exact match (pins) 1.2.3 only
> >1.2.3 Greater than >1.2.3
>= >=1.2.3 Greater than (or equal >=1.2.3
< <1.2.3 Less than <1.2.3
<= <=1.2.3 Less than or equal <=1.2.3
, >=1.2, <1.5 Multiple requirements (AND) intersection of both

Symbol Callouts

^ - Default specifier

  • Bare versions are caret requirements. "1.2.3" and "^1.2.3" are identical.
  • Allows updates that do not modify the left-most non-zero component:
    1.2.3 :=  >=1.2.3, <2.0.0
    1.2   :=  >=1.2.0, <2.0.0
    1     :=  >=1.0.0, <2.0.0
    0.2.3 :=  >=0.2.3, <0.3.0
    0.2   :=  >=0.2.0, <0.3.0
    0.0.3 :=  >=0.0.3, <0.0.4
    0.0   :=  >=0.0.0, <0.1.0
    0     :=  >=0.0.0, <1.0.0
    
  • This is the recommended form for almost all dependencies: it gives the resolver maximum flexibility while honoring SemVer.

~: minimum specifier

~1.2.3 := >=1.2.3, <1.3.0
~1.2   := >=1.2.0, <1.3.0
~1     := >=1.0.0, <2.0.0

* - wildcard specifier

  • Not allowed on crates.io for published dependencies.
  • A bare "*" matches any version and is almost always a mistake.
  • Useful only in local/path/workspace contexts where you intentionally want "whatever is available."

= - pin specifier

  • Pins to a single version. Use for tightly coupled crate pairs, e.g. a library and its companion proc-macro crate that must share an internal ABI.
  • Overuse causes unsolvable conflicts when transitive deps disagree.

Comparison operators (>, >=, <, <=)

  • >=X.0.0 is generally too loose: it allows incompatible major bumps.
  • <X upper bounds should be avoided unless the crate is known to break above that version; the SemVer-compatible upper bound from ^ is usually what you want.

Comma (,): combined ranges

  • ">=1.2, <1.5" is an intersection. Useful for narrow MSRV-style windows but, like manual upper bounds, can fragment the graph.

Library vs Binary/Artifact Crates

Same syntax, different strategy depending on what you ship.

lib crates:

  • Use caret requirements with the true minimum you support (serde = "1.0.130").
  • Avoid committing Cargo.lock: it is ignored when your crate is a dependency.
  • Avoid = and tight upper bounds; they fragment downstream graphs.
  • Bumping a public dep's major is a breaking change for your crate.

bin crates:

  • Commit Cargo.lock: source of truth for reproducible builds, CI, and cargo install --locked.
  • Keep caret requirements in Cargo.toml; the lockfile (not tight ranges) provides reproducibility.
  • Run cargo update deliberately so lockfile churn shows up as an intentional diff.

Mixed workspaces: commit the lockfile, define shared deps in [workspace.dependencies], and reference them via { workspace = true } to prevent version drift between members.

Version Unification & Hazards

Cargo unifies compatible requirements into one resolved version:

# Both resolve to a single version in [1.1.0, 2.0.0)
crate-a = { version = "1.0" }  # in package A
crate-b = { version = "1.1" }  # in package B

Incompatible ranges (=0.4.8 vs =0.4.11) produce an error.

SemVer-incompatible ranges ("0.6" vs "0.7") silently produce two copies in the graph. Types from different copies are distinct to the compiler:

  • downcast_ref and trait-object identity checks fail at runtime.
  • Find duplicates with cargo tree --duplicates.
  • Library authors can use the semver trick to bridge versions.
  • re-exporting

Feature Unification

Resolver v1 (legacy)

Features are unified globally: every enabled feature for a crate is enabled everywhere it appears.

Resolver v2 (Edition 2021): opt into with resolver = "2"

Features are not unified across:

  • Target-specific dependencies for inactive targets.
  • [build-dependencies] and [dev-dependencies] vs normal [dependencies].
[package]
resolver = "2"

[dependencies]
log = "0.4"

[build-dependencies]
log = { version = "0.4", features = ["std"] }  # "std" stays in build script only

Resolver v3 (Edition 2024)

Adds MSRV-aware resolution: when rust-version is set, the resolver prefers dependency versions compatible with the declared MSRV. Enable explicitly via resolver = "3" or resolver.incompatible-rust-versions = "fallback" in .cargo/config.toml.

Special Constraints

  • links field: only one version of a crate that links a given native library may appear in the graph. Different majors of libgit2-sys cannot coexist.
  • Yanked versions: ignored unless already in Cargo.lock or pinned via cargo update --precise X.Y.Z.
  • dev-dependency cycles: permitted; normal dependency cycles are not.

Troubleshooting

# Why is this dep in the graph?
cargo tree --workspace --target all --all-features --invert <crate>

# Who enabled this feature?
cargo tree --workspace --target all --all-features --edges features --invert <crate>

# Show duplicated versions
cargo tree --duplicates

# Trace the resolver
CARGO_LOG=cargo::core::resolver=trace cargo update

Recommendations

  • Use the bare/caret form ("1.2.3") by default.
  • Specify all three components to declare a true minimum ("1.2.3", not "1").
  • Avoid >=X.0.0 (too loose) and ~X.Y (too strict) unless you have a concrete reason.
  • Bump declared minimums to match what your code actually uses, so downstream resolvers don't pick a broken older version.
  • Reserve = for crate pairs that must share an internal ABI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment