Cargo Çalışma Alanları (Workspaces)
Bölüm 12’de hem bir ikili crate hem de bir kütüphane crate’i içeren bir paket oluşturmuştuk. Projeniz geliştikçe kütüphane crate’inin giderek büyüdüğünü ve paketinizi birden fazla kütüphane crate’ine bölmek istediğinizi fark edebilirsiniz. Cargo, eşzamanlı olarak birlikte geliştirilen birbiriyle ilişkili birden çok paketi yönetmeye yardımcı olan çalışma alanları (workspaces) adlı bir özellik sunar.
Çalışma Alanı Oluşturma
Çalışma alanı, aynı Cargo.lock dosyasını ve aynı çıktı dizinini paylaşan paketler kümesidir. Çalışma alanının yapısına odaklanabilmek için çok basit kod kullanan bir proje oluşturalım. Bir çalışma alanını yapılandırmanın birden fazla yolu vardır; biz burada yaygın kullanılan bir yöntemi göstereceğiz. Bir ikili crate ve iki kütüphane crate’i içeren bir çalışma alanımız olacak. Ana işlevselliği sunacak olan ikili crate, bu iki kütüphaneye bağımlı olacak. Kütüphanelerden biri bir_ekle fonksiyonunu, diğeri ise iki_ekle fonksiyonunu sağlayacak. Bu üç crate aynı çalışma alanının parçası olacak. İşe çalışma alanı için yeni bir dizin oluşturarak başlayalım:
$ mkdir topla
$ cd topla
Ardından topla dizininde, tüm çalışma alanını yapılandıracak Cargo.toml dosyasını oluşturuyoruz. Bu dosyada [package] bölümü yer almayacak; bunun yerine çalışma alanına üye paketler eklememizi sağlayan bir [workspace] bölümüyle başlayacak. Ayrıca çalışma alanımızda Cargo’nun bağımlılık çözümleyici algoritmasının en güncel ve gelişmiş sürümünü kullanmak için resolver değerini özellikle "3" olarak ayarlıyoruz:
Dosya adı: Cargo.toml
[workspace]
resolver = "3"
Not: Normalde tekli bir pakette edition = "2024" ayarı Cargo’nun 3. sürüm
bağımlılık çözümleyicisini otomatik olarak etkinleştirir. Ancak başında
[package] bölümü bulunmayan bu tür sanal çalışma alanı (virtual workspace)
Cargo.toml dosyalarında edition alanı yer almadığından, resolver = "3"
satırını açıkça belirtmezseniz Cargo geriye dönük uyumluluk amacıyla eski
çözümleyiciye ("1") döner ve derleme sırasında uyarı üretir.
Şimdi topla dizini içinde cargo new çalıştırarak toplayici ikili crate’ini oluşturalım:
$ cargo new toplayici
Created binary (application) `toplayici` package
Adding `toplayici` as member of workspace at `file:///projects/topla`
Bir çalışma alanı içinde cargo new çalıştırmak, yeni oluşturulan paketi çalışma alanının Cargo.toml dosyasındaki [workspace] tanımında yer alan members anahtarına da otomatik olarak ekler:
[workspace]
resolver = "3"
members = ["toplayici"]
Bu noktada cargo build çalıştırarak çalışma alanını derleyebiliriz. topla dizininizdeki dosyalar şu şekilde görünmelidir:
├── Cargo.lock
├── Cargo.toml
├── toplayici
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Çalışma alanının en üst düzeyde derlenmiş çıktıların yerleştirileceği tek bir target dizini vardır; toplayici paketinin kendine ait ayrı bir target dizini yoktur. Hatta cargo build komutunu toplayici dizininin içinden çalıştırsak bile derlenmiş çıktılar topla/toplayici/target yerine yine topla/target dizinine yerleştirilir. Cargo bir çalışma alanındaki target dizinini bu şekilde yapılandırır çünkü çalışma alanındaki crate’lerin birbirine bağımlı olması amaçlanır. Her crate’in kendi target dizini olsaydı, her crate derlenmiş çıktıları kendi target dizinine yerleştirmek için çalışma alanındaki diğer crate’lerin her birini yeniden derlemek zorunda kalırdı. Tek bir target dizinini paylaşarak crate’ler gereksiz yeniden derlemelerin önüne geçer.
Çalışma Alanında İkinci Paketi Oluşturma
Şimdi çalışma alanında bir üye paket daha oluşturalım ve adını bir_ekle koyalım. bir_ekle adında yeni bir kütüphane crate’i üretin:
$ cargo new bir_ekle --lib
Created library `bir_ekle` package
Adding `bir_ekle` as member of workspace at `file:///projects/topla`
Üst düzey Cargo.toml dosyası artık members listesinde bir_ekle yolunu da içerecektir:
Dosya adı: Cargo.toml
[workspace]
resolver = "3"
members = ["toplayici", "bir_ekle"]
topla dizininizde artık şu dizin ve dosyalar bulunmalıdır:
├── Cargo.lock
├── Cargo.toml
├── bir_ekle
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── toplayici
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
bir_ekle/src/lib.rs dosyasına bir bir_ekle fonksiyonu ekleyelim:
Dosya adı: bir_ekle/src/lib.rs
pub fn bir_ekle(x: i32) -> i32 {
x + 1
}
Artık ikili crate’imizi barındıran toplayici paketinin, kütüphanemizi içeren bir_ekle paketine bağımlı olmasını sağlayabiliriz. Öncelikle toplayici/Cargo.toml dosyasına bir_ekle için bir yol bağımlılığı (path dependency) eklememiz gerekecek.
Dosya adı: toplayici/Cargo.toml
[dependencies]
bir_ekle = { path = "../bir_ekle" }
Cargo bir çalışma alanındaki crate’lerin birbirine bağımlı olacağını kendiliğinden varsaymaz; bu nedenle bağımlılık ilişkilerini açıkça belirtmemiz gerekir.
Şimdi (bir_ekle crate’inden gelen) bir_ekle fonksiyonunu toplayici crate’inde kullanalım. toplayici/src/main.rs dosyasını açın ve main fonksiyonunu Liste 14-7’deki gibi bir_ekle fonksiyonunu çağıracak şekilde değiştirin.
fn main() {
let sayi = 10;
println!(
"Merhaba, dünya! {sayi} artı bir {} eder!",
bir_ekle::bir_ekle(sayi)
);
}
bir_ekle kütüphane crate’ini toplayici crate’i içinden kullanmakÜst düzey topla dizininde cargo build çalıştırarak çalışma alanını derleyelim!
$ cargo build
Compiling bir_ekle v0.1.0 (file:///projects/topla/bir_ekle)
Compiling toplayici v0.1.0 (file:///projects/topla/toplayici)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
İkili crate’i topla dizininden çalıştırmak için cargo run komutuyla birlikte -p argümanını ve paket adını kullanarak çalışma alanındaki hangi paketi çalıştırmak istediğimizi belirtebiliriz:
$ cargo run -p toplayici
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/toplayici`
Merhaba, dünya! 10 artı bir 11 eder!
Bu komut, toplayici/src/main.rs içindeki ve bir_ekle crate’ine bağımlı olan kodu çalıştırır.
Not: Kök Cargo.toml dosyasında [package] bölümü bulunmayan böyle bir
sanal çalışma alanında (virtual workspace) kök dizinden çalıştırdığınız
cargo build, cargo check ve cargo test gibi komutlar varsayılan olarak
tüm üye paketlere uygulanır. cargo run ise hangi ikili dosyayı
çalıştıracağını bilmek zorunda olduğundan, çalışma alanında birden fazla
ikili crate bulunduğunda -p bayrağı verilmezse hata verir. Üye paketlerden
birinin dizinine girip orada komut çalıştırdığınızda yalnızca o paket
hedeflenir; o konumdan tüm çalışma alanını derlemek için
cargo build --workspace kullanabilirsiniz. Kök dizinde varsayılan olarak
hangi paketlerin hedefleneceğini ise [workspace] bölümündeki
default-members anahtarıyla belirleyebilirsiniz.
Harici Bir Pakete Bağımlı Olma
Çalışma alanında her crate’in kendi dizininde ayrı bir Cargo.lock bulunması yerine, en üst düzeyde yalnızca tek bir Cargo.lock dosyası bulunduğuna dikkat edin. Bu yapı, tüm crate’lerin bütün bağımlılıkların aynı sürümünü kullanmasını güvence altına alır. rand paketini hem toplayici/Cargo.toml hem de bir_ekle/Cargo.toml dosyalarına eklersek, Cargo her ikisini de tek bir rand sürümüne çözümler ve bunu tek Cargo.lock dosyasına kaydeder. Çalışma alanındaki tüm crate’lerin aynı bağımlılıkları kullanmasını sağlamak, bu crate’lerin birbirleriyle her zaman uyumlu olacağı anlamına gelir. Şimdi rand crate’ini bir_ekle crate’inde kullanabilmek için bir_ekle/Cargo.toml dosyasındaki [dependencies] bölümüne ekleyelim:
Dosya adı: bir_ekle/Cargo.toml
[dependencies]
rand = "0.10.1"
Artık bir_ekle/src/lib.rs dosyasına use rand; ekleyebiliriz; topla dizininde cargo build çalıştırarak tüm çalışma alanını derlemek rand crate’ini indirip derleyecektir. Kapsama dahil ettiğimiz rand crate’ine kodda başvurmadığımız için bir uyarı alırız:
$ cargo build
Updating crates.io index
Downloaded rand v0.10.1
--snip--
Compiling rand v0.10.1
Compiling bir_ekle v0.1.0 (file:///projects/topla/bir_ekle)
warning: unused import: `rand`
--> bir_ekle/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
warning: `bir_ekle` (lib) generated 1 warning (run `cargo fix --lib -p bir_ekle` to apply 1 suggestion)
Compiling toplayici v0.1.0 (file:///projects/topla/toplayici)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
Üst düzey Cargo.lock dosyası artık bir_ekle crate’inin rand bağımlılığına ilişkin bilgileri içerir. Bununla birlikte, rand çalışma alanının bir yerinde kullanılıyor olsa bile, diğer crate’lerin Cargo.toml dosyalarına da rand eklemediğimiz sürece onu çalışma alanındaki diğer crate’lerde kullanamayız. Örneğin toplayici paketine ait toplayici/src/main.rs dosyasına use rand; eklersek bir hata alırız:
$ cargo build
--snip--
Compiling toplayici v0.1.0 (file:///projects/topla/toplayici)
error[E0432]: unresolved import `rand`
--> toplayici/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
Bunu düzeltmek için toplayici paketinin Cargo.toml dosyasını düzenleyip rand paketinin onun için de bir bağımlılık olduğunu belirtin. toplayici paketini derlemek, Cargo.lock dosyasında toplayici paketinin bağımlılık listesine rand paketini ekleyecektir; ancak rand paketinin ek bir kopyası indirilmeyecektir. Paketler birbiriyle uyumlu rand sürümleri belirttiği sürece Cargo, çalışma alanında rand kullanan tüm crate’lerin aynı sürümü paylaşmasını sağlar; bu hem disk alanından tasarruf ettirir hem de çalışma alanındaki crate’lerin birbiriyle uyumlu kalmasını güvence altına alır.
Çalışma alanındaki crate’ler aynı bağımlılığın birbiriyle uyumsuz sürümlerini belirtirse Cargo bunların her birini ayrı ayrı çözümler, ancak yine de mümkün olan en az sayıda sürümü çözümlemeye çalışır.
Not: Çok sayıda paketten oluşan projelerde aynı bağımlılık sürümünü veya ortak
paket meta verilerini (version, edition, license vb.) her üye paketin
Cargo.toml dosyasında tekrar tekrar yazmak ve güncel tutmak sürüm
uyuşmazlıklarına yol açabilir. Cargo, bunu tek merkezden yönetmek için
çalışma alanı mirası (workspace inheritance) özelliğini sunar. Üst düzey
Cargo.toml dosyasında ortak paket alanlarını [workspace.package] altında,
ortak bağımlılıkları ise [workspace.dependencies] altında bir kez
tanımlayabilirsiniz:
[workspace]
resolver = "3"
members = ["toplayici", "bir_ekle"]
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
rand = "0.10.1"
bir_ekle = { path = "bir_ekle" }
Ardından üye paketlerin kendi Cargo.toml dosyalarında (örneğin
toplayici/Cargo.toml) sürüm numarası veya dosya yolu tekrarlamak yerine
workspace = true anahtarını kullanması yeterlidir:
[package]
name = "toplayici"
version.workspace = true
edition.workspace = true
[dependencies]
rand.workspace = true
bir_ekle.workspace = true
Böylece bir bağımlılığı güncellemeniz gerektiğinde yalnızca kök Cargo.toml dosyasındaki tek bir satırı değiştirirsiniz ve çalışma alanındaki tüm crate’lerin aynı sürüm tanımını paylaşması garanti altına alınır.
Çalışma Alanına Test Ekleme
Bir iyileştirme daha yapalım ve bir_ekle crate’i içinde bir_ekle::bir_ekle fonksiyonunun bir testini ekleyelim:
Dosya adı: bir_ekle/src/lib.rs
pub fn bir_ekle(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calisir() {
assert_eq!(3, bir_ekle(2));
}
}
Şimdi üst düzey topla dizininde cargo test çalıştırın. Bu şekilde yapılandırılmış bir çalışma alanında cargo test çalıştırmak, çalışma alanındaki tüm crate’lerin testlerini çalıştıracaktır:
$ cargo test
Compiling bir_ekle v0.1.0 (file:///projects/topla/bir_ekle)
Compiling toplayici v0.1.0 (file:///projects/topla/toplayici)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/bir_ekle-93c49ee75dc46543)
running 1 test
test tests::calisir ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/toplayici-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests bir_ekle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Çıktının ilk bölümü, bir_ekle crate’indeki calisir testinin geçtiğini gösterir. Bir sonraki bölüm toplayici crate’inde sıfır test bulunduğunu, son bölüm ise bir_ekle crate’inde sıfır belgelendirme testi bulunduğunu gösterir.
Üst düzey dizinden -p bayrağını kullanıp test etmek istediğimiz crate’in adını belirterek çalışma alanındaki belirli bir crate’in testlerini de çalıştırabiliriz:
$ cargo test -p bir_ekle
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/bir_ekle-93c49ee75dc46543)
running 1 test
test tests::calisir ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests bir_ekle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Bu çıktı, cargo test komutunun yalnızca bir_ekle crate’i için testleri çalıştırdığını ve toplayici crate’inin testlerini çalıştırmadığını gösterir.
Çalışma alanındaki crate’leri crates.io üzerinde yayınlarsanız, çalışma alanındaki her crate’in ayrı ayrı yayınlanması gerekecektir. Tıpkı cargo test komutunda olduğu gibi, -p bayrağını kullanıp yayınlamak istediğimiz crate’in adını belirterek çalışma alanımızdaki belirli bir crate’i yayınlayabiliriz.
Ek pratik yapmak için bu çalışma alanına bir_ekle crate’ine benzer bir şekilde iki_ekle crate’ini ekleyin!
Projeniz büyüdükçe bir çalışma alanı kullanmayı değerlendirin: Tek parça büyük bir kod yığını yerine daha küçük ve anlaşılması daha kolay bileşenlerle çalışmanıza olanak tanır. Üstelik crate’ler sıklıkla aynı anda değiştiriliyorsa, onları bir çalışma alanında tutmak crate’ler arasındaki eşgüdümü de kolaylaştırabilir.