Aktivasi New Architecture pada React Native (Fabric dan TurboModules) memindahkan beban render dan bridging ke layer C++. Konsekuensinya, Android NDK dan CMake harus mengompilasi puluhan dependensi native seperti Folly, Glog, Yoga, dan ReactCommon dari source code. Di lingkungan CI yang bersifat ephemeral, kompilasi ulang kode C++ ini terjadi berulang kali pada setiap commit, membuang waktu eksekusi runner dan memperlambat feedback loop tim.

Ccache (Compiler Cache) memangkas durasi tersebut dengan menyimpan output kompilasi C/C++ ke dalam disk cache berdasarkan hash input preprosesor. Artikel ini menguraikan langkah integrasi Ccache ke dalam pipeline GitHub Actions untuk proyek React Native Android.

1. Injeksi Ccache ke CMake via Gradle

Android Gradle Plugin (AGP) mengeksekusi CMake untuk mengompilasi file native. Agar CMake menggunakan Ccache sebagai wrapper compiler Clang/Clang++, oper flag CMAKE_C_COMPILER_LAUNCHER dan CMAKE_CXX_COMPILER_LAUNCHER melalui block externalNativeBuild di android/app/build.gradle.

Gunakan deteksi binary agar konfigurasi tetap aman dijalankan di mesin lokal pengembang yang belum tentu memasang Ccache:

// android/app/build.gradle
android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                def ccacheExecutable = "which ccache".execute().text.trim()
                if (ccacheExecutable) {
                    arguments "-DCMAKE_C_COMPILER_LAUNCHER=${ccacheExecutable}",
                              "-DCMAKE_CXX_COMPILER_LAUNCHER=${ccacheExecutable}"
                }
            }
        }
    }
}

Logika ini memeriksa keberadaan executable ccache melalui perintah shell. Jika ditemukan di PATH, CMake akan mengarahkan pemanggilan Clang melalui launcher Ccache.

2. Konfigurasi Environment dan Normalisasi Path

Kesalahan umum integrasi Ccache pada CI adalah cache miss akibat path absolut runner yang berbeda di setiap workflow run. Variabel environment CCACHE_BASEDIR wajib disetel ke root repository untuk menormalkan path file dalam debug symbols dan tracking dependensi.

Variabel penting yang perlu disetel sebelum build:

  • CCACHE_DIR: Lokasi direktori cache di runner, misalnya ~/.ccache.
  • CCACHE_BASEDIR: Direktori root proyek (${{ github.workspace }}) agar hashing file tidak terpengaruh lokasi absolut.
  • CCACHE_MAXSIZE: Batas ukuran cache (misal 2G atau 5G) untuk mencegah cache melebihi quota runner.
  • CCACHE_COMPRESS: Bernilai 1 untuk mengompresi object files, menghemat transfer IO dan space cache storage.

3. Setup Workflow GitHub Actions

Pasang Ccache runner runner via apt-get, atur persistent cache menggunakan actions/cache, lalu jalankan task build. Tambahkan pembacaan metrik sebelum dan sesudah kompilasi.

name: Android Fabric Build
on: [push, pull_request]

jobs:
  build-android:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Install Ccache
        run: sudo apt-get update && sudo apt-get install -y ccache

      - name: Restore Ccache
        uses: actions/cache@v4
        with:
          path: ~/.ccache
          key: ccache-android-${{ runner.os }}-${{ hashFiles('package-lock.json', 'android/app/build.gradle') }}
          restore-keys: |
            ccache-android-${{ runner.os }}-

      - name: Setup Ccache Environment
        run: |
          echo "CCACHE_DIR=${HOME}/.ccache" >> $GITHUB_ENV
          echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
          echo "CCACHE_COMPRESS=1" >> $GITHUB_ENV
          echo "CCACHE_MAXSIZE=2G" >> $GITHUB_ENV
          ccache -z # Reset statistik sebelum build

      - name: Compile Android APK
        run: |
          cd android
          ./gradlew assembleRelease --no-daemon

      - name: Evaluasi Metrik Ccache
        if: always()
        run: ccache -s

4. Memantau Hit Rate via `ccache -s`

Perintah ccache -s menghasilkan ringkasan utilisasi cache. Parameter utama yang menentukan efektivitas integrasi adalah Cache hit rate.

  • Direct cache hit: Source code dan macro identik, file objek langsung dipakai dari cache tanpa pemanggilan preprosesor compiler.
  • Preprocessed cache hit: Source code berubah pada baris komentar atau whitespace, namun hasil preprosesor menghasilkan token identik.
  • Cache miss: Terjadi pada build awal (cold cache) atau ketika file C++ diubah.

Pada pipeline warm cache, hit rate Fabric builds idealnya berada di atas 75% - 90% untuk kode ReactCommon, Folly, dan TurboModules third-party yang tidak mengalami perubahan.

5. Strategi Invalidasi Cache

Cache C++ yang tidak di-invalidasi saat compiler berubah dapat menyebabkan build error misterius atau binary mismatch. Invalidasi harus dipicu secara ketat oleh parameter berikut:

  1. Update NDK: Perubahan versi Android NDK membawa toolchain dan standard library (libc++) baru. Sertakan versi NDK pada primary cache key GitHub Actions jika Anda mengontrol versinya via build.gradle.
  2. Update React Native Core: Perubahan versi di package.json sering kali membawa patch source code native C++ internal. Menyertakan lockfile (yarn.lock atau package-lock.json) pada hash key mencegah pemakaian cache dari source lama.
  3. Build Flags Compiler: Perubahan flag optimasi (seperti -O2 ke -O3) otomatis di-hash oleh Ccache, tetapi perubahan macro global sebaiknya diimbangi dengan mereset cache jika terjadi anomali linking.
Catatan: Hindari menyertakan timestamp atau commit SHA pada primary key cache, karena hal itu akan mengubah primary key di setiap push dan mematikan fungsi pemanfaatan kembali cache branch sebelumnya. Gunakan restore-keys prefix untuk fallback.