RustBrock/Cargo Workspaces.md
darkicewolf50 629cbea79d
Some checks failed
Test Gitea Actions / first (push) Successful in 15s
Test Gitea Actions / check-code (push) Failing after 13s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
finished ch14.4
2025-02-26 16:41:39 -07:00

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.