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.

Tipp

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"
Megjegyzés

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.

Jó tudni

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.

Tipp

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" }
Figyelem

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.