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.
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.
- Install Rust.
cargo new rust-versionsin a directory of your choice to create a new Rust project.
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 <firstname.lastname@example.org>"] 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
Cargo.toml. What is going on here?
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‘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.
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
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 can be specified with a
~1.2.3. This follows more traditional semantic versioning rules.
- Minor or patch-level requirements (e.g.
~1.2.3) allow patch-level updates.
- Major-level requirements (e.g.
~1) allow minor and patch-level updates.
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.
Perhaps the most explicit syntax, inequality requirements allow you to allow updates based on the
> operators. In our example above, if we want to pin an exact version, without allowing any updates, we would define the version as
Cargo also allows you to use multiple versions in specifying inequality requirements. For example,
>= 1.2, < 1.4 keeps the version between
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.