Cargo Workspaces: Monorepos in Rust
21 min read
A Cargo workspace lets several related crates live in one repository, share a single Cargo.lock and target/ directory, and be built/tested together. It is Rust’s answer to the npm workspaces / Lerna / Nx / Turborepo monorepo you have probably wrestled with in the JavaScript world — but it is built into the toolchain, with no extra dependency.
Quick Overview
Section titled “Quick Overview”When a project outgrows a single crate — say a shared core library, a command-line interface, and a web server that all reuse the same domain types — you reach for a workspace. You declare it with a [workspace] table in a top-level Cargo.toml that lists its members. Every member is a normal crate with its own Cargo.toml, but they share one resolved dependency graph (Cargo.lock), one build cache (target/), and can depend on each other by path. For a TypeScript/JavaScript developer this is exactly the monorepo pattern ("workspaces": ["packages/*"] in a root package.json), and the payoff is the same: atomic cross-package changes, one install/build, and no version-skew between internal packages.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”A typical npm-workspaces monorepo for a task tracker has a private root package.json that lists the packages, plus one package.json per package. Local packages depend on each other by name:
// package.json (the repo root — private, not published){ "name": "taskhub", "private": true, "workspaces": ["packages/*"]}// packages/core/package.json — the shared library{ "name": "@taskhub/core", "version": "0.3.0", "main": "index.js"}// packages/cli/package.json — depends on the local @taskhub/core{ "name": "@taskhub/cli", "version": "0.3.0", "dependencies": { "@taskhub/core": "0.3.0" }}A single npm install at the root resolves everything into one root package-lock.json and symlinks the local packages into the root node_modules/:
$ npm install$ find . -name package-lock.json./package-lock.json # one lockfile at the root$ ls -la node_modules/@taskhubcore -> ../../packages/core # local packages symlinked, not copiedcli -> ../../packages/cliThat single shared lockfile and the symlinked local packages are the two defining traits of a JavaScript monorepo. Cargo workspaces give you both — by design, not by convention.
Rust Equivalent
Section titled “Rust Equivalent”The same project as a Cargo workspace. The repo root holds a Cargo.toml with no [package] — only a [workspace] table (this is called a virtual manifest). Each member crate lives in its own directory with its own manifest.
The layout:
taskhub/├── Cargo.toml # the workspace root (virtual manifest — no [package])├── Cargo.lock # ONE shared lockfile for the whole workspace├── target/ # ONE shared build cache for the whole workspace└── crates/ ├── core/ # taskhub-core: the shared library crate │ ├── Cargo.toml │ └── src/lib.rs └── cli/ # taskhub-cli: a binary that depends on core ├── Cargo.toml └── src/main.rsThe root manifest:
# taskhub/Cargo.toml — the workspace root[workspace]resolver = "3"members = ["crates/core", "crates/cli"]
# Fields every member can inherit, declared once.[workspace.package]version = "0.3.0"edition = "2024"license = "MIT OR Apache-2.0"authors = ["Ada Zeybek <me@zeybek.dev>"]
# Dependency versions declared once, used by every member.[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"anyhow = "1"taskhub-core = { path = "crates/core" }The shared library member:
[package]name = "taskhub-core"version.workspace = true # inherit from [workspace.package]edition.workspace = truelicense.workspace = trueauthors.workspace = true
[dependencies]serde = { workspace = true } # inherit the version + features from the rootuse serde::{Deserialize, Serialize};
/// A single task tracked by the hub.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Task { pub id: u32, pub title: String, pub done: bool,}
impl Task { pub fn new(id: u32, title: impl Into<String>) -> Self { Task { id, title: title.into(), done: false } }}
/// Count how many tasks are still open.pub fn open_count(tasks: &[Task]) -> usize { tasks.iter().filter(|t| !t.done).count()}The binary member that depends on the library:
[package]name = "taskhub-cli"version.workspace = trueedition.workspace = truelicense.workspace = trueauthors.workspace = true
[dependencies]taskhub-core = { workspace = true } # the local path dep, declared at the rootserde_json = { workspace = true }anyhow = { workspace = true }use anyhow::{Context, Result};use taskhub_core::{open_count, Task};
fn main() -> Result<()> { let mut tasks = vec![ Task::new(1, "Write workspaces guide"), Task::new(2, "Verify every snippet"), ]; tasks[1].done = true;
let json = serde_json::to_string_pretty(&tasks) .context("failed to serialize tasks")?; println!("{json}"); println!("{} task(s) still open", open_count(&tasks)); Ok(())}Running the CLI member from the workspace root produces real output:
$ cargo run -p taskhub-cli --quiet[ { "id": 1, "title": "Write workspaces guide", "done": false }, { "id": 2, "title": "Verify every snippet", "done": true }]1 task(s) still openNote: The package is named
taskhub-core(with a hyphen) but the crate youusein code istaskhub_core(with an underscore). Cargo maps hyphens in package names to underscores in the language. This mirrors howserde_jsonthe crate corresponds to theserde_jsonimport name.
Detailed Explanation
Section titled “Detailed Explanation”The [workspace] table and members
Section titled “The [workspace] table and members”[workspace]resolver = "3"members = ["crates/core", "crates/cli"]membersis a list of directory paths (relative to the root). Each must contain a crate with its ownCargo.toml. Glob patterns work too:members = ["crates/*"]picks up every crate undercrates/.resolver = "3"opts into the version-3 feature resolver, which is the default for edition-2024 crates. In a workspace you set it once at the root because the resolver is workspace-wide; you cannot let individual members disagree. (Resolver 2 fixed long-standing issues where a build-time feature would leak into your runtime build; resolver 3 is its successor and the current default.)
A root Cargo.toml that contains only [workspace] (no [package]) is a virtual manifest. The root is not itself a crate — it is just the coordinator. You can also make the root both a package and a workspace by including a [package] table alongside [workspace] (a “root package”), but for monorepos the virtual-manifest style keeps responsibilities clean.
One Cargo.lock, one target/
Section titled “One Cargo.lock, one target/”This is the heart of why workspaces exist. Run any build command anywhere in the tree and Cargo produces exactly one lockfile and one build directory at the root:
$ cargo build --quiet$ find . -name Cargo.lock -not -path './target/*'./Cargo.lock # exactly one, at the workspace rootBecause every member resolves against the same Cargo.lock, two members can never accidentally pull in two different versions of serde. This is the version-skew problem that plagues JavaScript monorepos (where each package can hoist or nest a different copy in node_modules/); a Cargo workspace makes it structurally impossible. The shared target/ also means a dependency compiled for one member is reused by every other member — no rebuild per package.
Path dependencies between members
Section titled “Path dependencies between members”crates/cli depends on crates/core through a path dependency:
# declared once at the root:[workspace.dependencies]taskhub-core = { path = "crates/core" }# used by the cli member:[dependencies]taskhub-core = { workspace = true }A path dependency points at a sibling crate on disk rather than a registry. This is the direct analogue of a local package symlinked into node_modules/ by npm workspaces. Because it is a path (not a version range), edits to taskhub-core are picked up immediately by taskhub-cli — no publish, no npm link, no rebuild step you have to remember.
Workspace inheritance: [workspace.package] and [workspace.dependencies]
Section titled “Workspace inheritance: [workspace.package] and [workspace.dependencies]”Two tables let you declare shared settings once and inherit them with .workspace = true:
[workspace.package]holds package metadata (version,edition,license,authors,repository,rust-version, …) that members opt into withversion.workspace = true,edition.workspace = true, and so on. Bump the version once at the root and every member moves together.[workspace.dependencies]centralizes dependency versions and features. A member writesserde = { workspace = true }and inherits theversion = "1.0"andfeatures = ["derive"]from the root. A member can still add extra features on top:serde = { workspace = true, features = ["rc"] }.
This solves a real JavaScript-monorepo pain point: keeping the version of a third-party dependency identical across every package. In npm you reach for tooling (syncpack, Nx constraints) to enforce it; in Cargo it is a first-class manifest feature.
Workspace-wide lints
Section titled “Workspace-wide lints”You can also share lint configuration. Declare lints once at the root and have members opt in:
# root Cargo.toml[workspace.lints.clippy]unwrap_used = "warn"# each member Cargo.toml[lints]workspace = trueWith that in place, running Clippy on any member that calls .unwrap() surfaces the shared rule (real output, trimmed to the lint note):
warning: used `unwrap()` on an `Option` value = note: if this value is `None`, it will panic = help: consider using `expect()` to provide a better panic message = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_usedThis is the Rust equivalent of a single root .eslintrc that every package extends.
Key Differences
Section titled “Key Differences”| Concept | npm workspaces (JavaScript) | Cargo workspaces (Rust) |
|---|---|---|
| Declared in | "workspaces" array in root package.json | [workspace] table in root Cargo.toml |
| Root is a package? | Yes (usually "private": true) | Often a virtual manifest (no [package] at all) |
| Local package linkage | Symlink into node_modules/ | Path dependency ({ path = "..." }) |
| Lockfile | One root package-lock.json | One root Cargo.lock (enforced, not optional) |
| Build/output cache | Per-package, plus hoisting heuristics | One shared target/ for the whole workspace |
| Version skew of shared deps | Possible (different copies can be nested) | Impossible — single resolved graph |
| Shared dependency versions | External tooling (syncpack, Nx) | Built-in [workspace.dependencies] |
| Shared metadata | Manual or tooling | Built-in [workspace.package] inheritance |
| Run a task in one package | npm run build -w @taskhub/cli | cargo build -p taskhub-cli |
| Run a task everywhere | npm run build --workspaces | cargo build --workspace |
| Task orchestration | Turborepo / Nx / Lerna for caching + ordering | Cargo computes the build DAG itself; no extra tool |
The biggest conceptual shift: a JavaScript monorepo is an opt-in pattern layered on top of npm with help from extra tooling, whereas a Cargo workspace is a native concept the compiler and build system understand directly. There is no Rust equivalent of Turborepo because Cargo already knows the dependency graph between your crates and builds them in the right order with shared caching out of the box.
Tip: Selecting members on the command line uses
-p/--package(one crate) or--workspace/--all(every member). The flags mirror npm’s-w <name>and--workspaces.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Putting [profile.*] in a member crate
Section titled “Pitfall 1: Putting [profile.*] in a member crate”Profiles ([profile.release], etc.) are workspace-wide and may only live in the root manifest. Add one to a member and Cargo ignores it with a warning. Adding [profile.release] to crates/core/Cargo.toml and building produces this real warning:
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:package: /path/to/taskhub/crates/core/Cargo.tomlworkspace: /path/to/taskhub/Cargo.tomlFix: move every [profile.*] table to the root Cargo.toml. The same rule applies to the [patch] and [replace] tables — they are workspace-global.
Pitfall 2: Inheriting a dependency that the root never declared
Section titled “Pitfall 2: Inheriting a dependency that the root never declared”If a member writes regex = { workspace = true } but the root [workspace.dependencies] has no regex entry, the workspace fails to load. The real error:
error: failed to load manifest for workspace member `/path/to/taskhub/crates/cli`referenced by workspace at `/path/to/taskhub/Cargo.toml`
Caused by: failed to parse manifest at `/path/to/taskhub/crates/cli/Cargo.toml`
Caused by: error inheriting `regex` from workspace root manifest's `workspace.dependencies.regex`
Caused by: `dependency.regex` was not found in `workspace.dependencies`Fix: add regex = "1" to [workspace.dependencies] at the root first, then inherit it in the member with regex = { workspace = true }.
Pitfall 3: Listing a member that does not exist yet
Section titled “Pitfall 3: Listing a member that does not exist yet”The root manifest validates every listed member before any command runs. If members = ["crates/core", "crates/cli"] but crates/cli/ has no Cargo.toml yet, every cargo command in the workspace fails:
failed to load manifest for workspace member `/path/to/taskhub/crates/cli`referenced by workspace at `/path/to/taskhub/Cargo.toml`
Caused by: failed to read `/path/to/taskhub/crates/cli/Cargo.toml`
Caused by: No such file or directory (os error 2)Fix: create the member crate before adding it to members, or use a glob (members = ["crates/*"]) so only existing directories are picked up. When scaffolding with cargo new, create the crate first; Cargo writes the new package without a [workspace] table because it detects it sits inside an existing workspace.
Pitfall 4: cargo run in a workspace with several binaries
Section titled “Pitfall 4: cargo run in a workspace with several binaries”In a single-crate project cargo run “just works.” In a workspace with more than one runnable binary it cannot guess which one you mean:
$ cargo runerror: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.available binaries: taskhub-cli, taskhub-webFix: disambiguate with cargo run -p taskhub-cli, or set default-members = ["crates/cli"] in the root [workspace] so bare cargo run/cargo test target your usual crate.
Pitfall 5: Expecting cargo new mypkg to “add to the workspace” loudly
Section titled “Pitfall 5: Expecting cargo new mypkg to “add to the workspace” loudly”Coming from npm init -w packages/foo, you might expect explicit confirmation. cargo new crates/foo inside a workspace simply creates the crate and relies on your members list (or a glob) to include it — it does not print an “added to workspace” notice, and it deliberately omits a [workspace] table from the new child. If you used an explicit members = [...] list, remember to append the new path yourself.
Best Practices
Section titled “Best Practices”- Use a virtual manifest for monorepos. A root
Cargo.tomlwith only[workspace]keeps the root from accidentally becoming a publishable crate and makes the “this directory coordinates crates” intent obvious. - Centralize versions in
[workspace.dependencies]. Declare every third-party crate’s version once at the root and inherit it everywhere with{ workspace = true }. This is the single most valuable workspace feature for avoiding version drift. - Inherit shared metadata via
[workspace.package]. Putversion,edition,license, andrust-versionat the root and use.workspace = truein members so a single edit moves the whole workspace. - Prefer glob members (
crates/*) for fast-growing repos, and an explicit list when you want tight control over what builds. - Set
default-membersto the crate(s) you run most so barecargo run/cargo testdo the obvious thing. - Keep
[profile.*],[patch], and[replace]at the root only — they are workspace-global by definition. - Commit the single root
Cargo.lock. For a workspace that ships applications/binaries this is the right call (reproducible builds); a workspace that publishes only libraries follows the usual library lockfile convention. See cargo.md. - Reach for a workspace when crates share code or ship together — a CLI plus its library, a server plus shared domain types, a proc-macro crate plus the crate that uses it. Do not split a single cohesive crate into a workspace prematurely.
Warning: All members compile against one feature-unified dependency graph. If member A enables a heavy feature of a shared dependency, member B is built with that feature too (resolver 3 mitigates build-vs-normal leakage, not enabling-across-members). Keep heavyweight optional features behind crates that not every member depends on.
Real-World Example
Section titled “Real-World Example”A production-flavored three-crate workspace: a shared taskhub-core library, a taskhub-cli binary, and a taskhub-web binary stub. Because there are two binaries, default-members points bare commands at the CLI. Everything below was compiled and run as-is.
# taskhub/Cargo.toml — workspace root (virtual manifest)[workspace]resolver = "3"members = ["crates/core", "crates/cli", "crates/web"]default-members = ["crates/cli"]
[workspace.package]version = "0.3.0"edition = "2024"license = "MIT OR Apache-2.0"authors = ["Ada Zeybek <me@zeybek.dev>"]
[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"anyhow = "1"taskhub-core = { path = "crates/core" }Build the entire workspace at once:
$ cargo build --workspace Compiling taskhub-cli v0.3.0 (/path/to/taskhub/crates/cli) Compiling taskhub-web v0.3.0 (/path/to/taskhub/crates/web) Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.92sTest every member in one shot:
$ cargo test --workspace Compiling taskhub-core v0.3.0 (/path/to/taskhub/crates/core) Compiling taskhub-cli v0.3.0 (/path/to/taskhub/crates/cli) Compiling taskhub-web v0.3.0 (/path/to/taskhub/crates/web) Running unittests src/lib.rs (target/debug/deps/taskhub_core-...)test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/taskhub_cli-...)test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/taskhub_web-...)test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sInspect the unified dependency graph with cargo tree — note that taskhub-core appears as a path dependency of the CLI, exactly like a symlinked local package would in node_modules/ (real output, trimmed):
$ cargo tree -p taskhub-clitaskhub-cli v0.3.0 (/path/to/taskhub/crates/cli)├── anyhow v1.0.102├── serde_json v1.0.150│ ├── itoa v1.0.18│ ├── memchr v2.8.1│ ├── serde_core v1.0.228│ └── zmij v1.0.21└── taskhub-core v0.3.0 (/path/to/taskhub/crates/core) └── serde v1.0.228 ├── serde_core v1.0.228 └── serde_derive v1.0.228 (proc-macro)Because the entire graph resolves once into the root Cargo.lock, taskhub-cli and taskhub-web are guaranteed to use the same serde build — the version-skew problem simply cannot occur.
Tip:
cargo metadata --format-version 1reportsworkspace_rootandworkspace_membersas machine-readable JSON — the analogue of parsing a rootpackage.json’sworkspacesfield withjq, but with the full resolved graph included.
Further Reading
Section titled “Further Reading”Official documentation
Section titled “Official documentation”- The Cargo Book — Workspaces — the authoritative reference for
[workspace], members, default-members, and inheritance. - The Cargo Book — Dependency inheritance (
workspace = true) - The Cargo Book — The
[lints]and[workspace.lints]tables - The Cargo Book — Feature resolver versions — background on
resolver = "3".
Cross-links in this guide
Section titled “Cross-links in this guide”- Cargo.toml: the manifest, lockfile & profiles —
[package],Cargo.lock, and the[profile.*]tables a workspace shares. - Specifying dependencies — path/git deps and SemVer grammar that
[workspace.dependencies]builds on. - Dev & build dependencies —
[dev-dependencies]can also be inherited from the workspace. - Feature flags — how features unify across workspace members and why
resolver = "3"matters. - Cargo commands —
-p/--package,--workspace,cargo new,cargo tree. - Publishing to crates.io — publishing individual members of a workspace.
- The in-language module system that lives inside each crate: Modules · The module tree & paths ·
use& re-exports ·pubvisibility. - Foundations: Why Rust · Understanding Cargo (intro) · Basics.
- Next up after modules: Testing —
cargo test --workspaceruns every member’s tests together.
Exercises
Section titled “Exercises”Exercise 1: Turn two crates into a workspace
Section titled “Exercise 1: Turn two crates into a workspace”Difficulty: Beginner
Objective: Create a virtual-manifest workspace whose binary depends on its sibling library by path.
Instructions: Make a taskhub/ directory with a root Cargo.toml (no [package]) listing two members, crates/core (a library) and crates/cli (a binary). Have cli call a function exported from core. Run cargo run -p taskhub-cli and confirm there is exactly one Cargo.lock at the root.
Solution
[workspace]resolver = "3"members = ["crates/core", "crates/cli"][package]name = "taskhub-core"version = "0.1.0"edition = "2024"pub fn greeting(who: &str) -> String { format!("hello from {who}")}[package]name = "taskhub-cli"version = "0.1.0"edition = "2024"
[dependencies]taskhub-core = { path = "../core" }fn main() { println!("{}", taskhub_core::greeting("workspace"));}Build and verify the single lockfile:
$ cargo run -p taskhub-cli --quiethello from workspace$ find . -name Cargo.lock -not -path './target/*'./Cargo.lockOne Cargo.lock at the root is the proof that both members share a single resolved dependency graph.
Exercise 2: Centralize versions with [workspace.dependencies]
Section titled “Exercise 2: Centralize versions with [workspace.dependencies]”Difficulty: Intermediate
Objective: Declare a third-party crate’s version once at the root and inherit it in two members, plus inherit shared package metadata.
Instructions: Extend the workspace so the root declares serde (with the derive feature) and the shared version/edition in [workspace.package]. Both members inherit them with { workspace = true } / .workspace = true. Make core derive Serialize on a struct and cli serialize it. Fill in the /* ??? */ placeholders.
[workspace]resolver = "3"members = ["crates/core", "crates/cli"]
[workspace.package]version = "0.2.0"edition = "2024"
[workspace.dependencies]serde = /* ??? */ # version "1.0" with the "derive" featureserde_json = "1.0"taskhub-core = { path = "crates/core" }[package]name = "taskhub-core"version.workspace = trueedition.workspace = true
[dependencies]serde = /* ??? */ # inherit from the workspaceuse serde::Serialize;
#[derive(/* ??? */)]pub struct Task { pub id: u32, pub title: String,}Solution
[workspace]resolver = "3"members = ["crates/core", "crates/cli"]
[workspace.package]version = "0.2.0"edition = "2024"
[workspace.dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"taskhub-core = { path = "crates/core" }[package]name = "taskhub-core"version.workspace = trueedition.workspace = true
[dependencies]serde = { workspace = true }use serde::Serialize;
#[derive(Debug, Serialize)]pub struct Task { pub id: u32, pub title: String,}[package]name = "taskhub-cli"version.workspace = trueedition.workspace = true
[dependencies]taskhub-core = { workspace = true }serde_json = { workspace = true }use taskhub_core::Task;
fn main() { let task = Task { id: 1, title: "Centralize versions".into() }; println!("{}", serde_json::to_string(&task).unwrap());}Real output:
$ cargo run -p taskhub-cli --quiet{"id":1,"title":"Centralize versions"}The win: serde’s version and the derive feature are declared in exactly one place. Add a third member that needs serde and it inherits the same line — no chance of one crate using serde 1.0 and another a different copy.
Exercise 3: Share lints and run the whole workspace’s tests
Section titled “Exercise 3: Share lints and run the whole workspace’s tests”Difficulty: Advanced
Objective: Configure a workspace-wide Clippy lint that every member opts into, add a unit test to the library, and run all members’ tests together.
Instructions: Add a [workspace.lints.clippy] table at the root that sets unwrap_used = "warn", and have each member opt in with [lints] workspace = true. Add a #[test] to the library crate. Run cargo test --workspace, then run cargo clippy --workspace and observe the shared lint fire on a deliberate .unwrap().
Solution
# Cargo.toml (root)[workspace]resolver = "3"members = ["crates/mathlib", "crates/calc"]
[workspace.package]version = "0.1.0"edition = "2024"
[workspace.dependencies]mathlib = { path = "crates/mathlib" }
[workspace.lints.clippy]unwrap_used = "warn"[package]name = "mathlib"version.workspace = trueedition.workspace = true
[lints]workspace = truepub fn add(a: i64, b: i64) -> i64 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn adds() { assert_eq!(add(2, 3), 5); }}[package]name = "calc"version.workspace = trueedition.workspace = true
[dependencies]mathlib = { workspace = true }
[lints]workspace = true// crates/calc/src/main.rs — deliberately uses unwrap() to trip the shared lintfn first_word(s: &str) -> Option<&str> { s.split_whitespace().next()}
fn main() { let _ = mathlib::add(2, 3); let w = first_word("hello world"); println!("{}", w.unwrap());}Run the whole workspace’s tests (real output, compile lines trimmed):
$ cargo test --workspace Running unittests src/main.rs (target/debug/deps/calc-...)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/lib.rs (target/debug/deps/mathlib-...)
running 1 testtest tests::adds ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests mathlib
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRun Clippy and watch the shared lint fire on the .unwrap() in calc (real output, trimmed):
$ cargo clippy --workspacewarning: used `unwrap()` on an `Option` value --> crates/calc/src/main.rs:8:20 |8 | println!("{}", w.unwrap()); | ^^^^^^^^^^ | = note: if this value is `None`, it will panic = help: consider using `expect()` to provide a better panic message = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used = note: requested on the command line with `-W clippy::unwrap-used`The single [workspace.lints.clippy] table is the Rust analogue of one root .eslintrc that every package extends — change the rule once and it applies across the monorepo.