Rust Cargo & Why I Like It
About Rust Cargo
What is Rust Cargo
? For short, cargo
is a package manager used in Rust
. Like package manager in other programming languages, by common, it will manage the package dependencies, but in cargo is not just about the dependencies, but more.
Cargo is the Rust package manager. It is a tool that allows Rust packages to declare their various dependencies and ensure that you’ll always get a repeatable build.
Source: https://doc.rust-lang.org/cargo/guide/why-cargo-exists.html
Rust Cargo
Besides managing the package’s dependencies, the cargo
the package manager will give us a lot of abilities to manage our build, test, and many things.
There are four things that Cargo
will help us
- Introduces two metadata files with various bits of package information.
- Fetches and builds your package’s dependencies.
- Invokes
rustc
or another build tool with the correct parameters to build your package. - Introduces conventions to make working with Rust packages easier.
Rust Cargo
also provide a centralized registry where we as developers will be able to fetch crates from it.
A registry is a service that contains a collection of downloadable crates that can be installed or used as dependencies for a package. The default registry in the Rust ecosystem is crates.io. The registry has an index which contains a list of all crates, and tells Cargo how to download the crates that are needed.
A Rust crate is either a library or an executable program, referred to as either a library crate or a binary crate, respectively.
As a new-comer to the Rust language and ecosystem, there are some features that I really like about the Rust Cargo
.
Codebase Layout
In other programming languages, there are so many opinions about how they manage their codebase layout, which I actually hate. Too many opinions mean too many things to consider for me.
The cargo
provides a convention to manage our codebase. The structure is like this
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ └── bin/
│ ├── named-executable.rs
│ ├── another-executable.rs
│ └── multi-file-executable/
│ ├── main.rs
│ └── some_module.rs
├── benches/
│ ├── large-input.rs
│ └── multi-file-bench/
│ ├── main.rs
│ └── bench_module.rs
├── examples/
│ ├── simple.rs
│ └── multi-file-example/
│ ├── main.rs
│ └── ex_module.rs
└── tests/
├── some-integration-tests.rs
└── multi-file-test/
├── main.rs
└── test_module.rs
I’ve tried to explore some of the available crates out there, and all of them follow the same convention (as long as I know).
A description of the structure:
Cargo.toml
andCargo.lock
are stored in the root of your package (package root).- The source code goes in the
src
directory. - The default library file is
src/lib.rs
. - The default executable file is
src/main.rs
. - Other executables can be placed in
src/bin/
. - Benchmarks go in the
benches
directory. - Examples go in the
examples
directory. - Integration tests go in the
tests
directory.
Source: https://doc.rust-lang.org/cargo/guide/project-layout.html
Manifest
The Cargo Manifest
is a configuration, metadata, and description of the package. It will tell us how to build the package, the package description, the dependencies, and many things.
The manifest itself will take the form of a file called Cargo.toml
. An example content of this manifest
[package]
name = "hello_world" # the name of the package
version = "0.1.0" # the current version, obeying semver
authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
Why did I like this format? It’s simple and easy to learn. There are no magical scripts something like custom DSL (like Scala SBT). Everything is explicit and predicted.
Dependencies
For the package’s dependencies, like other programming language package managers, we’re able to define what kind of library we depend on and which version we need. And the interesting part is, that we are also able to define which feature flag we want to activate from it, we will talk about this feature later.
For the version, Rust Cargo
follow the Semver (Semantic Versioning)
format. Examples:
1.2.3 := >=1.2.3, <2.0.0
1.2 := >=1.2.0, <2.0.0
1 := >=1.0.0, <2.0.0
0.2.3 := >=0.2.3, <0.3.0
0.2 := >=0.2.0, <0.3.0
0.0.3 := >=0.0.3, <0.0.4
0.0 := >=0.0.0, <0.1.0
0 := >=0.0.0, <1.0.0
There are three sources to fetch the package's
- git
- path
- crates.io (or private registry)
Examples:
From private registry:
[dependencies]
some-crate = { version = "1.0", registry = "my-registry" }
From git:
[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git" }
From path:
[dependencies]
hello_utils = { path = "hello_utils" }
Workspace
I really like the way of Rust Cargo
help us to maintain a large codebase that may consist of multiple packages/crates, it’s called as Workspace
.
A workspace is a collection of one or more packages, called workspace members, that are managed together.
Source: https://doc.rust-lang.org/cargo/reference/workspaces.html
I’ve found some open-source projects that already used this method:
- Solana Blockchain: https://github.com/solana-labs/solana/blob/master/Cargo.toml
- NEAR Blockchain: https://github.com/near/nearcore/blob/master/Cargo.toml
- Polkadot SDK: https://github.com/paritytech/polkadot-sdk/blob/master/Cargo.toml
- Tokio: https://github.com/tokio-rs/tokio/blob/master/Cargo.toml
I’ve just thought that the way Rust maintains all of this large codebase is really elegant. We can maintain multiple members to include in workspace projects or exclude from it too. Example:
[workspace]
members = ["member1", "path/to/member2", "crates/*"]
exclude = ["crates/foo", "path/to/other"]
From this example, if we are using workspace, it’s possible to declare dependency from path/to/member2
that depends on member1
, which means, each internal package inside a workspace can refer to each other. Example
# [PROJECT_DIR]/bar/Cargo.toml
[package]
name = "bar"
version = "0.2.0"
[dependencies]
regex = { workspace = true, features = ["unicode"] }
[build-dependencies]
cc.workspace = true
[dev-dependencies]
rand.workspace = true
Feature Flag
Besides of workspace
, Rust Cargo
also provide a feature called conditional compilation or optional dependencies, or I call it a feature-flag.
Cargo “features” provide a mechanism to express conditional compilation and optional dependencies. A package defines a set of named features in the
[features]
table ofCargo.toml
, and each feature can either be enabled or disabled
Source: https://doc.rust-lang.org/cargo/reference/features.html
I’ve already tried this feature and I really like it. We can create a conditional block code that will be enabled as a “feature” if we activate it when defining it as a dependency, example:
In Cargo.toml
:
[features]
# Defines a feature named `webp` that does not enable any other features.
webp = []
In code:
// This conditionally includes a module which implements WEBP support.
#[cfg(feature = "webp")]
pub mod webp;
Each “flag” will able to depend on others too, for example:
[features]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
Optional dependencies example:
[dependencies]
gif = { version = "0.11.1", optional = true }
[features]
gif = ["dep:gif"]
This means that this dependency will only be included if the gif
the feature is enabled. Simple solution but really powerful. If we are building a large codebase or project or big application, this feature will help us to reduce our compilation binary size, since we’re able to pick the right dependency based on our specific needs.
Outro
From all the defined features I’ve written in this story, there are two features that I really like from Rust Cargo
Workspace
Feature-flag
There are still a lot of configurations and features available from it, but I’m not trying them yet, like custom build script, and many other things.
For me, the cargo
is not just a package manager that only sets up library dependencies, but more than that.
- It really helps us to maintain a large codebase (using
workspace
) - It helps us to maintain conditional dependencies (though it’s
feature-flag
) - Provides a clean and simple configuration using the format
TOML
without the need to learn new custom DSL or any magical scripts (likemakefile
) - It already gives us a standard convention to manage our codebase layout, no more opinionated layout.
- And many other things
I hope this story can help some of you that may decide to learn and use Rust as part of your technical stacks.