7
Perakitan Jar: BuildJar dan Stubifier
BuildJar merakit android.jar kustom sepenuhnya di memori: membaca base SDK jar, menimpakan hanya namespace hidden API dari jar boot hasil dex2jar, memangkas class yang menggantung, mengganti body method menjadi stub, lalu menulis jar keluaran tanpa pernah menaruh satu berkas .class di disk (cli/BuildJar.java:20-441). Langkah stub dikerjakan Stubifier di level byte, dan BuildJar memanggilnya per entri lewat Stubifier.stubify (cli/BuildJar.java:97-1092).
Alur perakitan di dalam main
Seluruh proses berjalan berurutan di satu JVM: base dibaca tanpa filter, setiap overlay dibaca dengan filter namespace, hasilnya diverifikasi lalu dipangkas, di-stub, dan akhirnya ditulis sebagai zip (cli/BuildJar.java:69-723).
graph TD
BASE["base SDK jar lewat --base"] --> READBASE["readInto(base, entries, null)"]
OV["overlay jar hasil dex2jar"] --> READOV["readInto(ov, entries, OVERLAY_NS)"]
NS["OVERLAY_NS: android/, com/android/internal/, dalvik/"] --> READOV
READBASE --> ENTRIES[("entries: LinkedHashMap, kunci case-sensitive")]
READOV --> ENTRIES
ENTRIES --> HARD{"hardMissing kosong?"}
HARD -->|"tidak"| ABORT["System.exit(1)"]
HARD -->|"ya"| SOAK["soakDangling"]
SOAK --> DROP["entries.remove(name + .class)"]
DROP --> BODIES{"keepBodies?"}
ENTRIES -->|"--keep-dangling"| BODIES
BODIES -->|"tidak"| STUBIFY["Stubifier.stubify"]
STUBIFY --> ZIP["ZipOutputStream ke --out"]
BODIES -->|"ya"| ZIP
ZIP --> OUT["android.jar kustom"]
style ENTRIES fill:#336791,color:#fff
style STUBIFY fill:#8b5cf6,color:#fff
style NS fill:#f59e0b,color:#000
style ABORT fill:#dc2626,color:#fff
style OUT fill:#2d7d46,color:#fff
%% fig: Alur perakitan jar di BuildJar
%% desc: Base dibaca tanpa filter, overlay difilter namespace, lalu prune, stubify, dan penulisan zip keluaran.Mengapa entri ditahan di memori
Pipeline lama mengekstrak class ke direktori kerja. Di filesystem yang case-INSENSITIVE (macOS APFS, Windows NTFS secara bawaan) langkah itu diam-diam menghilangkan class yang namanya hanya berbeda kapitalisasi, misalnya android.media.AudioAttributes versus android.media.Audioattributes yang keduanya class nyata di framework.jar. ZIP menyimpan keduanya sebagai kunci string yang berbeda, sehingga perakitan zip-ke-zip mempertahankan semua class di semua platform sekaligus menghapus penyalinan berkas per class (cli/BuildJar.java:25-314).
Karena itu entri ditampung di LinkedHashMap<String, byte[]> entries; readInto menyalin setiap entri jar ke map tersebut, melewati direktori dan META-INF/, dengan penulis terakhir menang saat nama entri bertabrakan (cli/BuildJar.java:126-1465). Penyempurnaan yang sama dicatat sebagai salah satu dari dua keunggulan CLI dibanding merge manual (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:141-1506).
Overlay difilter per namespace, bukan per jar
Filter tidak memilih jar sumber, melainkan prefiks path entri. Alasannya, hidden API android.* tinggal di modul APEX (framework-wifi, framework-bluetooth, dan seterusnya) sama banyaknya dengan di framework.jar (cli/BuildJar.java:33-397). Daftar yang diizinkan hanya tiga prefiks (cli/BuildJar.java:488).
| Prefiks entri | Sumber di perangkat | Alasan di-overlay |
|---|---|---|
android/ | framework.jar + APEX javalib | menampung sebagian besar hidden API yang ditandai @hide |
com/android/internal/ | framework.jar + APEX javalib | API internal yang tidak pernah masuk SDK jar |
dalvik/ | framework.jar | kelas runtime yang dipakai kode framework |
Yang sengaja tidak pernah di-overlay adalah runtime ART yang membayangi JDK (java/*, sun/*, jdk/*, libcore/*) dan pustaka hasil repackage (com.android.okhttp, com.android.org.bouncycastle, org.apache.xml*, gov.nist). Kalau ikut ditimpakan, toolchain menolak jar tersebut (issue #100) (cli/BuildJar.java:33-397). Base SDK jar sudah menyediakan stub publik java.*, javax.*, org.*, dan dalvik.* yang dikurasi (cli/BuildJar.java:33-397). Aturan "hanya tiga namespace" ini juga menjadi kesimpulan panduan merge manual (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:207-2109).
Prune: menutup graph supertype
Setiap entri .class diurai dengan ClosureVerify.read, yang hanya membaca header class (super dan interfaces) tanpa code, frame, atau debug. Hasilnya dipakai algoritme prune yang dipinjam dari ClosureVerify supaya verifikasi dan pemangkasan memakai satu definisi "resolvable" (cli/BuildJar.java:74-9510).
| Fase | Fungsi | Kriteria | Akibat |
|---|---|---|---|
| Guard | ClosureVerify.hardMissing | supertype hilang dan prefiksnya android/, dalvik/, java/, atau javax/ | System.exit(1); prune tidak dijalankan agar regresi tidak tertutupi (cli/ClosureVerify.java:114-13011) |
| Fixpoint | ClosureVerify.soakDangling | supertype tidak ada di himpunan class dan tidak bisa dimuat JDK | nama internal masuk remove, entri name + ".class" dihapus (cli/ClosureVerify.java:132-16212) |
Ambang keras ditentukan konstanta FAIL_PREFIXES = { "android/", "dalvik/", "java/", "javax/" }, yaitu namespace yang dijanjikan lengkap oleh jar (cli/ClosureVerify.java:39-4013). Keluaran normalnya satu baris [build] pruned N dangling class(es) (supertype graph closed) (cli/BuildJar.java:74-9510).
⚠️ Bila
hardMissingtidak kosong,BuildJarkeluar dengan kode 1 dan sengaja melewati prune. Jar yang dihasilkan dari jalur ini tidak boleh dipakai: itu regresi, bukan class internal yang memang dibuang (cli/BuildJar.java:74-9510).
Stubifier: body method menjadi stub
Stub bukan sekadar kosmetik. Jar overlay membawa bytecode hasil dex2jar dengan blok try/catch sungguhan. MockableJarGenerator milik Gradle menghapus instruksi method tetapi tidak menghapus tryCatchBlocks; label handler yang menggantung lalu membuat ClassWriter(COMPUTE_FRAMES) gagal dengan Cannot read field "outgoingEdges" because "handlerRangeBlock" is null (issue #46). Karena itu setiap method konkret ditulis ulang menjadi throw new RuntimeException("Stub!"), bentuk yang persis sama dengan stub SDK, sementara javac tetap bisa me-resolve semua simbol karena hanya membaca signature (cli/Stubifier.java:23-3814).
graph TD
CLS["byte[] satu entri .class"] --> PARSE["ClassReader.accept SKIP_FRAMES + SKIP_DEBUG"]
PARSE --> SKIP{"ACC_ABSTRACT atau ACC_NATIVE?"}
SKIP -->|"ya"| LEAVE["method dibiarkan, tidak ada Code attribute"]
SKIP -->|"tidak"| CLEAR["clear tryCatchBlocks, localVariables, anotasi variabel lokal"]
CLEAR --> CTOR{"method adalah init?"}
CTOR -->|"ya"| CHAIN["findSuperOrThisCall + defaultValue"]
CTOR -->|"tidak"| THROW
CHAIN --> THROW["NEW RuntimeException, LDC Stub!, ATHROW"]
THROW --> WRITE["ClassWriter dengan COMPUTE_MAXS"]
style CLEAR fill:#f59e0b,color:#000
style THROW fill:#dc2626,color:#fff
style WRITE fill:#2d7d46,color:#fff
style LEAVE fill:#8b5cf6,color:#fff
%% fig: Langkah Stubifier.stubify untuk satu class
%% desc: Body method konkret dibuang, rantai super/this dipertahankan, lalu class ditulis ulang dengan COMPUTE_MAXS.| Bagian class | Perlakuan |
|---|---|
| body method konkret | diganti rangkaian NEW RuntimeException + LDC "Stub!" + ATHROW, dengan maxStack/maxLocals di-reset ke 0 (cli/Stubifier.java:87-11915) |
tryCatchBlocks, localVariables, anotasi variabel lokal | dibersihkan sebelum body baru dipasang (cli/Stubifier.java:87-11915) |
method ACC_ABSTRACT dan ACC_NATIVE | dilewati karena tidak punya attribute Code (cli/Stubifier.java:68-8516) |
signature, generics, throws, annotation, field konstan, struktur enum, metadata inner class | dipertahankan (cli/Stubifier.java:23-3814) |
Konstruktor mendapat perlakuan khusus: rantai super()/this() asli dicari findSuperOrThisCall dengan mencocokkan owner ke cn.superName atau cn.name, sehingga new Foo() yang bersarang di argumen tidak salah dikenali. Panggilan itu dipancarkan ulang memakai argumen nilai bawaan dari defaultValue sebelum stub throw; tanpa itu JVM verifier menolak class dengan pesan Constructor must call super() or this() before return (cli/Stubifier.java:87-11915). findSuperOrThisCall mendahulukan target superclass, lalu menyimpan kandidat this(...) (cli/Stubifier.java:121-13717), sedangkan defaultValue memetakan tipe primitif ke konstanta nol dan sisanya ke ACONST_NULL (cli/Stubifier.java:139-15118). Writer memakai COMPUTE_MAXS, bukan COMPUTE_FRAMES, karena body stub berbentuk lurus sehingga ASM tidak perlu memanggil getCommonSuperClass yang akan mencoba memuat class framework (cli/Stubifier.java:68-8516).
Stubifier sebagai program mandiri
Selain dipanggil BuildJar, Stubifier punya main sendiri dengan kontrak Stubifier <classes-dir>: seluruh berkas .class di bawah direktori ditulis ulang di tempat (in place) (cli/Stubifier.java:41-6619). Class yang gagal di-stub dibiarkan apa adanya dan dilaporkan sebagai [stubify] WARN keep-as-is, lalu ringkasan [stubify] classes stubbed: <n>, failed: <n> dicetak ke stderr (cli/Stubifier.java:41-6619). Pada jalur BuildJar, kegagalan stub tidak menggagalkan build: entri tersebut hanya dilaporkan sebagai [build] keep-as-is (unstubbable) (cli/BuildJar.java:97-1092).
Pilihan baris perintah dan efeknya
Argumen dibaca dengan switch sederhana; apa pun yang bukan flag dianggap overlay, dan overlay diterapkan sesuai urutan argumen sehingga penulis terakhir menang (cli/BuildJar.java:50-6720).
| Argumen | Arti |
|---|---|
--base <jar> | SDK android.jar sebagai dasar; wajib, bersama --out (cli/BuildJar.java:50-6720) |
--out <jar> | path jar keluaran (cli/BuildJar.java:50-6720) |
--keep-bodies | melewati stubify dan mencetak peringatan bahwa lint/unit test Gradle akan gagal pada jar ini (cli/BuildJar.java:97-1092) |
--keep-dangling | melewati prune, sehingga class dengan supertype hilang tetap ikut (cli/BuildJar.java:74-9510) |
<overlay-jar>... | jar boot hasil dex2jar; hanya OVERLAY_NS yang diambil (cli/BuildJar.java:69-723) |
Tanpa --base atau --out, program mencetak usage: BuildJar --base <jar> --out <jar> [--keep-bodies] [--keep-dangling] <overlay-jar>... dan keluar dengan kode 2 (cli/BuildJar.java:50-6720). Penulisan zip memakai ZipEntry dengan setTime(0L) agar timestamp deterministik seperti jar SDK, lalu menutup dengan laporan [build] wrote <out> (<n> entries, <n> classes) (cli/BuildJar.java:111-12321).
⚠️
--keep-bodiesmenghasilkan jar dengan bytecode framework asli. Jar itu enak dibaca sebagai hasil dekompilasi, tetapi transform mockable-jar akan gagal di atasnya, jadi jangan dipakai untuklintatau unit test (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:199-20622).
Pemanggil dan kelanjutannya
Skrip cli/hiddenjar mengompilasi ketiga berkas Java (Stubifier.java, ClosureVerify.java, BuildJar.java) sekali dengan javac lalu menyimpannya di ${WORK_DIR}/cli-out, sehingga BuildJar bisa dijalankan berulang tanpa kompilasi ulang (cli/hiddenjar:205-222). Fungsi assemble_jar menerjemahkan OPT_KEEP_BODIES dan OPT_KEEP_DANGLING menjadi flag, memanggil java -cp … BuildJar --base … --out …, dan berhenti dengan die "assembly failed (see above)" bila BuildJar mengembalikan status bukan nol (cli/hiddenjar:224-236). Urutan langkah sebelum dan sesudah perakitan dibahas di Pipeline Build hiddenjar, sedangkan pemeriksaan hasil jar dibahas di Verifikasi Jar Kustom.