Cargo Workspaces - The Rust Programming Language

## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

## 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 your package further into multiple library crates. Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.

### Creating a Workspace

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 that 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 `add_one` function and the other 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:

    $ mkdir add
    $ cd add

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. We also make a point to use the latest and greatest version of Cargo’s resolver algorithm in our workspace by setting the `resolver` value to `"3"`:

Filename: Cargo.toml

    [workspace]
    resolver = "3"

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

    $ cargo new adder
         Created binary (application) `adder` package
          Adding `adder` as member of workspace at `file:///projects/add`

Running `cargo new` inside a workspace also automatically adds the newly created package to the `members` key in the `[workspace]` definition in the workspace Cargo.toml, like this:

    [workspace]
    resolver = "3"
    members = ["adder"]

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 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`. Generate a new library crate named `add_one`:

    $ cargo new add_one --lib
         Created library `add_one` package
          Adding `add_one` as member of workspace at `file:///projects/add`

The top-level Cargo.toml will now include the add_one path in the `members` list:

Filename: Cargo.toml

    [workspace]
    resolver = "3"
    members = ["adder", "add_one"]

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

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

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

    [dependencies]
    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 change the `main` function to call the `add_one` function, as in Listing 14-7.

Filename: adder/src/main.rs

    fn main() {
        let num = 10;
        println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
    }

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!

    $ 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

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`:

    $ 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 code in adder/src/main.rs, which depends on the `add_one` crate.

### Depending on an External Package

Notice that the workspace has only one Cargo.lock file at the top level, 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` package 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 will always be compatible with each other. Let’s add the `rand` crate to the `[dependencies]` section in the add_one/Cargo.toml file so that we can use the `rand` crate in the `add_one` crate:

Filename: add_one/Cargo.toml

    [dependencies]
    rand = "0.8.5"

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:

    $ 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

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:

    $ 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`

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 will ensure that every crate in every package in the workspace using the `rand` package will use the same version as long as they specify compatible versions of `rand`, saving us space and ensuring that the crates in the workspace will be compatible with each other.

If crates in the workspace specify incompatible versions of the same dependency, Cargo will resolve each of them but will still try to resolve as few versions as possible.

### 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

    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. Running `cargo test` in a workspace structured like this one will run the tests for all the crates in the workspace:

    $ 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-93c49ee75dc46543)

    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-3a47283c568d2b6a)

    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 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 that 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:

    $ 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-93c49ee75dc46543)

    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 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 crates.io, 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.

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 enables you to work with smaller, easier-to-understand 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.