Ha eddig csak egyetlen Cargo.toml-lal dolgoztál, előbb-utóbb eljön a pont, amikor a projekted szétválna: legyen egy külön crate a core logikának, egy a CLI-nek, egy a webes rétegnek, esetleg egy a közös típusoknak. Itt jön képbe a Cargo workspace.
Mi az a Cargo workspace, és mikor van rá szükséged?
A workspace nem más, mint több Cargo package (crate) közös kezelése egyetlen target/ mappa és egyetlen Cargo.lock alatt. Ez három nagy előnnyel jár:
- Egy build, egy lock fájl – nem duplikálódnak a függőségek fordítása közben, gyorsabb az inkrementális build.
- Egyszerűbb belső hivatkozás – a crate-ek
path = "../core"formában hivatkoznak egymásra, nincs szükség publikálásra a fejlesztés alatt. - Konzisztens verziók – egy helyen látod, melyik külső crate melyik verzióját használod az egész projektben.
Mikor érdemes belevágni? Ha azt látod, hogy a lib.rs fájlod ezer sor fölött van, több, egymástól jól elválasztható felelősségi kört tartalmaz, vagy szeretnél külön binárist (pl. CLI) a fő logikától, akkor a workspace pontosan erre van kitalálva.
Nem kell rögtön workspace-ben gondolkodni egy kisebb projektnél. Ha csak egy bin és egy lib van, simán elég egy crate-en belül src/bin/ mappát használni. A workspace ott éri meg igazán, amikor több, önállóan is verziózható vagy tesztelhető crate-ed van.
Workspace létrehozása lépésről lépésre
Kezdjük egy tipikus felállással: legyen egy core könyvtár crate, ami a domain logikát tartalmazza, és egy cli binary crate, ami ezt használja.
Először hozzunk létre egy gyökér mappát, benne egy workspace-szintű Cargo.toml-lal, amiben nincs [package] szekció, csak [workspace]:
# Cargo.toml (a workspace gyökerében)
[workspace]
members = [
"core",
"cli",
]
resolver = "2"
A resolver = "2" beállítás a 2021-es edition-nel érkezett feature-egyesítési logikát vezeti be, ami kevesebb meglepetést hoz a feature flag-ek terén több crate esetén. Ha 2021 edition-t használsz, érdemes explicit módon is beírni, még ha alapból is ezt kapod egy új workspace-nél.
Most hozzuk létre a tagokat:
cargo new --lib core
cargo new cli
A core/Cargo.toml egy sima library crate lesz:
# core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"
[dependencies]
A cli crate pedig a core-ra hivatkozik lokális path-tal:
# cli/Cargo.toml
[package]
name = "cli"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" }
Ez a path kulcs a lényeg: fejlesztés közben nem kell a core crate-et publikálni sehova, a Cargo egyszerűen a fájlrendszerről fordítja be. Amikor élesben publikálnál, a verziószámot (version = "0.1.0") használja a Cargo, ha path nélkül hivatkoznál rá crates.io-ról.
Írjunk egy egyszerű függvényt a core-ba, amit a cli felhasznál:
// core/src/lib.rs
pub fn greet(name: &str) -> String {
format!("Szia, {name}! Üdv a workspace-ben.")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greet_works() {
assert_eq!(greet("Rust"), "Szia, Rust! Üdv a workspace-ben.");
}
}
És a CLI-ben használjuk:
// cli/src/main.rs
fn main() {
let message = core::greet("fejlesztő");
println!("{message}");
}
Ennyi! Ha most a gyökérből futtatod a cargo build-et, a Cargo mindkét crate-et lefordítja, és egy közös target/ mappába teszi az eredményt.
Közös függőségek és verziószám-egyeztetés
Ha több crate-ed is használ mondjuk serde-t, könnyen előfordulhat, hogy az egyik 1.0.150-et kér, a másik 1.0.160-at – ez működni fog, mert a Cargo egységesíti a lock fájlban, de karbantartási szempontból jobb, ha egy helyen definiálod a verziókat.
Erre valók a workspace-szintű függőségek ([workspace.dependencies]), amiket a tag crate-ek workspace = true kulcsszóval vehetnek át:
# Cargo.toml (gyökér)
[workspace]
members = ["core", "cli"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
anyhow = "1"
És egy tag crate-ben:
# core/Cargo.toml
[dependencies]
serde = { workspace = true }
anyhow = { workspace = true }
Ez nem csak kényelmes, hanem véd is a verzió-drifttől: ha frissítesz, egy helyen teszed meg, és minden crate ugyanazt a definíciót kapja.
A [workspace.dependencies] csak a verzió és feature definíciót adja meg egy helyen, a tényleges felvételt (hogy egy crate-nek egyáltalán szüksége van-e a függőségre) továbbra is a [dependencies] szekcióban kell jelezni workspace = true-val. Ha kihagyod egy crate-ből, az a crate nem fogja látni azt a dependency-t.
Ha a projektedben már használsz let-else szintaxist vagy OnceCell/OnceLock-ot valamelyik crate-ben, a workspace ettől függetlenül simán működik – a fordítási beállítások (edition, feature flag-ek) crate-enként külön kezelhetők, csak a lock fájl közös.
Build és teszt futtatás egyszerre több crate-en
A workspace gyökeréből kiadott cargo build és cargo test alapesetben az összes tag crate-et lefordítja, illetve teszteli:
# Az összes workspace-tag build-je
cargo build
# Az összes teszt futtatása minden crate-en
cargo test
Ha csak egy adott crate-re akarsz fókuszálni, használd a -p (--package) kapcsolót:
cargo build -p core
cargo test -p cli
Ha a workspace-ben van egy közös bevy vagy más nagyobb crate, ami sok időt fordítási időt visz el, a -p kapcsolóval jelentősen gyorsíthatod az iterációt, mert nem kell minden alkalommal az egészet lefordítani.
Érdemes megjegyezni, hogy a cargo check is workspace-szinten működik, és fejlesztés közben ez a leggyorsabb visszajelzési kör:
cargo check --workspace
A --workspace flag explicit módon jelzi, hogy minden tagra vonatkozzon a parancs – ez hasznos, ha egy alkönyvtárból futtatod a parancsot, és nem akarod, hogy csak az aktuális crate-re szűküljön.
Ha CI-ban futtatod a teszteket, a cargo test --workspace --all-features kombinációval biztosíthatod, hogy minden crate minden feature flag-je alatt lefusson a tesztkészlet – ez sokszor kifog olyan hibákat, amik csak bizonyos feature-kombinációnál jönnek elő.
Gyakori hibák és buktatók workspace-ek használatakor
1. Elfelejtett resolver = "2". Ha régebbi projektet workspace-esítesz, és nem adod meg explicit módon a resolver verziót, előfordulhat, hogy a feature-egyesítés máshogy viselkedik, mint várnád – főleg ha valamelyik crate dev-dependencies-ben más feature-t kér, mint amit élesben használnál.
2. Körkörös path függőség. Ha az A crate a B-re, a B pedig visszahivatkozik A-ra, a Cargo hibát fog dobni. Ilyenkor gyakran egy harmadik, közös crate-be (common vagy types) kell kiszervezni azt, amire mindkettőnek szüksége van.
3. path függőség elfelejtése publikáláskor. Ha cargo publish-t akarsz futtatni egy olyan crate-re, ami path-tal hivatkozik egy másik, nem publikált crate-re, a publish sikertelen lesz. Publikálás előtt minden path függőségnek léteznie kell crates.io-n (megfelelő verziószámmal), vagy előbb azt kell publikálni.
// Ez hibát fog dobni, ha a `shared` crate nincs publikálva,
// és a `mycrate`-et próbálod publikálni:
// mycrate/Cargo.toml:
// [dependencies]
// shared = { path = "../shared", version = "0.1.0" }
A version kulcs megadása a path mellett nem elég ahhoz, hogy publikáláskor a path-ot figyelmen kívül hagyja a Cargo – mindkettőnek pontosan egyeznie kell a publikált verzióval, különben a cargo publish hibával leáll.
4. Workspace-szintű [patch] vagy [profile] szekció félreértése. Ezek csak a gyökér Cargo.toml-ban érvényesek, a tag crate-ekben megadott [profile] szekciókat a Cargo egyszerűen figyelmen kívül hagyja. Ha optimalizálási beállításokat akarsz módosítani (pl. opt-level), azt a gyökér Cargo.toml-ban tedd meg:
# Cargo.toml (gyökér)
[profile.release]
opt-level = 3
lto = true
5. Túl sok crate, túl korán. A workspace nem varázsütés a rossz modularizáció ellen. Ha csak azért bontasz szét egy projektet öt crate-re, mert "ez a jó gyakorlat", könnyen több lesz a boilerplate, mint a haszon. Válaszd szét a crate-eket ott, ahol valós határ van: eltérő élettartam, eltérő feature-igény, vagy külön publikálási szándék.
Összefoglalás
A Cargo workspace nem egy extra bonyolultsági szint, hanem pont az, ami rendet visz egy növekvő projektbe: egy Cargo.lock, egyszerű belső hivatkozások path-tal, és a [workspace.dependencies] segítségével egységes verziókezelés. Ha most kezdesz egy nagyobb projektet, vagy éppen egy monolitikus crate-et bontanál szét, a fent bemutatott minta – core + cli, közös dependency szekció, resolver = "2" – jó kiindulópont. A build és teszt parancsok (cargo build, cargo test, -p, --workspace) pedig ugyanolyan természetesen működnek, mint egyetlen crate esetén, csak most az egész projektedre vonatkoznak egyszerre.