Source incremental compilation
When Gradle compiles a module, it does more than convert Java and Kotlin into classes. It establishes the complete classpath, identifies Kotlin module boundaries, connects generated source, checks unchanged callers, and generates DEX under one consistent set of rules.
To shorten an everyday Run, Jugg processes only source changed in the current Run. With fewer files, these guarantees that Gradle normally provides implicitly also disappear. The challenge of source incremental compilation is therefore not how to invoke javac and kotlinc, but how to restore full-build semantics during local compilation and return to Gradle when that cannot be done.
What is lost when only changed source is compiled
A trusted Gradle build leaves APKs, classes, dependencies, and compilation parameters for later incremental compilation. The simplest incremental approach can reuse these classes as dependencies and send only changed files to the compiler:
latest Gradle artifacts
-> use as the classpath for the current Run
-> compile changed Java / Kotlin
-> generate new classesThis path avoids complete task scheduling and many unchanged inputs, but it also reduces what the compiler can see.
| Guarantee normally provided by Gradle | Problem in a simple incremental compilation | Jugg handling |
|---|---|---|
| Complete compilation context | Classpath, module identity, or tool version differs | Restore compilation context for the current module from the Gradle baseline |
| Java, Kotlin, and generated source visible in the same Run | The compiler reads stale symbols from the previous Run | Order compilation by artifact dependency and pass current source |
| Complete caller checks | Unchanged callers do not participate in compilation | Compare class structures and continue compiling affected source |
| Consistent DEX conversion | New DEX has a different desugared structure from the baseline APK | Read baseline state, supply affected classpath, and use the same conversion strategy |
The following sections explain why these gaps can cause compiler errors or runtime crashes and how Jugg contains them.
Reuse the Gradle baseline instead of reimplementing Gradle
Java compilation is relatively direct. Jugg restores parameters such as the classpath, source inputs, and target bytecode version from the project snapshot and uses the JDK compiler to generate classes. If an Android Studio runtime cannot provide the Java compiler through the standard entry point, Jugg uses another tool entry bundled with the JDK so that a different IDE runtime does not stop incremental compilation.
Kotlin is more sensitive to compilation context. The module name, output directory, and same-module classpath visible to the compiler are not merely command-line details; they change source visibility and final bytecode.
| Context mismatch | User-visible result |
|---|---|
| A same-module class is treated as an external dependency | A smart cast accepted by Gradle fails during incremental compilation |
| Module name or same-module classpath differs | An internal member becomes inaccessible, or runtime throws NoSuchMethodError |
| The new module description is not merged | Later Kotlin compilation cannot find a new top-level declaration or extension function |
Jugg writes Kotlin output back to the class directory for the current module and passes the same module identity used by Gradle. The compiler then treats old classes and current source as one module. A .kotlin_module generated by single-file compilation is also merged with the original module description so that other Kotlin files can reference new top-level declarations.
Another issue comes from Android Studio itself. The IDE and Kotlin compiler can include classes with the same package names but different versions. Running the compiler directly in the IDE class-loading environment can mix both implementations. Jugg loads the Kotlin compiler and its dependencies in an isolated class-loading environment so that the compiler runtime sees one consistent version.
Kotlin Multiplatform adds common source, platform source, and source set inheritance. Jugg prefers source set and fragment information from Gradle Kotlin tasks. If cache is missing or relationships cannot be confirmed, it does not guess module structure from directory names. Failure of auxiliary information affects only that capability, while ordinary Kotlin compilation keeps its existing path.
Mixed-language projects need current symbols from the same Run
Java and Kotlin frequently reference one another. Suppose one Run changes both a Java interface and its Kotlin implementation. If the Kotlin compiler sees only the old Java class from the baseline, it validates Kotlin against a stale method signature and can produce reference not found, type mismatch, or missing abstract method errors.
Jugg first lets the Kotlin compiler read current Java source. New Kotlin classes then enter the Java compilation classpath so that Java sees the latest Kotlin symbols.
generated source from annotation processing, DataBinding, and similar stages
-> Kotlin reads current Java source
-> Java reads current Kotlin classes
-> combine all new classesGenerated source must precede language compilation. If source produced by DataBinding, ViewBinding, annotation processors, or KSP/KAPT appears only after language compilation, the current Run cannot form complete classes. Jugg registers generated source already produced in the current Run as compilation input. If generation context cannot be confirmed, Gradle rebuilds the baseline.
Successful compilation does not prove the change is safe to run
A full compilation checks all source in the module. A local compilation checks only directly changed files and can defer what should have been a compile-time error until runtime.
For example, class A deletes a method while its caller B is unchanged:
A deletes a method
-> A compiles successfully
-> B is not compiled and retains the old call
-> after deployment, execution reaches the old call
-> runtime throws NoSuchMethodErrorAdding an abstract method, changing a field signature, or changing an inline method creates similar risks. After the first source compilation, Jugg compares old and new class structures, then uses references from the baseline and deployment history to find callers, subclasses, and corresponding source files and adds them to the next round.
changed class
-> compare with the currently effective class
-> find method, field, inheritance, or inline impact
-> query callers, subclasses, and corresponding source
-> append another compilation round until impact convergesThe comparison uses classes that the device can currently load, not only classes in the latest Gradle APK. Changes already deployed incrementally must participate as well; otherwise, consecutive changes would calculate impact from an outdated structure. See recompilation for propagation rules and constant reference analysis for compile-time constants.
New DEX must preserve the structure of the baseline APK
Java and Kotlin produce classes that D8 then converts to DEX. D8 can also perform desugaring, rewriting interface default methods and newer JDK APIs into structures that run on older Android versions.
Incremental compilation cannot decide whether to desugar only from the current module's minSdk. The device runs an APK produced by the latest Gradle build. If the baseline APK was desugared but current DEX is not, a recompiled subclass may no longer contain methods generated during desugaring while the old interface also lacks the original default method, producing AbstractMethodError at runtime. Conversely, if the baseline was not desugared, the current Run must not introduce another structural model unexpectedly.
Jugg infers whether the application baseline was desugared from class structures in the APK and uses the same strategy for current D8. When desugaring is enabled, D8 also needs interfaces related to default methods. Sending the complete module classpath back through D8 would expand the scope, so Jugg first uses the reference index to find related interfaces, subclasses, and callers and builds the minimum temporary classpath for the current Run:
new class
-> query desugaring state from the baseline APK
-> find interfaces related to default methods and core library rewrite information
-> assemble the minimum classpath required by the current Run
-> generate class-granularity DEX with the same desugaring strategyJugg also prefers D8 from the Android Gradle Plugin used by the current project, reducing bytecode differences caused by tool versions. It falls back to the Jugg-bundled version only if the project tool cannot load safely or execution fails, while preserving the original failure information for troubleshooting.
How a source incremental compilation ends
The preceding handling converges into a fixed flow:
trusted Gradle baseline
-> collect directly changed source and generated source from the current Run
-> Kotlin / Java compilation
-> compare class structures and append affected source
-> D8 generates DEX and handles desugaring structure differences
-> release variants process class names with the original mapping
-> pass local artifacts to deploymentDeleting an entire source file does not create a new compilation input. After a rename, the new path can compile, but deletion of the old path still does not generate a class removal. An old class no longer produced by new source therefore remains in the installed APK or incremental deployment result and may still be reached by direct references, reflection, or class loading. Use a full Gradle build to generate a new APK baseline when you need to verify that the class no longer exists.
This path depends on a trusted baseline. On the first Run, after build scripts or dependency context changes, when generated code cannot be confirmed, when incremental impact grows too large, or when compilation fails without recovery, Jugg runs Gradle and refreshes APKs, classes, and local indexes. Returning to Gradle is a boundary of incremental compilation and never presents incomplete artifacts as a successful result.