Sebagian kebutuhan program tidak bisa — atau tidak efisien — dipenuhi oleh kode yang ditulis tangan. Daftar kota yang dibaca dari berkas data perlu menjadi array Rust saat kompilasi; nomor versi rilis perlu tertanam di binary; program lintas platform perlu menyesuaikan diri dengan sistem operasi target jauh sebelum main berjalan. Cargo menyediakan pintu untuk ketiganya: build script, program Rust kecil bernama build.rs yang dikompilasi dan dijalankan lebih dulu setiap kali package dibangun. Build script dapat membaca berkas, menghasilkan kode sumber baru, dan mengirim instruksi ke cargo lewat directive khusus. Chapter ini membuat package yang mengubah daftar kota berteks polos menjadi konstanta array bawaan, membuktikan kode yang di-generate benar-benar terkompilasi, lalu mengamati kapan cargo memutuskan menjalankan ulang script tersebut.

Kapan Build Script Dibutuhkan #

Tiga keluarga kebutuhan paling umum. Pertama, generated code: data yang dikelola di luar Rust — daftar kota, tabel konversi, skema — diterjemahkan menjadi kode Rust supaya pemakaianya secepat konstanta biasa, tanpa membaca berkas saat runtime. Kedua, deteksi lingkungan: build script dapat memeriksa sistem operasi dan library terpasang di mesin build, lalu menyesuaikan flag kompilasi. Ketiga, environment build: menanam nilai seperti waktu kompilasi atau hash commit ke dalam binary.

Keluarga ketiga punya pintu khusus yang lebih ringan daripada menghasilkan berkas: directive cargo:rustc-env. Script cukup mencetak println!("cargo:rustc-env=WARUNG_NAMA=Warung Nusantara");, lalu kode program mengambil nilainya dengan makro env!("WARUNG_NAMA") — nilai terukir sebagai string literal saat kompilasi tanpa satu berkas pun di-generate. Percobaan nyata pada pola ini menghasilkan output Selamat datang di Warung Nusantara dari program satu baris.

Satu batasan penting dipahami sejak awal: build script berjalan di mesin tempat cargo build dijalankan, sebelum program kita jadi. Build script bukan tempat untuk logika program, dan kode yang di-generate harus tetap diperiksa seperti kode tulisan tangan — kesalahan di dalamnya muncul sebagai galat kompilasi.

Proyek kota-app #

Package kota-app menampilkan daftar kota dari berkas data. Struktur foldernya:

text
kota-app/
├── Cargo.toml
├── build.rs
├── daftar-kota.txt
└── src/
    └── main.rs

Berkas data daftar-kota.txt berisi nama kota satu per baris:

text
Bandung
Jakarta
Surabaya
Yogyakarta

Manifestnya package biasa — keberadaan berkas build.rs di akar sudah cukup membuat cargo memperlakukannya sebagai build script tanpa deklarasi tambahan. Nama berkasnya memang khusus: hanya build.rs di akar package yang dikenali secara otomatis; nama lain harus dinyatakan lewat kunci build di manifest. Isi build.rs:

rust
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    println!("cargo:rerun-if-changed=daftar-kota.txt");

    let isi = fs::read_to_string("daftar-kota.txt").expect("daftar-kota.txt tidak ditemukan");
    let baris: Vec<String> = isi
        .lines()
        .map(|kota| format!("    \"{}\",", kota.trim()))
        .collect();

    let out_dir = env::var("OUT_DIR").unwrap();
    let tujuan = Path::new(&out_dir).join("kota_generated.rs");
    fs::write(
        tujuan,
        format!("pub const DAFTAR_KOTA: &[&str] = &[\n{}];\n", baris.join("\n")),
    )
    .expect("gagal menulis berkas hasil generate");
}

Sedangkan src/main.rs memakai hasil generate:

rust
include!(concat!(env!("OUT_DIR"), "/kota_generated.rs"));

fn main() {
    println!("Jumlah kota terdaftar: {}", DAFTAR_KOTA.len());
    for (urut, kota) in DAFTAR_KOTA.iter().enumerate() {
        println!("{}. {}", urut + 1, kota);
    }
}

Jalankan programnya:

bash
cargo run

Output yang Dihasilkan:

text
   Compiling kota-app v0.1.0 (/tmp/w5a/kota-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/kota-app`
Jumlah kota terdaftar: 4
1. Bandung
2. Jakarta
3. Surabaya
4. Yogyakarta

Perhatikan laporan build tersebut hanya menampilkan satu baris Compiling kota-app — padahal di baliknya cargo sudah mengompilasi build.rs sebagai program terpisah, menjalankannya, menunggu berkas kota_generated.rs selesai ditulis, baru kemudian mengompilasi src/main.rs dengan hasil generate di tempatnya. Seluruh tarian itu dianggap detail internal dan diringkas menjadi satu baris.

Mekanisme: Kode yang Berjalan Sebelum Kode #

Alur kerjanya bertingkat. Pertama, cargo mengompilasi build.rs menjadi program terpisah di target/debug/build/kota-app-*/, lalu menjalankan program tersebut dengan working directory di akar package — alasan daftar-kota.txt cukup ditulis sebagai path relatif. Kedua, cargo menyediakan variable environment OUT_DIR berisi path folder output khusus untuk script ini; folder tersebut berada di dalam target/ sehingga ikut terbersihkan oleh cargo clean. Ketiga, setiap baris yang dicetak build script ke stdout dengan awalan cargo: dibaca sebagai directive — instruksi untuk cargo, bukan teks biasa. Contoh kota-app memakai satu directive, tetapi daftar directive cargo panjang: cargo:rustc-env untuk menanam environment seperti contoh WARUNG_NAMA di atas, cargo:rerun-if-env-changed untuk memicu ulang saat environment berubah, dan beberapa lainnya tercantum di dokumentasi resmi The Cargo Book.

Bedah build.rs baris demi baris:

  1. Baris 6 (println!("cargo:rerun-if-changed=daftar-kota.txt");): directive rerun-if-changed memberi tahu cargo satu-satunya berkas yang membuat script perlu dijalankan ulang. Tanpa directive ini, cargo memakai kebiasaan lama: script dijalankan ulang saat berkas package mana pun berubah — perilaku yang membuang waktu pada package besar.
  2. Baris 8 (let isi = fs::read_to_string("daftar-kota.txt")): membaca seluruh isi berkas data menjadi satu String; .expect menghentikan build dengan pesan jelas bila berkas tidak ada — galat pada build script diperlakukan cargo sebagai kegagalan build, bukan peringatan.
  3. Baris 9-12 (let baris: Vec<String> = isi): memotong teks per baris, memangkas spasi, lalu memformat tiap kota menjadi item array "Nama", dengan indentasi — murni pembentukan teks kode Rust.
  4. Baris 14 (let out_dir = env::var("OUT_DIR").unwrap();): mengambil path folder output yang disediakan cargo.
  5. Baris 16-20 (fs::write(): merangkai kepala deklarasi pub const DAFTAR_KOTA: &[&str], isi dari baris, lalu menuliskan berkas kota_generated.rs ke dalam OUT_DIR.

Bedah src/main.rs:

  1. Baris 1 (include!(concat!(env!("OUT_DIR"), "/kota_generated.rs"));): makro bekerja dari dalam ke luar — env!("OUT_DIR") membaca variable environment tersebut saat kompilasi dan menuliskannya sebagai string literal, concat! menempelkan nama berkas, lalu include! menyalin seluruh isi berkas tujuan ke posisi baris ini seolah diketik tangan.
  2. Baris 4 (println!("Jumlah kota terdaftar: {}", DAFTAR_KOTA.len());): konstanta hasil generate dipakai seperti konstanta biasa — tidak ada pembacaan berkas saat runtime sama sekali.

Hasil generate bisa kita lihat langsung di folder outputnya, target/debug/build/kota-app-*/out/kota_generated.rs:

rust
pub const DAFTAR_KOTA: &[&str] = &[
    "Bandung",
    "Jakarta",
    "Surabaya",
    "Yogyakarta",
    "Medan",
    "Denpasar",];

Isinya kode Rust tulen — persis bentuk yang akan kita tulis sendiri bila tidak malas.

rerun-if-changed dalam Praktik #

Ketika kita menambah dua baris kota di daftar-kota.txt lalu menjalankan cargo run, hasilnya berubah:

bash
cargo run

Output yang Dihasilkan:

text
   Compiling kota-app v0.1.0 (/tmp/w5a/kota-app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/kota-app`
Jumlah kota terdaftar: 6
1. Bandung
2. Jakarta
3. Surabaya
4. Yogyakarta
5. Medan
6. Denpasar

Baris Compiling muncul karena rantai kejadian menyala berurutan: berkas data berubah, directive rerun-if-changed memicu cargo menjalankan ulang build script, script menulis ulang kota_generated.rs, rustc mendeteksi berkas include berubah dan mengompilasi ulang crate, dan binary baru mencetak enam kota. Percobaan sebaliknya sama meyakinkannya: menyentuh src/main.rs saja membuat crate dikompilasi ulang, tetapi build script tidak dijalankan karena daftar-kota.txt tidak berubah — directive mempersempit pemicu rebuild dengan tepat. Detail pembuktian ini tampak dengan flag verbose: cargo build -v memperlihatkan baris Running .../build-script-build hanya pada kasus pertama, bukan pada kasus kedua.

Ringkasan #

Berkas build.rs menjadikan package memiliki dua program: script yang berjalan saat build dan crate yang dibangunnya. Script menerima OUT_DIR dari cargo, membaca berkas data dengan path relatif ke akar package, menuliskan kode Rust hasil generate, dan mengirim directive cargo:rerun-if-changed=... agar cargo tahu kapan menjalankan ulang; sisi crate memuat hasil generate lewat include!(concat!(env!("OUT_DIR"), "/...")). Pola ini menjadikan data eksternal secepat konstanta tanpa pembacaan berkas saat runtime. Perkakas cargo yang tersisa — memeriksa pohon dependency, memperbarui versi, dan menumbuhkan dokumentasi — kita rangkai dalam alur kerja pada Chapter berikutnya.