Cargo Workspaces
In Chapter 12, we built a package that included a binary crate and a library crate. As your project develops, you might find that the library crate continues to get bigger and you want to split up your package further into multiple library crates. In this situation, Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.
A workspace is a set of packages that share the same Cargo.lock and output
directory. Let’s make a project using a workspace and use trivial code so we
can concentrate on the structure of the workspace. There are multiple ways to
structure a workspace; we’re going to show a common way. We’ll have a workspace
containing a binary and two libraries. The binary will provide the main
functionality to be used as a command line tool, and it will depend on the two
libraries. One library will provide an add_one
function, and a second library
will provide an add_two
function. These three crates will be part of the same
workspace. We’ll start by creating a new directory for the workspace:
$ mkdir add
$ cd add
In the add directory, create a Cargo.toml file. This is the Cargo.toml
file that configures the entire workspace. It won’t have a [package]
section
or metadata we’ve seen in other Cargo.toml files. Instead, we’ll start with a
[workspace]
section and add a member to the workspace by specifying the path
adder, which is where we’ll put our binary crate:
Filename: Cargo.toml
[workspace]
members = [
"adder",
]
Next, we’ll create the adder
binary crate by running cargo new
within the
add directory:
$ cargo new --bin adder
Created binary (application) `adder` project
At this point, we can build the workspace by running cargo build
. The files
in your add directory should look like this:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
The workspace has one target directory at the top level; the adder
crate
doesn’t have its own target directory. Even if we go into the adder
directory and run cargo build
, the compiled artifacts end up in
add/target rather than add/adder/target. The crates in a workspace are
meant to depend on each other. If each crate had its own target directory,
each crate in the workspace would have to recompile each of the other crates in
the workspace to have the artifacts in its own target directory. By sharing
one target directory, the crates in the workspace can avoid rebuilding the
other crates in the workspace more than necessary.
Creating the Second Crate in the Workspace
Next, let’s specify another member crate in the workspace. This crate will be in the add-one directory, so change the top-level Cargo.toml to have the add-one path as well:
Filename: Cargo.toml
[workspace]
members = [
"adder",
"add-one",
]
Then generate a new library crate named add-one
:
$ cargo new add-one
Created library `add-one` project
Your add directory should now have these directories and files:
├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
In the add-one/src/lib.rs file, let’s add an add_one
function:
Filename: add-one/src/lib.rs
# #![allow(unused_variables)] #fn main() { pub fn add_one(x: i32) -> i32 { x + 1 } #}
Now that we have a library crate in the workspace, let’s have the binary crate
adder
depend on the library crate add-one
. First, we’ll need to add a path
dependency on add-one
to adder/Cargo.toml:
Filename: adder/Cargo.toml
[dependencies]
add-one = { path = "../add-one" }
Crates in a workspace don’t have to depend on each other, so we still need to be explicit about the dependency relationships between the crates in a workspace.
Next, let’s use the add_one
function from the add-one
crate in the adder
crate. Open the adder/src/main.rs file and add an extern crate
line at
the top to bring the new add-one
library crate into scope. Then change the
main
function to call the add_one
function, as in Listing 14-7:
Filename: adder/src/main.rs
extern crate add_one;
fn main() {
let num = 10;
println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}
Let’s build the workspace by running cargo build
in the add directory!
$ cargo build
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68 secs
To run the binary crate from the top-level add directory, we need to specify
which package in the workspace we want to use by using the -p
argument and
the package name with cargo run
:
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
This runs the code in adder/src/main.rs, which depends on the add-one
crate.
Depending on an External Crate in a Workspace
Notice that the workspace has only one Cargo.lock file at the top level of
the workspace rather than having a Cargo.lock in each crate’s directory. This
ensures that all crates are using the same version of all dependencies. If we
add the rand
crate to the adder/Cargo.toml and add-one/Cargo.toml
files, Cargo will resolve both of those to one version of rand
and record
that in the one Cargo.lock. Making all crates in the workspace use the same
dependencies means the crates in the workspace will always be compatible with
each other. Let’s add the rand
crate to the [dependencies]
section in the
add-one/Cargo.toml file to be able to use the rand
crate in the add-one
crate:
Filename: add-one/Cargo.toml
[dependencies]
rand = "0.3.14"
We can now add extern crate rand;
to the add-one/src/lib.rs file, and
building the whole workspace by running cargo build
in the add directory
will bring in and compile the rand
crate:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
--snip--
Compiling rand v0.3.14
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs
The top-level Cargo.lock now contains information about the dependency of
add-one
on rand
. However, even though rand
is used somewhere in the
workspace, we can’t use it in other crates in the workspace unless we add
rand
to their Cargo.toml files as well. For example, if we add extern crate rand;
to the adder/src/main.rs file for the adder
crate, we’ll get
an error:
$ cargo build
Compiling adder v0.1.0 (file:///projects/add/adder)
error: use of unstable library feature 'rand': use `rand` from crates.io (see
issue #27703)
--> adder/src/main.rs:1:1
|
1 | extern crate rand;
To fix this, edit the Cargo.toml file for the adder
crate and indicate that
rand
is a dependency for that crate as well. Building the adder
crate will
add rand
to the list of dependencies for adder
in Cargo.lock, but no
additional copies of rand
will be downloaded. Cargo has ensured that any
crate in the workspace using the rand
crate will be using the same version.
Using the same version of rand
across the workspace saves space because we
won’t have multiple copies and ensures that the crates in the workspace will be
compatible with each other.
Adding a Test to a Workspace
For another enhancement, let’s add a test of the add_one::add_one
function
within the add_one
crate:
Filename: add-one/src/lib.rs
# #![allow(unused_variables)] #fn main() { pub fn add_one(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(3, add_one(2)); } } #}
Now run cargo test
in the top-level add directory:
$ cargo test
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running target/debug/deps/add_one-f0253159197f7841
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/adder-f88af9d2cc175a5e
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
The first section of the output shows that the it_works
test in the add-one
crate passed. The next section shows that 0 tests were found in the adder
crate, and then the last section shows 0 documentation tests were found in the
add-one
crate. Running cargo test
in a workspace structured like this one
will run the tests for all the crates in the workspace.
We can also run tests for one particular crate in a workspace from the
top-level directory by using the -p
flag and specifying the name of the crate
we want to test:
$ cargo test -p add-one
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/add_one-b3235fea9a156f74
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
This output shows cargo test
only ran the tests for the add-one
crate and
didn’t run the adder
crate tests.
If you publish the crates in the workspace to https://crates.io/, each crate
in the workspace will need to be published separately. The cargo publish
command does not have an --all
flag or a -p
flag, so you must change to
each crate’s directory and run cargo publish
on each crate in the workspace
to publish them.
For additional practice, add an add-two
crate to this workspace in a similar
way as the add-one
crate!
As your project grows, consider using a workspace: it’s easier to understand smaller, individual components than one big blob of code. Keeping the crates in a workspace can make coordination between them easier if they are often changed at the same time.