Rust 專案管理與 Cargo ─ 依賴管理(Dependencies)
簡介
在 Rust 生態系統中,Cargo 是唯一官方的建置與套件管理工具。它不僅負責編譯、測試與發佈,最關鍵的功能就是依賴管理:讓開發者可以輕鬆地把第三方函式庫(crate)加入專案、指定版本範圍、以及在不同環境下切換依賴。
對於剛踏入 Rust 的新手而言,了解 Cargo 如何處理依賴、什麼是 semver(語意化版本號)以及如何避免常見的衝突,都是寫出可維護、可擴充程式碼的基礎。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握依賴管理的技巧。
核心概念
1. Cargo.toml 與依賴的宣告
每個 Rust 專案根目錄都會有一個 Cargo.toml 檔案,裡面以 TOML 格式描述專案的 metadata 與依賴。最基本的寫法如下:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0"
serde = "1.0"表示「相容於 1.0 版的最新次要版與修補版」;Cargo 會自動解析符合條件的最高版本(例如1.0.156)。- 若要固定到特定版號,可使用
=:serde = "=1.0.130"。
2. 版本範圍與語意化版本(SemVer)
Rust 社群採用 語意化版本(Semantic Versioning),格式為 MAJOR.MINOR.PATCH。
- MAJOR:不相容的 API 變更。
- MINOR:向下相容的功能新增。
- PATCH:向下相容的錯誤修正。
Cargo 支援多種範圍運算子:
| 運算子 | 含義 |
|---|---|
^1.2.3 |
允許升級到 <2.0.0(預設) |
~1.2.3 |
允許升級到 <1.3.0 |
>=1.2, <1.5 |
明確範圍 |
3. 功能(Features)與可選依賴
許多大型 crate 會提供 features,讓使用者自行選擇要啟用哪些子功能,減少二進位大小或避免不必要的依賴。例如 serde 提供 derive 功能:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
若想在自己的 crate 中定義自訂 feature:
[features]
default = ["json"]
json = ["serde_json"]
4. 開發依賴與測試依賴
- dev-dependencies:只在測試、範例或
cargo bench時編譯。 - build-dependencies:在 build script (
build.rs) 中使用的依賴。
[dev-dependencies]
assert_cmd = "2.0"
5. 工作區(Workspace)與依賴共享
當一個專案包含多個子 crate 時,可透過 workspace 讓它們共用同一個 Cargo.lock,避免版本衝突。
# Cargo.toml at workspace root
[workspace]
members = ["core", "cli", "gui"]
程式碼範例
範例 1:加入 rand 並使用特定功能
// Cargo.toml
[dependencies]
rand = { version = "0.8", features = ["std"] }
// src/main.rs
use rand::Rng; // 只匯入需要的 trait
fn main() {
// 產生 0~9 的隨機數
let n: u8 = rand::thread_rng().gen_range(0..10);
println!("隨機數: {}", n);
}
說明:features = ["std"] 讓 rand 使用標準函式庫,若在 no‑std 環境則可改為 features = ["alloc"]。
範例 2:使用 serde 的 derive 功能與自訂 feature
# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
[features]
json = ["serde_json"]
// src/main.rs
#[cfg(feature = "json")]
use serde_json::to_string_pretty;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u8,
}
fn main() {
let alice = Person { name: "Alice".into(), age: 30 };
#[cfg(feature = "json")]
{
// 只在啟用 json feature 時編譯
let pretty = to_string_pretty(&alice).unwrap();
println!("{}", pretty);
}
#[cfg(not(feature = "json"))]
{
println!("{:?}", alice);
}
}
說明:optional = true 讓 serde_json 成為「可選」依賴,只有在 cargo run --features json 時才會被加入編譯圖。
範例 3:工作區內部依賴(crate 間相互引用)
# workspace root Cargo.toml
[workspace]
members = ["core", "app"]
# core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
# app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
// core/src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}", name)
}
// app/src/main.rs
use core::greet;
fn main() {
println!("{}", greet("Rustacean"));
}
說明:path 依賴允許子 crate 直接引用同一工作區內的其他 crate,Cargo 會自動處理版本與編譯順序。
範例 4:使用 cargo update 鎖定特定依賴的版本
# 先檢查目前依賴的版本
cargo tree
# 將 `rand` 更新到最新符合 "^0.8" 的版本
cargo update -p rand
說明:Cargo.lock 記錄了實際使用的版本;cargo update 只會改變符合範圍的依賴,不會突破 Cargo.toml 設定的上限。
範例 5:自訂 build script 使用 build-dependencies
# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
// 使用 cc crate 編譯 C 檔案
cc::Build::new()
.file("src/native.c")
.compile("native");
}
/* src/native.c */
int add(int a, int b) { return a + b; }
// src/main.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
fn main() {
unsafe {
println!("3 + 4 = {}", add(3, 4));
}
}
說明:build-dependencies 僅在 build.rs 執行時編譯,不會出現在最終二進位的依賴圖中。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 |
|---|---|---|
未鎖定依賴版本(使用 * 或過寬的範圍) |
CI 與本機環境產生不同的二進位,難以重現 bug。 | 盡量使用具體的範圍(如 ^1.2),並在 Cargo.lock 中鎖定。 |
| 功能衝突(兩個 crate 開啟了互相衝突的 feature) | 編譯失敗或產生不必要的二進位膨脹。 | 利用 [patch] 或 cargo tree -d 觀察重複依賴,必要時手動統一版本。 |
過度依賴 dev-dependencies |
測試時才需要的 crate 會被意外帶入正式發佈。 | 確認 dev-dependencies 僅在測試、範例或 benchmark 中使用。 |
忽略 cargo audit |
可能把已知安全漏洞的 crate 推上線。 | 定期執行 cargo audit,並更新受影響的依賴。 |
| 未使用工作區 | 多 crate 專案會產生多個 Cargo.lock,導致版本不一致。 |
為相關 crate 建立 workspace,共享同一 Cargo.lock。 |
最佳實踐
- 語意化版本:遵循
^(預設)或~,除非真的需要固定版本。 - Feature 最小化:只啟用真正需要的 feature,減少編譯時間與二進位大小。
- CI 中檢查依賴:在 CI pipeline 加入
cargo check --locked,確保Cargo.lock未被意外變更。 - 使用
cargo tree:定期檢視依賴樹,快速發現重複或過舊的 crate。 - 文件化依賴決策:在 README 或專案文件說明為何選擇特定版本或 feature,方便團隊成員了解背景。
實際應用場景
Web 服務(使用 Actix-Web)
- 需要
actix-web = "4"、serde(JSON 序列化)以及sqlx(非同步資料庫)。 - 透過
features = ["runtime-tokio-rustls"]只啟用 TLS 支援,避免把不必要的openssl拉進專案。
- 需要
嵌入式開發(no‑std)
rand = { version = "0.8", default-features = false, features = ["alloc"] }- 透過
default-features = false關閉標準函式庫依賴,確保編譯到 microcontroller。
CLI 工具(使用 Clap)
clap = { version = "4", features = ["derive", "env"] }derive讓 struct 直接映射參數,env允許從環境變數讀取設定,提升使用者體驗。
大型單元測試套件
dev-dependencies中加入mockall、assert_cmd、criterion,分別負責 mock、命令列測試與效能基準。- 測試時使用
cargo test --all-features,確保所有 optional 功能都能正確運作。
跨平台 GUI(使用 Tauri)
tauri = { version = "1", features = ["api-all"] }- 只在
tauri專案中啟用api-all,其他子 crate(例如 core library)則保持輕量。
總結
依賴管理是 Rust 專案成功的關鍵之一。透過 Cargo.toml 的清晰宣告、語意化版本的彈性、以及 feature 機制的可選擇性,我們可以在不犧牲安全性與效能的前提下,快速整合社群提供的高品質 crate。
本文從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到真實的應用情境,提供了一套完整的依賴管理思考框架。只要遵循以下三點:
- 明確指定版本範圍,避免
*或過寬的範圍。 - 最小化 Feature,只開啟真正需要的功能。
- 定期檢查依賴圖與安全性(
cargo tree、cargo audit、CI lock 檢查)。
就能在開發過程中保持依賴的可預測性與可維護性,讓你的 Rust 專案在成長與迭代時依舊保持穩定與高效。祝你在 Rust 的世界裡玩得開心、寫得順手! 🚀