Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cargo Workspaces

Di Bab 12, kita udah bikin sebuah package yang isinya satu binary crate sama satu library crate. Seiring berkembangnya project kita, kita mungkin menemukan bahwa library crate kita terus jadi makin besar dan kita mau membagi package kita lebih jauh lagi jadi beberapa library crates. Cargo menawarkan fitur bernama workspaces (ruang kerja) yang bisa membantu mengelola beberapa packages yang saling terkait yang dikembangkan secara beriringan (in tandem).

Membuat Workspace

Sebuah workspace adalah sekumpulan packages yang berbagi Cargo.lock dan direktori output yang sama. Mari kita bikin project yang memakai workspace—kita bakal pakai kode yang sepele biar kita bisa fokus ke struktur dari workspace tersebut. Ada banyak cara buat menata struktur sebuah workspace, jadi kita cuma bakal nunjukin satu cara yang umum. Kita bakal punya sebuah workspace yang berisi satu binary dan dua libraries. Si binary, yang bakal menyediakan fungsionalitas utama, bakal bergantung pada (depend on) kedua libraries itu. Satu library bakal menyediakan fungsi add_one dan library yang satunya lagi menyediakan fungsi add_two. Ketiga crates ini bakal jadi bagian dari workspace yang sama. Kita bakal memulainya dengan membuat direktori baru buat workspace tersebut:

$ mkdir add
$ cd add

Berikutnya, di dalam direktori add, kita bikin file Cargo.toml yang bakal mengkonfigurasi seluruh workspace. File ini tidak bakal punya bagian [package]. Sebaliknya, file ini bakal diawali dengan bagian [workspace] yang bakal memungkinkan kita buat menambahkan anggota (members) ke dalam workspace. Kita juga sengaja menentukan buat memakai algoritma resolver (pemecah) Cargo versi yang paling baru dan paling bagus di workspace kita dengan menge-set nilai resolver ke "3".

Nama file: Cargo.toml

[workspace]
resolver = "3"

Selanjutnya, kita bakal membikin binary crate adder dengan menjalankan cargo new di dalam direktori add:

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

Menjalankan cargo new di dalam sebuah workspace juga secara otomatis menambahkan package yang baru dibuat itu ke dalam key members di definisi [workspace] yang ada di Cargo.toml tingkat workspace, kayak gini:

[workspace]
resolver = "3"
members = ["adder"]

Pada titik ini, kita bisa mem-build workspace ini dengan menjalankan cargo build. File-file di direktori add kita seharusnya kelihatan seperti ini:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Workspace ini cuma punya satu direktori target di tingkat teratas (top level) tempat artefak hasil kompilasi bakal ditaruh; package adder tidak punya direktori target-nya sendiri. Bahkan kalau pun kita menjalankan cargo build dari dalam direktori adder, artefak hasil kompilasinya bakal tetap berujung di add/target bukannya di add/adder/target. Cargo menata struktur direktori target di sebuah workspace seperti ini karena crates di dalam sebuah workspace itu memang ditujukan buat bergantung satu sama lain. Kalau setiap crate punya direktori target-nya sendiri, setiap crate harus men-compile ulang setiap crate lainnya di dalam workspace itu buat menaruh artefaknya di direktori target-nya sendiri-sendiri. Dengan berbagi satu direktori target, crates bisa menghindari kompilasi ulang yang tidak diperlukan.

Membuat Package Kedua di dalam Workspace

Berikutnya, mari kita buat member package (paket anggota) lain di dalam workspace ini dan namakan dia add_one. Generate sebuah library crate baru bernama add_one:

$ cargo new add_one --lib
     Created library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

File Cargo.toml tingkat teratas sekarang bakal menyertakan path add_one ke dalam daftar members:

Nama file: Cargo.toml

[workspace]
resolver = "3"
members = ["adder", "add_one"]

Direktori add kita seharusnya sekarang punya direktori dan file berikut ini:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

Di dalam file add_one/src/lib.rs, mari kita tambahkan sebuah fungsi add_one:

Nama file: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

Sekarang kita bisa bikin package adder dengan binary kita bergantung pada package add_one yang punya library kita. Pertama-tama, kita harus menambahkan path dependency pada add_one ke dalam adder/Cargo.toml.

Nama file: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo tidak mengasumsikan kalau crates di dalam sebuah workspace bakal bergantung satu sama lain, jadi kita harus secara eksplisit mendefinisikan hubungan dependensi (ketergantungan) mereka.

Selanjutnya, mari kita pakai fungsi add_one (dari crate add_one) di dalam crate adder. Buka file adder/src/main.rs dan ubah fungsi main buat memanggil fungsi add_one, seperti di Listing 14-7.

Filename: adder/src/main.rs
fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Listing 14-7: Memakai library crate add_one dari dalam crate adder

Mari kita build workspace ini dengan menjalankan cargo build di direktori tingkat teratas add!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

Buat menjalankan binary crate tersebut dari direktori add, kita bisa menentukan package mana di dalam workspace yang mau kita jalankan dengan memakai argumen -p beserta nama package-nya dengan cargo run:

$ cargo run -p adder
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

Ini bakal menjalankan kode di adder/src/main.rs, yang mana bergantung pada crate add_one.

Bergantung pada Package Eksternal di dalam Workspace

Perhatikan bahwa workspace ini cuma punya satu file Cargo.lock di tingkat teratas, bukannya punya file Cargo.lock di setiap direktori crate. Ini memastikan kalau semua crates memakai versi yang persis sama buat semua dependensinya. Kalau kita menambahkan package rand ke dalam file adder/Cargo.toml dan add_one/Cargo.toml, Cargo bakal me-resolve keduanya ke satu versi dari rand dan mencatat hal itu di dalam satu file Cargo.lock tersebut. Membuat semua crates di workspace memakai dependensi yang sama berarti semua crates tersebut bakal selalu kompatibel satu sama lain. Mari kita tambahkan crate rand ke bagian [dependencies] di file add_one/Cargo.toml supaya kita bisa memakai crate rand di dalam crate add_one:

Nama file: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

Sekarang kita bisa menambahkan use rand; ke dalam file add_one/src/lib.rs, dan saat mem-build seluruh workspace dengan menjalankan cargo build di direktori add bakal ikut membawa dan men-compile crate rand. Kita bakal dapat satu peringatan (warning) karena kita belum memakai rand yang sudah kita bawa ke dalam scope tersebut:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
   --snip--
   Compiling rand v0.8.5
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

File Cargo.lock di tingkat teratas sekarang berisi informasi mengenai dependensi dari add_one terhadap rand. Namun, meskipun rand sudah dipakai di suatu tempat di dalam workspace, kita tidak bisa memakainya di crates lainnya di workspace ini kecuali kita menambahkan rand ke dalam file Cargo.toml mereka juga. Misalnya, kalau kita menambahkan use rand; ke dalam file adder/src/main.rs untuk package adder, kita bakal dapat error:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

Buat memperbaikinya, edit file Cargo.toml untuk package adder dan indikasikan kalau rand juga adalah sebuah dependensi buatnya. Mem-build package adder bakal menambahkan rand ke dalam daftar dependensi untuk adder di dalam Cargo.lock, tapi tidak akan ada salinan tambahan dari rand yang bakal di-download. Cargo bakal memastikan kalau setiap crate di setiap package di dalam workspace yang memakai package rand bakal memakai versi yang persis sama selama mereka menentukan versi dari rand yang kompatibel, hal ini menghemat kapasitas penyimpanan kita dan memastikan kalau semua crates di workspace ini bakal kompatibel satu sama lain.

Kalau crates di workspace menentukan versi yang tidak kompatibel dari dependensi yang sama, Cargo bakal mencoba me-resolve masing-masing dari mereka, tapi tetap bakal berusaha me-resolve ke sesedikit mungkin versi.

Menambahkan Pengujian ke Workspace

Untuk peningkatan selanjutnya, mari kita tambahkan sebuah pengujian buat fungsi add_one::add_one di dalam crate add_one:

Nama file: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

Sekarang jalankan cargo test di dalam direktori add di tingkat teratas. Menjalankan cargo test di sebuah workspace yang ditata seperti ini bakal menjalankan pengujian untuk semua crates di dalam workspace tersebut:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... 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/adder-3a47283c568d2b6a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Bagian pertama dari outputnya menunjukkan kalau pengujian it_works di dalam crate add_one itu sukses (passed). Bagian selanjutnya menunjukkan kalau ada nol pengujian yang ditemukan di dalam crate adder, dan lalu bagian terakhir menunjukkan kalau ada nol pengujian dokumentasi yang ditemukan di dalam crate add_one.

Kita juga bisa menjalankan pengujian buat satu crate tertentu di dalam sebuah workspace dari direktori tingkat teratas dengan memakai flag -p dan menentukan nama dari crate yang mau kita uji:

$ cargo test -p add_one
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Output ini menunjukkan kalau cargo test cuma menjalankan pengujian untuk crate add_one dan tidak menjalankan pengujian untuk crate adder.

Kalau kita mempublikasikan crates yang ada di dalam workspace ke crates.io, setiap crate di dalam workspace itu harus dipublikasikan secara terpisah. Sama seperti cargo test, kita bisa mempublikasikan satu crate tertentu di dalam workspace kita dengan memakai flag -p dan menentukan nama dari crate yang mau kita publikasikan.

Sebagai latihan tambahan, coba tambahkan crate add_two ke dalam workspace ini dengan cara yang sama seperti crate add_one!

Seiring project kita bertambah besar, pertimbangkanlah buat memakai sebuah workspace: ini memungkinkan kita buat bekerja dengan komponen-komponen yang lebih kecil dan lebih gampang dipahami ketimbang bekerja dengan satu gumpalan kode (blob of code) yang super besar. Selain itu, menyimpan crates di dalam sebuah workspace bisa bikin koordinasi antar crates jadi lebih mudah kalau mereka sering diubah secara bersamaan.