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:
#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:
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 -3Output yang Dihasilkan:
/tmp/test_repro_dir_a/main_v2.cPerintah 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:
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 -3Output yang Dihasilkan:
/tmp/test_repro_dir_b/main_v2.cPath 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:
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 -3Output yang Dihasilkan:
/src/main_v2.cSekarang 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:
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_bOutput yang Dihasilkan:
2d509ff983692fea28b6694827982d1c63d87cf652513a5cc4f01209e72d31d7 /tmp/test_repro_dir_a/app_a
2d509ff983692fea28b6694827982d1c63d87cf652513a5cc4f01209e72d31d7 /tmp/test_repro_dir_b/app_bKedua 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:
-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/srcyang sama. Path yang di-embed di binary menjadi/src/main_v2.cdi kedua build.-frandom-seed=myapp: seed deterministik yang sama di kedua build. GCC memakai stringmyappuntuk menurunkan nilai acak internal, menghilangkan dependensi pada path file sebagai sumber entropi.-O2: tingkat optimasi harus sama di kedua build. Bila satu build memakai-O2dan yang lain-O3, output assembly berbeda.-std=c11 -Wall -Wextra: flag standar harus sama. Bahkan perbedaan kecil seperti-std=c11vs-std=gnu11bisa 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:
#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:
SOURCE_DATE_EPOCH=1704067200 gcc -std=c11 -Wall -Wextra -Wdate-time main.c -o app_deterministic
./app_deterministicOutput yang Dihasilkan:
source path: main.c
build date: Jan 1 2024
build time: 00:00:00Timestamp 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):
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 appMari kita bedah flag-flag di atas:
-ffile-prefix-map=$(pwd)=/src: remap path di__FILE__macro dan assertion message.-fdebug-prefix-map=$(pwd)=/src: remap path di DWARF debug info (.debug_info,.debug_line).-fmacro-prefix-map=$(pwd)=/src: remap path yang muncul di macro lain (misalnya__BASE_FILE__).-frandom-seed=myapp: seed deterministik untuk GCC internal random.-Wdate-time: aktifkan peringatan saat__DATE__/__TIME__/__TIMESTAMP__dipakai, untuk identifikasi sumber non-determinisme.
Set SOURCE_DATE_EPOCH di environment sebelum memanggil gcc.