Cargo and Package Management

Dependency Version Requirements

Dependency version requirements specify which versions of an external crate are compatible with your package in Cargo.toml. By default, Cargo interprets bare version strings using Caret (^) Semantic Versioning rules, allowing automatic non-breaking updates while preventing breaking changes.

Caret Requirement (Default in Cargo):
  "1.2.3"  ===  "^1.2.3"  --->  >=1.2.3, <2.0.0   (1.x.y is compatible)
  "0.2.3"  ===  "^0.2.3"  --->  >=0.2.3, <0.3.0   (0.2.x is compatible; 0.3.0 may break!)
  "0.0.3"  ===  "^0.0.3"  --->  >=0.0.3, <0.0.4   (Exact only for 0.0.z)

Version Requirement Operators in Cargo.toml

Notice how Cargo treats 0.y.z pre-1.0 versions specially: before 1.0.0, the leftmost non-zero digit acts as the major breaking version number.

Operator Syntax Example Equivalent Range When to Use
Bare / Caret (^) "1.4" or "^1.4.2" >=1.4.2, <2.0.0 Default & Recommended for almost all dependencies.
Pre-1.0 Caret "0.4.2" >=0.4.2, <0.5.0 Standard for 0.y.z crates where 0.5.0 can introduce breaking API changes.
Tilde (~) "~1.4.2" >=1.4.2, <1.5.0 Restricts updates to patch releases (1.4.x) only.
Wildcard (*) "1.*" >=1.0.0, <2.0.0 Equivalent to "^1.0.0" (crates.io rejects bare "*" wildcards).
Inequality / Compound ">= 1.2, < 1.5" >=1.2.0, <1.5.0 Excluding known incompatible minor versions.
Exact (=) "=1.4.2" Exactly 1.4.2 Tightly coupled companion crates (such as my_crate and my_crate_macros); avoid in general libraries to prevent version conflicts.

Why Exact Pinning ("=1.2.3") Should Be Avoided in Libraries

Cargo allows at most one version of a crate per major SemVer track (e.g., one 1.x version of serde) in the same dependency graph unless package renaming is used. If Library A requires serde = "=1.0.200" and Library B requires serde = "^1.0.210", Cargo cannot satisfy both constraints simultaneously and fails dependency resolution!

Instead:

  • Specify the minimum version that provides the APIs you actually call in Cargo.toml (e.g., serde = "1.0.190" or "1.0").
  • Rely on Cargo.lock (not "=..." in Cargo.toml) to pin exact versions for reproducible builds.
#[derive(Debug, PartialEq, Eq)]
struct SemVer {
    major: u32,
    minor: u32,
    patch: u32,
}

impl SemVer {
    fn satisfies_caret(&self, req: &SemVer) -> bool {
        if self.major != req.major {
            return false;
        }
        if req.major > 0 {
            (self.minor, self.patch) >= (req.minor, req.patch)
        } else if req.minor > 0 {
            self.minor == req.minor && self.patch >= req.patch
        } else {
            self.minor == 0 && self.patch == req.patch
        }
    }
}

fn main() {
    let req_1_2 = SemVer { major: 1, minor: 2, patch: 3 };
    let v_1_9 = SemVer { major: 1, minor: 9, patch: 0 };
    let v_2_0 = SemVer { major: 2, minor: 0, patch: 0 };

    println!("1.9.0 satisfies ^1.2.3: {}", v_1_9.satisfies_caret(&req_1_2));
    println!("2.0.0 satisfies ^1.2.3: {}", v_2_0.satisfies_caret(&req_1_2));

    let req_0_2 = SemVer { major: 0, minor: 2, patch: 3 };
    let v_0_3 = SemVer { major: 0, minor: 3, patch: 0 };
    println!("0.3.0 satisfies ^0.2.3: {}", v_0_3.satisfies_caret(&req_0_2));
}
1.9.0 satisfies ^1.2.3: true
2.0.0 satisfies ^1.2.3: false
0.3.0 satisfies ^0.2.3: false