Rust Workspaces

While working on a Rust project involving multiple libraries and applications, I explored Rust workspaces to manage interconnected components. I found the process somewhat unintuitive and poorly documented, especially when it came to importing libraries into applications. To streamline future efforts, I’ve summarized the steps I followed here.

Why Use Workspaces?

For small projects, a single binary or library is sufficient, created with a simple command like:

cargo new app --bin  

However, for larger projects involving multiple libraries and applications that need to interact with each other, Rust workspaces provide a convenient way to organize the structure.

Workspace Structure

Here’s an example structure I used for my project:

.  
├── Cargo.lock  
├── Cargo.toml  
├── app/  
│   ├── app1/  
│   │   ├── Cargo.lock  
│   │   ├── Cargo.toml  
│   │   └── src/  
│   │       └── main.rs  
│   ├── app2/  
│   │   ├── Cargo.lock  
│   │   ├── Cargo.toml  
│   │   └── src/  
│           └── main.rs  
├── lib/  
│   ├── lib1/  
│   │   ├── Cargo.lock  
│   │   ├── Cargo.toml  
│   │   └── src/  
│   │       └── lib.rs  
│   ├── lib2/  
│   │   ├── Cargo.lock  
│   │   ├── Cargo.toml  
│   │   └── src/  
│           └── lib.rs  
└── tests/  
    ├── some-integration-tests.rs  
    └── multi-file-test/  
        ├── main.rs  
        └── test_module.rs  

Setting Up a Workspace

  1. Create the Workspace Root

    Start with an empty directory containing a Cargo.toml file:

    [workspace]  
    members = []  
    

    It would be nice to have a cargo new --workspace workspaceName command, but apparently, it is not implemented.

  2. Create Applications and Libraries

    In the same folder, run the following commands to create your applications and libraries:

    cargo new app/app1 --vcs none --bin  
    cargo new app/app2 --vcs none --bin  
    cargo new lib/lib1 --vcs none --lib  
    cargo new lib/lib2 --vcs none --lib  
    

    The --vcs none flag prevents each subproject from initializing its own Git repository.

    These commands will automatically update the members section in the workspace’s ./Cargo.toml file, located in the root directory. The resulting file should look like this:

    [workspace]  
    members = ["app/app1", "app/app2", "lib/lib1", "lib/lib2"]  
    
  3. Adding Dependencies in the Workspace

    To make library dependencies available to all applications, add them to the workspace’s ./Cargo.toml:

    [workspace.dependencies]
    lib1 = { path = "lib/lib1" }
    lib2 = { path = "lib/lib2" }
    

Adding Dependencies

To use lib1 in app1, add it as a dependency in ./app/app1/Cargo.toml:

[dependencies]
lib1 = { workspace = true }

Then, in ./app/app1/src/main.rs, you can reference it like this:

use lib1;  

fn main() {  
    println!("Hello, world!");  
    println!("{}", lib1::add(1, 2));  
}  

Building and Running

To build the entire workspace, use:

cargo build  

To execute a specific application (e.g., app1):

cargo run --bin app1  

By following this structure, you can effectively manage complex Rust projects while maintaining a clear organization of your codebase.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Data-Oriented Programming in Rust
  • The Evolution of Scientific Curiosity - From Triangles to Gaussians in Graphics
  • Inquiry-Based Learning - Navigating the Path to Deep Understanding in Class
  • Docker Dependency Magic - Unleashing sed to Tailor Your Packages
  • Function Hashing - A Simple Idea with Powerful Implications (Unison)