Pertanyaan paling sederhana sekaligus paling sulit di dunia build tool: apakah build hari ini menghasilkan berkas yang identik bit demi bit dengan build kemarin? Build yang memenuhi syarat itu disebut reproducible — dapat direproduksi. Kemampuan ini bukan kemewahan akademik: tim keamanan membutuhkannya untuk memverifikasi bahwa berkas yang berjalan di produksi benar-benar lahir dari kode sumber yang diaudit, dan tim pengembang membutuhkannya agar bug tidak muncul-hilang karena classpath diam-diam berubah. Pada chapter ini kita akan melihat sumber nondeterminisme pada build Java — versi dependency, timestamp JAR, versi compiler — dan cara mengunci semuanya.

Mengapa Build Harus Dapat Direproduksi #

Tiga skenario menjelaskan urgensinya. Pertama, audit keamanan: ketika laporan kerentanan menyebut library tertentu, tim perlu memastikan versi mana saja yang benar-benar masuk ke artifact produksi — mustahil bila versi berpindah tiap build. Kedua, investigasi regresi: bug yang hanya muncul pada build tertentu dapat dibandingkan bila artifact lama dapat dibangun ulang; tanpa itu, kita hanya punya tebakan. Ketiga, konsistensi lingkungan: developer, CI, dan mesin rilis menghasilkan artifact yang sama dari commit yang sama, sehingga “di mesin saya jalan” tidak pernah menjadi variabel. Inilah misi gerakan Reproducible Builds: dari kode sumber yang terverifikasi menuju berkas binary yang terverifikasi.

Hash sebagai Sidik Jari Build #

Alat dasar reproduktifitas adalah hash kriptografis — fungsi yang mengubah input berapa pun panjangnya menjadi sidik jari tetap 256-bit. Gradle memakai mekanisme ini di build cache dan dependency verification; kita bisa mengamatinya langsung:

java HashSumber.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;

public class HashSumber {

    public static void main(String[] args) throws Exception {
        String sumberA = "public class AplikasiGudang { }";
        String sumberB = "public class AplikasiGudang  { }";

        System.out.println("sumber A : " + sumberA);
        System.out.println("hash A   : " + sha256(sumberA));
        System.out.println("hash A   : " + sha256(sumberA) + "  (dihitung ulang)");
        System.out.println();
        System.out.println("sumber B : " + sumberB + "  (satu spasi ekstra)");
        System.out.println("hash B   : " + sha256(sumberB));
        System.out.println();
        System.out.println("input identik menghasilkan hash identik");
        System.out.println("satu karakter berbeda mengubah seluruh hash");
    }

    /** Sidik jari SHA-256 dari sebuah teks sumber. */
    private static String sha256(String teks) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hash = md.digest(teks.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(hash);
    }
}

Output yang Dihasilkan:

text
sumber A : public class AplikasiGudang { }
hash A   : 56ca7aeb78825c2cd4b63e9e3e44d171e61ed9a53e8741a7927c9b0cdf1a47d2
hash A   : 56ca7aeb78825c2cd4b63e9e3e44d171e61ed9a53e8741a7927c9b0cdf1a47d2  (dihitung ulang)

sumber B : public class AplikasiGudang  { }  (satu spasi ekstra)
hash B   : 77caa78e49dd82307772b1f0315ff59c3393d551570e8a57617dbcab9b8c44cd

input identik menghasilkan hash identik
satu karakter berbeda mengubah seluruh hash

Dua sifat terbukti pada output di atas. Pertama, dua perhitungan pada sumberA menghasilkan hash yang persis sama — sifat deterministik yang membuat hash layak dipakai sebagai kunci cache dan alat verifikasi. Kedua, satu spasi tambahan pada sumberB mengubah hash secara total — sifat avalanche yang membuat perubahan sekecil apa pun tidak bisa bersembunyi. Build cache Gradle memakai prinsip pertama: hash input task menjadi alamat arsip output. Dependency verification memakai keduanya: checksum dependency yang berbeda dari metadata akan ditolak keras.

Sumber Nondeterminisme pada Build Java #

Lima penyusup paling sering membuat dua build berbeda. Pertama, versi dependency yang mengapung: rentang versi dan -SNAPSHOT (chapter Dependency Versioning) membawa classpath berbeda antar waktu. Kedua, timestamp pada entri JAR: alat jar mencatat waktu pembuatan pada setiap entri, sehingga sumber yang sama dibangun pukul berbeda menghasilkan berkas berbeda. Ketiga, urutan entri yang mengikuti urutan penemuan berkas oleh sistem operasi. Keempat, versi compiler dan target yang tak tertulis: JDK yang berbeda di mesin berbeda menghasilkan bytecode yang tidak dijamin identik. Kelima, metadata yang ikut tertulis ke dalam artifact seperti path absolut atau nomor versi alat — misalnya atribut Created-By pada manifest yang kita lihat di chapter Packaging JAR.

Mengunci Compiler dan Versi Target #

Langkah pertama reproduktivitas adalah menyematkan compiler: satu versi JDK untuk semua mesin — dibantu toolchain Maven atau Java toolchain Gradle yang mengunduh JDK tertentu secara otomatis — dan satu target bytecode lewat --release:

bash
javac --release 21 -Xlint:all -d out src/toko/AplikasiKasir.java

Flag --release 21 memastikan bytecode dan API yang diizinkan identik di mesin mana pun, sedangkan -Xlint:all menjaga peringatan compiler tetap terlihat — peringatan yang hilang-timbul antar mesin biasanya petunjuk pertama bahwa dua compiler berbeda sedang bekerja. Setelan yang sama diteruskan ke build tool lewat maven.compiler.release di POM atau toolchain di Gradle, sehingga seluruh tim dan CI memakai setelan yang sama tanpa bergantung JDK yang kebetulan terpasang.

Menstabilkan Timestamp dan Urutan JAR #

Untuk arsip, Maven dan Gradle menyediakan mode reproducible pada plugin pengemas. Di Maven, property project.build.outputTimestamp membekukan timestamp seluruh entri:

xml Timestamp dibekukan di pom.xml
<properties>
    <project.build.outputTimestamp>2026-01-15T08:00:00Z</project.build.outputTimestamp>
</properties>

Di Gradle, dua property pada task jar menormalkan arsip — timestamp entri diseragamkan dan urutan entri diurutkan stabil:

kotlin Jar reproducible di build.gradle.kts
tasks.jar {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

Dengan kedua setelan ini, membangun commit yang sama pada mesin mana pun menghasilkan berkas JAR yang identik — dan karenanya memiliki hash SHA-256 yang sama, siap dibandingkan di CI sebagai gerbang akhir rilis.

Cache, Lockfile, dan Verifikasi Dependency #

Sisi dependency dikunci dua lapis di Gradle. Dependency locking membekukan hasil resolusi ke berkas gradle.lockfile per configuration — sekali dikunci, build menolak resolusi yang berbeda meski repositori berubah:

bash
gradle dependencies --write-locks

Dependency verification melangkah lebih jauh: berkas verification-metadata.xml mencatat checksum setiap dependency, dan Gradle memverifikasi setiap unduhan terhadapnya — pertahanan langsung terhadap supply chain attack di mana artifact di repositori diganti pihak lain. Maven tidak memiliki lockfile bawaan; posisi pengamannya adalah kunci versi eksplisit plus dependencyManagement, dibantu alat pemindai dependensi seperti OWASP Dependency-Check untuk mengecek kerentanan yang diketahui.

Kunci versi, hash, dan lockfile melengkapi peta build tool yang kita bangun sejak javac manual. Satu pertanyaan terakhir menanti: dengan Maven dan Gradle sama-sama matang, alat mana yang tepat untuk proyek dan tim Anda? Chapter penutup section ini membandingkan keduanya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

HashSumber.java

Unduh berkas
java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;

public class HashSumber {

    public static void main(String[] args) throws Exception {
        String sumberA = "public class AplikasiGudang { }";
        String sumberB = "public class AplikasiGudang  { }";

        System.out.println("sumber A : " + sumberA);
        System.out.println("hash A   : " + sha256(sumberA));
        System.out.println("hash A   : " + sha256(sumberA) + "  (dihitung ulang)");
        System.out.println();
        System.out.println("sumber B : " + sumberB + "  (satu spasi ekstra)");
        System.out.println("hash B   : " + sha256(sumberB));
        System.out.println();
        System.out.println("input identik menghasilkan hash identik");
        System.out.println("satu karakter berbeda mengubah seluruh hash");
    }

    /** Sidik jari SHA-256 dari sebuah teks sumber. */
    private static String sha256(String teks) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hash = md.digest(teks.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(hash);
    }
}