rust-workspace-setup

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rust Workspace Setup

Rust工作区设置

This is a one-time setup skill. It produces a repository root
Cargo.toml
and a member-crate layout that can grow without coupling. The guiding principle is separation of concerns at the crate boundary — each member crate owns one responsibility and exposes only what its dependents need. A workspace that does not enforce this from the first commit accumulates cross-cutting
use
statements until splitting is more expensive than tolerating the mess.
The templates here are grounded in a real workspace: an API backend (
core/
), an async background worker (
worker/
), and shared crates under
crates/
— a task-contracts crate that owns the gRPC wire types exchanged between the API and the worker, a connectors-runtime crate shared by both, and a kubernetes-primitives crate that is a dependency of the connectors-runtime crate but carries no service logic.
The cross-language note: Python's equivalent is
uv workspaces
(a
uv.toml
[workspace]
table with a
members
list). The principles — shared dependency declarations, one-way boundaries, shared lint config — translate directly. The rest of this skill is Rust-specific.

这是一项一次性设置技能。它会生成仓库根目录的Cargo.toml文件以及可扩展且无耦合的成员crate结构。指导原则是在crate层面实现关注点分离——每个成员crate负责单一职责,仅向依赖它的模块暴露必要内容。如果工作区从初始提交就不遵循这一原则,会逐渐积累大量跨模块的
use
语句,直到拆分的成本高于容忍混乱的成本。
此处的模板基于一个真实的工作区:API后端(
core/
)、异步后台工作进程(
worker/
),以及
crates/
下的共享crate——task-contracts crate(负责API与工作进程之间交换的gRPC通信类型)、connectors-runtime crate(双方共享的探针/连接逻辑),以及kubernetes-primitives crate(作为connectors-runtime的依赖,但不包含业务逻辑的纯k8s清单基础组件)。
跨语言说明:Python中的等效方案是
uv workspaces
(即
uv.toml
中的
[workspace]
表及
members
列表)。其核心原则——共享依赖声明、单向依赖边界、共享lint配置——完全适用。本文剩余内容为Rust专属内容。

Repository layout

仓库结构

my-project/                          ← workspace root (no src/, no bin/)
├── Cargo.toml                       ← [workspace] manifest only
├── rust-toolchain.toml              ← toolchain pin (see rust-project-setup)
├── rustfmt.toml                     ← shared formatter config
├── core/                            ← API/backend binary crate
│   └── Cargo.toml
├── worker/                          ← async background-worker binary crate
│   └── Cargo.toml
└── crates/                          ← shared library crates
    ├── task-contracts/              ← wire types shared by core ↔ worker
    │   └── Cargo.toml
    ├── connectors-runtime/          ← probe/connectivity logic shared by core ↔ worker
    │   └── Cargo.toml
    └── kubernetes-primitives/       ← pure k8s manifest primitives (no service logic)
        └── Cargo.toml
The workspace root carries no
src/
directory
and no
[lib]
or
[[bin]]
section
. Its only job is to declare the workspace, own the lint table, hoist shared profiles, and provide a
[workspace.dependencies]
table that prevents version skew.

my-project/                          ← 工作区根目录(无src/、无bin/)
├── Cargo.toml                       ← 仅包含[workspace]清单
├── rust-toolchain.toml              ← 工具链固定(参考rust-project-setup)
├── rustfmt.toml                     ← 共享格式化配置
├── core/                            ← API/后端二进制crate
│   └── Cargo.toml
├── worker/                          ← 异步后台工作进程二进制crate
│   └── Cargo.toml
└── crates/                          ← 共享库crate
    ├── task-contracts/              ← core ↔ worker共享的通信类型
    │   └── Cargo.toml
    ├── connectors-runtime/          ← core ↔ worker共享的探针/连接逻辑
    │   └── Cargo.toml
    └── kubernetes-primitives/       ← 纯k8s清单基础组件(无业务逻辑)
        └── Cargo.toml
工作区根目录不包含src/目录,也没有
[lib]
[[bin]]
配置段
。它的唯一职责是声明工作区、管理lint规则、统一共享编译配置,并提供
[workspace.dependencies]
表以避免版本不一致。

Step 1 — Workspace root
Cargo.toml

步骤1 — 工作区根目录的Cargo.toml

toml
undefined
toml
undefined

Workspace manifest. The root carries no source code — members live in

工作区清单。根目录不包含源代码——成员位于core/、worker/和crates/中。

core/, worker/, and crates/.

[workspace] resolver = "2" members = [ "core", "worker", "crates/task-contracts", "crates/connectors-runtime", "crates/kubernetes-primitives", ]
[workspace] resolver = "2" members = [ "core", "worker", "crates/task-contracts", "crates/connectors-runtime", "crates/kubernetes-primitives", ]

---------------------------------------------------------------------------

---------------------------------------------------------------------------

Shared lint table — members opt in via
[lints] workspace = true
.

共享lint规则表——成员通过
[lints] workspace = true
启用。

---------------------------------------------------------------------------

---------------------------------------------------------------------------

[workspace.lints.rust] unsafe_code = "deny"
[workspace.lints.clippy] all = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 }
[workspace.lints.rust] unsafe_code = "deny"
[workspace.lints.clippy] all = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 }

Adjust suppressions to your project's honest needs — do not silence

根据项目实际需求调整禁用规则——不要为了简化clippy配置而批量关闭诊断信息。

diagnostics wholesale to keep the clippy section small.

missing_errors_doc = "allow" missing_panics_doc = "allow" type_complexity = "allow"
missing_errors_doc = "allow" missing_panics_doc = "allow" type_complexity = "allow"

---------------------------------------------------------------------------

---------------------------------------------------------------------------

Shared build profiles — Cargo silently ignores member-level profile tables.

共享编译配置——Cargo会忽略成员Cargo.toml中的配置段。

Move them here from member Cargo.toml files when adding workspace support.

添加工作区支持时,将成员Cargo.toml中的配置移至此处。

---------------------------------------------------------------------------

---------------------------------------------------------------------------

[profile.release] lto = true codegen-units = 1 panic = "abort" strip = true
[profile.dev] debug = true
[profile.release] lto = true codegen-units = 1 panic = "abort" strip = true
[profile.dev] debug = true

---------------------------------------------------------------------------

---------------------------------------------------------------------------

Shared dependency versions — members reference these as

共享依赖版本——成员通过
serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
引用。

serde = { workspace = true }
or
serde = { workspace = true, features = ["derive"] }
.

仅声明被两个及以上成员共享的依赖版本。

Declare only versions that are shared across two or more members.

---------------------------------------------------------------------------

---------------------------------------------------------------------------

[workspace.dependencies] tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" uuid = { version = "1", features = ["v7", "serde"] } chrono = { version = "0.4", features = ["serde"] } tracing = "0.1"
[workspace.dependencies] tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" uuid = { version = "1", features = ["v7", "serde"] } chrono = { version = "0.4", features = ["serde"] } tracing = "0.1"

Internal crates — declared here so members can reference them without

内部crate——在此声明以便成员无需重复路径即可引用。

repeating the path.

task-contracts = { path = "crates/task-contracts" } connectors-runtime = { path = "crates/connectors-runtime" } kubernetes-primitives = { path = "crates/kubernetes-primitives" }
undefined
task-contracts = { path = "crates/task-contracts" } connectors-runtime = { path = "crates/connectors-runtime" } kubernetes-primitives = { path = "crates/kubernetes-primitives" }
undefined

Why
resolver = "2"
is required

为什么需要
resolver = "2"

Resolver v2 is mandatory for workspaces that mix
std
and
no_std
members, or that have members with different feature combinations of the same dependency. Without it, Cargo v1's feature unification merges features across all members, silently enabling capabilities in members that did not request them.
对于混合了
std
no_std
成员,或同一依赖在不同成员中有不同特性组合的工作区,Resolver v2是必需的。如果没有它,Cargo v1的特性统一机制会合并所有成员的特性,静默地为未请求该特性的成员启用相关功能。

Why profiles live only in the root

为什么编译配置仅放在根目录

Cargo reads
[profile.*]
tables only from the workspace root and silently ignores them in member
Cargo.toml
files. If you have profile overrides in member manifests today, move them to the root when converting to a workspace.

Cargo仅从工作区根目录读取
[profile.*]
配置段,会静默忽略成员Cargo.toml中的对应配置。如果当前成员清单中有编译配置覆盖项,转换为工作区结构时需将其移至根目录。

Step 2 — Toolchain and formatter files

步骤2 — 工具链和格式化文件

These two files live at the repository root and are shared by every member.
这两个文件位于仓库根目录,供所有成员共享。

rust-toolchain.toml

rust-toolchain.toml

Pin a specific stable release. "stable" without a version drifts with whatever rustup updates to, making
cargo check
output non-reproducible across machines.
toml
[toolchain]
channel    = "1.87.0"
components = ["rustfmt", "clippy"]
See
rust-project-setup
for the full rationale.
固定特定的稳定版本。仅写"stable"而不指定版本会随rustup更新而变化,导致不同机器上的
cargo check
输出无法复现。
toml
[toolchain]
channel    = "1.87.0"
components = ["rustfmt", "clippy"]
详细原理参考
rust-project-setup

rustfmt.toml

rustfmt.toml

The 2024 edition style guide is the sensible baseline:
toml
edition       = "2024"
style_edition = "2024"
Add project-specific overrides only when the default produces output the team actively dislikes — a long list of overrides is a maintenance burden.

2024版风格指南是合理的基准:
toml
edition       = "2024"
style_edition = "2024"
仅当默认格式输出被团队明确反感时,才添加项目特定的覆盖项——过多的覆盖项会增加维护负担。

Step 3 — Member crate layout and
Cargo.toml
patterns

步骤3 — 成员crate结构和Cargo.toml模式

The task-contracts crate (shared wire types)

task-contracts crate(共享通信类型)

This crate owns the types that cross the API ↔ worker boundary: gRPC stubs, message DTOs, and any enumerations the two sides must agree on. It depends on nothing internal.
toml
undefined
该crate负责API ↔ 工作进程之间的跨边界类型:gRPC存根、消息DTO以及双方必须达成一致的枚举类型。它不依赖任何内部模块。
toml
undefined

crates/task-contracts/Cargo.toml

crates/task-contracts/Cargo.toml

[package] name = "task-contracts" version = "0.1.0" edition = "2024" publish = false description = "Shared wire contracts between the API backend and the worker: message DTOs and any shared enumerations."
[dependencies] serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } chrono = { workspace = true }
[lints] workspace = true

Key rules for this crate:
- No dependency on `core/` or `worker/` — ever. The arrow is always inward.
- No infrastructure crates (no `sqlx`, no `axum`, no `kube`). Contracts are pure data.
- Keep the public surface minimal; every field that crosses the wire is a commitment.
[package] name = "task-contracts" version = "0.1.0" edition = "2024" publish = false description = "API后端与工作进程之间的共享通信协议:消息DTO和共享枚举类型。"
[dependencies] serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } chrono = { workspace = true }
[lints] workspace = true

该crate的核心规则:
- 永远不依赖`core/`或`worker/`——依赖方向始终是向内的。
- 不依赖基础设施crate(如`sqlx`、`axum`、`kube`)。协议仅包含纯数据。
- 保持公共接口最小化;每个跨边界的字段都是一种承诺。

The API backend crate

API后端crate

toml
undefined
toml
undefined

core/Cargo.toml

core/Cargo.toml

[package] name = "my-project-core" version = "0.1.0" edition = "2024" publish = false description = "HTTP API and background scheduler for my-project."
[dependencies] tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } tracing = { workspace = true }
[package] name = "my-project-core" version = "0.1.0" edition = "2024" publish = false description = "my-project的HTTP API和后台调度器。"
[dependencies] tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } tracing = { workspace = true }

Internal shared crates

内部共享crate

task-contracts = { workspace = true } connectors-runtime = { workspace = true } kubernetes-primitives = { workspace = true }
task-contracts = { workspace = true } connectors-runtime = { workspace = true } kubernetes-primitives = { workspace = true }

Backend-specific dependencies (not shared — declared locally)

后端专属依赖(不共享——本地声明)

axum = "0.8" sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] }
[features] default = []
axum = "0.8" sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "migrate"] }
[features] default = []

Gate infrastructure-fixture tests in CI (no external cluster available).

在CI中启用基础设施 fixture 测试(无外部集群可用)。

ci = []
[lints] workspace = true
undefined
ci = []
[lints] workspace = true
undefined

The worker crate

工作进程crate

toml
undefined
toml
undefined

worker/Cargo.toml

worker/Cargo.toml

[package] name = "my-project-worker" version = "0.1.0" edition = "2024" publish = false description = "Async background-task worker for my-project."
[dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } tracing = { workspace = true } serde_json = { workspace = true }
[package] name = "my-project-worker" version = "0.1.0" edition = "2024" publish = false description = "my-project的异步后台任务工作进程。"
[dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } tracing = { workspace = true } serde_json = { workspace = true }

Internal shared crates — the worker never depends on core/.

内部共享crate——工作进程永远不依赖core/。

task-contracts = { workspace = true } connectors-runtime = { workspace = true }
[lints] workspace = true

The worker must **never** depend on `core/`. All types the worker and the API share must
live in a shared crate under `crates/`. This is the single-responsibility rule applied
at the crate boundary: the worker's job is task execution, not re-hosting API logic.
task-contracts = { workspace = true } connectors-runtime = { workspace = true }
[lints] workspace = true

工作进程**绝对不能**依赖`core/`。工作进程与API共享的所有类型必须放在`crates/`下的共享crate中。这是在crate层面应用单一职责原则:工作进程的任务是执行任务,而非复用API逻辑。

Shared library crates under
crates/

crates/
下的共享库crate

A crate belongs under
crates/
when:
  • Two or more members need it, and
  • It has no transitive dependency on either of those members (no circular paths).
A crate that is only needed by one member belongs inside that member, not in
crates/
. Moving a crate to
crates/
when only one consumer uses it is premature extraction that adds maintenance overhead for no benefit.

当满足以下条件时,crate应放在
crates/
下:
  • 两个及以上成员需要它,并且
  • 它不依赖任何使用它的成员(无循环依赖)。
仅被一个成员使用的crate应放在该成员内部,而非
crates/
下。在只有一个消费者时就将crate移至
crates/
属于过早提取,会增加维护开销却无任何收益。

Step 4 — Opt-in workspace lints

步骤4 — 启用工作区lint规则

Every member crate that should be held to the workspace lint policy adds exactly one line:
toml
[lints]
workspace = true
A crate that omits this line is not linted by the workspace rules — useful for vendored or generated code that you don't own. For all first-party crates,
workspace = true
is the default.

每个需要遵循工作区lint策略的成员crate只需添加一行配置:
toml
[lints]
workspace = true
省略该行的crate不会应用工作区lint规则——这适用于你不拥有的 vendored 或生成代码。对于所有自研crate,
workspace = true
是默认设置。

Step 5 — Consuming workspace dependencies in members

步骤5 — 在成员中使用工作区依赖

Two patterns:
toml
undefined
两种模式:
toml
undefined

1. Take the version and all declared workspace features as-is:

1. 直接使用工作区声明的版本和所有特性:

serde = { workspace = true }
serde = { workspace = true }

2. Take the version but add extra features on top:

2. 使用工作区声明的版本,但添加额外特性:

tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

You cannot remove workspace-declared features from a member — you can only add more.
This means the features listed under `[workspace.dependencies]` should be the
**minimum** set required by any consumer. Do not add `features = ["full"]` to the
workspace entry for a crate like `tokio` if most members only need a subset; it inflates
compile times for every member.

---
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

你无法移除成员中工作区声明的特性——只能添加更多特性。这意味着`[workspace.dependencies]`中列出的特性应是**所有消费者所需的最小集合**。如果大多数成员只需要子集特性,不要在`tokio`等特性丰富的crate的工作区配置中添加`features = ["full"]`,否则会增加所有成员的编译时间。

---

Step 6 — Verify the workspace compiles clean

步骤6 — 验证工作区编译正常

bash
undefined
bash
undefined

Check all members in one pass:

一次性检查所有成员:

cargo check --workspace
cargo check --workspace

Run the full test suite across all members:

运行所有成员的完整测试套件:

cargo test --workspace
cargo test --workspace

Lint all members:

检查所有成员的lint:

cargo clippy --workspace -- -D warnings
cargo clippy --workspace -- -D warnings

Format check:

格式化检查:

cargo fmt --check

The `--workspace` flag is the shorthand for "all members". Prefer it over per-member
invocations in CI so new members are automatically included.

---
cargo fmt --check

`--workspace`标志是"所有成员"的简写。在CI中优先使用它,而非逐个成员调用,这样新成员会自动被包含。

---

Anti-Patterns

反模式

  1. Putting source in the workspace root. The root manifest's job is coordination, not implementation. Source in the root means
    cargo test --workspace
    includes the root as a member, which causes confusing output and breaks the mental model that the root is "above" the members.
  2. Version-pinning the same dependency differently in two members. Cargo will merge the requirement sets but may select different patch versions for
    cargo update
    , which produces non-reproducible builds across developer machines. Hoist to
    [workspace.dependencies]
    the moment a dependency is shared.
  3. Worker depending on core. When the worker needs a type from the API backend, the fix is to extract it to a shared crate, not to add
    core = { path = "../core" }
    in the worker's manifest. A dependency from worker → core pulls in every backend dependency (axum, sqlx, the full HTTP stack) into the worker binary.
  4. Profiles in member manifests. Cargo silently ignores
    [profile.*]
    in any manifest that is not the workspace root. This is a silent misconfiguration: the member author believes a profile applies; it does not.
  5. One mega-crate under
    crates/
    that "shares everything."
    Each crate under
    crates/
    should have one clear responsibility. A catch-all shared crate grows until it is indistinguishable from a second
    core/
    , and its compile time inflates every consumer.
  6. features = ["full"]
    in
    [workspace.dependencies]
    for feature-rich crates.
    As noted in Step 5, workspace features are a floor, not a ceiling. Setting them too high penalises every member that doesn't need the extras.

  1. 在工作区根目录放置源代码。根目录清单的职责是协调,而非实现。根目录包含源代码意味着
    cargo test --workspace
    会将根目录作为成员包含在内,导致输出混乱,并打破根目录位于"成员之上"的认知模型。
  2. 同一依赖在两个成员中版本固定不一致。Cargo会合并依赖要求,但
    cargo update
    可能为不同成员选择不同的补丁版本,导致不同开发机器上的构建无法复现。一旦依赖被共享,立即将其移至
    [workspace.dependencies]
  3. 工作进程依赖core。当工作进程需要API后端的某个类型时,正确做法是将该类型提取到共享crate,而非在工作进程的清单中添加
    core = { path = "../core" }
    。worker → core的依赖会将所有后端依赖(axum、sqlx、完整HTTP栈)引入工作进程二进制文件。
  4. 成员清单中的编译配置。Cargo会静默忽略非工作区根目录清单中的
    [profile.*]
    配置。这是一种静默错误配置:成员作者认为配置已生效,但实际并未生效。
  5. crates/
    下的一个巨型crate负责"共享所有内容"
    crates/
    下的每个crate应具有明确的单一职责。一个包罗万象的共享crate会逐渐膨胀,变得与第二个
    core/
    无异,并且会增加所有消费者的编译时间。
  6. 特性丰富的crate在
    [workspace.dependencies]
    中设置
    features = ["full"]
    。如步骤5所述,工作区特性是基础要求,而非上限。设置过高会惩罚所有不需要额外特性的成员。

Quick Reference

快速参考

TaskCommand
Check all members
cargo check --workspace
Test all members
cargo test --workspace
Lint all members
cargo clippy --workspace -- -D warnings
Format all members
cargo fmt
Format check (CI)
cargo fmt --check
Build release
cargo build --release --workspace
Add a new memberAdd path to
members
in root
Cargo.toml
, create
<member>/Cargo.toml
Add a shared depAdd to
[workspace.dependencies]
, reference with
workspace = true
in members

任务命令
检查所有成员
cargo check --workspace
测试所有成员
cargo test --workspace
检查所有成员的lint
cargo clippy --workspace -- -D warnings
格式化所有成员
cargo fmt
格式化检查(CI)
cargo fmt --check
构建发布版本
cargo build --release --workspace
添加新成员在根目录Cargo.toml的
members
中添加路径,创建
<member>/Cargo.toml
添加共享依赖
[workspace.dependencies]
中添加,成员中通过
workspace = true
引用

Cross-references

交叉引用

  • rust-project-setup — toolchain pinning, which applies unchanged to a workspace. Its
    cargo-make
    section does not: a workspace's task runner is
    just
    at the repository root (
    justfile-setup
    ), because a project gets one command surface and only a language-agnostic runner can own a polyglot one.
  • rust-project-structure — module, file, and folder conventions inside a member crate.
  • rust-hexagonal-architecture — layering (domain / application / infrastructure) inside a member crate; the crate-split workspace pattern described in
    rust-architecture-test-setup
    maps one layer per workspace member.
  • rust-architecture-test-setup — the
    tests/structure/
    cargo-test gate; run it per member that uses hexagonal layers.
    SourceTree
    resolves via
    CARGO_MANIFEST_DIR
    so it naturally scopes to the member crate. Its manifest dependency gate is what keeps the workspace's dependency directions permanent: give every shared library crate an in-crate gate forbidding its consumers, and give the worker a gate forbidding the core crate and direct database drivers.
  • justfile-setup — workspace-level task runner;
    just check
    can invoke
    cargo check --workspace
    rather than per-member commands.
The
task-contracts
crate is the API ↔ worker broker boundary — the only place where the two binary members may share types. Keep it thin. Any logic that belongs to the domain of the API or the worker must stay in its respective member crate.
  • rust-project-setup — 工具链固定,该内容完全适用于工作区。但其中的
    cargo-make
    部分不适用:工作区的任务运行器是根目录的
    just
    (参考
    justfile-setup
    ),因为项目只有一个命令入口,只有语言无关的运行器才能支持多语言场景。
  • rust-project-structure — 成员crate内部的模块、文件和文件夹约定。
  • rust-hexagonal-architecture — 成员crate内部的分层(领域/应用/基础设施);
    rust-architecture-test-setup
    中描述的crate拆分工作区模式将每个层映射为一个工作区成员。
  • rust-architecture-test-setup
    tests/structure/
    下的cargo测试 gate;对使用六边形架构的成员逐个运行。
    SourceTree
    通过
    CARGO_MANIFEST_DIR
    解析,因此自然限定在成员crate范围内。其清单依赖 gate用于维持工作区的依赖方向:为每个共享库crate添加禁止依赖其消费者的gate,为工作进程添加禁止依赖core crate和直接数据库驱动的gate。
  • justfile-setup — 工作区级别的任务运行器;
    just check
    可以调用
    cargo check --workspace
    ,而非逐个成员调用命令。
task-contracts crate是API ↔ 工作进程的边界——这是两个二进制成员唯一可以共享类型的地方。保持其精简。任何属于API或工作进程领域的逻辑必须留在各自的成员crate中。