Lompat ke konten
dokumentasi.id
Commit 98ceeb4
v2
Star3,6 rb

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-44). Langkah stub dikerjakan Stubifier di level byte, dan BuildJar memanggilnya per entri lewat Stubifier.stubify (cli/BuildJar.java:97-109).

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-72).

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.
Fig. 1: Alur perakitan jar di BuildJarBase 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-31).

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-146). Penyempurnaan yang sama dicatat sebagai salah satu dari dua keunggulan CLI dibanding merge manual (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:141-150).

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-39). Daftar yang diizinkan hanya tiga prefiks (cli/BuildJar.java:48).

Prefiks entriSumber di perangkatAlasan di-overlay
android/framework.jar + APEX javalibmenampung sebagian besar hidden API yang ditandai @hide
com/android/internal/framework.jar + APEX javalibAPI internal yang tidak pernah masuk SDK jar
dalvik/framework.jarkelas 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-39). Base SDK jar sudah menyediakan stub publik java.*, javax.*, org.*, dan dalvik.* yang dikurasi (cli/BuildJar.java:33-39). Aturan "hanya tiga namespace" ini juga menjadi kesimpulan panduan merge manual (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:207-210).

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-95).

FaseFungsiKriteriaAkibat
GuardClosureVerify.hardMissingsupertype hilang dan prefiksnya android/, dalvik/, java/, atau javax/System.exit(1); prune tidak dijalankan agar regresi tidak tertutupi (cli/ClosureVerify.java:114-130)
FixpointClosureVerify.soakDanglingsupertype tidak ada di himpunan class dan tidak bisa dimuat JDKnama internal masuk remove, entri name + ".class" dihapus (cli/ClosureVerify.java:132-162)

Ambang keras ditentukan konstanta FAIL_PREFIXES = { "android/", "dalvik/", "java/", "javax/" }, yaitu namespace yang dijanjikan lengkap oleh jar (cli/ClosureVerify.java:39-40). Keluaran normalnya satu baris [build] pruned N dangling class(es) (supertype graph closed) (cli/BuildJar.java:74-95).

⚠️ Bila hardMissing tidak kosong, BuildJar keluar 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-95).

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-38).

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.
Fig. 2: Langkah Stubifier.stubify untuk satu classBody method konkret dibuang, rantai super/this dipertahankan, lalu class ditulis ulang dengan COMPUTE_MAXS.
Bagian classPerlakuan
body method konkretdiganti rangkaian NEW RuntimeException + LDC "Stub!" + ATHROW, dengan maxStack/maxLocals di-reset ke 0 (cli/Stubifier.java:87-119)
tryCatchBlocks, localVariables, anotasi variabel lokaldibersihkan sebelum body baru dipasang (cli/Stubifier.java:87-119)
method ACC_ABSTRACT dan ACC_NATIVEdilewati karena tidak punya attribute Code (cli/Stubifier.java:68-85)
signature, generics, throws, annotation, field konstan, struktur enum, metadata inner classdipertahankan (cli/Stubifier.java:23-38)

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-119). findSuperOrThisCall mendahulukan target superclass, lalu menyimpan kandidat this(...) (cli/Stubifier.java:121-137), sedangkan defaultValue memetakan tipe primitif ke konstanta nol dan sisanya ke ACONST_NULL (cli/Stubifier.java:139-151). 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-85).

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-66). 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-66). Pada jalur BuildJar, kegagalan stub tidak menggagalkan build: entri tersebut hanya dilaporkan sebagai [build] keep-as-is (unstubbable) (cli/BuildJar.java:97-109).

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-67).

ArgumenArti
--base <jar>SDK android.jar sebagai dasar; wajib, bersama --out (cli/BuildJar.java:50-67)
--out <jar>path jar keluaran (cli/BuildJar.java:50-67)
--keep-bodiesmelewati stubify dan mencetak peringatan bahwa lint/unit test Gradle akan gagal pada jar ini (cli/BuildJar.java:97-109)
--keep-danglingmelewati prune, sehingga class dengan supertype hilang tetap ikut (cli/BuildJar.java:74-95)
<overlay-jar>...jar boot hasil dex2jar; hanya OVERLAY_NS yang diambil (cli/BuildJar.java:69-72)

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-67). 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-123).

⚠️ --keep-bodies menghasilkan jar dengan bytecode framework asli. Jar itu enak dibaca sebagai hasil dekompilasi, tetapi transform mockable-jar akan gagal di atasnya, jadi jangan dipakai untuk lint atau unit test (docs/BUILD_CUSTOM_ANDROID_JAR_FROM_EMULATOR.md:199-206).

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.