Jugg: Sub-3-Second Android Incremental Builds via Gradle-Bypass Sidecar Compilation
Tencent Music open-sources Jugg, an Android incremental build tool that achieves sub-3-second iteration by bypassing Gradle's fixed overhead via a sidecar compilation pipeline, using custom AAPT2 incremental linking, bytecode-level impact propagation, and JVMTI-based hot reload with hot-fix fallback, validated across 800k production builds.
Problem: Slow Incremental Builds in Large Android Projects
In large Android codebases, daily development suffers from multi-minute compile times even for trivial changes. This interruption degrades developer experience and productivity.
Jugg Overview
Tencent Music's Jugg is an open-source, Gradle-bypass incremental build solution developed over 5 years and validated with 800,000+ production builds across apps like QQ Music, WeSing, and JOOX. It reduces incremental build latency to under 3 seconds, saving an estimated 36,000+ hours (≈20 person-years) of wait time.
Industry Comparison: Trade-offs Between Gradle Proximity and Speed Ceiling
Existing approaches fall on a spectrum:
Close to Gradle (AGP task optimization, cache tuning): low maintenance, but speed capped by Gradle's fixed overhead.
Far from Gradle (Instant Run, Apply Changes, internal sidecar solutions): higher speed potential but greater compatibility cost.
Instant Run modified compilation, runtime, and deployment, causing compatibility issues and was deprecated. Apply Changes only optimizes deployment via JVMTI, leaving Gradle compilation time untouched. Internal sidecar solutions still run inside Gradle plugins, incurring Gradle startup and configuration overhead that becomes the bottleneck.
Jugg's Approach: Independent Sidecar Incremental Compilation
Jugg keeps Gradle as the trusted full-build source but routes daily incremental changes through a completely separate pipeline that skips Gradle's fixed costs. The minimal necessary steps are: detect changed files, compile them to deployable artifacts, and apply artifacts to the device.
Quick Start
Jugg ships as an Android Studio plugin (install from https://github.com/tencentmusic/jugg/releases), requires zero project modifications, and works with AS, command line, and cloud builds. All projects share a single implementation with no business-specific logic.
Incremental Build Pipeline (6 Steps)
Reuse Baseline : After a full Gradle build, Jugg captures APK, class files, generated sources, and R.jar as the baseline.
Detect Changes : Listens to IDE file-change events and Git diffs; if build.gradle or major dependencies change, it safely falls back to a full Gradle build.
Incremental Compilation : Bypasses Gradle task orchestration; directly invokes javac, kotlinc, d8, and a custom aapt2 inclink to produce minimal .dex and resources.arsc artifacts.
Impact Propagation / Chained Compilation : Performs bytecode/AST-level analysis to find dependent files that must be recompiled (e.g., method signature changes, constant inlining, abstract method additions). Affected sources are added to the next compilation loop until no new impacts appear.
Incremental Deployment : Chooses strategy based on change type and device capabilities: Hot Reload (JVMTI) for body-only changes (ms-level, no restart), Hot Fix (Dex injection) for structural changes (lightweight Activity/process restart), or Reinstall as last resort.
Save State : Persists deployed artifacts and checksums; the successful deployment becomes the new baseline for the next run.
Technical Deep Dive
1. Environment Preparation
Gradle Artifact Reuse : Collects {module}/build/outputs/apk/debug.apk, javac classes, kotlin-classes, and R.jar from the build directory. Paths vary by AGP version and build.gradle config; Jugg handles these variations.
Project Info Reading : Uses a dual data source — Android Studio APIs plus a Gradle init script ( readProjectInfo.gradle.kts injected via -I) — to gather 40+ parameters (source dirs, dependencies, classpath, jvm-target, language-version, module-name, etc.).
Change Detection : Combines IDE callbacks (instant) with Git diff (catches changes during IDE closure or git pull). Filters to actual source files and classifies them by type.
2. Source Incremental Compilation
2.1 Java Compilation
Uses javax.tools.JavaCompiler with parameters: -cp (classpath from baseline), -g (debug symbols), -source / -target (compatibility), -d (output dir). Only changed .java files are compiled.
2.2 Kotlin Compilation
Invokes K2JVMCompiler from org.jetbrains.kotlin:kotlin-compiler-embeddable. Kotlin's richer syntax and JVM interop require extra handling:
Java/Kotlin Mixed Compilation : Compile Kotlin first with -Xjava-source-roots pointing to changed Java sources; otherwise missing references cause class is not abstract..., reference not found, etc.
.kotlin_module Merging : Top-level declarations and extension functions are stored in .kotlin_module. Jugg reads the old module via kotlin-metadata-jvm, merges new declarations after compilation, and writes back.
internal Visibility : Kotlin mangles internal members with $module_name suffix (e.g., func1$app_debug). Jugg sets -module-name correctly to avoid NoSuchMethodError at runtime.
2.3 Class to Dex (D8)
Runs d8 to convert class files to dex, including desugaring (lambdas, JDK8+ methods, method references, repeatable annotations, default interface methods). Default-method desugaring generates a companion class and bridge methods, requiring --classpath with all supertypes. Jugg also supports coreLibraryDesugaring for API desugaring.
3. Resources and Other Inputs
3.1 Custom AAPT2 inclink (97% Speedup)
Standard aapt2 splits into compile (XML → .flat) and link (assign IDs, produce resources.arsc, R.java). link remains slow (10s+ for 10k+ resources). Jugg patches aapt2 adding inclink: inclink --load loads existing APK resources and resources.arsc. inclink incrementally links changed .flat files, outputting updated .dex and resources.arsc.
If no new IDs are added, R.java generation is skipped (saves 2-3s).
Result: resource incremental compile drops from 10-15s to ~100ms. Trade-off: deleted resource IDs are retained until next full build (acceptable for debug).
3.2 Assets, Manifest, Native Libraries
Assets : copied directly.
AndroidManifest.xml : not incrementally deployable. Jugg diffs, merges changes, recompiles with aapt2, updates the APK, resigns, reinstalls, and replays previous incremental deployments.
Native Libraries (.so) : updated via APK reinstall; incremental dex injection not yet implemented.
4. Impact Propagation and Chained Compilation
Scenarios handled:
Method signature change/delete → recompile all referencing classes.
Field signature change/delete → recompile all referencing classes.
Abstract method added to superclass/interface → recompile all subclasses.
Constant ( const val / static final) change → recompile all inlining sites (via AST parsing since constants are inlined into bytecode).
Bytecode parsing (via Dex) provides reference graphs for 1-3; AST parsing handles 4. Affected sources are queued for the next 03 → 04 loop until closure.
5. Incremental Deployment
5.1 Strategy Selection: Hot Reload vs Hot Fix
After compilation, Jugg performs Class Diff :
No structural change (only method bodies) → Hot Reload via JVMTI (Apply Changes channel), ms-level, no restart.
Structural change (added/removed methods, fields, interfaces, inheritance) → Hot Fix : inject incremental dex into ClassLoader chain, lightweight restart.
5.2 Reusing Apply Changes Channel
Reverse-engineered Android Studio Deploy Agent's Socket+Protobuf protocol. Jugg constructs the required deployment metadata (deployment ID, incremental class bytes, resource streams) and pushes through the same channel. Average transport+apply time: ~0.9s. Zero SDK integration required on the app side.
Apply Changes natively lacks hot-fix support. Jugg exploits a behavior: submitting structurally changed classes as "new Class" bypasses JVMTI reload checks, persists them in the dex list, and triggers Apply Changes' restore flow on next app start — achieving hot-fix without custom runtime.
5.3 Classic Hot Fix and APK Patching
For vendor-modified frameworks (HarmonyOS 4.2 ClassLoader timing, Oppo/Vivo Android 11 AssetManager crashes, Xiaomi JVMTI disable), Jugg falls back to a classic hot-fix: reflectively inject incremental dex in Application.attachBaseContext.
For RemoteView and CI scenarios, Jugg patches the APK directly: update AndroidManifest.xml, resign, place incremental dex in assets, and load via reflection at startup.
Engineering Robustness
Jugg handles numerous edge cases encountered during 800k+ runs:
Detecting file changes while IDE was closed.
Mixed Java/Kotlin/module compilation.
Device switching mid-session.
Restoring incremental deployment state after app uninstall.
Multi-project, multi-device debugging.
Retry mechanisms: incremental cache cleanup, progressive deployment timeout recovery, ADB auto-reconnect.
These practical fixes, though individually simple, collectively make Jugg a reliable daily driver.
Conclusion
Jugg is now open source at https://github.com/tencentmusic/jugg (100k+ words of technical docs and full Wiki). The team welcomes issues, stars, and community contributions to help more Android developers escape compile-time pain.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
