Cargo Package Management

So far in this series, we've gotten a good starter look at Rust. We've considered different elements of basic syntax, including making data types. We've also looked at the important concepts of ownership and memory management.

Now, we're going to look at the more practical side of things. We'll explore how to actually build out a project using Cargo. Cargo is the Rust build system and package manager. This is Rust's counterpart to Stack and Cabal. We'll look at creating, building and testing projects. We'll also consider how to add dependencies and link our code together.

If you want to see Cargo in action, take a look at our Rust Video Tutorial. It gives a demonstration of most all the material in this article and then some more!

Cargo

As we mentioned above, Cargo is Rust's version of Stack. It exposes a small set of commands that allow us to build and test our code with ease. We can start out by creating a project with:

cargo new my_first_rust_project

This creates a bare-bones application with only a few files. The first of these is Cargo.toml. This is our project description file, combining the roles of a .cabal file and stack.yaml. It's initial layout is actually quite simple! We have four lines describing our package, and then an empty dependencies section:

[package]
name = "my_first_rust_project"
version = "0.1.0"
authors = ["Your Name <you@email.com>"]
edition = "2018"

[dependencies]

Cargo's initialization assumes you use Github. It will pull your name and email from the global Git config. It also creates a .git directory and .gitignore file for you.

The only other file it creates is a src/main.rs file, with a simple Hello World application:

fn main() {
  println!("Hello World!");
}

Building and Running

Cargo can, of course, also build our code. We can run cargo build, and this will compile all the code it needs to produce an executable for main.rs. With Haskell, our build artifacts go into the .stack-work directory. Cargo puts them in the target directory. Our executable ends up in target/debug, but we can run it with cargo run.

There's also a simple command we can run if we only want to check that our code compiles. Using cargo check will verify everything without creating any executables. This runs much faster than doing a normal build. You can do this with Stack by using GHCI and reloading your code with :r.

Like most good build systems, Cargo can detect if any important files have changed. If we run cargo build and files have changed, then it won't re-compile our code.

Adding Dependencies

Now let's see an example of using an external dependency. We'll use the rand crate to generate some random values. We can add it to our Cargo.toml file by specifying a particular version:

[dependencies]
rand = "0.7"

Rust uses semantic versioning to ensure you get dependencies that do not conflict. It also uses a .lock file to ensure that your builds are reproducible. But (to my knowledge at least), Rust does not yet have anything like Stackage. This means you have to specify the actual versions for all your dependencies. This seems to be one area where Stack has a distinct advantage.

Now, "rand" in this case is the name of the "crate". A crate is either an executable or a library. In this case, we'll use it as a library. A "package" is a collection of crates. This is somewhat like a Haskell package. We can specify different components in our .cabal file. We can only have one library, but many executables.

We can now include the random functionality in our Rust executable with the use keyword:

use rand::prelude::Rng;

fn main() {
  let mut rng = rand::thread_rng();
  let random_num: i32 = rng.gen_range(-100, 101);

  println!("Here's a random number: {}", random_num);
}

When we specify the import, rand is the name of the crate. Then prelude is the name of the module, and Rng is the name of the trait we'll be using.

Making a Library

Now let's enhance our project by adding a small library. We'll write this file in src/lib.rs. By Cargo's convention, this file will get compiled into our project's library. We can delineate different "modules" within this file by using the mod keyword and naming a block. We can expose the function within this block by declaring it with the pub keyword. Here's a module with a simple doubling function:

pub mod doubling {
  pub fn double_number(x: i32) -> i32 {
    x * 2
  }
}

We also have to make the module itself pub to export and use it! To use this function in our main binary, we need to import our library. We refer to the library crate with the name of our project. Then we namespace the import by module, and pick out the specific function (or * if we like).

use my_first_rust_project::doubling::double_number;
use rand::prelude::Rng;

fn main() {
  let mut rng = rand::thread_rng();
  let random_num: i32 = rng.gen_range(-100, 101);

  println!("Here's a random number: {}", random_num);
  println!("Here's twice the number: {}", double_number(random_num));
}

Adding Tests

Rust also allows testing, of course. Unlike most languages, Rust has the convention of putting unit tests in the same file as the source code. They go in a different module within that file. To make a test module, we put the cfg(test) annotation before it. Then we mark any test function with a test annotation.

// Still in lib.rs!

#[cfg(test)]
mod tests {
  use crate::doubling::double_number;

  #[test]
  fn test_double() {
    assert_eq!(double_number(4), 8);
    assert_eq!(double_number(5), 10);
  }
}

Notice that it must still import our other module, even though it's in the same file! Of course, integration tests would need to be in a separate file. Cargo still recognizes that if we create a tests directory it should look for test code there.

Now we can run our tests with cargo test. Because of the annotations, Cargo won't waste time compiling our test code when we run cargo build. This helps save time.

What's Next

We've done a very simple example here. We can see that a lot of Cargo's functionality relies on certain conventions. We may need to move beyond those conventions if our project demands it. You can see more details by watching the Video Tutorial! In the last part, we'll wrap up our study of Rust by looking at different container types!