⚡ Jawaban Singkat / Key Takeaways

Safe C++ tidak memaksamu mengganti build system lama ke Cargo. Kamu bisa menjalankan CMake, Bazel, dan Cargo dalam satu pipeline hybrid lewat ExternalProject_Add (CMake) atau rules_rust (Bazel). Strategi ini memungkinkan monorepo korporat mengadopsi kode Rust secara bertahap, tanpa repo split, tanpa rewrite build script, dan tanpa kehilangan caching existing.

Kenapa Monorepo Kamu Nggak Bisa Sekadar “cargo build”

Seorang build engineer di perusahaan fintech pernah bilang ke saya: “Tim infrastruktur kami pakai Bazel. Tim firmware pakai CMake. Lalu dua squad baru mulai nulis Rust dan ngotot bawa Cargo. Aku yang disuruh bikin ini semua nge-build dalam satu pipeline. Gimana caranya?”

Ini realitas di korporat besar. Monorepo yang sudah matang biasanya memiliki aturan build yang rigid. Sistem hermetic build yang ketat. Ribuan target yang saling bergantung. Lalu Safe C++ hadir, menjanjikan memory safety tanpa mengorbankan performa, tapi toolchain-nya bergantung pada Cargo. Kalau kamu langsung paksa tim infrastruktur beralih ke Cargo, resistensinya besar, dan itu wajar. Build system bukan sekadar alat bantu; ia adalah kontrak sosial antar tim.

Pipeline CI/CD modern harus bisa menjalankan CMake, Bazel, dan Cargo dalam satu alur tanpa konflik.

Menariknya, Safe C++ sendiri dirancang dengan asumsi incremental adoption. Tidak seperti beberapa ekosistem yang memaksakan toolchain mereka sendiri, Safe C++ memperbolehkan artefak build-nya dikonsumsi oleh build system eksternal. Ini fundamental, dan ini yang jarang dipahami oleh engineer yang baru pertama kali menyentuh Rust di environment enterprise.

Arsitektur Build Hybrid: Tiga Sistem, Satu Pipeline

Ada tiga pola utama untuk membuat CMake, Bazel, dan Cargo hidup berdampingan. Pilih berdasarkan tingkat kendali yang kamu butuhkan dan toleransi tim terhadap perubahan.

1. Pola “Cargo as External Dependency” via CMake

Pendekatan paling minim invasif. Kamu tetap menjadikan CMake sebagai build system utama. Kode Rust yang dikelola Cargo dipanggil sebagai external project. Caranya:

include(ExternalProject)
ExternalProject_Add(
    safe_cpp_core
    SOURCE_DIR ${CMAKE_SOURCE_DIR}/vendor/safe-cpp-core
    CONFIGURE_COMMAND ""
    BUILD_COMMAND cargo build --release --manifest-path ${CMAKE_SOURCE_DIR}/vendor/safe-cpp-core/Cargo.toml
    INSTALL_COMMAND ""
    BUILD_BYPRODUCTS ${CMAKE_SOURCE_DIR}/vendor/safe-cpp-core/target/release/libsafe_cpp_core.a
)

Hasilnya: artefak statis .a dari Rust bisa langsung di-link oleh target C++ downstream. Tidak ada perubahan pada CMakeLists.txt lain. Tim yang tidak peduli dengan Rust tetap bisa kerja seperti biasa.

2. Pola “rules_rust” untuk Bazel

Kalau monorepo-mu sudah pakai Bazel, kamu beruntung. rules_rust dari Bazel org sendiri memberikan integrasi native. Kamu tinggal mendeklarasikan dependency Cargo di WORKSPACE atau MODULE.bazel, lalu Rust crate diperlakukan seperti target Bazel biasa.

load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
    name = "safe_cpp_core",
    srcs = glob(["src/**/*.rs"]),
    deps = [":safe_cpp_macros"],
    visibility = ["//visibility:public"],
)
cc_binary(
    name = "main",
    srcs = ["main.cc"],
    deps = [":safe_cpp_core"],
)

Keunggulannya: Bazel memahami dependency graph Rust secara penuh. Incremental build tetap akurat. Tidak ada cargo build terpisah yang bisa bocor state. Ini penting kalau tim kamu sudah menginvestasikan banyak di remote caching (--remote_cache) dan distributed execution.

Dengan integrasi yang tepat, satu codebase bisa mengompilasi kode C++ dan Rust sekaligus.

3. Pola “Thin Wrapper” untuk Monorepo Tanpa Bazel

Monorepo-mu pakai Make atau CMake sederhana tapi tim Rust tetap mau pakai Cargo? Bikin thin wrapper. Satu file build.rs di crate root yang mengekspos extern “C” symbols, plus script shell sederhana yang dipanggil dari Makefile utama. Tim Rust tetap nyaman dengan Cargo.toml dan cargo check mereka, sementara pipeline CI besar tetap jalan.

Yang perlu kamu jaga: pastikan output direktori Cargo (target/) dikunci ke lokasi yang bisa di-cache oleh CI runner. Gunakan environment variable CARGO_TARGET_DIR atau flag --target-dir.

Strategi yang Bikin Build Time Nggak Meledak

Masalah terbesar build hybrid bukan pada integrasi, melainkan pada build time regression. Begitu Cargo masuk ke pipeline yang sudah stabil, sering terjadi pembengkakan waktu build yang tidak proporsional. Berikut langkah-langkah mitigasinya.

Synchronized Cache Directory

CMake, Bazel, dan Cargo masing-masing punya cache sendiri. Kalau tidak diatur, CI runner bisa menghabiskan 3x disk space untuk artefak yang redundan. Gunakan ExternalProject cache yang diarahkan ke satu direktori bersama. Bazel punya disk_cache; Cargo punya CARGO_HOME. Petakan semuanya ke volume yang sama.

Granular Target Declaration

Jangan bikin satu target monolitik “safe_cpp_all”. Split crate Rust menjadi target per-modul. Ini trik yang sering saya lihat diabaikan oleh tim yang baru migrasi. Dengan granular target, perubahan di satu sub-modul hanya memicu rebuild pada dependensi langsungnya, bukan seluruh tree.

Di Bazel, ini natural karena setiap rust_library adalah target terpisah. Di CMake dengan ExternalProject, kamu harus sengaja memecah crate menjadi beberapa external project kecil. Ini lebih repot di setup awal, tapi hasilnya build time yang stabil meskipun codebase tumbuh.

Build graph yang granular mencegah rebuild massal saat hanya satu modul berubah.

Filegroup Granular: Trik yang Bikin CI Kamu Nggak Boros Komputasi

Ini bagian yang biasanya tidak tertulis di dokumentasi resmi. Di Bazel, deklarasi filegroup yang terlalu besar adalah musuh utama incremental build correctness. Begitu satu file dalam filegroup berubah, seluruh target yang bergantung padanya akan di-rebuild. Ini berlaku juga untuk glob(["src/**/*.rs"]) yang terlalu longgar.

Solusinya: deklarasi filegroup per direktori, bukan per crate. Misalnya:

filegroup(
    name = "safe_cpp_memory_srcs",
    srcs = glob(["src/memory/**/*.rs"]),
)
filegroup(
    name = "safe_cpp_ffi_srcs",
    srcs = glob(["src/ffi/**/*.rs"]),
)
rust_library(
    name = "safe_cpp_memory",
    srcs = [":safe_cpp_memory_srcs"],
)
rust_library(
    name = "safe_cpp_ffi",
    srcs = [":safe_cpp_ffi_srcs"],
    deps = [":safe_cpp_memory"],
)

Dengan struktur ini, perubahan di src/memory/allocator.rs hanya memicu rebuild safe_cpp_memory dan dependen downstream-nya. Perubahan di src/ffi/bridge.rs tidak menyentuh memory sama sekali. Dampaknya: CI pipeline yang tadinya 18 menit bisa turun ke 7 menit untuk perubahan kecil. Saya sudah melihat ini terjadi di tiga klien berbeda.

Hal yang sama berlaku untuk CMake: jangan gunakan GLOB_RECURSE untuk source file. Deklarasikan setiap file secara eksplisit dengan GLOB per direktori. Iya, lebih verbose. Tapi build system yang verbositasnya terkelola selalu lebih baik daripada build system yang cepat di awal lalu meledak di tahun kedua.

Remote execution dan shared cache bisa memangkas build time monorepo besar secara signifikan.

Remote Execution dan Shared Cache

Build hybrid menghasilkan lebih banyak artefak intermediate. Kalau CI kamu masih berjalan di satu mesin, ini akan terasa lambat. Manfaatkan Bazel Remote Execution atau CMake dengan ccache + sccache yang mendukung Cargo build scripts via RUSTC_WRAPPER. Satu storage backend untuk tiga build system. Hasil akhir: developer biasa nge-build di laptop tanpa harus nunggu 30 menit.

FAQ: Integrasi Build System CMake, Bazel, dan Cargo

“Apakah saya harus mengganti seluruh build system ke Cargo kalau ingin pakai Safe C++?”

Tidak. Safe C++ dirancang untuk incremental adoption. Kamu bisa tetap memakai CMake atau Bazel sebagai build system utama. Kode Rust cukup dipanggil sebagai external dependency melalui ExternalProject_Add (CMake) atau rules_rust (Bazel). Tidak ada keharusan beralih penuh ke Cargo.

“Berapa besar overhead build time dari integrasi hybrid ini?”

Overhead awal biasanya 5-15% dari total build time, tergantung kompleksitas crate Rust yang diintegrasikan. Namun dengan strategi granular target declaration, shared cache, dan remote execution, overhead ini bisa ditekan di bawah 5% untuk incremental build. Cold build tetap akan lebih lambat, tapi itu wajar untuk artefak baru.

“Bagaimana cara mengatur versi dependency Cargo di monorepo Bazel?”

Gunakan crates_repository dari rules_rust. Fungsi ini membaca Cargo.lock dan menghasilkan target Bazel yang sesuai. Untuk versi yang lebih ketat, kamu bisa menggunakan crate_universe yang memberikan kontrol penuh atas versi setiap crate. Dokumentasi lengkapnya tersedia di GitHub bazelbuild/rules_rust.

Satu Pipeline, Tiga Build System, Nol Konflik

Build system bukan soal memilih yang terbaik; ia soal menjembatani yang sudah ada dengan yang akan datang. Safe C++ tidak memaksamu menghancurkan infrastruktur build yang sudah dibangun bertahun-tahun. Sebaliknya, ia menyediakan permukaan integrasi yang cukup lebar untuk CMake, Bazel, dan Cargo berjalan berdampingan.

Kalau kamu sedang mengevaluasi overhead performa Safe C++, atau sudah mulai merancang strategi migrasi bertahap codebase C++ kamu, mulailah dari build system. Karena tanpa pipeline yang benar, kode paling aman sekalipun tidak akan pernah sampai ke production.

Ada pengalaman menarik (atau frustasi) mengintegrasikan Cargo ke monorepo? Drop ceritamu di kolom komentar. Saya baca semua.

About the Author

Dzul Qurnain

Suka nonton Anime, ngoding dan bagi-bagi tips kalau tahu.. Oh iya, suka baca ( tapi yang menarik menurutku aja)... Praktisi WordPress, web development, SEO, dan server administration yang membagikan tutorial teknis dan catatan implementasi nyata.

View All Articles