Pengantar
F-Droid berkontribusi dalam penyebaran reproducible builds di seluruh ekosistem perangkat lunak bebas Android. Tujuannya adalah memungkinkan proses pembangunan perangkat lunak yang bisa dijalankan berulang kali dan menghasilkan APK yang identik dengan rilis aslinya. Fokus utamanya meliputi tiga bidang utama berikut:
- Lingkungan build kami (build environment) dirancang untuk memudahkan proses reproduksi build, sekaligus memungkinkan transparansi dan audit atas prosesnya.
- Kami melacak permasalahan pada alat build yang menghambat terwujudnya reproducible builds, membantu pengelola alat build memperbaikinya, serta mendokumentasikan solusi alternatif untuk pengembang aplikasi di halaman ini.
- Kami membantu pengembang aplikasi hulu yang aplikasinya dirilis di f-droid.org dengan dukungan teknis, pengajuan laporan bug, serta saran perbaikan kode sumber untuk mencapai reproducible builds.
F-Droid memverifikasi reproducible builds dengan menggunakan proses penyalinan tanda tangan APK (signature copying), yaitu mencocokkan hasil build F-Droid dengan hasil build asli dari pengembang. Untuk memeriksa apakah suatu aplikasi dapat direproduksi oleh server build kami, lihat bagian “Reproducibility Status” di halaman aplikasinya. Data ini berguna untuk mengidentifikasi faktor-faktor yang berubah dari waktu ke waktu.
Standar tertinggi dalam reproducible build untuk menghindari masalah Trusting Trust adalah metode Diverse Double-Compiling. Prinsip dasarnya adalah menggunakan dua sistem build yang sepenuhnya berbeda untuk menghasilkan biner identik. Ini sulit dicapai, tetapi sangat bernilai untuk keamanan.
Untuk mendekati standar tersebut, F-Droid dapat mereproduksi APK yang
dibangun oleh pengembang asli menggunakan peralatan mereka sendiri.
Sering kali, aplikasi tersebut dibangun dengan toolchain atau sistem
operasi berbeda.
Periksa berkas
metadata
aplikasi untuk bidang
Binaries:
atau
binary:
guna memastikan apakah metode ini sudah diaktifkan.
Tanda Tangan yang Dapat Direproduksi (Reproducible Signatures)
F-Droid memverifikasi reproducible builds menggunakan tanda tangan APK (APK signature) — sebuah bentuk dari tanda tangan tertanam (embedded signature) — dengan cara menyalin tanda tangan dari APK bertanda tangan ke APK tidak bertanda tangan, lalu memverifikasinya. Tanda tangan versi lama (v1 / JAR) hanya mencakup isi APK, sehingga metadata ZIP dan urutan file diabaikan. Namun, tanda tangan versi baru (v2/v3) mencakup seluruh byte dalam APK. Artinya, untaian byte APK harus identik sebelum dan sesudah proses penandatanganan agar dapat diverifikasi dengan benar.
Proses penyalinan tanda tangan menggunakan algoritma yang sama seperti yang
digunakan perintah apksigner.
Karena itu, penting bagi pengembang asli untuk menggunakan metode yang sama
ketika menandatangani APK, idealnya menggunakan apksigner dari Android
SDK.
apksigner juga telah dibangun secara reproducible di
Debian.
Build Verifikasi (Verification Builds)
Banyak individu atau organisasi tertarik untuk mereproduksi proses build agar memastikan bahwa build dari f-droid.org benar-benar sesuai dengan kode sumber aslinya dan tidak dimodifikasi. Dalam kasus seperti ini, APK hasil verifikasi tidak diterbitkan untuk instalasi publik. Server Verifikasi mengotomatiskan proses ini.
Situasi
Sebagian besar build sudah dapat diverifikasi tanpa usaha ekstra karena kode
Java umumnya dikompilasi menjadi bytecode yang identik di berbagai versi
Java.
Alat build-tools dalam Android SDK dapat menghasilkan perbedaan kecil pada
berkas XML, PNG, dan lainnya, namun ini biasanya tidak masalah karena berkas
build.gradle menentukan versi tepat dari build-tools yang digunakan.
Semua yang dibangun menggunakan NDK akan jauh lebih sensitif.
Misalnya, meskipun dua build menggunakan versi NDK yang sama (seperti
r13b), tetapi dijalankan di platform berbeda (misalnya macOS vs Ubuntu),
hasil binernya dapat berbeda.
Selain itu, kita juga harus mewaspadai elemen build yang menyertakan timestamp, jalur build, atau yang peka terhadap urutan pemrosesan dan faktor serupa lainnya.
Google juga sedang berupaya menuju build Android yang dapat direproduksi, sehingga menggunakan versi SDK terbaru sangat membantu. Contohnya, mulai dari Gradle Android Plugin v2.2.2, cap waktu (timestamp) pada metadata ZIP file APK secara otomatis diatur menjadi 0.
Menerbitkan APK dengan tanda tangan dari pengembang asli
Sebuah aplikasi dapat dikonfigurasikan agar menerbitkan biner bertanda
tangan dari pengembang hulu setelah memastikan bahwa hasil tersebut cocok
dengan hasil build yang dihasilkan melalui resep build di fdroiddata.
Proses penerbitan hanya dilakukan jika hasil cocok secara sempurna.
Dengan demikian, F-Droid dapat memverifikasi bahwa aplikasi benar-benar
bebas sambil tetap menggunakan tanda tangan asli pengembang.
Prosedur ini diimplementasikan melalui perintah fdroid
publish.
Pemeriksaan reproduktibilitas di tahap penerbitan mengikuti logika berikut:

Menerbitkan hanya APK yang ditandatangani oleh pengembang (hulu)
Untuk metode ini, metadata aplikasi sama seperti biasa, dengan tambahan
direktif Binaries atau
Builds.binary untuk
menentukan lokasi biner (APK) serta direktif
AllowedAPKSigningKeys
untuk memastikan kunci tanda tangan yang digunakan sudah sesuai.
Dalam konfigurasi ini, F-Droid tidak akan pernah mencoba menerbitkan APK
yang ditandatangani oleh F-Droid sendiri.
Jika fdroid publish berhasil memverifikasi bahwa APK yang diunduh identik
dengan hasil build dari resep fdroiddata, maka APK tersebut akan
diterbitkan.
Jika tidak, versi aplikasi tersebut akan dilewati.
Menerbitkan APK yang ditandatangani oleh pengembang (hulu) dan F-Droid
Pendekatan ini memungkinkan penerbitan dua versi: APK bertanda tangan pengembang hulu dan APK bertanda tangan F-Droid. Hal ini memungkinkan F-Droid mengirimkan pembaruan untuk pengguna yang memasang aplikasi dari sumber lain (seperti Play Store), sekaligus menyediakan pembaruan untuk versi yang dibangun sepenuhnya oleh F-Droid.
Proses ini memerlukan ekstraksi tanda tangan pengembang hulu dan menambahkannya ke data fdroiddata. Tanda tangan tersebut kemudian disalin ke APK tanpa tanda tangan yang dihasilkan dari resep build. F-Droid menyediakan perintah untuk mengekstrak tanda tangan dari APK dengan mudah:
$ cd /path/to/fdroiddata
$ fdroid signatures F-Droid.apk
Selain dari berkas lokal, Anda juga dapat memberikan URL HTTPS ke perintah
fdroid signatures.
Berkas tanda tangan akan diekstraksi ke direktori metadata aplikasi dan siap
digunakan untuk fdroid publish.
Satu tanda tangan terdiri dari 2–6 berkas:
-
tanda tangan v1 (manifest, berkas tanda tangan, dan signature block),
-
dan/atau tanda tangan v2/v3 (APK Signing Block dan offset).
Jika APK ditandatangani dengan metode non-standar seperti signflinger
alih-alih menggunakan apksigner, akan ada tambahan berkas
differences.json.
Hasil ekstraksi akan tampak seperti contoh di bawah ini:
```bash
$ ls metadata/org.fdroid.fdroid/signatures/1000012/ # hanya tanda tangan v1
CIARANG.RSA CIARANG.SF MANIFEST.MF
$ ls metadata/your.app/signatures/42/ # tanda tangan v1 + v2/v3
APKSigningBlock APKSigningBlockOffset MANIFEST.MF YOURKEY.RSA YOURKEY.SF
### Alat
#### Membandingkan file APK (*Diffing the APK*)
Disarankan menggunakan [`diffoscope`](https://diffoscope.org/) untuk
menemukan perbedaan antara APK referensi dari pengembang dan APK yang
dihasilkan oleh `fdroidserver`.
Anda dapat menemukan APK yang dihasilkan oleh `fdroidserver` di folder
seperti:
`fdroiddata/build/com.example.app/app/build/outputs/apk/prod/release/example-1.0.0-prod-release-unsigned.apk`
(saat dijalankan secara lokal),
atau di artefak *pipeline* (jika menggunakan GitLab CI).
Sesuaikan jalur sesuai kebutuhan, misalnya untuk *flavour* selain `prod`.
#### Prioritas dan Perbaikan Perbedaan (*Prioritising & fixing differences*)
Panduan [HOWTO: diff & fix APKs for Reproducible
Builds](https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds)
di wiki F-Droid menjelaskan secara mendalam jenis perbedaan yang umum
terjadi, perbedaan mana yang harus lebih diprioritaskan saat debugging,
serta cara mengatasi permasalahan umum tersebut.
Panduan tersebut juga menjelaskan penggunaan berbagai alat khusus yang dapat
memberikan hasil lebih baik ketika `diffoscope` tidak cukup memadai.
#### Alat Reproducible APK (*Reproducible APK tools*)
Skrip dari proyek
[reproducible-apk-tools](https://github.com/obfusk/reproducible-apk-tools)
(tersedia di *fdroiddata* sebagai `srclib`) dapat membantu membuat build
menjadi lebih konsisten — misalnya dengan memperbaiki format akhir baris
(*newline*: CRLF vs LF) atau membuat urutan dalam file ZIP menjadi
deterministik.
Jika penyebab perbedaannya tidak realistis untuk dihilangkan, skrip ini bisa
dijalankan oleh pengembang hulu sebelum menandatangani APK, oleh resep
*fdroiddata*, atau keduanya.
Alat [disorderfs](https://salsa.debian.org/reproducible-builds/disorderfs),
yang awalnya dibuat untuk menambahkan ketidakpastian ke proses build, juga
dapat digunakan sebaliknya — untuk membuat pembacaan sistem file menjadi
deterministik.
Dalam beberapa kasus, ini dapat membantu membuat file seperti
`resources.arsc` menjadi lebih konsisten.
Berikut contoh penggunaannya:
```console
```bash
$ mv my.app my.app_underlying
$ disorderfs --sort-dirents=yes --reverse-dirents=no my.app_underlying my.app
### Sumber Potensial dari Build yang Tidak Dapat Direproduksi (*Potential sources of unreproducible builds*)
Terdapat berbagai faktor yang dapat menyebabkan build tidak dapat
direproduksi.
Beberapa di antaranya mudah dihindari, tetapi sebagian lainnya cukup sulit
diperbaiki.
Berikut adalah beberapa penyebab umum yang ditemui:
Lihat juga [masalah GitLab
ini](https://gitlab.com/fdroid/fdroiddata/-/issues/2816).
#### Bug: Android Studio menghasilkan urutan ZIP yang tidak deterministik
[Lihat laporan bug: Non-deterministic order of ZIP entries in APK makes
builds not reproducible](https://issuetracker.google.com/issues/265653160)
(memerlukan akun Google untuk diakses).
Catatan: masalah ini telah diperbaiki pada *Android Gradle Plugin*
(`com.android.tools.build:gradle` / `com.android.application`) versi `7.1.X`
dan yang lebih baru.
Saat membangun APK menggunakan Android Studio, urutan *entry* dalam struktur
ZIP dapat berbeda dibandingkan dengan APK yang dibangun langsung menggunakan
`gradle`. Hal ini memengaruhi reproducibility karena urutan file bisa
benar-benar acak, bahkan antar build dari sumber kode yang sama.
Solusi untuk versi lama adalah menjalankan `gradle` langsung (seperti pada
build F-Droid atau CI), sehingga melewati Android Studio sepenuhnya:
```console
```bash
$ ./gradlew assembleRelease
Catatan: tergantung pada konfigurasi penandatanganan Anda, mungkin perlu
menandatangani APK menggunakan `apksigner` setelah proses build selesai,
karena Android Studio tidak melakukan penandatanganan otomatis dalam metode
ini.
#### Bug: `apksigner` dari build-tools ≥ 35.0.0-rc1 menghasilkan APK yang tidak dapat diverifikasi
Menggunakan `apksigner` dari *build-tools* versi 34 menghasilkan APK yang
dapat diverifikasi oleh `apksigcopier`, tetapi versi yang lebih baru akan
gagal.
Masalah ini sedang dilacak di
[#3299](https://gitlab.com/fdroid/fdroiddata/-/issues/3299), dengan detail
tambahan di [apksigcopier issue
#105](https://github.com/obfusk/apksigcopier/issues/105).
Karena *Ubuntu images* GitHub Actions sejak Juli 2024 sudah menggunakan
versi 35, solusinya adalah memilih versi `apksigner` dari *build-tools* 34
secara manual daripada menggunakan versi terbaru bawaan.
#### Bug: `baseline.prof` tidak deterministik
Kadang berkas `baseline.prof` tidak dapat direproduksi dengan hasil yang
sama. Beberapa solusi yang dapat dicoba:
1. Jalankan build ulang beberapa kali hingga file `baseline.prof` cocok.
1. Gunakan jumlah inti CPU (*cores*) yang sama dengan yang digunakan
pengembang hulu.
1. Nonaktifkan profil dasar (*baseline profile*).
Tambahkan potongan berikut ke dalam `build.gradle`:
```groovy
tasks.whenTaskAdded { task ->
if (task.name.contains("ArtProfile")) {
task.enabled = false
}
}
atau ini ke build.gradle.kts:
tasks.whenTaskAdded {
if (name.contains("ArtProfile")) {
enabled = false
}
}
Bug: baseline.profm tidak deterministik
Non-stable
assets/dexopt/baseline.profm
(mungkin memerlukan akun Google untuk dilihat).
Lihat juga panduan solusi berikut.
Bug: coreLibraryDesugaring tidak deterministik
Catatan: masalah ini telah diperbaiki di R8 (com.android.tools:r8) versi
3.0.69 dan yang lebih baru.
Dalam beberapa kasus, build menjadi tidak dapat direproduksi karena bug
pada
coreLibraryDesugaring
(mungkin memerlukan akun Google untuk dilihat); masalah ini juga
mempengaruhi NewPipe.
Bug: Perbedaan akhir baris (line ending differences) antara build di Windows dan Linux
Perbedaan newline antara build di Windows vs Linux membuat hasil build tidak dapat direproduksi (mungkin memerlukan akun Google untuk dilihat).
Solusi alternatif adalah menjalankan skrip
fix-newlines.py
pada APK unsigned yang memiliki line ending berbeda untuk mengubah
antara LF dan CRLF (atau sebaliknya dengan opsi --from-crlf), kemudian
jalankan zipalign kembali setelahnya.
Konkruensi (Concurrency): Reproduksibilitas dapat bergantung pada jumlah CPU/core
Kondisi ini dapat memengaruhi berkas .dex (meskipun jarang terjadi) atau
kode native seperti Rust.
Gunakan hanya satu CPU/core sebagai langkah sementara:
```bash
export CPUS_MAX=1
export CPUS=$(getconf _NPROCESSORS_ONLN)
for (( c=$CPUS_MAX; c<$CPUS; c++ )) ; do echo 0 > /sys/devices/system/cpu/cpu$c/online; done
Catatan: solusi ini memengaruhi seluruh sistem, jadi sebaiknya digunakan
pada mesin virtual atau kontainer sementara untuk menghindari gangguan
permanen.
Untuk kode Rust, Anda dapat mengatur [`codegen-units =
1`](https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units).
Lihat juga [masalah GitLab
ini](https://gitlab.com/fdroid/rfp/-/issues/1519#note_1226216164).
#### Jalur build yang tertanam (*Embedded build paths*)
[Jalur build](https://reproducible-builds.org/docs/build-path/) yang
tertanam sering menjadi penyebab masalah reproducibility, terutama pada
aplikasi yang dibangun menggunakan Flutter, *python-for-android*, atau
bahasa native seperti Rust dan C/C++ (contohnya berkas `libfoo.so`).
Aplikasi yang ditulis sepenuhnya dengan Java atau Kotlin biasanya tidak
terpengaruh.
Cara termudah untuk menghindarinya adalah dengan selalu menggunakan
direktori kerja yang sama saat melakukan build — misalnya:
`/builds/fdroid/fdroiddata/build/your.app.id` (F-Droid CI),
`/home/vagrant/build/your.app.id` (server build F-Droid), atau `/tmp/build`,
atau buat direktori yang menyerupai jalur build di sistem pengembang hulu,
misalnya di macOS: `/Users/runner`.
Catatan: menggunakan subdirektori di dalam `/tmp` yang dapat ditulis oleh
semua pengguna memiliki potensi risiko keamanan pada sistem multi-pengguna.
Jika jalur SDK tertanam ke dalam build Flutter, Anda dapat memindahkan SDK ke jalur tersebut di resep build, lalu konfigurasikan dengan:
`flutter config --android-sdk <path>`
karena pengaturan `ANDROID_SDK_ROOT` saja terkadang tidak cukup.
Jika pustaka (`lib`) tidak di-*strip* dengan benar, informasi debug dapat
tetap tersimpan, yang biasanya berisi banyak jalur direktori. Mengaktifkan
*strip* dapat menghapus data tersebut. Hal ini dapat dilakukan dengan
menentukan versi NDK yang tepat atau menambahkan opsi `-s` ke perintah
*linker*. Proses ini juga bisa dilakukan secara manual menggunakan alat
seperti `llvm-strip`.
#### Cap waktu tertanam (*Embedded timestamps*)
[*Timestamps*](https://reproducible-builds.org/docs/timestamps/) yang
tertanam merupakan salah satu penyebab paling umum dari masalah
reproducibility dan sebaiknya dihindari sebisa mungkin.
#### *Stripping* pustaka native (*Native library stripping*)
Proses *stripping* pada pustaka native (misalnya `libfoo.so`) kadang dapat
menyebabkan hasil build tidak sepenuhnya identik.
Karena itu, penting untuk menggunakan versi NDK yang sama saat melakukan
*rebuild*, misalnya `r21e`.
Menonaktifkan *stripping* kadang dapat membantu.
Secara default, Gradle akan melakukan *strip* pustaka secara otomatis,
bahkan jika pustaka tersebut berasal dari dependensi AAR.
Berikut cara menonaktifkannya di Gradle:
```gradle
android {
packagingOptions {
doNotStrip '**/*.so'
}
}
NDK build-id
Ketika proyek dibangun di mesin yang berbeda, dengan jalur NDK dan direktori
proyek (jni) yang berbeda pula, jalur sumber dalam simbol debug ikut
berubah.
Hal ini menyebabkan linker menghasilkan build-id yang berbeda, yang
tetap tersimpan bahkan setelah pustaka di-strip.
Salah satu solusi adalah menambahkan opsi --build-id=none pada linker
untuk menonaktifkan pembuatan build-id sepenuhnya.
Gaya hash NDK
LLVM meneruskan nilai bawaan yang berbeda ke linker di berbagai platform.
Setelah commit
ini
digabungkan ke NDK, opsi --hash-style=gnu akan digunakan secara default
pada Debian.
Untuk mengubah gaya hash, Anda dapat menambahkan --hash-style=gnu secara
eksplisit ke perintah linker.
String versi NDK clang di bagian .comment
Sejak NDK r26, string versi Clang di bagian .comment berbeda antara build
di macOS dan Linux, yang tampak seperti berikut:
Android (12027248, +pgo, -bolt, +lto, +mlgo, based on r522817) clang version 18.0.1 (https://android.googlesource.com/toolchain/llvm-project d8003a456d14a3deb8054cdaa529ffbf02d9b262)
Perbedaan ini terjadi karena pengoptimalan yang berbeda diaktifkan untuk
masing-masing platform. Seluruh bagian .comment dapat dihapus dengan
perintah berikut:
```bash
objcopy --remove-section .comment <file>
#### Revisi *Platform*
Pada tahun 2014, *Android SDK tools*
[diubah](https://issuetracker.google.com/issues/37132313) untuk menambahkan
dua elemen data, yaitu
[`platformBuildVersionName`](https://android.googlesource.com/platform/frameworks/base/+/ad2d07d%5E!/)
dan
[`platformBuildVersionCode`](https://android.googlesource.com/platform/frameworks/base/+/5283fab%5E!/),
ke dalam `AndroidManifest.xml` sebagai bagian dari proses build.
`platformBuildVersionName` berisi *revision* dari paket *platform* yang
digunakan (mis. `android-23`).
Namun, versi revisi yang berbeda dari paket *platform* yang sama tidak dapat
diinstal secara bersamaan, dan SDK tools juga tidak menyediakan cara untuk
menentukan revisi yang diperlukan saat build.
Akibatnya, dua build yang seharusnya identik dapat berbeda hanya karena
atribut `platformBuildVersionName`.
Komponen _platform_ adalah bagian dari *Android SDK* yang mewakili pustaka
standar yang terpasang pada perangkat.
Mereka memiliki dua jenis versi:
1. *Version code* — bilangan bulat yang mewakili rilis SDK.
2. *Revision* — versi perbaikan bug untuk tiap *platform*.
Versi-versi ini dapat ditemukan dalam berkas `build.prop`, di mana setiap
revisi memiliki nilai berbeda untuk `ro.build.version.incremental`.
Gradle tidak menyediakan cara untuk menentukan revisi melalui properti
`compileSdkVersion` atau `targetSdkVersion`.
Hanya satu *platform-23* yang dapat dipasang dalam satu waktu, berbeda
dengan *build-tools* yang bisa diinstal beberapa versi sekaligus.
Berikut dua contoh di mana perbedaan hasil diduga berasal dari revisi
*platform* yang berbeda:
* <https://verification.f-droid.org/de.nico.ha_manager_25.apk.diffoscope.html>
* <https://verification.f-droid.org/de.nico.asura_12.apk.diffoscope.html>
#### PNG Crush/Crunch
Tahap standar dalam proses build Android mencakup optimisasi gambar PNG
menggunakan alat seperti `aapt singleCrunch`, `pngcrush`, `zopflipng`, atau
`optipng`.
Sayangnya, hasilnya tidak deterministik dan penyebab pastinya masih belum
diketahui.
Karena berkas PNG biasanya sudah dikomit di repositori sumber (misalnya
lewat *git*), solusi praktis adalah menjalankan alat optimisasi pilihan Anda
terhadap berkas-berkas PNG lalu mengomitas perubahan tersebut ke
repositori.
Setelah itu, nonaktifkan proses optimisasi PNG bawaan dengan menambahkan
kode berikut ke `build.gradle`:
```gradle
android {
aaptOptions {
cruncherEnabled = false
}
}
Perlu dicatat bahwa alat seperti svgo dapat melakukan optimisasi serupa
pada berkas SVG.
PNG yang dihasilkan dari vector drawables
Sayangnya, berkas PNG yang dihasilkan tidak selalu dapat direproduksi secara deterministik.
Anda dapat menonaktifkan pembuatan PNG dengan menambahkan potongan berikut
ke build.gradle:
android {
defaultConfig {
vectorDrawables.generatedDensities = []
}
}
Pengoptimal R8
Beberapa optimisasi R8 bersifat tidak deterministik, menghasilkan bytecode yang berbeda antar build.
Sebagai contoh, R8 berusaha mengoptimalkan penggunaan ServiceLoader dengan
membuat daftar statis dari seluruh layanan dalam kode.
Urutan daftar ini dapat berbeda, atau bahkan tidak lengkap, di setiap
build.
Satu-satunya cara untuk mencegahnya adalah menonaktifkan optimisasi terkait
dengan mendeklarasikan kelas yang dioptimalkan dalam berkas
proguard-rules.pro.
-keep class kotlinx.coroutines.CoroutineExceptionHandler
-keep class kotlinx.coroutines.internal.MainDispatcherFactory
Berhati-hatilah saat menggunakan R8. Selalu lakukan pengujian berulang kali terhadap hasil build Anda, dan nonaktifkan setiap optimisasi yang menghasilkan keluaran tidak deterministik.
Jika bytecode DEX berbeda dan bergantung pada jumlah inti CPU, coba perbarui R8 ke versi 8.6.33, 8.7.20, 8.8, atau lebih baru, karena beberapa masalah terkait hal ini telah diperbaiki.
Urutan kelas DEX salah (DEX classes in wrong order)
Meskipun isi file DEX mungkin sama, jika nama file kelas ditukar atau dalam urutan berbeda, reproduksibilitas akan gagal. Masalah ini telah diperbaiki untuk bundles di AGP 8.8, namun kasus serupa juga ditemukan pada APK. Cobalah menggunakan versi AGP yang lebih baru terlebih dahulu.
Resource Shrinker
Salah satu cara untuk mengurangi ukuran file APK adalah dengan menghapus sumber daya yang tidak digunakan dari paket. Fitur ini berguna ketika proyek menggunakan pustaka besar seperti AppCompat, terutama jika penyusutan kode (code shrinking) melalui R8 atau ProGuard juga diaktifkan.
Namun, dalam beberapa kasus, resource shrinker justru dapat meningkatkan ukuran APK di platform tertentu, terutama jika jumlah sumber daya yang dihapus tidak signifikan. Dalam situasi seperti itu, Gradle akan memilih APK asli ketimbang hasil penyusutan, menyebabkan perilaku tidak deterministik. Hindari penggunaan resource shrinker kecuali jika benar-benar memberikan pengurangan ukuran yang signifikan.
Informasi VCS
Sejak Android Gradle Plugin versi 8.3, informasi VCS dihasilkan secara
otomatis dan disertakan dalam APK di berkas
META-INF/version-control-info.textproto, misalnya:
```proto
repositories {
system: GIT
local_root_path: "$PROJECT_DIR"
revision: "3a443877cd53e37d85cbc52adc8cfd558919d373"
}
Kami memahami bahwa pengembang perlu membangun dan menguji aplikasi selama
proses kerja. Namun, pastikan untuk mengunggah APK *release* yang dibangun
setelah *tagging*, menggunakan *tree* bersih pada *commit* bertanda tersebut
(tanpa perubahan lokal atau artefak build sebelumnya). Hanya dalam kasus
luar biasa, ketika tidak memungkinkan, `vcsInfo` boleh dinonaktifkan (karena
dapat menimbulkan masalah jika salah konfigurasi). Hal ini dapat dilakukan
sebagai berikut:
```gradle
buildTypes {
release {
vcsInfo.include false
}
}
metadata ZIP
APK menggunakan format file ZIP, yang awalnya dirancang untuk sistem berkas MSDOS FAT. Izin file UNIX ditambahkan kemudian sebagai ekstensi. Sebenarnya, APK hanya membutuhkan format ZIP dasar tanpa ekstensi tambahan ini. Namun, ekstensi ZIP kadang tetap tertinggal dalam proses build dan baru dihapus pada tahap penandatanganan akhir. Sebagai contoh:
```diff
--- a2dp.Vol_137.apk
+++ sigcp_a2dp.Vol_137.apk
@@ -1,50 +1,50 @@
--rw---- 2.0 fat 8976 bX defN 79-Nov-30 00:00 AndroidManifest.xml
--rw---- 2.0 fat 1958312 bX defN 79-Nov-30 00:00 classes.dex
--rw---- 1.0 fat 78984 bx stor 79-Nov-30 00:00 resources.arsc
+-rw-rw-rw- 2.3 unx 8976 b- defN 80-000-00 00:00 AndroidManifest.xml
+-rw---- 2.4 fat 1958312 b- defN 80-000-00 00:00 classes.dex
+-rw-rw-rw- 2.3 unx 78984 b- stor 80-000-00 00:00 resources.arsc
#### Perbedaan *Toolchain* (*Mismatched Toolchains*)
Berbagai *toolchain* dapat menghasilkan biner yang berbeda. Kasus umum
terjadi ketika lebih dari satu versi atau distribusi JDK digunakan untuk
membangun APK. Bahkan Gradle terkadang mencampur beberapa versi JDK dalam
satu proses build. Untuk menghindari masalah ini, pastikan hanya satu versi
JDK yang digunakan dan hapus JDK yang tidak relevan.
Perbedaan file `classes.dex` akibat versi Java yang berbeda dapat terlihat
seperti:
```diff
- .annotation system Ldalvik/annotation/Signature;
- ```
value = {
```
- ```
"()V"
```
- ```
}
```
- ```
.end annotation
```
atau seperti ini, misalnya antara build menggunakan Java 17 dan Java 21:
- .annotation system Ldalvik/annotation/MethodParameters;
- accessFlags = {
- 0x8010
- ```
}
```
- names = {
- null
- ```
}
```
- ```
.end annotation
```
Versi NDK yang berbeda juga dapat menghasilkan biner berbeda. Biasanya
perbedaan ini dapat dikenali melalui metadata seperti versi LLD pada pustaka
native. Namun, sejak NDK r26d terdapat perilaku aneh di mana hanya bagian
.shstrtab dalam berkas ELF pustaka native yang berubah saat NDK dipasang
ulang. Pustaka native dapat dibangun bersama aplikasi atau diambil dari
repositori Maven. Jika AGP mendeteksi bahwa NDK tersedia, NDK akan digunakan
untuk melakukan proses strip pustaka, tetapi dalam beberapa kasus justru
merusak bagian .shstrtab. Karena itu, pastikan konfigurasi NDK Anda sesuai
dengan pengaturan di hulu, termasuk versi NDK dan cara penggunaannya oleh
AGP.
Dukungan untuk ukuran halaman 16 KB
Mulai Android 15, sistem mendukung perangkat dengan ukuran halaman memori 16 KB. Jika aplikasi Anda menggunakan pustaka NDK, baik secara langsung maupun melalui SDK eksternal, Anda harus membangun ulang aplikasi agar berfungsi pada perangkat 16 KB tersebut. Informasi selengkapnya tersedia di sini.
Panduan Khusus Bahasa (Language-specific instructions)
Pustaka native dapat dibangun menggunakan berbagai alat dan bahasa. Meskipun mengalami masalah reproducible build yang serupa, metode untuk memperbaikinya berbeda. Beberapa solusi yang sudah diketahui tercantum di bawah ini:
ndk-build
Anda dapat menambahkan LOCAL_CFLAGS += mpiler args> dan LOCAL_LDFLAGS += -Wl,<linker args> ke dalam berkas Android.mk atau ke dalam build.gradle/build.gradle.kts:
android {
defaultConfig {
externalNativeBuild {
ndkBuild {
arguments "LOCAL_CFLAGS+=<compiler args> LOCAL_LDFLAGS+=-Wl,<linker args>"
}
}
}
}
CMake
Sejak CMake versi 3.13, Anda dapat menambahkan add_compile_options("mpiler args>") dan add_link_options(LINKER:<linker args>) langsung di CMakeLists.txt secara global, contohnya untuk menonaktifkan build-id:
```cmake
add_link_options("LINKER:--build-id=none")
Perintah ini hanya berlaku untuk pustaka yang ditambahkan setelah perintah ini dipanggil, jadi sebaiknya diletakkan di awal berkas CMake.
Untuk versi CMake sebelum 3.13, gunakan `target_compile_options(<target> PRIVATE mpiler args>)` dan `target_link_libraries(<target> LINKER:<linker args>)` untuk setiap target.
Alternatifnya, konfigurasi ini juga dapat ditetapkan langsung di berkas Gradle:
```gradle
android {
defaultConfig {
externalNativeBuild.cmake {
cFlags "<compiler args> -Wl,<linker args>" // or
arguments "-DCMAKE_C_FLAGS=<compiler args> -DCMAKE_SHARED_LINKER_FLAGS=-Wl,<linker args>"
}
}
}
Gunakan flag -ffile-prefix-map untuk menghapus jejak jalur build yang
tertanam. Tambahkan perintah berikut langsung di CMakeLists.txt:
```cmake
add_compile_options("-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.")
Atau di dalam `build.gradle`:
```gradle
```groovy
externalNativeBuild {
cmake {
cFlags "-ffile-prefix-map=${rootDir}=."
cppFlags "-ffile-prefix-map=${rootDir}=."
}
}
##### Golang
Argumen *linker* dapat ditambahkan ke `CGO_LDFLAGS`. Beberapa argumen
berguna yang dapat diteruskan saat menjalankan `go build` adalah
`-ldflags="-buildid="`, `-trimpath` (untuk menghindari jalur build tertanam)
dan`-buildvcs=false`.
##### Rust
Argumen *compiler* dan *linker* dapat ditetapkan melalui [Cargo `build.rustflags`](https://doc.rust-lang.org/cargo/reference/config.html#buildrustflags) dan [Opsi Kodegen `rustc`](https://doc.rust-lang.org/rustc/codegen-options/index.html).
Argumen *linker* dapat ditambahkan dengan `-C link-args=-Wl,<linker args>`, sementara `--remap-path-prefix=<old>=<new>` digunakan untuk menghapus jalur build dari hasil akhir.
Versi *toolchain Rust* harus disamakan dengan versi yang digunakan oleh pengembang hulu. Hal ini dapat dilakukan saat instalasi menggunakan perintah: `rustup-init.sh -y --default-toolchain <version>`.
Ketika *crate openssl* menggunakan metode *vendored build*, pustaka OpenSSL
perlu dikonfigurasi secara khusus agar hasilnya dapat direproduksi. Variabel
`SOURCE_DATE_EPOCH` dapat diatur untuk menghapus cap waktu (*timestamp*),
dan `CARGO_TARGET_DIR` sebaiknya diarahkan ke jalur absolut seperti
`/tmp/build`, agar jalur tertanam tetap konsisten di berbagai mesin. Jalur
instalasi NDK juga mencerminkan hal yang sama agar konsisten antara satu
sistem dengan yang lain.
Jalur `CARGO_HOME` juga berpengaruh besar karena direkam di pustaka hasil
build. Sebaiknya disamakan antar build dengan cara mengekspornya sebelum
menjalankan `rustup` atau perintah build lainnya. Setelah itu, pastikan Anda
memuat (*source*) berkas `env` dari direktori tersebut.
#### Instruksi Khusus Pustaka (*Library-specific instructions*)
Beberapa pustaka menghasilkan kode yang tidak deterministik karena
keberadaan cap waktu, iterasi yang tidak terurut, dan faktor
lainnya. Beberapa solusi umum dijelaskan di bawah:
##### Plugin Gradle AboutLibraries (*AboutLibraries Gradle plugin*)
Untuk mencegah plugin `com.mikepenz.aboutlibraries.plugin` menambahkan cap
waktu (*timestamp*) ke dalam berkas JSON yang dihasilkannya, tambahkan
konfigurasi berikut ke dalam `build.gradle`:
```gradle
```groovy
aboutLibraries {
// Hapus cap waktu "generated" agar build dapat direproduksi
excludeFields = ["generated"]
}
Untuk `build.gradle.kts`, gunakan konfigurasi berikut:
```gradle
```kotlin
aboutLibraries {
// Hapus cap waktu "generated" agar build dapat direproduksi
excludeFields = arrayOf("generated")
}
```
EventBus
Pustaka ini menghasilkan kode yang tidak deterministik, namun urutan kelas dapat disortir ulang setelah file kelas dihasilkan. Instruksi lengkapnya dapat ditemukan di kode sumber Eternity.
Migrasi ke build yang dapat direproduksi
TODO
- Urutan JAR untuk APK (jar sort order for APKs)
- Versi alat
aaptdapat menghasilkan keluaran berbeda (misalnya dalam penamaan XML dan subfolder di direktori res/).
Sumber Referensi
- https://gitlab.com/fdroid/fdroidserver/commit/8568805866dadbdcc6c07449ca6b84b80d0ab03c
- Server Verifikasi
- https://verification.f-droid.org
- https://reproducible-builds.org
- https://wiki.debian.org/ReproducibleBuilds
- https://gitian.org/
- Masalah Google #70292819 — platform-27_r01.zip diganti dengan pembaruan baru
- Masalah Google #37132313 — platformBuildVersionName menyebabkan build sulit direproduksi dan menciptakan perbedaan yang tidak perlu
- Masalah Google #110237303 — resources.arsc dibangun dengan non-determinisme, mencegah APK dapat direproduksi
- Generasi kode non-deterministik oleh navigation.safeargs.kotlin
- Perbedaan kode DEX yang tidak diperlukan berdasarkan jumlah CPU yang digunakan dalam proses build
- Penyusunan bundle dengan AssembleBundleTask menghasilkan file DEX dalam urutan yang salah, menyebabkan ketidakcocokan checksum
- Perbedaan bytecode DEX bergantung pada jumlah inti CPU menyebabkan Reproducible Builds gagal
- Perbedaan kode DEX yang tidak diperlukan berdasarkan jumlah CPU yang digunakan dalam proses build
