Baris rand = "0.10.3" di manifest sering disalahpahami sebagai versi terpasang yang beku. Angka tersebut sebenarnya adalah requirement — potongan rentang versi yang kita izinkan — dan cargo-lah yang memilih satu versi final dari rentang itu saat menyelesaikan dependency. Kemampuan membaca requirement merupakan keterampilan harian di ekosistem Rust: kita membacanya saat menulis manifest, saat membaca Cargo.lock, dan saat memutuskan apakah hasil cargo update aman diterima. Chapter ini membedah notasi SemVer beserta dua simbol requirement terpenting — caret ^ dan tilde ~ — lalu mengamati resolusi cargo secara nyata lewat crate rand dan bitflags.

SemVer: Tiga Angka dengan Tiga Janji #

Seluruh crate di crates.io menomori rilisnya dengan pola MAJOR.MINOR.PATCH — misalnya 2.4.2. Tiap angka membawa janji yang berbeda kepada pemakainya:

  • PATCH (angka ketiga) naik untuk perbaikan galat tanpa perubahan API — 2.4.1 ke 2.4.2 seharusnya aman dipasang tanpa mengubah kode.
  • MINOR (angka kedua) naik untuk penambahan fitur baru tanpa merusak pemakaian lama — 2.4 ke 2.5 menambah API, tetapi kode lama tetap dikompilasi.
  • MAJOR (angka pertama) naik ketika API berubah secara merusak — 2.x ke 3.0 berarti sebagian kode pemakai berpotensi harus ditulis ulang.

Konvensi Rust menambahkan satu aturan khusus untuk versi pra-stabil: selama angka MAJOR masih 0, posisi MINOR berlaku sebagai batas patah — 0.8 ke 0.9 dianggap setara rilis major. Aturan tersebut menjelaskan mengapa crate besar biasanya menomori API stabilnya mulai dari 1.0.0.

Satu sudut lagi perlu dihafal: caret pada versi 0.0.x bahkan mengunci PATCH — requirement "0.0.3" berarti rentang >=0.0.3, <0.0.4, karena pada fase paling awal ini komponen PATCH-lah satu-satunya komponen yang masih bergerak. Semakin kiri angka pertama yang bukan nol, semakin lebar jendela yang dibuka caret.

Caret: Requirement Bawaan #

Ketika kita menulis rand = "0.8" atau hasil cargo add berupa rand = "0.10.3", keduanya sama-sama requirement caret — tanda ^ boleh tidak ditulis karena merupakan bawaan. Caret mengizinkan perubahan naik sampai batas patah terdekat: untuk versi 1.x ke atas, batasnya adalah MAJOR berikutnya; untuk versi 0.x, batasnya justru MINOR berikutnya sesuai aturan pra-stabil di atas.

Kita amati perilakunya pada package percobaan semver-demo dengan manifest:

toml
[package]
name = "semver-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
rand = "0.8"

Bedah baris terpentingnya:

  1. Baris 7 (rand = "0.8"): dependency dengan requirement dua komponen. Tanda kutip wajib karena nilai TOML berupa string; tanpa komponen PATCH, rentang bermula di 0.8.0.
  2. Baris 3 (version = "0.1.0"): versi package percobaan kita sendiri — tidak terkait versi rand, karena keduanya hidup di tabel yang berbeda.

Cargo menyelesaikan dependency dengan strategi versi tertinggi di dalam rentang: dari semua versi yang memenuhi requirement, versi paling baru yang dipilih. Strategi tersebut bukan selera — memilih versi tertinggi memastikan perbaikan galat terkumpul sejak awal, dan pembaruhan berikutnya tinggal menaiki tangga yang sama.

Bangun proyek tersebut saat Cargo.lock belum ada:

bash
cargo tree

Output yang Dihasilkan:

text
    Updating crates.io index
     Locking 14 packages to highest Rust 1.99.0 compatible versions
      Adding rand v0.8.8 (available: v0.10.3)

Baris Adding rand v0.8.8 (available: v0.10.3) adalah bukti eksplisit cara kerja caret: registry menawarkan rand sampai 0.10.3, tetapi requirement "0.8" dibaca sebagai rentang >=0.8.0 dan <0.9.0, sehingga cargo memilih versi tertinggi di dalam rentang — 0.8.8 — dan menolak melompat ke 0.10.x. Requirement "0.10.3" punya makna setara: rentang >=0.10.3, <0.11.0.

Tilde: Jendela yang Lebih Sempit #

Requirement tilde memakai tanda ~ dan mengizinkan perubahan hanya pada komponen paling kanan yang disebut. Untuk versi bertiga seperti ~2.4.0, rentangnya >=2.4.0, <2.5.0 — patch bebas naik, MINOR dikunci. Tanda tersebut menarik pada crate yang perubahan MINOR-nya terasa berisiko bagi kita.

Perbedaan tilde dan caret paling terlihat saat keduanya dipakai pada crate yang rilis MINOR-nya aktif. Ganti dependency semver-demo menjadi bitflags:

toml
[dependencies]
bitflags = "~2.4.0"

Setelah menghapus Cargo.lock lama dan membangun ulang, resolusinya:

text
    Updating crates.io index
     Locking 1 package to highest Rust 1.99.0 compatible versions
      Adding bitflags v2.4.2 (available: v2.13.2)

Registry menawarkan bitflags sampai 2.13.2, tetapi tilde mengunci jendela di bawah 2.5.0, sehingga cargo memasang 2.4.2. Sekarang bandingkan dengan caret. Ubah requirement menjadi bitflags = "^2.4.0" lalu jalankan cargo update:

text
    Updating crates.io index
     Locking 1 package to highest Rust 1.99.0 compatible versions
    Updating bitflags v2.4.2 -> v2.13.2

Requirement caret membuka rentang sampai <3.0.0, sehingga cargo update bebas menaikkan bitflags ke 2.13.2. Kembalikan requirement ke tilde "~2.4.0" lalu jalankan cargo update lagi:

text
    Updating crates.io index
     Locking 1 package to highest Rust 1.99.0 compatible versions
 Downgrading bitflags v2.13.2 -> v2.4.2 (available: v2.13.2)

Kali ini cargo justru menurunkan versi — requirement baru di manifest menghapus 2.13.2 dari rentang sah, dan cargo update wajib mengembalikan pilihan ke versi tertinggi yang masih diizinkan, yaitu 2.4.2. Pesan (available: v2.13.2) sengaja menampilkan versi yang tersedia tetapi berada di luar jangkauan requirement kita.

Tabel berikut merangkum empat bentuk requirement yang telah kita temui:

Requirement di manifestRentang versi yang diterima
"2.4.0" atau "^2.4.0">=2.4.0, <3.0.0
"0.8">=0.8.0, <0.9.0
"~2.4.0">=2.4.0, <2.5.0
"=0.8.5"tepat 0.8.5, tidak ada rentang

Requirement di Manifest, Versi di Cargo.lock #

Pembagian kerja dua berkas inilah inti sistem versi cargo. Cargo.toml menyimpan niat kita — rentang versi yang boleh dipakai. Cargo.lock menyimpan keputusan — satu versi pasti hasil resolusi, misalnya entri name = "bitflags" dengan version = "2.4.2". Selama lock ada dan semua versinya masih masuk rentang, cargo build memakai versi terkunci tanpa menyentuh registry — itulah yang membuat build tim reproducible.

Perintah cargo update mengulang resolusi dari awal: setiap dependency dinaikkan ke versi tertinggi yang masih berada di dalam rentang manifest. Ketika kita ingin versi tertentu — misalnya untuk menguji satu rilis — opsi --precise memilihnya secara eksplisit:

bash
cargo update -p bitflags --precise 2.6.0

Output yang Dihasilkan:

text
    Updating crates.io index
 Downgrading bitflags v2.13.2 -> v2.6.0

Opsi -p bitflags membatasi pembaruan pada crate tersebut saja, dan --precise 2.6.0 menjatuhkan pilihan persis ke 2.6.0. Versi yang berada di luar rentang requirement tetap ditolak — permintaan --precise 3.0.0 saat requirement "^2.4.0" berakhir pada error no matching package named bitflags found, karena 3.0.0 tidak pernah masuk jendela caret.

Versi Persis dengan Tanda = #

Tanda sama dengan di depan nomor versi mematikan rentang sepenuhnya: "=0.8.5" hanya menerima 0.8.5 dan tidak lain. Kita coba pada rand:

toml
[dependencies]
rand = "=0.8.5"

Output yang Dihasilkan saat manifest diperbarui:

text
    Updating crates.io index
 Downgrading rand v0.8.8 -> v0.8.5 (available: v0.8.8)

Cargo menurunkan rand dari 0.8.8 ke 0.8.5 persis mengikuti perintah manifest.

Ringkasan #

Requirement manifest adalah rentang, bukan versi: caret "0.8" berarti >=0.8.0, <0.9.0 (batas MINOR untuk versi pra-stabil), tilde "~2.4.0" berarti >=2.4.0, <2.5.0, dan "=0.8.5" berarti satu versi tepat. Cargo.lock menyimpan hasil resolusi dari rentang tersebut, cargo update mengulang resolusi ke atas, dan cargo update -p <crate> --precise <versi> memilih versi eksplisit di dalam rentang. Rentang yang sama juga menjadi dasar komposisi fitur crate — dan komposisi fitur itulah topik Chapter berikutnya.