Cargo workspaces group related Rust packages under one root manifest so builds, tests, dependency resolution, and output artifacts can be managed from the repository root. They fit projects that split a reusable library, CLI, integration test harness, or helper crate into separate packages without losing a shared build workflow.

A virtual workspace root is a Cargo.toml that has [workspace] but no [package] section. Because that root has no package edition for Cargo to inspect, set resolver explicitly and list member directories that contain their own manifests.

A small library member and binary member under crates/ are enough to verify member discovery, the shared root Cargo.lock, and package selection from the root. The binary depends on the library by path, so a workspace-wide test and a package run prove more than two unrelated packages sitting under the same directory.

Steps to create a Rust workspace with Cargo:

  1. Create the workspace root directory.
    $ mkdir demo-workspace
  2. Enter the workspace root.
    $ cd demo-workspace
  3. Create the library member package.
    $ cargo new crates/demo-lib --lib --vcs none
        Creating library `demo-lib` package
    note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    Use existing package directories instead of cargo new when converting an existing repository. --vcs none leaves version control ownership to the workspace root.

  4. Create the binary member package.
    $ cargo new crates/demo-cli --bin --vcs none
        Creating binary (application) `demo-cli` package
    note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  5. Create the virtual workspace manifest.
    Cargo.toml
    [workspace]
    members = ["crates/demo-cli", "crates/demo-lib"]
    resolver = "3"

    resolver = “3” matches the current resolver for Edition 2024 packages. Virtual workspaces should set it explicitly because the root manifest has no package edition.

  6. Replace the library source with a small function and test.
    crates/demo-lib/src/lib.rs
    pub fn add(left: u64, right: u64) -> u64 {
        left + right
    }
     
    #[cfg(test)]
    mod tests {
        use super::*;
     
        #[test]
        fn adds_two_numbers() {
            assert_eq!(add(2, 3), 5);
        }
    }
  7. Add the library as a path dependency in the binary member manifest.
    crates/demo-cli/Cargo.toml
    [package]
    name = "demo-cli"
    version = "0.1.0"
    edition = "2024"
     
    [dependencies]
    demo-lib = { path = "../demo-lib" }

    Cargo.toml uses the package name demo-lib, while Rust code imports it as demo_lib.

  8. Replace the binary entry point so it calls the library member.
    crates/demo-cli/src/main.rs
    fn main() {
        println!("workspace total: {}", demo_lib::add(2, 3));
    }
  9. Confirm that Cargo sees both workspace members.
    $ cargo tree --workspace --depth 0
    demo-cli v0.1.0 (/work/demo-workspace/crates/demo-cli)
    
    demo-lib v0.1.0 (/work/demo-workspace/crates/demo-lib)
  10. Run the workspace test selection from the root.
    $ cargo test --workspace
       Compiling demo-lib v0.1.0 (/work/demo-workspace/crates/demo-lib)
       Compiling demo-cli v0.1.0 (/work/demo-workspace/crates/demo-cli)
        Finished `test` profile [unoptimized + debuginfo] target(s) in 0.36s
         Running unittests src/main.rs (target/debug/deps/demo_cli-0766424aa2cc7c52)
    
    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/demo_lib-ecdde925a2b988c2)
    
    running 1 test
    test tests::adds_two_numbers ... ok
    
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    
       Doc-tests demo_lib
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  11. Check that Cargo wrote one root lockfile.
    $ cat Cargo.lock
    # This file is automatically @generated by Cargo.
    # It is not intended for manual editing.
    version = 4
    
    [[package]]
    name = "demo-cli"
    version = "0.1.0"
    dependencies = [
     "demo-lib",
    ]
    
    [[package]]
    name = "demo-lib"
    version = "0.1.0"

    The lockfile lives beside the workspace root manifest rather than inside each member package.

  12. Run the binary package from the workspace root.
    $ cargo run -p demo-cli --quiet
    workspace total: 5

    -p selects one package from the workspace when the root manifest has more than one runnable member.