Workspaces and multiple crates

A project that grows past one crate usually splits into a library and one or more binaries: the library holds the logic, the binaries are thin entry points, and a workspace root builds them together. The members keep their own manifests, and cargo shares one lock file and one target folder between them.

This lesson builds a workspace of two members, adds a path dependency, runs the binary, and shows the error for a function the library did not make public and for a dependency path that points at nothing.

The workspace root

The root manifest has no package of its own: it lists the members by folder and the resolver that decides how features of shared dependencies are unified. Each member is an ordinary package with its own Cargo.toml.

The library has two public functions. Both are pub because a member sees only what the library exports, which the fourth section turns into an error.

[workspace]
members = ["mathlib", "app"]
resolver = "3"

pub fn double(value: i32) -> i32 { value * 2 } pub fn triple(value: i32) -> i32 { value * 3 }

cargo new --vcs none --lib mathlib
cargo new --vcs none app
vi Cargo.toml
cargo build --workspace

   Compiling mathlib v0.1.0 (/tmp/rw7/L082/mathlib)
   Compiling app v0.1.0 (/tmp/rw7/L082/app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.08s

Both members compiled in one command, and neither had a dependency between them yet. The resolver field is the one for edition 2024; an older workspace writes resolver = "2", and the lesson on editions explains what changes.

A path dependency

The binary declares the library as a dependency with a relative path instead of a version. Cargo reads the manifest in that folder, builds the library first and links it by its crate name, which is the name in its own [package] table.

The program calls both functions through the crate name and prints the two results, which are the same number twice by design.

[dependencies]
mathlib = { path = "../mathlib" }

fn main() { println!("{} {}", mathlib::double(21), mathlib::triple(14)); }

cargo run -p app

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/app`
42 42

-p names the package, which matters as soon as a workspace holds two binaries. A path dependency is resolved at build time and not published: a library that ends up on crates.io needs a version requirement for its dependencies, and a path for the workspace it is developed in.

One lock file and one target

The workspace resolves every member at once, so it writes a single Cargo.lock at the root and a single target folder. A dependency used by two members is compiled once and shared, and the two members cannot end up with different versions of the same crate.

cargo build --workspace
cat Cargo.lock
ls target/debug

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "app"
version = "0.1.0"
dependencies = [
 "mathlib",
]

[[package]]
name = "mathlib"
version = "0.1.0"
app
app.d
build
deps
examples
incremental
libmathlib.d
libmathlib.rlib

The lock file lists both packages and the dependency edge between them, and the target folder holds the binary and the library in the same debug directory. Deleting the root target deletes the artefacts of both members, which is the fastest way to a clean build.

What the binary can see

A member is a separate crate, so privacy applies at its boundary: a function without pub exists inside the library and nowhere else. The compiler reports E0603 and points at both the call and the definition.

The library now has a private triple, and the binary still calls it. The build stops in the binary.

fn main() { println!("{} {}", mathlib::double(21), mathlib::triple(14)); }

cargo build -p app

   Compiling mathlib v0.1.0 (/tmp/rw7/L082/mathlib)
   Compiling app v0.1.0 (/tmp/rw7/L082/app)
error[E0603]: function `triple` is private
--> app/src/main.rs:2:53
|
2 | println!("{} {}", mathlib::double(21), mathlib::triple(14));
| ^^^^^^ private function
|
note: the function `triple` is defined here
--> mathlib/src/lib.rs:5:1
|
5 | fn triple(value: i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: could not compile `app` (bin "app") due to 1 previous error

The note with the definition is the part that makes the error readable across two crates, and the path in it is relative to the workspace root. The lesson on library crates is about the API a library chooses to expose.

A path that does not resolve

A path dependency is read before anything is compiled, so a wrong folder stops the build with the chain of causes that led to it: the workspace member, the dependency, the missing manifest. The path is relative to the manifest that writes it, not to the folder the command runs in.

The dependency line names ../mathlibb, a folder that does not exist. The output names each step of the resolution.

[dependencies]
mathlib = { path = "../mathlibb" }

cargo build -p app

error: failed to load manifest for workspace member `/tmp/rw7/L082/app`
referenced by workspace at `/tmp/rw7/L082/Cargo.toml`

Caused by:
  failed to load manifest for dependency `mathlib`

Caused by:
  failed to read `/tmp/rw7/L082/mathlibb/Cargo.toml`

Caused by:
  No such file or directory (os error 2)

The first cause is the general one and the last is the exact one, which is why the whole chain is worth reading. Correcting the folder name is enough; the program then builds and prints the two numbers.

Exercises

  1. Add a second binary member to the workspace and run each one with -p.
  2. Move a function from the binary into the library and add the pub it needs.
  3. Delete the root target folder and compare the next cargo build --workspace with an incremental one.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a workspace root manifest hold?

    Answer

    The member list and the resolver

    The root has no package of its own, only the members and the settings they share.

  2. How many lock files does a workspace write?

    Answer

    One, at the root

    The workspace resolves all members together, so one file records the result.

  3. What is a path dependency resolved against?

    Answer

    The folder of the manifest that writes it

    The relative path is read from the manifest, not from the shell.

  4. Why can the binary not call a private function of the library?

    Answer

    They are separate crates, so privacy applies at the boundary

    Only the public items of a crate are visible to the crates that use it.

  5. What does this program print? fn double(value: i32) -> i32 { value * 2 } fn triple(value: i32) -> i32 { value * 3 } fn main() { println!("{} {}", double(21), triple(14)); }

    Answer

    42 42

    Both calls return forty-two, and privacy does not apply inside one crate.

  6. What does this program print? mod mathlib { pub fn double(value: i32) -> i32 { value * 2 } } fn main() { println!("{}", mathlib::double(4)); }

    Answer

    8

    The function is public inside the module, and main is in the parent module.

  7. What does this program print? fn main() { let values = [3, 1, 2]; let mut sorted = values; sorted.sort(); println!("{:?} {:?}", values, sorted); }

    Answer

    [3, 1, 2] [1, 2, 3]

    An array of integers is Copy, so the assignment duplicated it and only the copy was sorted.

  8. What does this program print? fn main() { let name = "app"; let version = "0.1.0"; println!("{}-{}", name, version); }

    Answer

    app-0.1.0

    The literal dash sits between the two placeholders.

  9. What is the first line of the compiler output for this program? mod mathlib { fn triple(value: i32) -> i32 { value * 3 } } fn main() { println!("{}", mathlib::triple(14)); }

    Answer

    error[E0603]: function triple is private

    The function exists but is not public, so the parent module cannot call it.

  10. What is the first line of the compiler output for this program? fn main() { println!("{}", mathlib::double(21)); }

    Answer

    error[E0433]: cannot find module or crate mathlib in this scope

    No dependency and no module of that name exists in this crate.

The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev