跳到主要内容

工作空间(Workspaces)

工作空间是 Rust 中管理多个相关包的强大工具。它允许你在一个项目中组织多个 crate,共享依赖和配置,简化大型项目的管理。

什么是工作空间?

工作空间是一个包含多个包的目录,这些包共享:

  • Cargo.lock 文件
  • 输出目录 (target/)
  • 依赖版本 解析

创建工作空间

基本工作空间结构

my_workspace/
├── Cargo.toml # 工作空间根配置
├── Cargo.lock # 共享的锁文件
├── target/ # 共享的构建目录
├── common/ # 共享库
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── server/ # 服务器应用
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
└── client/ # 客户端应用
├── Cargo.toml
└── src/
└── main.rs

工作空间根配置

# 根目录 Cargo.toml
[workspace]
members = [
"common",
"server",
"client",
]

# 可选:排除某些目录
exclude = [
"old_code",
"experiments",
]

# 工作空间级别的元数据
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
license = "MIT OR Apache-2.0"

# 共享依赖版本
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
clap = "4.0"
anyhow = "1.0"

成员包配置

共享库包

# common/Cargo.toml
[package]
name = "common"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true

[dependencies]
serde.workspace = true
anyhow.workspace = true

# 库特定的依赖
uuid = { version = "1.0", features = ["v4"] }
// common/src/lib.rs
use serde::{Serialize, Deserialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}

impl User {
pub fn new(name: String, email: String) -> Self {
User {
id: Uuid::new_v4(),
name,
email,
}
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Message {
pub id: Uuid,
pub content: String,
pub user_id: Uuid,
}

pub mod utils {
pub fn validate_email(email: &str) -> bool {
email.contains('@')
}
}

服务器包

# server/Cargo.toml
[package]
name = "server"
version.workspace = true
edition.workspace = true

[dependencies]
common = { path = "../common" }
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true

# 服务器特定依赖
axum = "0.7"
tower = "0.4"
// server/src/main.rs
use common::{User, Message, utils};
use axum::{
extract::Path,
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use std::collections::HashMap;
use tokio::sync::Mutex;
use std::sync::Arc;

type UserStore = Arc<Mutex<HashMap<String, User>>>;

#[tokio::main]
async fn main() {
let store: UserStore = Arc::new(Mutex::new(HashMap::new()));

let app = Router::new()
.route("/users", post(create_user))
.route("/users/:id", get(get_user))
.with_state(store);

println!("Server running on http://localhost:3000");

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();

axum::serve(listener, app).await.unwrap();
}

async fn create_user(
axum::extract::State(store): axum::extract::State<UserStore>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<User>, StatusCode> {
let name = payload["name"].as_str().ok_or(StatusCode::BAD_REQUEST)?;
let email = payload["email"].as_str().ok_or(StatusCode::BAD_REQUEST)?;

if !utils::validate_email(email) {
return Err(StatusCode::BAD_REQUEST);
}

let user = User::new(name.to_string(), email.to_string());
let mut store = store.lock().await;
store.insert(user.id.to_string(), user.clone());

Ok(Json(user))
}

async fn get_user(
Path(id): Path<String>,
axum::extract::State(store): axum::extract::State<UserStore>,
) -> Result<Json<User>, StatusCode> {
let store = store.lock().await;
let user = store.get(&id).ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user.clone()))
}

客户端包

# client/Cargo.toml
[package]
name = "client"
version.workspace = true
edition.workspace = true

[dependencies]
common = { path = "../common" }
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true
clap.workspace = true

# 客户端特定依赖
reqwest = { version = "0.11", features = ["json"] }
// client/src/main.rs
use common::{User, utils};
use clap::{App, Arg, SubCommand};
use reqwest::Client;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
let matches = App::new("User Client")
.version("1.0")
.subcommand(
SubCommand::with_name("create")
.arg(Arg::with_name("name").required(true))
.arg(Arg::with_name("email").required(true))
)
.subcommand(
SubCommand::with_name("get")
.arg(Arg::with_name("id").required(true))
)
.get_matches();

let client = Client::new();

match matches.subcommand() {
("create", Some(sub_m)) => {
let name = sub_m.value_of("name").unwrap();
let email = sub_m.value_of("email").unwrap();

if !utils::validate_email(email) {
eprintln!("Invalid email format");
return Ok(());
}

let user = create_user(&client, name, email).await?;
println!("Created user: {:?}", user);
}
("get", Some(sub_m)) => {
let id = sub_m.value_of("id").unwrap();
let user = get_user(&client, id).await?;
println!("User: {:?}", user);
}
_ => {
println!("Use --help for usage information");
}
}

Ok(())
}

async fn create_user(client: &Client, name: &str, email: &str) -> Result<User> {
let payload = serde_json::json!({
"name": name,
"email": email
});

let response = client
.post("http://localhost:3000/users")
.json(&payload)
.send()
.await?;

let user: User = response.json().await?;
Ok(user)
}

async fn get_user(client: &Client, id: &str) -> Result<User> {
let response = client
.get(&format!("http://localhost:3000/users/{}", id))
.send()
.await?;

let user: User = response.json().await?;
Ok(user)
}

工作空间命令

构建和运行

# 构建整个工作空间
cargo build

# 构建特定包
cargo build -p server
cargo build -p client

# 运行特定包
cargo run -p server
cargo run -p client -- create "Alice" "alice@example.com"

# 测试整个工作空间
cargo test

# 测试特定包
cargo test -p common

发布和管理

# 检查所有包
cargo check --workspace

# 更新依赖
cargo update

# 清理构建产物
cargo clean

# 发布包(需要按依赖顺序)
cargo publish -p common
cargo publish -p server
cargo publish -p client

高级工作空间配置

继承配置

# 根 Cargo.toml
[workspace]
members = ["crate1", "crate2"]

[workspace.package]
version = "1.0.0"
edition = "2021"
rust-version = "1.70"
authors = ["Team <team@example.com>"]
license = "MIT"
repository = "https://github.com/org/project"

[workspace.dependencies]
# 内部依赖
shared-utils = { path = "shared-utils" }

# 外部依赖
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", default-features = false }
# crate1/Cargo.toml
[package]
name = "crate1"
# 继承工作空间配置
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true

[dependencies]
shared-utils.workspace = true
serde.workspace = true
tokio = { workspace = true, features = ["rt", "net"] }

条件成员

[workspace]
members = [
"core",
"cli",
]

# 根据特性条件包含
[workspace.dependencies]
gui = { path = "gui", optional = true }

# 在特定条件下包含成员
[workspace]
members = [
"core",
"cli",
]

# 使用 resolver 版本
resolver = "2"

最佳实践

1. 项目组织

project/
├── Cargo.toml # 工作空间根
├── README.md
├── LICENSE
├── .gitignore
├── core/ # 核心库
│ ├── Cargo.toml
│ └── src/
├── api/ # API 服务
│ ├── Cargo.toml
│ └── src/
├── cli/ # 命令行工具
│ ├── Cargo.toml
│ └── src/
├── web/ # Web 前端
│ ├── Cargo.toml
│ └── src/
└── tests/ # 集成测试
├── Cargo.toml
└── tests/

2. 依赖管理

# 在工作空间级别统一管理版本
[workspace.dependencies]
# 核心依赖
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"

# 开发依赖
criterion = "0.5"
proptest = "1.0"

# 可选依赖
gui-framework = { version = "0.1", optional = true }

3. 特性管理

# 根 Cargo.toml
[workspace]
members = ["core", "cli", "gui"]

[workspace.dependencies]
core = { path = "core" }

# core/Cargo.toml
[package]
name = "core"

[features]
default = []
gui = ["dep:gui-framework"]
cli = ["dep:clap"]

# cli/Cargo.toml
[dependencies]
core = { workspace = true, features = ["cli"] }

# gui/Cargo.toml
[dependencies]
core = { workspace = true, features = ["gui"] }

4. 版本发布策略

#!/bin/bash
# release.sh - 发布脚本

# 1. 更新版本号
cargo ws version patch

# 2. 按依赖顺序发布
cargo publish -p core
sleep 10 # 等待 crates.io 索引更新
cargo publish -p api
sleep 10
cargo publish -p cli

# 3. 创建 git 标签
git tag -a v$(cargo metadata --format-version 1 | jq -r '.workspace_members[0]' | cut -d' ' -f2)
git push --tags

常见问题和解决方案

1. 循环依赖

# 错误:A 依赖 B,B 依赖 A
# 解决:提取共同依赖到新的 crate

[workspace]
members = ["shared", "module-a", "module-b"]

# shared/Cargo.toml - 共同依赖
[package]
name = "shared"

# module-a/Cargo.toml
[dependencies]
shared = { path = "../shared" }

# module-b/Cargo.toml
[dependencies]
shared = { path = "../shared" }

2. 版本不一致

# 使用工作空间依赖确保版本一致
[workspace.dependencies]
serde = "1.0"

# 所有成员使用相同版本
[dependencies]
serde.workspace = true

3. 构建优化

# 根 Cargo.toml
[profile.release]
lto = true
codegen-units = 1
panic = "abort"

# 开发时的优化
[profile.dev.package."*"]
opt-level = 2

工作空间是管理大型 Rust 项目的强大工具,合理使用可以大大简化项目管理和依赖协调。