Intro
Cargo allows for more than installing crates, testing , and running our Rust code.
Quick summary:
- Customize your build through release profiles.
- Publish libraries on crates.io.
- Organize large projects with workspaces.
- Install binaries from crates.io.
- Extend Cargo using custom commands.
Custom Build with Release profiles.
These profiles are predefined in Rust, but also customizeable, configurations that allow control over options when compiling the code. Profiles are independent from one another.
There are two main profiles that you’ll interact with a lot.
- Dev
- Release
The dev profile for when running cargo build, it’s fast and not optimized, just right for development. Whereas release is when you build with the --release flag.
The output of the build confirms the profile used:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32sCargo has defaults for both profiles if nothing is specified in the [profile.*] section in the cargo.toml for your project. But you can override these configurations.
Here’s an example that changes the opt-level, which is a range of optimizations that Rust will apply to your code, ranging from 0 - 3. Remember that applying more optimizations extends compiling time…So weigh your needs, based on where you are in the sdlc.
The default opt-level for the release profile is 3 since you compile production code once and run it a lot more, whereas dev is opt-level 0.
For more details, read the docs.
Publishing to crates.io
I won’t get into all the details of this section here. It’s best to review the source.
It mostly covers documentation, testing, comments, APIs, publication, etc.
Documentation
Rust has documentation comments that use /// instead of //. These doc comments build HTML documentations for your crate when running cargo doc, which runs the rustdoc tool distributed with Rust, and stores it in target/doc directory.
Therefore, it’s imperative and very appreciated to take the time to document your crate’s API using doc comments so others may understand how to use it.
Here’s an example:
/// 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
}Opening in Browser
Running cargo doc --open will build the HTML for the current crate’s documentation (and all its dependencies) then open the result in a web browser.
Contained Comments
These add the docs to the containing item rather than the item following the comments. For example, the crate root file src/lib.rs, which is great for explaining what a crate or module does as a whole rather than what specific function or method does.
Contained Item comments are denoted using //!.
Filename: src/lib.rs
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--Public API
Our code’s structure no matter how useful to us, isn’t the most intuitive to an outsider looking to make use of it. So instead of restructuring your code to make it friendly to the public consuming your API, use pub use i,e, re-exporting.
Re-exporting takes a public item in one location and makes it public in another location, as if it were defined in the other location instead.
//! # 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--
}“Another common use of
pub useis to re-export definitions of a dependency in the current crate to make that crate’s definitions part of your crate’s public API.”
Workspaces
When a project grows, lib.rs can get too big and the need to split the library into more library crates arises.
Rust offers Workspaces to manage related packages that are developed in tandem.
They’re a set of packages that share the same Cargo.lock and output directory.
Creating Workspaces
There are a few ways to do so.
- start by creating a new directory for the workspace:
mkdir wspace1
cd wspace1inside, create cargo.toml file to configure the workspace. But instead of a [package] section, it’ll start with a [workspace] section in the TOML.
Under this section we add members, and set Cargo’s resolver algorithm to 3.
Filename: Cargo.toml
[workspace]
resolver = "3"- Create binary crate by running
cargo new name. By running the command in a workspace, it’ll automatically add the binary crate to the workspace in the TOML file.
[workspace]
resolver = "3"
members = ["binz"]- Now we can build the project.
Resulting in a project tree like this
├── Cargo.lock
├── Cargo.toml
├── binz
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── targetThe workspace has one target directory at the top level that the compiled artifacts will be placed into; t
Cargo structures the target directory in a workspace like this because the crates in a workspace are meant to depend on each other. If each crate had its own target directory, each crate would have to recompile each of the other crates in the workspace to place the artifacts in its own target directory. By sharing one target directory, the crates can avoid unnecessary rebuilding.
Creating Second Package
To generate a new library crate, run cargo new --lib and the top-level cargo.toml will include it in the members list.
[workspace]
resolver = "3"
members = ["binz", "lib_one"]The tree becomes
├── Cargo.lock
├── Cargo.toml
├── lib_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── binz
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── targetIn lib_one/src/lib.rs add some code, now the binary can depend on the lib_one package.
First, we’ll need to add a path dependency on lib_one to binz/Cargo.toml.
Filename: binz/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }"Cargo doesn’t assume that crates in a workspace will depend on each other, so we need to be explicit about the dependency relationships."
We can now use the dependency in the binary crate and build it.
To run a specific package in the workspace, use the the -p argument.
$ cargo run -p binz
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/binz`
Hello, world! 10 plus one is 11!External Packages
Workspaces have only one Cargo.lock at the top level of the project, ensuring they’re all using the same version of the dependencies.
So if we add an external package to our library crates or even the binary, Cargo will resolve and only add one version of the external dependency to Cargo.lock. For more details read this section.
Tests in Workspaces
Write tests in the crates as you normally would, then run cargo test in the top-level directory.
Tests should run and results displayed.
To run a particular crate, we use the -p argument again, cargo test -p crate_name.
Publishing Crates from Workspaces to Crates.io
“If you publish the crates in the workspace to crates.io, each crate in the workspace will need to be published separately. Like
cargo test, we can publish a particular crate in our workspace by using the-pflag and specifying the name of the crate we want to publish.”
Installing Binaries
The command cargo install allows developers to install binary crates for local use, this does NOT replace system packages but rather a convenient way to install tools from crates.io.
// These tools are installed system wide...
Note that you can only install packages that have binary targets.
A binary target is the runnable program that is created if the crate has a src/main.rs file or another file specified as a binary, as opposed to a library target that isn’t runnable on its own but is suitable for including within other programs.
Binaries installed are stored in the root’s bin folder, as in *PATH==
Example of installing ripgrep:
Updating crates.io index
Downloaded ripgrep v14.1.1
Downloaded 1 crate (213.6 KB) in 0.40s
Installing ripgrep v14.1.1
--snip--
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)The location of the installation is ~/.cargo/bin/rg and the command rg has been added to $PATH.
Extending Cargo
Cargo can be extended with custom subcommands easily, without needing to modify it.
Any binary in $PATH named cargo-something can be run as a Cargo subcommand.
Example, if I had a binary to brew a pour-over coffee called cargo-brew-poc then I can run it using
cargo brew-pocCustom commands can be listed using cargo --list.