Lewati ke konten

Fitur Cargo dan Kompilasi Kondisional (#[cfg]) di Rust

Bayangkan kamu memesan pizza. Semua pizza dapat adonan dasar dan saus tomat. Setelah itu kamu memilih tambahan: jamur, ekstra keju, zaitun. Dapur hanya membuat apa yang kamu pesan, jadi kamu tidak membayar (atau menunggu) topping yang tidak kamu mau.

Kode Rust bekerja dengan cara yang sama. Dengan conditional compilation (kompilasi kondisional, #[cfg]), kamu bilang ke compiler “build bagian ini hanya di Windows” atau “hanya saat test”. Dengan fitur Cargo (Cargo features), orang yang memakai crate-mu memilih sendiri tambahan opsional yang mereka butuhkan, misalnya dukungan JSON. Kode yang tidak dipilih sama sekali tidak di-compile. Build tidak jadi lebih lambat karenanya, dan kode itu tidak ikut masuk ke program akhir.

Halaman ini melanjutkan Cargo dan Crate (Cargo.toml, cargo add) dan Tools dan setup Rust. Kalau istilah-istilah itu masih baru, mampir dulu sebentar ke sana.

#[cfg(...)] adalah sebuah attribute (atribut) yang bisa kamu pasang di fungsi, struct, baris use, modul, bahkan satu statement saja. Kalau kondisinya benar, kodenya tetap ada. Kalau salah, compiler membuangnya sepenuhnya, seolah-olah kamu tidak pernah menulisnya.

#[cfg(target_os = "linux")]
fn os_name() -> &'static str {
"Linux"
}
#[cfg(target_os = "windows")]
fn os_name() -> &'static str {
"Windows"
}
#[cfg(target_os = "macos")]
fn os_name() -> &'static str {
"macOS"
}
fn main() {
println!("Halo dari {}!", os_name());
}

Output:

Halo dari Linux!
▶ Coba sendiri

Rust Playground berjalan di Linux, jadi yang tercetak adalah Halo dari Linux!. Di laptop Windows-mu sendiri, kode yang sama mencetak Halo dari Windows!. Ada tiga fungsi dengan nama yang sama, tapi itu tidak masalah: di satu komputer, hanya satu yang tersisa.

Ada satu jebakan kecil di sini. Di FreeBSD, tidak ada satu pun dari tiga kondisi itu yang benar, jadi os_name tidak ada dan build berhenti dengan E0425: “cannot find function os_name in this scope” (fungsi os_name tidak ditemukan). Bagian-bagian berikutnya menunjukkan cara menambahkan fallback (cadangan).

Kadang kamu tidak ingin membuang kode. Kamu cuma ingin bertanya. Makro cfg!() menerima kondisi yang sama, tapi hasilnya berupa bool biasa:

fn main() {
let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 };
println!("Ini program {bits}-bit");
println!("usize terbesar adalah {}", usize::MAX);
if cfg!(target_os = "windows") {
println!("Bentuk path: C:\\Users\\ana");
} else {
println!("Bentuk path: /home/ana");
}
}

Output:

Ini program 64-bit
usize terbesar adalah 18446744073709551615
Bentuk path: /home/ana
▶ Coba sendiri

target_pointer_width adalah ukuran sebuah alamat memori: "64" di hampir semua komputer modern, "32" di beberapa perangkat kecil. Itu juga ukuran usize, karena itulah usize::MAX di sini besar sekali.

Jadi, kapan pakai yang mana?

#[cfg(...)] cfg!(...)
Apa yang dilakukannya Membuang kode kalau kondisinya salah Menjadi true atau false
Apakah kode untuk kondisi “salah” tetap di-compile? Tidak, kodenya hilang Ya, kedua cabang harus bisa di-compile di mana saja
Pakai untuk Kode yang tidak bisa di-compile di tempat lain (API khusus OS, crate opsional) Perbedaan kecil dalam perilaku

Baris tengah itu penting. Hal-hal khusus Unix seperti std::os::unix::fs::PermissionsExt tidak ada di Windows, jadi kode yang memakainya harus berada di balik #[cfg(unix)]. if cfg!(unix) saja tidak cukup, karena compiler di Windows tetap akan mencoba meng-compile cabang itu.

Kamu bisa menyusun kondisi besar dari kondisi-kondisi kecil:

  • not(a): benar kalau a salah
  • any(a, b, ...): benar kalau minimal satu benar (seperti “atau”)
  • all(a, b, ...): benar kalau semuanya benar (seperti “dan”)

Ini contoh OS tadi lagi, kali ini dengan fallback supaya bisa di-compile di mana saja:

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn shell() -> &'static str {
"sh"
}
#[cfg(target_os = "windows")]
fn shell() -> &'static str {
"cmd"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn shell() -> &'static str {
"unknown"
}
fn main() {
println!("Shell bawaan: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) {
println!("Sistem Unix 64-bit");
}
}

Output:

Shell bawaan: sh
Sistem Unix 64-bit
▶ Coba sendiri

unix dan windows adalah nama pendek yang praktis. unix bernilai benar di Linux, macOS, keluarga BSD, dan sistem sejenis. Inilah kondisi yang paling sering akan kamu temui:

Kondisi Benar kalau…
target_os = "linux" / "windows" / "macos" Kamu build untuk sistem operasi itu
unix / windows Kamu build untuk sistem mirip Unix apa pun / untuk Windows
target_arch = "x86_64" / "aarch64" / "wasm32" Kamu build untuk jenis prosesor itu (WebAssembly adalah wasm32)
target_pointer_width = "64" Alamat (dan usize) berukuran 64 bit
debug_assertions Ini build debug (cargo build), bukan --release
test Kamu sedang menjalankan cargo test
feature = "json" Fitur Cargo json sedang aktif (dibahas di bawah)

Mau lihat daftar lengkap untuk komputermu? Jalankan rustc --print cfg. Di PC Linux 64-bit, perintah itu mencetak baris seperti target_os="linux", target_family="unix" dan debug_assertions. Kamu akan sering melihat #[cfg(windows)] dan #[cfg(unix)] di kode FFI, karena library C sering berbeda antara satu sistem dengan yang lain.

Sebenarnya kamu sudah pernah memakai satu kondisi tanpa sadar. Setiap modul test di halaman Testing diawali dengan #[cfg(test)]:

pub fn add_points(total: u32, points: u32) -> u32 {
total + points
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_points() {
assert_eq!(add_points(10, 5), 15);
}
}

cargo test menyalakan test, jadi modul itu ikut di-build dan test-nya dijalankan. cargo build biasa membuang seluruh modul itu, jadi program aslimu tidak membawa kode test sama sekali. #[cfg] bisa dipakai di modul mana pun seperti ini (lihat Modul), misalnya #[cfg(windows)] mod windows_paths;.

#[cfg_attr(kondisi, attribute)] artinya: “kalau kondisinya benar, tambahkan attribute ini”. Contoh pemakaian yang umum adalah derive yang hanya kamu inginkan sesekali:

#[cfg_attr(debug_assertions, derive(Debug))]
struct Player {
name: String,
score: u32,
}
fn main() {
let player = Player { name: "Ana".to_string(), score: 120 };
// hanya di build debug; masuk ke stderr, bukan stdout
#[cfg(debug_assertions)]
eprintln!("debug: {:?}", player);
println!("{} punya {} poin", player.name, player.score);
}

Output:

Ana punya 120 poin
▶ Coba sendiri

Di build debug (default di Playground), kamu juga melihat debug: Player { name: "Ana", score: 120 } di bagian “Standard Error” pada output. Ubah Playground ke “Release”, dan baris itu hilang, bersama dengan kode Debug untuk Player. Perhatikan bahwa eprintln! butuh #[cfg(debug_assertions)] sendiri. Tanpa itu, build release gagal, karena di sana Player tidak mengimplementasikan Debug. Penjelasan lebih lanjut tentang build debug dan release ada di Cargo dan Crate.

Sekarang saatnya menu pizza. Sebuah fitur (feature) adalah saklar bernama yang kamu definisikan di Cargo.toml, di dalam tabel [features]. Ayo buat program kecil, pizza_order, dengan dua fitur:

  • extra_cheese: tambahan gratis, aktif secara default.
  • json: mencetak pesanan dalam format JSON. Fitur ini butuh crate serde dan serde_json, jadi keduanya hanya diunduh dan di-compile kalau ada yang meminta json.
[package]
name = "pizza_order"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
[features]
default = ["extra_cheese"]
extra_cheese = []
json = ["dep:serde", "dep:serde_json"]

Baris demi baris:

  • optional = true artinya “jangan pakai crate ini kecuali ada fitur yang memintanya”.
  • extra_cheese = [] membuat fitur yang tidak menyalakan apa-apa lagi. Isinya cuma saklar yang bisa dicek oleh kodemu.
  • json = ["dep:serde", "dep:serde_json"] berarti: kalau json aktif, nyalakan juga dua dependency opsional ini. dep: artinya “ini nama dependency, bukan nama fitur”.
  • default = [...] berisi daftar fitur yang aktif kalau tidak ada yang bilang lain.

Di dalam kode, kamu mengecek sebuah fitur dengan feature = "nama":

src/main.rs
#[cfg_attr(feature = "json", derive(serde::Serialize))]
struct Order {
size: String,
toppings: Vec<String>,
}
fn main() {
let mut toppings = vec!["tomat".to_string(), "kemangi".to_string()];
if cfg!(feature = "extra_cheese") {
toppings.push("ekstra keju".to_string());
}
let order = Order { size: "besar".to_string(), toppings };
println!("Pesanan: pizza {} dengan {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")]
println!("Dalam JSON: {}", serde_json::to_string(&order).unwrap());
}

Baris JSON memakai #[cfg], bukan cfg!. Tanpa fitur json, serde_json sama sekali bukan dependency, jadi baris itu harus benar-benar hilang. Pengecekan keju tetap bisa di-compile dalam kondisi apa pun, jadi cfg! sudah cukup.

Sekarang pesan pizzamu lewat command line. (-q hanya menyembunyikan baris “Compiling…” dari Cargo.)

Terminal window
cargo run -q # hanya fitur default
cargo run -q --features json # default + json
cargo run -q --no-default-features # tanpa tambahan, bahkan tanpa default
cargo run -q --all-features # semua fitur

Ini output aslinya:

$ cargo run -q
Pesanan: pizza besar dengan tomat, kemangi, ekstra keju
$ cargo run -q --features json
Pesanan: pizza besar dengan tomat, kemangi, ekstra keju
Dalam JSON: {"size":"besar","toppings":["tomat","kemangi","ekstra keju"]}
$ cargo run -q --no-default-features
Pesanan: pizza besar dengan tomat, kemangi
$ cargo run -q --all-features
Pesanan: pizza besar dengan tomat, kemangi, ekstra keju
Dalam JSON: {"size":"besar","toppings":["tomat","kemangi","ekstra keju"]}

Flag-flag ini bisa digabung. --no-default-features --features json memberimu JSON tanpa keju. Untuk beberapa fitur sekaligus, pisahkan dengan koma: --features json,extra_cheese. Flag yang sama juga berlaku untuk cargo build, cargo check dan cargo test.

Sering kali kamu berada di sisi lain meja kasir: kamu memakai crate yang punya fitur. Kamu sudah pernah melihat ini di Cargo dan Crate:

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
serde_json = { version = "1", features = ["preserve_order"] }
  • Fitur derive milik serde memberimu #[derive(Serialize, Deserialize)].
  • tokio tidak punya fitur default. Kalau dipasang begitu saja, hampir tidak ada yang bisa kamu pakai. full menyalakan semuanya: runtime, #[tokio::main], timer, networking, file, dan lainnya. Itu memudahkan selama kamu belajar. Di proyek sungguhan, kamu bisa meminta lebih sedikit, misalnya features = ["rt-multi-thread", "macros"].
  • preserve_order milik serde_json menjaga key objek tetap dalam urutan saat kamu memasukkannya.

cargo add menuliskan baris-baris ini untukmu: cargo add tokio --features full.

Kenapa crate memecah dirinya seperti ini? Karena setiap bagian yang tidak kamu pakai adalah kode yang tidak perlu kamu unduh, compile, atau kirim. Crate besar seperti tokio akan membuat setiap proyek kecil lambat di-build kalau selalu meng-compile semuanya. Fitur menjaga waktu compile dan ukuran binary tetap kecil.

Kamu juga bisa mematikan fitur default sebuah crate dan memilih hanya yang kamu butuhkan, dengan default-features = false:

[dependencies]
serde_json = { version = "1", default-features = false, features = ["alloc"] }

Untuk melihat fitur mana saja yang aktif di proyekmu, tanyakan ke Cargo:

Terminal window
cargo tree -e features --features json --depth 1

Untuk pizza_order, perintah ini mencetak:

pizza_order v0.1.0 (/home/ana/pizza_order)
├── serde feature "default"
├── serde feature "derive"
└── serde_json feature "default"

Tanpa --depth 1, kamu melihat seluruh pohonnya, termasuk fitur yang dinyalakan dependency-mu di dalam dependency mereka. Hasilnya panjang, tapi ini tool terbaik saat kamu bertanya-tanya “kenapa crate ini ikut di-compile?”.

Fitur sebaiknya hanya menambahkan sesuatu. Menyalakan sebuah fitur tidak boleh menghapus fungsi atau mengubah apa yang dilakukan fungsi yang sudah ada. Alasannya begini. Kalau dua crate di proyekmu sama-sama memakai serde_json, dan satu meminta preserve_order sementara yang lain tidak, Cargo meng-compile serde_json satu kali saja, dengan fitur dari keduanya dinyalakan. Jadi setiap crate harus tetap berfungsi ketika pihak lain menyalakan fitur tambahan.

Hindari fitur yang tidak bisa aktif bersamaan. Desain seperti “fitur postgres ATAU fitur sqlite, tapi jangan dua-duanya” langsung rusak begitu dua crate memilih yang berbeda, dan --all-features jadi tidak berfungsi. Lebih baik buat fitur yang bisa aktif semuanya sekaligus.

Dokumentasikan fiturmu. Tulis daftar singkat di README atau dokumentasi crate-mu: nama fitur, apa yang ditambahkannya, dan apakah fitur itu aktif secara default. Di docs.rs, setiap crate punya halaman “Feature flags” (contohnya milik tokio), dan banyak crate menandai item dengan “Available on crate feature … only” (hanya tersedia dengan fitur tertentu).

Cek setiap kombinasi di CI. Kode di balik sebuah fitur hanya di-compile kalau fitur itu aktif. Tambahkan perintah-perintah ini ke CI-mu supaya tidak ada error yang bersembunyi di sana:

Terminal window
cargo check --all-features
cargo check --no-default-features
cargo test --all-features

Untuk crate yang lebih besar, tool cargo-hack bisa mengecek setiap fitur satu per satu (cargo hack check --each-feature).

Salah ketik nama fitur. Kalau kamu menulis #[cfg(feature = "jsno")], kodenya tetap bisa di-compile. Baris JSON-nya saja yang tidak pernah tercetak, bahkan dengan --features json. Untungnya, Rust versi sekarang memperingatkanmu:

warning: unexpected `cfg` condition value: `jsno`
--> src/main.rs:17:11
|
17 | #[cfg(feature = "jsno")]
| ^^^^^^^^^^------
| |
| help: there is a expected value with a similar name: `"json"`
|
= note: expected values for `feature` are: `default`, `extra_cheese`, and `json`
= help: consider adding `jsno` as a feature in `Cargo.toml`
= note: `#[warn(unexpected_cfgs)]` on by default

Warning ini bilang nilai jsno tidak dikenal, ada nilai dengan nama mirip ("json"), dan fitur apa saja yang memang ada. Jangan abaikan warning ini. Di command line, Cargo lebih tegas: cargo run --features jsno berhenti dengan error: the package 'pizza_order' does not contain this feature: jsno (package ini tidak punya fitur tersebut) dan menyarankan json.

Mengecek nama dependency, bukan nama fitur. Karena ada dep:serde, tidak ada fitur bernama serde di pizza_order. Kalau kamu menulis #[cfg_attr(feature = "serde", ...)], kamu mendapat warning “unexpected cfg condition value” yang sama, kali ini untuk serde. Lalu cargo run --features json gagal dengan E0277: “the trait bound Order: serde::Serialize is not satisfied” (Order tidak mengimplementasikan serde::Serialize), karena derive-nya tidak pernah ditambahkan. Cek nama yang ada di [features]: feature = "json".

Lupa menulis dep:. Begitu ada satu fitur yang menulis dep:serde, kamu harus memakai dep: untuk serde di mana-mana. Tambahkan fitur baru yaml = ["serde"], dan Cargo menolak membaca file-nya:

error: failed to parse manifest at `/home/ana/pizza_order/Cargo.toml`
Caused by:
feature `yaml` includes `serde`, but `serde` is an optional dependency without an implicit feature
Use `dep:serde` to enable the dependency.

Cargo menjelaskan bahwa serde adalah dependency opsional tanpa fitur implisit, lalu memintamu memakai dep:serde. Perbaikannya adalah yaml = ["dep:serde"]. Dan kalau kamu tidak menulis dep: sama sekali di mana pun, Cargo diam-diam membuat fitur publik untuk setiap dependency opsional (di sini --features serde akan berfungsi). Itu gaya lama. Dengan dep:, daftar fiturmu tetap rapi dan disengaja.

Kode yang tidak pernah di-compile. Misalnya kamu salah ketik serde_json::to_strng di baris yang memakai #[cfg(feature = "json")]. cargo check bilang semuanya baik-baik saja, karena baris itu tidak di-compile secara default. Hanya cargo check --all-features yang menemukannya:

error[E0425]: cannot find function `to_strng` in crate `serde_json`
--> src/main.rs:18:44
|
18 | println!("Dalam JSON: {}", serde_json::to_strng(&order).unwrap());
| ^^^^^^^^
|
help: a function with a similar name exists
|
18 | println!("Dalam JSON: {}", serde_json::to_string(&order).unwrap());
| +

Compiler tidak menemukan to_strng di serde_json dan menyarankan to_string. Inilah alasan --all-features wajib ada di CI.

Tulis fungsi config_dir() yang mengembalikan lokasi penyimpanan pengaturan sebuah aplikasi catatan: "~/.config/notes" di Linux, "~/Library/Application Support/notes" di macOS, r"%APPDATA%\notes" di Windows, dan "./notes" di sistem lainnya. Cetak hasilnya di main. Petunjuk: satu fungsi untuk setiap kasus dengan #[cfg(target_os = "...")], dan not(any(...)) untuk fallback-nya.

Lihat solusi
#[cfg(target_os = "linux")]
fn config_dir() -> &'static str {
"~/.config/notes"
}
#[cfg(target_os = "macos")]
fn config_dir() -> &'static str {
"~/Library/Application Support/notes"
}
#[cfg(target_os = "windows")]
fn config_dir() -> &'static str {
r"%APPDATA%\notes"
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn config_dir() -> &'static str {
"./notes"
}
fn main() {
println!("Pengaturan disimpan di {}", config_dir());
}

Output:

Pengaturan disimpan di ~/.config/notes
▶ Coba sendiri

Di komputer mana pun, tepat satu dari empat fungsi itu yang tersisa, jadi main selalu menemukan sebuah config_dir. Playground berjalan di Linux, jadi kamu melihat path Linux. Aplikasi sungguhan akan memakai crate seperti dirs untuk menemukan folder-folder ini dengan benar.

Kamu punya program kecil leaderboard yang mencetak skor. Tambahkan fitur export (mati secara default) yang menarik serde_json dan menambahkan fungsi export_json. Dengan --features export, program itu juga harus mencetak skor dalam format JSON. Petunjuk: jadikan serde_json opsional, pakai dep:serde_json di fiturnya, dan pasang #[cfg(feature = "export")] di fungsinya sekaligus di baris yang memanggilnya. Playground tidak bisa mengatur fitur Cargo, jadi coba latihan ini dengan cargo new leaderboard di komputermu.

Lihat solusi
[package]
name = "leaderboard"
version = "0.1.0"
edition = "2024"
[dependencies]
serde_json = { version = "1", optional = true }
[features]
export = ["dep:serde_json"]
src/main.rs
struct Score {
name: &'static str,
points: u32,
}
#[cfg(feature = "export")]
fn export_json(scores: &[Score]) -> String {
let list: Vec<serde_json::Value> = scores
.iter()
.map(|s| serde_json::json!({ "name": s.name, "points": s.points }))
.collect();
serde_json::Value::Array(list).to_string()
}
fn main() {
let scores = [
Score { name: "Ana", points: 120 },
Score { name: "Bo", points: 95 },
];
for s in &scores {
println!("{}: {} poin", s.name, s.points);
}
#[cfg(feature = "export")]
println!("Ekspor: {}", export_json(&scores));
}
$ cargo run -q
Ana: 120 poin
Bo: 95 poin
$ cargo run -q --features export
Ana: 120 poin
Bo: 95 poin
Ekspor: [{"name":"Ana","points":120},{"name":"Bo","points":95}]

Tidak ada baris default = [...], jadi export mati kecuali kamu memintanya. Tanpa fitur itu, serde_json bahkan tidak diunduh. Fungsi dan pemanggilannya sama-sama butuh #[cfg], karena keduanya menyebut serde_json.

  • #[cfg(kondisi)] mempertahankan kode hanya kalau kondisinya benar. Kalau tidak, kode itu dibuang sebelum di-compile.
  • cfg!(kondisi) hanyalah sebuah bool. Kedua cabangnya tetap harus bisa di-compile di setiap platform.
  • Gabungkan kondisi dengan not(..), any(..) dan all(..). Yang paling umum adalah target_os, unix/windows, debug_assertions dan test.
  • #[cfg_attr(kondisi, attr)] menambahkan sebuah attribute hanya kalau kondisinya benar.
  • Fitur Cargo ditulis di [features]. Pakai optional = true ditambah dep:nama untuk crate opsional, dan default = [...] untuk fitur yang aktif secara default.
  • Fitur sebaiknya hanya menambahkan sesuatu. Jalankan cargo check --all-features di CI, dan perhatikan warning unexpected_cfgs.

Kuis singkat

1. Apa yang terjadi pada fungsi bertanda #[cfg(target_os = "windows")] kalau kamu build di Linux?

2. Kenapa if cfg!(unix) { ... } tidak cukup untuk kode yang memakai API khusus Unix?

3. Di Cargo.toml, apa fungsi json = ["dep:serde_json"]?

4. Kamu tidak sengaja menulis #[cfg(feature = "jsno")]. Apa yang terjadi?

Skor: 0 / 4