Rust Dependency Management with Cargo

This past week, I started learning Rust. While I have enjoyed learning about Rust’s clever approach to system programming, I have to say that I found its dependency management a bit confusing at first. Today, I will share my new understanding of this with you, whether you are a new, experienced, or aspiring Rustacean.

Setting Up

For this post, I will create a basic Rust project using Cargo (Rust’s dependency management tool). Feel free to follow along using the following steps.

  1. Install Rust.
  2. Run cargo new rust-versions in a directory of your choice to create a new Rust project.

Building

In order to examine how dependencies work with Cargo, we first need to add one. I’m adding a dependency to rand in my Cargo.toml. The end result looks like this:

[package]
name = "rust-versions"
version = "0.1.0"
authors = ["Levi Payne <levi@concisecoder.io>"]
edition = "2018"

[dependencies]
rand = "0.3.14"

Now, we can run cargo build to retrieve the dependencies and compile the code. After this process is complete, a Cargo.lock file should have been created. If we look at this file, it lists various packages with information about versions, dependencies, and where the package was downloaded. Most notably, the section for the rand package we just added looks like this (your versions may vary):

[[package]]
name = "rand"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)",
 "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]

Wait a minute. This indicates that version 0.3.23 was downloaded when we specified 0.3.14 in Cargo.toml. What is going on here?

Caret Requirements

It turns out that, by default, Cargo treats versions defined as 0.3.14 that same as ^0.3.14, or a caret requirement. This basically means that any version that does not alter the left-most non-zero grouping is allowed. So with our example above, 0.3.23 is allowed because the patch version is updated, but the minor version (which is the left-most non-zero grouping) remains the same. If the major version were non-zero, this behavior would shift to the left by one grouping. Read more about caret requirements here.

Cargo.lock

Cargo.lock‘s purpose is to keep track of the versions of dependencies that are actually installed, instead of the ones that are specified in Cargo.toml. It should not be edited by hand, and should be committed to source control with the rest of your code. This is so that you or someone else doesn’t checkout the code, build it, and end up with different versions of packages than the code was committed with.

Updating

Once you run cargo build and don’t delete Cargo.lock, your versions stay the same, even if a new allowed version becomes available. You can update it yourself with one of two ways.

If you want to upgrade to a new version of a package outside the allowed range of what is specified, update the version manually in Cargo.toml. By doing this, the next build will fetch the new version specified.

If you want to update to the latest allowed version of a package, run cargo update. This will automatically fetch the latest allowed version without changing Cargo.toml.

Custom Version Behavior

Although Cargo assumes caret requirements by default, it allows a varied set of explicit syntax to specify different behavior. I won’t go over all of them, but here are some of the more common ones. Read about Cargo’s requirements in depth here.

Tilde Requirements

Tilde requirements can be specified with a ~, like ~1.2.3. This follows more traditional semantic versioning rules.

  • Minor or patch-level requirements (e.g. ~1.2 or ~1.2.3) allow patch-level updates.
  • Major-level requirements (e.g. ~1) allow minor and patch-level updates.

Wildcard Requirements

Wildcard requirements are like tilde requirements, except you explicitly define which groupings are allowed to be updated with a *.

  • 1.2.* allows patch-level updates.
  • 1.* allows minor and patch-level updates.
  • * allows any version.

Inequality Requirements

Perhaps the most explicit syntax, inequality requirements allow you to allow updates based on the =, <, and > operators. In our example above, if we want to pin an exact version, without allowing any updates, we would define the version as = 0.3.14.

Cargo also allows you to use multiple versions in specifying inequality requirements. For example, >= 1.2, < 1.4 keeps the version between 1.2 and 1.4.

Closing Thoughts

Specifying dependencies with Cargo is powerful, but can be a bit confusing for a newcomer. Hopefully, understanding the details of Cargo’s default behavior and how to define custom behavior explicitly will allow you to have more confidence in developing Rust applications. With this understanding, you can prevent headaches caused by versioning discrepancies.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.