Rust Cargo & Why I Like It

Hiraq Citra M
lifefunk
6 min readFeb 19, 2024

--

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

Source: https://blog.logrocket.com/demystifying-cargo-in-rust/

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 and Cargo.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>"]

The only fields required by Cargo are name and version

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:

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 of Cargo.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 (like makefile )
  • 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.

--

--