11 KiB
Cargo Workspaces
As our project developsm you might find that the library crate continues to get bigger and you want to split your package further into multiple library crates.
Cargo offers a feature called workspaces that can help manage related packages that are developed at the same time
Creating a Workspace
A workspace is a set of packages that share the same Cargo.lock and output directory.
Lets explore this by a making a project using a workspace
Here we will use trivial code so that the focus is on the structure of the workspace.
There are mulitple ways to structure a workspace, here we will just show one common way.
Here we will have a workspace containing a binary and two libraries.
The binary will provide the main functionality, will depend on the two libraries.
One lib will provide an add_one
function
Another lib will provide an add_two
function
These three crates will be part of the same workspace.
We will start by creating a new directory for the workspace
$ mkdir add
$ cd add
Now in the add directory, we create the Cargo.toml file that will config the entire workspace.
This file wont have a [package]
section, instead it will start with a [workspace]
section.
This allows us to add members to the workspace.
We also make it a point to use the latest and greatest version of Cargo's resolver algorithm in our workspace by setting the resolver
to "2"
Here is the written out version of the toml file until this point
[workspace]
resolver = "2"
Next we will create the adder
binary crate
We do this by running cargo new
within the add directory
$ cargo new adder
Creating binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
By running the cargo new
command inside a workspace it automatically adds the newly created package to the members
key in the [workspace]
definition in the workspace Cargo.toml
It will look like this
[workspace]
resolver = "2"
members = ["adder"]
Now at this point we can build the workspace by rnning cargo build
The files in the add directory should now look like this
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
The workspace has one target directory at the top level that compled artifacts will be placed into
The adder
package doesn't have its own target directory.
Even if we were to run cargo build
from inside the addre dir, the compiled artifacts would still end up in add/target rather than add/adder/target
Cargo structs the target directory in a workspace like this because the crates in a workspace are meant to depend on each other.
Without this we would have to repeat compiled artifacts and this would cause unnecessary rebuilding.
So we share one target directory to avoid this.
Creating the Second Package in the Wrokspace
Now lets add another package to the workspace, we will call it
First we will change the top level Cargo.toml to specify the add_one path in the members
list
Here is the updated toml
[workspace]
resolver = "2"
members = ["adder", "add_one"]
Now lets generate a new library crate named add_one
$ cargo new add_one --lib
Creating library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
Now the add directory should look like this
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
In add_one/src/lib.rs lets add the add_one
function
pub fn add_one(x: i32) -> i32 {
x + 1
}
Now we need to have the adder
package with our binary depend on the add_one
package that has our library.
We need to add a path dependency on add_one
to adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo doesnt assume that crates in a workspace will depend on each other.
We must explicit about the dependency relationships.
Now lets use the add_one
function in the adder
crate.
You modify the adder/src/main.rs file and change the main
function to call the add_one
function
Here is the updated main
function in the binary crate
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Now we can build the workspace from the top-level 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` profile [unoptimized + debuginfo] target(s) in 0.22s
In order to run the binary crate from the add directory have have to specify which package in the workspace we want to run.
We do this by using the -p
argument and the package name with cargo run
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
This runs the main
function in adder/src/main.rs, which depends on the add_one
crate
Depending on an External Package in a Wrokspace
Notice that the workspace only has one Cargo.lock file at the top level.
This ensure that all crates are using the same version of all dependencies.
If we add the rand
package to the adder/Cargo.toml and add_one/Cargo.toml files
Cargo will resolve both of thos to one version of rand
and record that in the one Cargo.lock.
This ensures that all the crates in the workspace use the same dependencies, this also ensures that all crates will always be compatible with each other.
Lets demonstrate how to add the rand
crate to the [dependencies]
section in the add_one/Cargo.toml file so we can use the rand
crate in the add_one
crate
[dependencies]
rand = "0.8.5"
Now we can add use rand;
to the add_one/src/lib.rs file.
Then build the whole workspace in the add directory will bring in and compile the rand
crate.
We will get a warning about a unused rand
becasue we dont refer to it after we bring it into scope
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
Now the top level Cargo.lock now contains info 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 thier Cargo.toml files as well
For example if we add use rand;
to the adder/src/main.rs file for the adder
package we would get an error
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
In order to fix this we need to edit the Cargo.toml file for the adder
package and indicate that rand
is a dependency for it as well.
Rebuilding the adder
package with the eidt will now add the rand
crate to the list of dependencies for adder
in Cargo.lock.
No additional copies of rand
will be downloaded.
Cargo will ensure that every crate in every package in the workspace using the rand
package will be using the same version as log as they specify compatible versions of rand
.
This saves us space and ensures that the crates in the workspace will be compatible with each other.
If crates in the workspace specify incompatible verions of the same dependency, Cargo will attempt to resolve each of them but it will still try to resolve it with as few versions as possible
Adding a Test to a Workspace
For another enchancement lets add a test of the add)one::add_one
function within the add_one
crate
Here is the updated add_one/src/lib.rs
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 if we run cargo test
in the top level add directory in a workspace structured like thisone will run the tests for all the crates in the workspace.
Here is what the output will look like this
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (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; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The first section shows that the it_works
test in the add_one
crate pased
The next section shows that zero tests were found in the adder
crate.
Thhe last section shows zero documentation tests were found in the add_one
crate.
We can also run tests for a particular crate in a workspace from the top-level directory.
We do this by adding the -p
flag and specifying th name of the crate we want to test.
Here is an example of this
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (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; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
This shows that cargo test
only ran tests for the add_one
crate and didn't run the adder
crate tests.
If you publish the crates in the workspace to crates.io, each crate in the workspace will need to be published separately.
Just like cargo test
, you can publish a particular crate in the workspace by using the -p
flag and specifying the name of the crate we want to publish.
As your project grows, consider using a workspace.
It makes it easier to understand smaller, individual components than one big blog of code.
Also by keeping the crates in a workspace can make coordination between crates easier if they are often changed at the same time.