Kotlin incremental compilation lands in Buck2

Meta’s Buck2 build system now supports Kotlin incremental compilation. For modules that have grown beyond the handful of files that Buck2’s architecture favors, the payoff is substantial: some critical modules now build up to three times faster.

Buck2 has long promoted small modules as the path to fast builds, which made incremental compilation seem unnecessary. But as codebases grow and modules inevitably balloon, that assumption stops holding. Integrating Kotlin’s incremental compiler changes the math.

Choosing the right integration point

As of Kotlin 2.2.0, the only guaranteed public interface to the compiler is the command-line interface (CLI), which does not support incremental compilation. The alternative — wiring directly into Kotlin’s internal compiler components — would work but leaves the toolchain fragile, since those APIs carry no backward-compatibility guarantees.

The Kotlin Build Tools API, introduced in Kotlin 1.9.20, offered a third path. Although still experimental, it provides official support for incremental compilation. Adopting it early meant living with instability, but it also meant influencing the API’s direction rather than betting on internals that could break with every Kotlin release.

One wrinkle surfaced immediately: the Build Tools API depends on the shaded Kotlin compiler (kotlin-compiler-embeddable), but the Android toolchain was built against the unshaded kotlin-compiler. That mismatch produced java.lang.NoClassDefFoundError crashes at runtime.

Rather than migrate the entire toolchain at once, the team used jarjar to strip the org.jetbrains.kotlin prefix from the Build Tools API itself — an unshading workaround that kept development moving. Once the prototype worked, they circled back and fully migrated to the shaded compiler, restoring alignment with the API and a more stable foundation.

Preserving state between builds

Incremental compilation requires access to the previous build’s output. Buck2 deletes that output by default before rebuilding a module. Incremental actions solve this by letting build rules skip the automatic cleanup, giving the compiler the artifacts it needs. The tradeoff is manual responsibility for deciding what remains useful and what should be cleaned up.

The same mechanism supports change detection. Incremental actions expose hash digests for every action input, so comparing digests between builds yields a precise list of changed files — no need for the compiler to detect changes itself. Buck2 passes that list to the compiler, and incremental compilation begins immediately.

Making the cache portable

A non-relocatable compiler cache is fine for a single developer on a single machine. It fails badly under distributed builds, where compilation may execute remotely and mismatched paths in cached data produce spurious ambiguity errors and conflicting overloads.

The fix was straightforward: explicitly configure the root project directory and build directory in the incremental compilation settings. That keeps the cache consistent no matter where a build runs.

Tracking changes inside and outside the module

The Kotlin incremental compiler decides what to recompile by monitoring two things: files within the module being rebuilt, and the module’s dependencies.

For intra-module changes, the compiler can detect differences automatically, but only on Kotlin 2.1.20 or later. On earlier versions, the changed-file list must be supplied by the build tool. Even on newer versions, providing the list upfront is more efficient than letting the compiler figure it out. Buck2’s incremental actions supply that list from input hash digests with no extra work.

For dependency changes, the compiler relies on classpath snapshots that capture each dependency’s Application Binary Interface (ABI). Comparing current snapshots against previous ones reveals ABI changes, which lets the compiler determine which files in the module are affected — a layer of filtering on top of standard compilation avoidance.

Buck2 generates these snapshots in a dedicated action from library outputs:

Because the snapshot action runs separately, it can execute remotely or be pulled from cache rather than taxing the local machine. When only the module changed and dependencies are untouched, the Build Tools API also allows skipping snapshot comparison entirely — an optimization that came almost for free since Buck2 already had the data from incremental actions.

Making compiler plugins incremental-aware

Custom compiler plugins, many of which support Buck2’s build optimization strategy, posed two challenges.

Handling partial inputs

Incremental compilation means the compiler does not see all source files. Plugins built for full-module compilation produced incomplete results on partial inputs. They had to become incremental themselves.

Managing multiple compilation rounds

The incremental compiler may recompile more than just changed files. When circular dependencies make the affected set hard to determine precisely, it approximates by compiling in multiple rounds within a single build. Each round runs compiler plugins with a different file set, and later plugin runs can overwrite earlier outputs. The fix was updating plugins to accumulate results across rounds instead of replacing them.

Annotation processors: mostly a non-issue

Most annotation processors rely on Kotlin Symbol Processing (KSP2), which operates independently of the standard compilation flow via the Kotlin Analysis API. No changes were needed, and KSP2’s built-in incremental processing support is self-contained.

Processors still on KAPT, which runs as a compiler plugin, fared differently. They previously executed in a separate, always non-incremental step before the main compilation.

ABI compilation trade-offs remain open

Buck2 builds Android modules against class ABI rather than full JARs to maximize cache hits, using the jvm-abi-gen compiler plugin to produce ABI during compilation. Incremental compilation created two problems: the plugin lacks incremental support, and ABI extraction now happens twice — once via the plugin and again when classpath snapshots are created.

Switching to full JAR compilation and relying entirely on classpath snapshots could solve both issues in theory, but it would sacrifice existing build optimizations. The current solution — a custom merge of newly generated ABI with the previous result — works but is admittedly suboptimal. Better options under exploration include reusing the information collected for classpath snapshots or moving ABI generation into the compiler itself. JetBrains tracks the latter on KT-62881.

Measuring the Real-World Impact

Benchmarks can hint at a feature's potential, but they rarely capture how a change behaves under the pressure of a large, constantly evolving codebase. To get a true picture of the Kotlin incremental compiler's value at Meta, we relied on A/B testing. This methodology gave us a clean, isolated view of the change's effect on build performance at scale, although it required extra effort to maintain cache health across the different test variants.

We began by targeting the largest modules—those already known to be the primary bottlenecks for build times. Given their size, we expected to see immediate dividends, and we were not disappointed.

Incremental Build Results

Early data from a four-week period shows the effect of enabling incremental compilation for select targets on local incremental build times. These numbers include not just the compilation phase but also annotation processing and other optimizations added along the way.

The results were decisive. For the average developer, we observed roughly a 30% improvement in build speed. For modules that skip annotation processing, the speedup was even more dramatic, nearly doubling performance. These findings made it clear that the Kotlin incremental compiler is a permanent fixture.

Rollout and Future Work

With Kotlin incremental compilation now supported in Buck2, we are actively rolling it out across Meta's codebase. The feature is currently available for internal use only, but work is underway to bring it to the recently released open source Buck2 toolchain.

Beyond this, we are investigating how to extend incrementality across the full Android toolchain. This includes work on tools like Kosabi, the Kotlin counterpart to Jasabi, with the goal of unlocking further build-time reductions and an even smoother developer experience. You can find more about our efforts on the Meta Open Source site.