Cargo and dependencies

For projects that depend on other libraries, the package manager has to find all of the direct dependencies in the project and also any indirect dependencies, and then compile and link them to the project. Package managers are not just a tool for facilitating dependency resolution; they should also ensure predictable and reproducible builds of a project. Before we cover building and running our project, let's discuss how Cargo manages dependencies and ensures repeatable builds.

A Rust project managed with Cargo has two files through which it does all its magic: Cargo.toml (introduced before) is the file where you, as the developer, write dependencies and their needed versions with SemVer syntax (like 1.3.*), and a lock file called Cargo.lock, which gets generated by Cargo upon building the project and that contains absolute versions (like 1.3.15) of all the immediate dependencies and any indirect dependencies. This lock file is what ensures repeatable builds in binary crates. Cargo minimizes the work it has to do by referring to this lock file for any further changes to the project. As such, it is advised that binary crates include the .lock file in their repository, while library crates can be stateless and don't need to include it.

Dependencies can be updated using the cargo update command. This updates all of your dependencies. For updating a single dependency, we can use cargo update -p <crate-name>. If you update the version of a single crate, Cargo makes sure to only update parts that are related to that crate in the Cargo.lock file and leaves other versions untouched.

Cargo follows the semantic versioning system (SemVer), where your library version is specified in the format of major.minor.patch. These can be described as follows:

  • Major: Is only increased when new breaking changes (including bug fixes) are made to a project.
  • Minor: Is only increased when new features are added in backward compatible ways.
  • Patch: Is only increased when bug fixes are made in backward compatible ways and no features are added.

For example, you might want to include the serialization library, serde, in your project. At the time of writing this book, the latest version of serde is 1.0.85 , and you probably only care about the major version number. Therefore, you write serde = "1" as the dependency (this translates to 1.x.x in SemVer format) in your Cargo.toml and Cargo will figure it out for you and fix it to 1.0.85 in the lock file. The next time you update Cargo.lock with the cargo update command, this version might get upgraded to whichever is the latest version in the 1.x.x match. If you don't care that much and just want the latest released version of a crate, you can use * as the version, but it's not a recommended practice because it affects the reproducibility of your builds as you might pull in a major version that has breaking changes. Publishing a crate with * as the dependency version is also prohibited.

With that in mind, let's take a look at the cargo build command, which is used to compile, link, and build our project. This command does the following for your project:

  • Runs cargo update for you if you don't yet have a Cargo.lock file and puts the exact versions in the lock file from Cargo.toml
  • Downloads all of your dependencies that have been resolved in Cargo.lock
  • Builds all of those dependencies
  • Builds your project and links it with the dependencies

 By default, cargo build creates a debug build of your project under the target/debug/ directory. A --release flag can be passed to create an optimized build for production code at the target/release/ directory. The debug build offers faster build time, shortening the feedback loop, while production builds are a bit slower as the compiler runs more optimization passes over your source code. During development, you need to have a shorter feedback time of fix-compile-check. For that, one can use the cargo check command, which results in even shorter compile times. It basically skips the code generation part of the compiler and only runs the source code through the frontend phase, that is, parsing and semantic analysis in the compiler. Another command is cargo run, which performs double duty. It runs cargo build, followed by running your program in the target/debug/ directory. For building/running a release version, you can use cargo run --release . On running Cargo run in our imgtool/ directory, we get the following output: