Cargo
Cargo is a super slick management tool that you can use to create, build, and manage projects. Cargo can even be used to install Rust binaries! Cargo is such a large and important suite that it has its own documentation that is most definitely worth a bookmark.
Create A New Project
Use the cargo new
command to build a new project. We will see later that we can use the --bin
and --lib
options to build specific types of projects. For now, lets use the default option that builds a project that will compile to a binary executable.
$ cargo new learning_rust Created binary (application) `learning_rust` package
If we navigate the directory that the Cargo tool creates we see that we now have whole project. Importantly the project includes a cargo.toml file, a src/main.rs file, a .gitignore file, and more. Note that Cargo wont create a .gitignore file if the command is run within an existing Git repository. This behavior can be overridden using the --vcs=git
when creating the project.
$ tree -la learning_rust/learning_rust/├── .git│ ├── HEAD│ ...├── .gitignore├── Cargo.toml└── src └── main.rs
10 directories, 8 files
The TOML (Tom’s Obvious, Minimal Language) file is the project’s configuration file. Opening up the Cargo.toml file we see a package
definition heading includes keys that Cargo uses to compile the program(s). These include name
, version
, and edition
key-value pairs. The dependencies
definition is where we’ll list all the program’s dependencies. In Rust, dependency packages are stored in “crates”.
Notice the main.rs
file and its location. All source code files (and sub-directories) exist within the src
directory.
Build The Project
Use Cargo to build the project using the cargo build
command.
$ cargo build Compiling learning_rust v0.1.0 (/Users/peterschmitz/IdeaProjects/learning_rust) Finished dev [unoptimized + debuginfo] target(s) in 1.30s
If we run the tree
program again for this directory we will see that the build process creates a lot of new files.
$ tree -la learning_rust/learning_rust/├── .git│ ├── HEAD│ ...├── .gitignore├── Cargo.lock├── Cargo.toml├── src│ └── main.rs└── target ├── .rustc_info.json ├── CACHEDIR.TAG └── debug ├── .cargo-lock ├── .fingerprint │ └── learning_rust-6ec14cbb0e0a32e8 │ ├── bin-learning_rust │ ... ├── build ├── deps │ ├── learning_rust-6ec14cbb0e0a32e8 │ ... ├── examples ├── incremental │ └── learning_rust-1fo03fg0yiz2r │ ├── s-gpqvn54udp-ql417m-369bg06utwhgh │ │ ├── 1lrknxftybsq7bzs.o │ │ ... │ └── s-gpqvn54udp-ql417m.lock ├── learning_rust └── learning_rust.d
20 directories, 36 files
The first build
command creates a couple of important elements. First we will notice a new Cargo.lock file. The Cargo.lock file keeps track of the exact versions of dependencies we’re using. The first time we run the build command it will also create a new /target directory. The /target directory contains a bunch of stuff we won’t get too specific about right now, but does contain one super important element. Notice from the cargo build
command example above that the output includes a compile step. Running the build process compiles the code and creates one or more binary executable files that it places in the /target/debug directory. The executables correspond with our Rust source code programs within the /src directory. For a simple project with one src/main.rs file the build step creates an executable in the target/debug with the name of the project. In the example above we can see the binary executable called learning_rust. Notice that the file does not have a file type extension. In projects with more than one .rs file in the /src directory the binary executables are named after the source code files. Subsequent build steps skip elements that do not change.
Build Profiles
Build profiles (aka release profiles) allow us to customize how the code is compiled. By default, Rust uses dev
, release
, test
, and bench
build profiles. The two most common profiles are dev
and release
. The dev
profile is the default preset when running cargo build
or cargo run
commands. The release
build is what we use for a software release (natch). Each of these profiles contains presets that are optimized for the kind of compilation we’re after. They all do similar things, and all compile the source code, but contain options for different behaviors along the way. The profiles can be customized per project. Customize build profiles by setting flags in the Cargo.toml
project file. For example, the opt-level
option represents a range (0..3) of optimizations applied at compilation. More optimization levels take more time. The language is configured with the following defaults.
[profile.dev]opt-level = 0
[profile.release]opt-level = 3
To change these values, simply insert the desired levels into the Cargo.toml
project file, save, and build. See the profiles section of Cargo’s documentation for more information including options and usage.
Run the binary executable
One of the major advantages of compiled languages is that we can execute programs without spinning up a runtime. We can run the learning_rust binary by simply typing ./
with the executable’s (relative) path into a shell press enter.
$ ./target/debug/learning_rustHello, world!
Strange how much like C this is so far, right?
Combined build & run
It is possible, and indeed desirable, to combine the build and run process much of the time. This can be done with Cargo itself. This works from anywhere within the project! The system will automatically detect if source code files have changed and will only compile and build a new executable binary if they have. Otherwise, the command simply moves on to the run process. For projects with only one executable simply use the cargo run
command.
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/learning_rust`Hello, world!
For projects that have more than one executable we will need to specify by adding the --bin
option with the executable file’s name.
cargo run --bin learning_rust Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/learning_rust`Hello, world!
Check for compilation errors
As we progress on our developer journey, we may want to periodically check that our code compiles without actually going through the process of producing an executable. As projects grow in size and complexity, the full build step may take quite a bit of time. Use the check
command to ensure that the project is indeed able to be compiled without producing any new executables.
$ cargo check Checking learning_rust v0.1.0 (/Users/peterschmitz/IdeaProjects/learning_rust) Finished dev [unoptimized + debuginfo] target(s) in 1.66s
The check
option can be shortened to c
if you’re thin on time. Additionally, it may be helpful during development to suppress all the warnings that the compiler throws. This sounds much more dangerous than it is, but Rust is much safer than most languages. Warnings in Rust are more like caution messages. The code will still compile and run, but it may not include all the functionality of a finished product. To show only errors, use the following flag during compilation checks for valid code.
$ RUSTFLAGS=-Awarnings cargo c Checking learning_rust v0.1.0 (/Users/peterschmitz/IdeaProjects/learning_rust) Finished dev [unoptimized + debuginfo] target(s) in 0.12s
One might be tempted to use this option with a simple binary file output execution in debug
, but recall that cargo check
doesn’t actually build the project. Running the ./binary_name
will only execute the last build
Build/run for release
When we’re finally ready to release our software into the wild we can add the --release
option to either the build
or run
command to add an optimization step. When run for the first time in a project, this option creates a directory in the /target directory called “release”. The structure is similar to /debug, but contains optimizations for runtime speed at the expense of initial build time.
$ cargo build --release Compiling learning_rust v0.1.0 (/Users/peterschmitz/IdeaProjects/learning_rust) Finished release [optimized] target(s) in 0.27s
Formatting & Linting
The standard Rust distribution includes a rustfmt
/fmt
(format) tool to keep your code looking spiffy. To clean up a single file, navigate to the directory that the file is in and run the following command.
$ rustfmt <file>
To clean up an entire project, navigate to the top of the crate and use the cargo
tool to format the entire crate. We will
$ cargo fmt
Cargo also includes a built-in linter to keep our code clean. Cargo checks for and issues warnings for all sorts of elements. See the rustc book for lints information. This is super helpful for release output, but may be undesirable during development with unimplemented elements. I frequently include diagnostic attributes until Im ready to release. More about attributes can be found in the Rust Reference book. See the list of built-in attributes or run rustc -W help
for more on the following diagnostic attributes.
#![allow(dead_code)]#![allow(unused_variables)]#![allow(unused_assignments)]
Reproducible Builds
Cargo includes a mechanism to rebuild the same artifact any time the code is built via the Cargo.lock file.
Crates
Remember that crates are just collections of source files. In Rust there are two basic types of crates; binary and library crates. The binary crates are directories that house the source code for the programs we’re building and are used to create binary executables. The library crates are how we import external source code for program functionality and cannot be executed on their own. Visit https://crates.io for a community-oriented collection of crates. When crates are imported into a project via the Cargo.toml file they need to be downloaded and compiled before they can be used. This is accomplished by running cargo build
(or similar). Once the dependencies are compiled they won’t be recompiled unless we specify a new dependency or version.
Crates use semantic versioning and are listed in the Cargo.toml file accordingly. They are technically shorthand for ^x.x.x so if we list a dependency as 0.8.5 its really ^0.8.5. When the build command is executed the system will download and compile any version that is at least 0.8.5 up to 0.9.0. This information is then stored in the Cargo.lock file so even if we run another build the system won’t automatically upgrade until we explicitly specify it.
When we do want to update a crate, simply run cargo update
which ignores the Cargo.lock file and figures out the latest versions of the dependencies listed in the Cargo.toml file. This process only updates the versions within the listed range of the semantic version listed in the Cargo.toml file. To go above the semantic version listed there you must specify it explicitly and then update the project with a cargo update
and a cargo build
which will reset the Cargo.lock file.
Publishing Crates
We can make our code publicly available by publishing it to crates.io. Crates.io is intended to provide a permanent archive of crates. This means that no version can be overwritten or deleted. Crates are defined by the [package]
section of our Cargo.toml
project files. We can publish crates with the cargo publish
command. We can update code on crates.io by updating the version
number in the [package]
metadata. Before we can publish, our crates we need an active crates.io account, an API key, and the requisite package metadata.
Crates.io account & API key
As of March 2024 crates.io still requires a GitHub account. While not explicitly necessary in The Book, I found that I also needed to update my crates.io account with an email address to publish. After we establish an account, go to your crates.io profile and generate a new token. There are several options for naming and permissions. After we have the token, we can log in with
$ cargo login keystring123
This stores our login credentials locally in ~/.cargo/credentials
for use.
Package metadata
To add metadata to our crates we need to edit the Cargo.toml
file, and specifically the [package]
information.
name
: Cargo automatically populates a name when we create our project with thecargo new
command. This is fine for local projects, but packages on crates.io must be unique. Names are granted on a first come, first served basis. This means that even if we have a super stellar name for our package, we cannot publish it if its already in use. If we’re writing public crates it may be wise to search for names on crates.io before we begin to ease the disappointment later on.license
: Each published package requires a Linux Foundation Software Package Data Exchange (SPDX) identifier. Multiple identifiers can be named using theOR
operator. If we want to define our own license use thelicense-file
key instead with the name of the file in the project that contains the license.license = "MIT"license = "MIT OR Apache-2.0"license-file = "license.txt"description
: Just a simple, one sentence description of the crate.
Putting it all together we’ll end up with something that looks like this.
[package]name = "minigrep"version = "0.1.0"edition = "2021"license = "MIT"description = "A fun little grep clone"
Managing dependencies
Use the cargo.toml file in your package’s root to add dependencies. You can either add a dependency manually by editing the file, or use Cargo.
[package]name = "learning_rust"version = "0.1.0"edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]rand = "0.8.5" #formatted as dependency name and version numberjson = "0.12.4"regex = "1.10.2"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
This example adds the reqwest
dependency with an additional feature flag.
cargo add reqwest --features="json"
Deprecating crates
Because the content on crates.io is permanent we cannot delete it. If we notice that there is an issue with the code such as a security vulnerability, functional issue, or its just an old version thats no longer maintained, we can deprecate the crate. This prevents any future projects from using the crate as a dependency, but wont break any projects that have already listed our crate as a dependency. Use the cargo yank
command from the local crate directory to deprecate a crate from crates.io.
$ cargo yank --vers 0.1.0
Deprecation is reversible.
$ cargo yank --version 0.1.0 --undo
Publishing workspaces
Each crate (module) within a workspace needs to be published separately. Use the -p
command with the module name to specify a module to publish.
$ cargo publish -p my_module
Installing Binaries
Sometimes we dont want to code, we just want to use Rust binaries. Cargo is capable of installing binaries from crates.io with the cargo install
command. This command only works with binaries (not libraries). The results are stored in the installation root’s bin
folder. By default this is $HOME/.cargo/bin
. To run the program, you’ll also have to ensure that its on your $PATH
.