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.1ke2.4.2seharusnya aman dipasang tanpa mengubah kode. - MINOR (angka kedua) naik untuk penambahan fitur baru tanpa merusak pemakaian lama —
2.4ke2.5menambah API, tetapi kode lama tetap dikompilasi. - MAJOR (angka pertama) naik ketika API berubah secara merusak —
2.xke3.0berarti 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:
[package]
name = "semver-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.8"Bedah baris terpentingnya:
- Baris 7 (
rand = "0.8"): dependency dengan requirement dua komponen. Tanda kutip wajib karena nilai TOML berupa string; tanpa komponen PATCH, rentang bermula di0.8.0. - 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:
cargo treeOutput yang Dihasilkan:
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:
[dependencies]
bitflags = "~2.4.0"Setelah menghapus Cargo.lock lama dan membangun ulang, resolusinya:
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:
Updating crates.io index
Locking 1 package to highest Rust 1.99.0 compatible versions
Updating bitflags v2.4.2 -> v2.13.2Requirement 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:
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 manifest | Rentang 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:
cargo update -p bitflags --precise 2.6.0Output yang Dihasilkan:
Updating crates.io index
Downgrading bitflags v2.13.2 -> v2.6.0Opsi -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:
[dependencies]
rand = "=0.8.5"Output yang Dihasilkan saat manifest diperbarui:
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.