Automating dependency management
You learned in the previous section how Cargo can be used to set up the base project directory structure and scaffolding for a new project, and how to build various types of binary and library crates. We will look at the dependency management features of Cargo in this section.
Rust comes with a built-in standard library consisting of language primitives and commonly used functions, but it is small by design (compared to other languages). Most real-world programs in Rust depend on additional external libraries to improve functionality and developer productivity. Any such external code that is used is a dependency for the program. Cargo makes it easy to specify and manage dependencies.
In the Rust ecosystem, crates.io is the central public package registry for discovering and downloading libraries (called packages or crates in Rust). It is similar to npm in the JavaScript world. Cargo uses crates.io
as the default package registry.
Dependencies are specified in the [dependencies]
section of Cargo.toml
. Let's see an example.
Start a new project with this command:
cargo new deps-example && cd deps-example
In Cargo.toml
, make the following entry to include an external library:
[dependencies] chrono = "0.4.0"
Chrono
is a datetime library. This is called a dependency because our deps-example
crate depends on this external library for its functionality.
When you run cargo build
, cargo looks for a crate on crates.io
with this name and version. If found, it downloads this crate along with all of its dependencies, compiles them all, and updates a file called Cargo.lock
with the exact versions of packages downloaded. The Cargo.lock
file is a generated file and not meant for editing.
Each dependency in Cargo.toml
is specified in a new line and takes the format <crate-name> = "<semantic-version-number>"
. Semantic versioning or Semver has the form X.Y.Z, where X is the major version number, Y is the minor version, and Z is the patch version.
Specifying the location of a dependency
There are many ways to specify the location and version of dependencies in Cargo.toml
, some of which are summarized here:
- Crates.io registry: This is the default option and all that is needed is to specify the package name and version string as we did earlier in this section.
- Alternative registry: While
crates.io
is the default registry, Cargo provides the option to use an alternate registry. The registry name has to be configured in the.cargo/config
file, and inCargo.toml
, an entry is to be made with the registry name, as shown in the example here:[dependencies] cratename = { version = "2.1", registry = "alternate- registry-name" }
- Git repository: A Git repo can be specified as the dependency. Here is how to do it:
[dependencies] chrono = { git = "https://github.com/chronotope/chrono" , branch = "master" }
Cargo will get the repo at the branch and location specified, and look for its
Cargo.toml
file in order to fetch its dependencies. - Specify a local path: Cargo supports path dependencies, which means the library can be a sub-crate within the main cargo package. While building the main cargo package, the sub-crates that have also been specified as dependencies will be built. But dependencies with only a path dependency cannot be uploaded to the crates.io public registry.
- Multiple locations: Cargo supports the option to specify both a registry version and either a Git or path location. For local builds, the Git or path version is used, and the registry version will be used when the package is published to crates.io.
Using dependent packages in source code
Once the dependencies are specified in the Cargo.toml
file in any of the preceding formats, we can use the external library in the package code as shown in the following example. Add the following code to src/main.rs
:
use chrono::Utc; fn main() { println!("Hello, time now is {:?}", Utc::now()); }
The use
statement tells the compiler to bring the chrono
package Utc
module into the scope of this program. We can then access the function now()
from the Utc
module to print out the current date and time. The use
statement is not mandatory. An alternative way to print datetime would be as follows:
fn main() { println!("Hello, time now is {:?}", chrono::Utc::now()); }
This would give the same result. But if you have to use functions from the chrono
package multiple times in code, it is more convenient to bring chrono
and required modules into scope once using the use
statement, and it becomes easier to type.
It is also possible to rename the imported package with the as
keyword:
use chrono as time; fn main() { println!("Hello, time now is {:?}", time::Utc::now()); }
For more details on managing dependencies, refer to the Cargo docs: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html.
In this section, we have seen how to add dependencies to a package. Any number of dependencies can be added to Cargo.toml
and used within the program. Cargo makes the dependency management process quite a pleasant experience.
Let's now look at another useful feature of Cargo – running automated tests.