6 min read #rust #cargo-and-build
On this page

Workspace and Project Management

When a project contains multiple interdependent crates (such as a core library + API server + CLI), a workspace allows them to share the same Cargo.lock and target directory—enabling compilation cache reuse and unified dependency versions. The [patch] feature allows temporarily replacing a dependency's source code during development, while build.rs executes code generation before compilation.

Workspace: Multiple Crates in a Single Repository

When a project contains multiple interdependent crates (such as a core library + API server + CLI), placing them in a workspace offers three main benefits:

[workspace]
members = ["crates/core", "crates/api", "crates/cli"]
resolver = "2"
  1. Shared build artifacts: All members share a single target/ directory—dependencies are compiled only once, significantly reducing disk usage and compilation time.
  2. Consistent versions: Cargo.lock is shared at the workspace level—every member sees the same versions of dependencies.
  3. Internal cross-referencing: api can directly reference core using path = "../../crates/core" (no need to publish to crates.io).

[workspace.dependencies]: Shared Version Declarations

Starting with version 1.64, you can declare versions in the root Cargo.toml, and members can reference them by key:

[workspace.dependencies]
serde = "1"
tokio = { version = "1", features = ["full"] }

# Each member: [dependencies] → serde = { workspace = true }

This solves the classic problem in multi-crate projects where dependency versions are scattered across various files, leading to missed updates during upgrades. Change the version in one place, and all members update accordingly.

[patch]: Temporarily Replace Dependencies

When an upstream crate has a bug and you need to use a fork until the upstream fixes it:

[patch.crates-io]
serde = { git = "https://github.com/myfork/serde", branch = "fix" }

Note: The version specified in [patch] must be globally compatible within the workspace—if one member depends on serde 1.0 and another on serde 1.3, the fork must satisfy the API requirements of both. This is why forks should ideally only modify internal logic and not public APIs.

Advanced build.rs

// build.rs — executed before compiling the main code
fn main() {
    // Rerun conditions: only rerun when these conditions change
    println!("cargo:rerun-if-changed=src/c/wrapper.h");
    println!("cargo:rerun-if-env-changed=MY_LIB_PATH");

    // Link system libraries
    println!("cargo:rustc-link-search=native={}", my_lib_dir);
    println!("cargo:rustc-link-lib=static=my_lib");

    // Pass cfg flags to the main code — enables #[cfg(has_avx2)]
    println!("cargo:rustc-cfg=has_avx2");
}

build.rs is commonly used for: compiling C/C++ dependencies (via the cc crate), detecting the presence of system libraries, generating code (e.g., protobuf, OpenAPI), and setting feature flags.

References

  • Cargo Book: workspaces, build scripts
  • cc crate: docs.rs/cc

Keywords: workspace, members, patch, build.rs, shared dependencies, multi-crate, Cargo.lock