Reproducible build adalah proses build yang menghasilkan binary yang byte-identical setiap kali dijalankan di kondisi yang sama — kode sumber yang sama, compiler yang sama, dan flag yang sama harus menghasilkan file binary dengan hash SHA-256 yang identik. Tanpa reproducible build, dua orang yang mengompilasi kode sumber yang sama bisa mendapatkan binary dengan hash berbeda, sehingga mustahil memverifikasi bahwa binary yang didistribusikan benar-benar berasal dari kode sumber yang dipublikasikan. Sumber non-determinisme yang umum: path file yang di-embed, timestamp build, urutan file di archive, dan nomor acak internal compiler. Pada chapter ini kita akan mempraktikkan flag -ffile-prefix-map, -frandom-seed, dan variabel SOURCE_DATE_EPOCH untuk menghasilkan binary reproducible.

Sumber Non-Determinisme: Path Embedding #

Compiler sering menyimpan path file sumber di binary — di info debug, di assertion message, atau di symbol table. Bila dua developer mengompilasi kode yang sama tetapi dari direktori yang berbeda (/home/budi/myapp/main.c vs /home/siti/myapp/main.c), binary yang dihasilkan akan berbeda. Mari kita verifikasi:

c
#include <stdio.h>

int sum_array(const int *a, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += a[i];
    }
    return total;
}

int main(void) {
    int a[] = {10, 20, 30, 40};
    printf("sum = %d\n", sum_array(a, 4));
    printf("source: %s\n", __FILE__);
    return 0;
}

Kompilasi dengan path absolut di direktori yang berbeda:

bash
mkdir -p /tmp/test_repro_dir_a /tmp/test_repro_dir_b
cp main_v2.c /tmp/test_repro_dir_a/
cp main_v2.c /tmp/test_repro_dir_b/

cd /tmp/test_repro_dir_a
gcc -std=c11 -Wall -Wextra -O2 $(pwd)/main_v2.c -o app_abs_no_map
strings app_abs_no_map | grep -E "test_repro" | head -3

Output yang Dihasilkan:

text
/tmp/test_repro_dir_a/main_v2.c

Perintah strings mengekstrak string tercetak dari binary. Path /tmp/test_repro_dir_a/main_v2.c muncul di binary karena GCC menyimpan nama file di tabel debug info dan assertion macro. Sekarang kita kompilasi file yang sama di direktori berbeda:

bash
cd /tmp/test_repro_dir_b
gcc -std=c11 -Wall -Wextra -O2 $(pwd)/main_v2.c -o app_abs_no_map
strings app_abs_no_map | grep -E "test_repro" | head -3

Output yang Dihasilkan:

text
/tmp/test_repro_dir_b/main_v2.c

Path yang berbeda di binary menyebabkan hash SHA-256 berbeda — padahal kode sumber identik.

Solusi: -ffile-prefix-map #

Flag -ffile-prefix-map=<old>=<new> menyuruh GCC mengganti prefix <old> dengan <new> di semua path yang di-embed (di debug info, di __FILE__ macro, di assertion message). Konvensi: ganti path direktori kerja dengan konstanta seperti /src:

bash
cd /tmp/test_repro_dir_a
gcc -std=c11 -Wall -Wextra -O2 \
    -ffile-prefix-map=$(pwd)=/src \
    $(pwd)/main_v2.c -o app_abs_with_map
strings app_abs_with_map | grep -E "/src|test_repro" | head -3

Output yang Dihasilkan:

text
/src/main_v2.c

Sekarang path yang di-embed adalah /src/main_v2.c — sama di semua mesin yang memakai flag ini, terlepas dari direktori kerja sebenarnya.

Menggabungkan dengan -frandom-seed #

Beberapa fitur GCC (misalnya mangling anonymous namespace, locale-specific static initialization, atau generation of unique symbol names) memakai nilai acak sebagai seed. Tanpa seed eksplisit, GCC memakai hash dari path file — yang berbeda di direktori berbeda. Flag -frandom-seed=<string> menyediakan seed deterministik.

Mari kita bangun kode yang sama di dua direktori berbeda, dengan flag reproducible yang lengkap:

bash
cd /tmp/test_repro_dir_a
gcc -std=c11 -Wall -Wextra -O2 \
    -ffile-prefix-map=/tmp/test_repro_dir_a=/src \
    -frandom-seed=myapp \
    main_v2.c -o app_a

cd /tmp/test_repro_dir_b
gcc -std=c11 -Wall -Wextra -O2 \
    -ffile-prefix-map=/tmp/test_repro_dir_b=/src \
    -frandom-seed=myapp \
    main_v2.c -o app_b

sha256sum /tmp/test_repro_dir_a/app_a /tmp/test_repro_dir_b/app_b

Output yang Dihasilkan:

text
2d509ff983692fea28b6694827982d1c63d87cf652513a5cc4f01209e72d31d7  /tmp/test_repro_dir_a/app_a
2d509ff983692fea28b6694827982d1c63d87cf652513a5cc4f01209e72d31d7  /tmp/test_repro_dir_b/app_b

Kedua binary memiliki hash SHA-256 yang identik meskipun dibangun di direktori berbeda. Inilah tujuan reproducible build: dua developer yang membangun kode sumber yang sama dengan flag yang sama akan mendapatkan binary yang byte-identical, sehingga bisa diverifikasi independen.

Mari kita telaah flag yang dipakai di atas:

  1. -ffile-prefix-map=/tmp/test_repro_dir_a=/src (untuk direktori A) dan -ffile-prefix-map=/tmp/test_repro_dir_b=/src (untuk direktori B): kedua direktori dipetakan ke konstanta /src yang sama. Path yang di-embed di binary menjadi /src/main_v2.c di kedua build.
  2. -frandom-seed=myapp: seed deterministik yang sama di kedua build. GCC memakai string myapp untuk menurunkan nilai acak internal, menghilangkan dependensi pada path file sebagai sumber entropi.
  3. -O2: tingkat optimasi harus sama di kedua build. Bila satu build memakai -O2 dan yang lain -O3, output assembly berbeda.
  4. -std=c11 -Wall -Wextra: flag standar harus sama. Bahkan perbedaan kecil seperti -std=c11 vs -std=gnu11 bisa menghasilkan binary berbeda karena ekstensi GNU mengubah makro standar.

Sumber Non-Determinisme: Timestamp Build #

Macro __DATE__ dan __TIME__ di C dievaluasi saat preprocessor sebagai string tanggal dan jam build. Bila kita menyertakan macro ini di kode, setiap build menghasilkan output yang berbeda:

c
#include <stdio.h>

int main(void) {
    printf("build date:  %s\n", __DATE__);
    printf("build time:  %s\n", __TIME__);
    return 0;
}

Kompilasi dengan dua waktu berbeda akan menghasilkan binary berbeda. Solusinya adalah memakai variabel environment SOURCE_DATE_EPOCH — timestamp Unix (jumlah detik sejak 1 Januari 1970). GCC menghormati variabel ini bila aktif dan mengganti __DATE__/__TIME__ dengan nilai deterministik dari timestamp tersebut:

bash
SOURCE_DATE_EPOCH=1704067200 gcc -std=c11 -Wall -Wextra -Wdate-time main.c -o app_deterministic
./app_deterministic

Output yang Dihasilkan:

text
source path: main.c
build date:  Jan  1 2024
build time:  00:00:00

Timestamp 1704067200 adalah Mon Jan 1 00:00:00 UTC 2024. GCC memformat timestamp ini menjadi string "Jan 1 2024" dan "00:00:00" untuk __DATE__ dan __TIME__. Sekarang setiap build dengan SOURCE_DATE_EPOCH=1704067200 akan menghasilkan output yang identik.

Flag -Wdate-time mengaktifkan peringatan saat kode memakai __DATE__, __TIME__, atau __TIMESTAMP__ — berguna untuk mengidentifikasi bagian kode yang perlu diperbaiki bila ingin reproducible build. Saat SOURCE_DATE_EPOCH diset, peringatan ini muncul sebagai pengingat bahwa macro tersebut kini menghasilkan nilai deterministik.

Konfigurasi Reproducible Build Standar #

Berikut set flag standar yang direkomendasikan untuk reproducible build (didasarkan pada spesifikasi di reproducible-builds.org):

bash
gcc -std=c11 -Wall -Wextra -O2 \
    -ffile-prefix-map=$(pwd)=/src \
    -fdebug-prefix-map=$(pwd)=/src \
    -fmacro-prefix-map=$(pwd)=/src \
    -frandom-seed=myapp \
    -Wdate-time \
    main.c -o app

Mari kita bedah flag-flag di atas:

  1. -ffile-prefix-map=$(pwd)=/src: remap path di __FILE__ macro dan assertion message.
  2. -fdebug-prefix-map=$(pwd)=/src: remap path di DWARF debug info (.debug_info, .debug_line).
  3. -fmacro-prefix-map=$(pwd)=/src: remap path yang muncul di macro lain (misalnya __BASE_FILE__).
  4. -frandom-seed=myapp: seed deterministik untuk GCC internal random.
  5. -Wdate-time: aktifkan peringatan saat __DATE__/__TIME__/__TIMESTAMP__ dipakai, untuk identifikasi sumber non-determinisme.

Set SOURCE_DATE_EPOCH di environment sebelum memanggil gcc.