How I Use Cargo Workspace to structure my Rust projects
Table of Contents
Iβve learn to use Cargo Workspace from Oliviaβs post in bluesky, which you should checkout. This article is a way of self-documenting since Iβve come back to it more often.
πWhat is a Cargo Workspace
A cargo workspace is collection of one or more library or binary crates. Cargo workspace letβs you break your rust project into multiple different crates (or packages), which may aid you in better project structure and management.
πWhy use Cargo Workspace
- Your rust project has grown big and is hard to manage
- Multiple people work on a project, so breaking your project into multiple crates can help with better ownership and less conflict among developers
- You want to reduce your projectβs compile time
Read this (external link): Cutting Down Rust Compile Times From 30 to 2 Minutes With One Thousand Crates
πCreating a Workspace
Create a new workspace directory
mkdir runner
cd runner
Now create Cargo.toml
with following content
[workspace]
resolver = "3"
Here we are using resolver
version 3, which is the latest version and comes in Rust 2024 edition (Rust 1.84+). Resolver tells cargo how to resolve the dependencies.
πCreate a binary crate
We are going to use cargo new
command to create a binary crate. When no argument is passed, cargo new command will create binary crate by default, you can use --lib
as argument to create library crate or --bin
to explicit that you want binary crate.
cargo new run
Now your workspace will look like this:
.
βββ Cargo.toml
βββ run
βββ Cargo.toml
βββ src
βββ main.rs
If you look at your workspaceβs Cargo.toml
you will notice that cargo has already added the newly added crate run
against the members
key.
[workspace]
resolver = "3"
members = ["run"]
πAdding lib crates
Letβs add two lib crates
cargo new participant --lib
cargo new track --lib
Updated structure:
.
βββ Cargo.lock
βββ Cargo.toml
βββ participant
βΒ Β βββ Cargo.toml
βΒ Β βββ src
βΒ Β βββ lib.rs
βββ run
βΒ Β βββ Cargo.toml
βΒ Β βββ src
βΒ Β βββ main.rs
βββ track
βββ Cargo.toml
βββ src
βββ lib.rs
πAdding dependencies
If you try to simply add a dependency like this
cargo add uuid --features v4
βΌοΈ it will fail and you will see this error
error: `cargo add` could not determine which package to modify. Use the `--package` option to specify a package.
available packages: participant, run, track
Since inside our Cargo workspace we have 3 packages, therefore cargo is unable to determine to which package dependency belongs to. β
We can solve this by using --package
arg.
cargo add uuid --features v4 --package participant
πDependency Resolution
Since in cargo workspace we can have multiple crates, which means we have to add dependency to each crate separately and in that sometimes there may be a version mismatch.
Like with uuid
in participant
crate, it is using version 1.18.1
and now you if you try to add uuid
of different version in run
crate, for example if we want to stick to only 1.17
version of the uuid
crate:
cargo add uuid@=1.17 --features v4 --package run
But wait, we got error
error: failed to select a version for `uuid`.
... required by package `run v0.1.0 (/home/vivek/Documents/code/hands-on/runner/run)`
versions that meet the requirements `=1.17` are: 1.17.0
all possible versions conflict with previously selected packages.
previously selected package `uuid v1.18.1`
... which satisfies dependency `uuid = "^1.18.1"` of package `participant v0.1.0 (/home/vivek/Documents/code/hands-on/runner/participant)`
failed to select a version for `uuid` which could resolve this conflict
Itβs because a Cargo workspace can only have one Cargo.lock
file, which is shared between all the members of the workspace, so it can only have one version of the dependency across the workspace. So instead of pinning to =1.17
version of uuid, we had used just 1.17
, cargo would have choosen [email protected]
since that was already being used by participant
crate.
In order to resolve the dependency across workspace, cargo will try to find the most suitable version of a crate, mostly by downgrading to the appropriate version.
πBetter way to declare dependency
Itβs very normal to have common dependencies across crates, that is why Cargo Workspace let us declare dependencies in workspaceβs Cargo.toml
and then you can re-use the same dependency inside of your crate.
# Cargo.toml
[workspace.dependencies]
uuid = { version = "=1.17", features = ["v4"] }
And then in your crates you can use the dependency with:
# participant/Cargo.toml
[dependencies]
uuid = { workspace = true }
# run/Cargo.toml
[dependencies]
uuid = { workspace = true }
πImporting members as dependencies
A common way to import a crate (or package) inside another crate is like this:
# run/Cargo.toml
[dependencies]
participant = { path = "../participant" }
track = { path = "../track" }
# track/Cargo.toml
[dependencies]
participant = { path = "../participant" }
But now if you had to do this for each of the crate in your project and sometime there can be a lot of crates. Also the path is relative to the current Cargo.toml file, so if you dare re-structure your workspace, it can backfire and you will have another mess to deal with.
However we can use the same way of delcaring the dependency in the workspaceβs Cargo.toml
and then just use it inside the members by telling cargo to use workspaceβs dependency.
# Cargo.toml
[workspace]
resolver = "3"
members = ["participant", "run", "track"]
[workspace.dependencies]
participant = { path = "participant" }
track = { path = "track" }
# run/Cargo.toml
[dependencies]
participant = { workspace = true }
track = { workspace = true }
# track/Cargo.toml
[dependencies]
participant = { workspace = true }
πMultiple Binaries
When you have multiple binaries in your workspace, cargo run
wonβt work since it doesnβt know which binary crate to run. So instead explicitly pass the crate name:
cargo run --package my_bin_crate
πCheat sheet
Create workspace:
# Cargo.toml
[workspace]
resolver = "3"
Adding bin and lib to workspace:
# creates binary crate
cargo new my_bin
# crates lib crate
cargo new my_lib --lib
Importing dependencies:
# Add dependencies in workspace's Cargo.toml
[workspace.dependencies]
uuid = { version = "=1.17", features = ["v4"] }
# import in members like this: my_lib/Cargo.toml
[dependencies]
uuid = { workspace = true }
Import members like this:
# Cargo.toml
[workspace]
resolver = "3"
members = ["my_bin", "my_lib"]
[workspace.dependencies]
my_lib = { path = "my_lib" }
# my_bin/Cargo.toml
[dependencies]
my_lib = { workspace = true }
Other commands:
# run if multiple binaries exist
cargo run --package my_bin
# build specific binary
cargo build --package my_bin
# build all
cargo build --all
# test all
cargo test --all