14 - More about Cargo and Crates
14.1 - Customizing Builds with Release Profiles
Cargo has four built-in release profiles called dev
, release
, test
, and bench
. cargo build
will build in the dev
profile, and cargo build --release
in the release
profile, and the other two are used when running tests. Cargo has various settings for for these profiles, which you can override in Cargo.toml:
[profile.dev]
opt-level = 0
overflow-checks = true
[profile.release]
opt-level = 3
overflow-checks = false
In this example opt-level
controls how much Rust tries to optimize your code and can be set from 0 to 3 (these are also the defaults - 0 for dev because you want the build to be fast, 3 for release because you want your program to be fast). overflow-checks
is used to determine whether or not Rust will add in runtime checks to see if integer arithmetic overflows any values.
For a full list of options see the cargo documentation.
14.2 - Publishing a Crate to Crates.io
If you write a library crate, you can publish it to crates.io so other people can use it.
Setting Up a Crates.io Account
To publish something on crates.io, first you'll need to create an account. Then visit https://crates.io/me, grab your API key, and run:
$ cargo login your-key-here
Your API key will be stored in ~/.cargo/credentials. This is secret so if anyone gets ahold of your API key, be sure to revoke it and generate a new one, otherwise people can publish crates in your name, and before you know it all your crates will be full of crypto miners or worse.
Making Useful Documentation Comments
One thing we haven't done much of in our examples so far is to document our public structs and functions, but if you look at any package on crates.io you'll see everything has automatically generated helpful documentation. This documentation comes from documentation comments which start with three slashes instead of two, and support markdown. Documentation comments should be placed immediately before the thing they are documenting:
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
If you run cargo doc --open
in your project, you can see what the generated documentation for your project will look like. This will also include documentation for any crates that you depend on.
Commonly Used Sections
We used the # Examples
markdown heading above to make a section for our examples. Some other commonly used headings:
# Panics
describes the scenarios in which the given function might panic.# Errors
describes the kinds of conditions under which this function might return an error, and what kinds of errors are returned.# Safety
should be present for any function that isunsafe
(see chapter 19).
You don't need all of these on every function. Any examples you provide will automatically be run as tests when you cargo test
, so you'll know your examples actually work.
Commenting Contained Items
There's a second kind of documentation comment //!
that, instead of documenting what comes right after it, documents the "parent" of the comment. You can use these at the root of src/lib.rs to document the crate as a whole, or inside a module to document the module:
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
Exporting a Convenient Public API with pub use
We talked about this back in chapter 7, but sometimes we might organize the internals of our library in such a way that makes sense to use when we're developing it, but is at odds with how someone would actually want to use our crate. If you have some struct or module that is frequently used by users of your crate, but is buried deep in the module hierarchy, this is going to be a pain point for your users.
Here's an example:
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
Users of this crate would have to write code like:
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
but users of this crate probably don't care about kinds
or utils
- to them this should be top level functionality for this crate. We can "re-export" these at the top level with pub use
:
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
}
pub mod utils {
// --snip--
}
Adding Metadata to a New Crate
In order to publish on crates.io, our crate needs a name, a description, and a valid license identifier:
[package]
name = "my_awesome_colors"
version = "0.1.0"
edition = "2021"
description = "A fancy awesome crate for generating colored text in the terminal."
license = "MIT"
Publishing Your Crate
To publish your package run:
$ cargo publish
If someone has already used your name on crates.io then this will fail - names are handed out first-come-first-served. If you make some changes to your crate, bump the version number (using semantic versioning rules) and then run cargo publish
again.
Deprecating Versions from Crates.io with cargo yank
Sometimes an older version of your crate will have some terrible security bug, or has a fatal bug that completely breaks it. For this or other reasons, you'll want to stop people from installing this version and warn existing users they need to upgrade. You can't remove an old version, but you can mark it as deprecated:
$ cargo yank --vers 1.0.1
If you accidentally yank the wrong version, you can undo this:
$ cargo yank --vers 1.0.1 --undo
14.3 - Cargo Workspaces
Sometimes a library crate gets so large that you want to split it up into multiple smaller crates. Cargo workspaces lets you do this while keeping all the crates together in the same git repo. It's a bit like the Rust version of a monorepo. You can build all packages in a workspace by running cargo build
from the root folder of the workspace, and run tests in all workspaces with cargo test
.
Creating a Workspace
A workspace consists of multiple packages with their own individual Cargo.yaml files, with a single Cargo.lock file at the root of the workspace. We'll create a simple example here with a single binary crate and two library crates. If you want to see the code for this, check this book's GitHub repo. First we'll create a new directory for our workspace and initialize it as a git repo:
$ mkdir add
$ cd add
$ git init .
$ echo "/target" > .gitignore
The add directory is the root of our workspace, so all other files we create will be relative to this folder. We're going to add three packages to this workspace: "adder", our binary package, and "add_one" and "add_two", our libraries. Let's start by creating these packages as subdirectories:
$ cargo new adder
$ cargo new add_one --lib
$ cargo new add_two --lib
You may have noticed that we ran git init
in the add directory - we did this because generally we want to commit the entire workspace as a single repo, and if we hadn't run git init
, then cargo new ...
would have "helpfully" initialized all three new packages as git repos for us.
Now in the add folder - the root folder of our workspace - we are going to create a Cargo.toml for the entire workspace. This Cargo.toml won't have any metadata or dependencies, it will simply list all the packages that make up the workspace:
[workspace]
members = [
"adder",
"add_one",
"add_two",
]
If you do these in the opposite order - create the top-level Cargo.toml first and then create the child packages - it will still work, but as you create each package you'll get warnings from cargo about not being able to find the packages you haven't created yet.
We can build this workspace, to make sure we did everything right:
$ cargo build
Compiling add_two v0.1.0 (add/add_two)
Compiling adder v0.1.0 (add/adder)
Compiling add_one v0.1.0 (add/add_one)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
At this point you should have a directory structure inside add that looks like:
├── .git
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── add_two
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Note that the top level folder has a Cargo.lock file, but none of the child projects do.
Referencing Other Packages in the Workspace
We'll put this in add_one/src/lib.rs:
pub fn add_one(x: i32) -> i32 {
x + 1
}
We want to use this library in our binary crate in the adder folder. To do this, first we have to add the add_one
package as a dependency of adder
:
[dependencies]
add_one = { path = "../add_one" }
And then we can use add_one
in the adder package, just as we would any other dependency:
use add_one;
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
From the add directory we can now run:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
If we had multiple packages with binary crates in the workspace, we'd have to specify which package to run with cargo run -p adder
, or we could cd adder
and then cargo run
from the adder folder.
Depending on an External Package in a Workspace
We can depend on an external create in a workspace by adding it to the [dependencies]
section of the appropriate package's Cargo.toml. For example, we can add the rand
crate to add_one
in add_one/Cargo.toml:
[dependencies]
rand = "0.8.5"
If we add use rand;
inside add_one/src/lib.rs, then cargo build
, we'll see the rand
package being downloaded. We'll also get a warning because we're use
ing rand, but we never reference it in the library. Oops!
If we want to use rand
in other packages in our workspace, we have to add it again to the appropriate Cargo.yaml. Since there's only one Cargo.lock file for the whole workspace, if adder
and add_one
both depend on rand
, we know that they will both depend on the same version of rand
thanks to the common lockfile (at least, assuming the have compatible semver versions in the different Cargo.toml files).
14.4 - Installing Binaries with cargo install
You can publish more than just library crates to crates.io - you can also publish binary crates! Users can install your crates with cargo install
. (This is very similar to npm install -g
if you're a node.js developer.) For example, ripgrep
is a very fast alternative to the grep
command:
$ cargo install ripgrep
Binaries you install this way get put in ~/.cargo/bin
(assuming you're running a default installation of cargo from rustup). You'll probably want to put this folder in your shell's $PATH
. The name of the installed binary is not necessarily the same as the name of the crate. If you try installing ripgrep
above, for example, it will install ~/.cargo/rg
.
14.5 - Extending Cargo with Custom Commands
Much like git, you can create your own custom cargo commands. If there's an executable on your path called cargo-something
, then you can run cargo something
to run that executable. These custom commands will also show up in cargo --list
.
One handy command you can install this way is cargo-audit, which will check your dependencies against the rustsec security advisory database:
$ cargo install cargo-audit
$ cargo audit
Continue to chapter 15.