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!