A workspace is a set of packages that share the same Cargo.lock and output directory. Let’s make a project using a workspace—we’ll use trivial code so we can concentrate on the structure of the workspace. There are multiple ways to structure a workspace, so we'll just show one common way. We’ll have a workspace containing a binary and two libraries. The binary, which will provide the main functionality, will depend on the two libraries. One library will provide an function, and a second library 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:

    Next, in the add directory, we create the Cargo.toml file that will configure the entire workspace. This file won’t have a [package] section. Instead, it will start with a [workspace] section that will allow us to add members to the workspace by specifying the path to the package with our binary crate; in this case, that path is adder:

    Filename: Cargo.toml

    1. [workspace]
    2. members = [
    3. "adder",
    4. ]

    Next, we’ll create the adder binary crate by running cargo new within the add directory:

    1. $ cargo new adder
    2. Created binary (application) `adder` package

    At this point, we can build the workspace by running cargo build. The files in your add directory should look like this:

    1. ├── Cargo.lock
    2. ├── Cargo.toml
    3. ├── adder
    4. ├── Cargo.toml
    5. └── src
    6. └── main.rs
    7. └── target

    The workspace has one target directory at the top level that the compiled 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 adder directory, the compiled artifacts would still end up in add/target rather than add/adder/target. Cargo structures the target directory in a workspace like this because the crates in a workspace are meant to depend on each other. If each crate had its own target directory, each crate would have to recompile each of the other crates in the workspace to place the artifacts in its own target directory. By sharing one target directory, the crates can avoid unnecessary rebuilding.

    Creating the Second Package in the Workspace

    Next, let’s create another member package in the workspace and call it add_one. Change the top-level Cargo.toml to specify the add_one path in the members list:

    Filename: Cargo.toml

    1. [workspace]
    2. members = [
    3. "adder",
    4. "add_one",
    5. ]

    Then generate a new library crate named add_one:

    1. $ cargo new add_one --lib
    2. Created library `add_one` package

    Your add directory should now have these directories and files:

    Filename: add_one/src/lib.rs

    1. pub fn add_one(x: i32) -> i32 {
    2. x + 1
    3. }

    Now we can have the adder package with our binary depend on the add_one package that has our library. First, we’ll need to add a path dependency on add_one to adder/Cargo.toml.

    Filename: adder/Cargo.toml

    1. [dependencies]
    2. add_one = { path = "../add_one" }

    Cargo doesn’t assume that crates in a workspace will depend on each other, so we need to be explicit about the dependency relationships.

    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 a use 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

    1. use add_one;
    2. fn main() {
    3. let num = 10;
    4. println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
    5. }

    Listing 14-7: Using the add_one library crate from the adder crate

    Let’s build the workspace by running cargo build in the top-level add directory!

    1. $ cargo build
    2. Compiling add_one v0.1.0 (file:///projects/add/add_one)
    3. Finished dev [unoptimized + debuginfo] target(s) in 0.68s

    To run the binary crate from the add directory, we can specify which package in the workspace we want to run by using the -p argument and the package name with cargo run:

    1. $ cargo run -p adder
    2. Finished dev [unoptimized + debuginfo] target(s) in 0.0s
    3. Running `target/debug/adder`
    4. 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 Package in a Workspace

    We can now add use 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. We will get one warning because we aren’t referring to the rand we brought into scope:

    1. $ cargo build
    2. Updating crates.io index
    3. Downloaded rand v0.8.3
    4. --snip--
    5. Compiling rand v0.8.3
    6. Compiling add_one v0.1.0 (file:///projects/add/add_one)
    7. warning: unused import: `rand`
    8. --> add_one/src/lib.rs:1:5
    9. |
    10. 1 | use rand;
    11. | ^^^^
    12. |
    13. = note: `#[warn(unused_imports)]` on by default
    14. warning: 1 warning emitted
    15. Compiling adder v0.1.0 (file:///projects/add/adder)
    16. Finished dev [unoptimized + debuginfo] target(s) in 10.18s

    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 use rand; to the adder/src/main.rs file for the adder package, we’ll get an error:

    1. $ cargo build
    2. --snip--
    3. Compiling adder v0.1.0 (file:///projects/add/adder)
    4. error[E0432]: unresolved import `rand`
    5. --> adder/src/main.rs:2:5
    6. |
    7. 2 | use rand;
    8. | ^^^^ no external crate `rand`

    To fix this, edit the Cargo.toml file for the adder package and indicate that rand is a dependency for it as well. Building the adder package 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 every crate in every package in the workspace using the rand package will be using the same version, saving us space and ensuring 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

    1. pub fn add_one(x: i32) -> i32 {
    2. }
    3. #[cfg(test)]
    4. mod tests {
    5. use super::*;
    6. fn it_works() {
    7. assert_eq!(3, add_one(2));
    8. }
    9. }

    Now run cargo test in the top-level add directory. Running cargo test in a workspace structured like this one will run the tests for all the crates in the workspace:

    1. $ cargo test
    2. Compiling add_one v0.1.0 (file:///projects/add/add_one)
    3. Compiling adder v0.1.0 (file:///projects/add/adder)
    4. Finished test [unoptimized + debuginfo] target(s) in 0.27s
    5. Running target/debug/deps/add_one-f0253159197f7841
    6. running 1 test
    7. test tests::it_works ... ok
    8. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    9. Running target/debug/deps/adder-49979ff40686fa8e
    10. running 0 tests
    11. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    12. Doc-tests add_one
    13. running 0 tests
    14. test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    The first section of the output shows that the it_works test in the add_one crate passed. The next section shows that zero tests were found in the adder crate, and then the last section shows zero documentation tests were found in the add_one crate.

    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:

    1. $ cargo test -p add_one
    2. Finished test [unoptimized + debuginfo] target(s) in 0.00s
    3. Running target/debug/deps/add_one-b3235fea9a156f74
    4. running 1 test
    5. test tests::it_works ... ok
    6. test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    7. Doc-tests add_one
    8. running 0 tests

    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 , each crate in the workspace will need to be published separately. Like cargo test, we can publish a particular crate in our 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’s easier to understand smaller, individual components than one big blob of code. Furthermore, keeping the crates in a workspace can make coordination between crates easier if they are often changed at the same time.