anvil: Compiler doesn't pick incremental changes to multibindings in some circumstances
When:
- We have classes with
@ContributesMultibindingannotation in a library module - Said library module includes Jetpack Compose compiler
- New incremental compilation is enabled (
kotlin.incremental.useClasspathSnapshot=trueingradle.properties)
Changes on those annotations may not get picked up when making incremental builds.
Steps to reproduce:
- Download and open Project.zip
- Run project
- Check logcat
- Note that logs will print two entries (as it should)
- Open LibraryMultibinds.kt
- Comment out
@ContributesMultibindingannotation - Run again and check logcat
- Logs print two entries again, even though we have commented one
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 16
- Comments: 44 (6 by maintainers)
Commits related to this issue
- Only clear the output dir the first round Hopeefully resolves #693 — committed to ZacSweers/anvil by ZacSweers a year ago
- Remove kotlin incremental for wait fix Kotlin and Anvil (#642) **Background** Now Kotlin compiles not save annotation data in metadata in .class for use in plugin **Changes** Remove incremental bu... — committed to flipperdevices/Flipper-Android-App by Programistich a year ago
- Add fix from https://github.com/square/anvil/issues/693#issuecomment-1744013947 to the gradle plugin — committed to r0adkll/anvil by r0adkll 8 months ago
Yes.
TL;DR fix
To solve it for yourself immediately, put this in your Anvil project(s) or in a convention plugin:
Kotlin DSL (build.gradle.kts)
Groovy DSL (build.gradle)
Why?
Anvil’s output directory isn’t being added to the
KotlinCompiletask’s outputs. This is why incremental compilation (and compilation from a remote build cache) is so hit-or-miss.Assume you have a single
:libkotlin-jvm library, and this is the only source:Handling Deletions and Build Cache
This script reproduces running a build on an Anvil module where the sources aren’t in the build directory, but they are cached (locally, in this case).
The second
compileKotlintask will beFROM-CACHE, because Gradle does know that it needs to run something. The console output will be:Note that there are only three outputs listed, and none of them have anything to do with Anvil:
Those three outputs were restored from cache, but the Anvil-generated code was not. That means no hints, no factories, and somewhere downstream, no Dagger bindings for
MyClass.Now if you apply the fix above run the same three commands, the console output will be:
Notice that the Anvil-generated code is now listed as an output, and it has now been restored. That means compilation can continue as intended.
Handling Incremental Changes
Incremental builds in Gradle and Kotlin both monitor outputs.
Without the fix from above, if you comment out the annotation in
MyClass.ktand runcompileKotlinagain, the console output will be:(full output)
Here’s the important part:
Kotlin is not watching the Anvil-generated code as part of its incremental compilation logic. That’s why we’re here.
Now if I:
MyClass.kt./gradlew compileKotlinMyClass.ktagain./gradlew compileKotlin -iThe output is now:
(full output)
And here’s that same important part:
KGP is populating its incremental compilation outputs based upon the outputs of the
KotlinCompiletask. Simply adding the directory to outputs is enough to fix the behavior.Permanent Fix?
I’ll do a quick update to
AnvilPluginand get a snapshot out tonight, and I’ll comment here when it’s available.This issue highlights a lack of test coverage around the Gradle plugin itself. There’s currently nothing in the way of Gradle Test Kit tests. I’ll be adding the scaffolding and tests themselves within the next few days. Hopefully we can get a new release with a permanent fix out soon.
We’re currently evaluating a couple different options but the TLDR is that it’s not a simple fix and will likely require changes on both the Anvil side and Kotlin side to fully address. I’ll post an update when we have more concrete details to share.
Quick update on where we’re at with this: Currently there doesn’t appear to be a feasible way for us to completely fix this issue without a new IC API for compiler plugins. That new API is being tracked in KT-51733.
Separately, we’ve also begun working on KSP support for Anvil. KSP has the type of IC API that we need in-place already, which should help to resolve this issue. However, I can’t give an estimate on when KSP support will be ready since we are just starting and it will also depend on when Dagger finishes adding its KSP support.
In the meantime, the best known work-around is still using the old IC by setting
kotlin.incremental.useClasspathSnapshot=false, as noted by @hungvietnguyen above.You may also notice slightly fewer instances of incremental compilation issues with Kotlin 1.9.0 and Anvil 2.4.7+ thanks to KT-58289 being fixed.
Yes, as far as I can tell. I retested the use-case from #710 and observed the same behavior outlined in issue 2 around the difficulties with managing the generated output files.
Thanks for expanding on my comment @hungvietnguyen, I could have been clearer about it being situationally helpful for reducing the incremental issue occurrences (as opposed to a complete work-around).
Also adding a quick clarification on this ^: although full Anvil KSP support will depend on Dagger KSP, adding KSP support for factory generation is something we can do independently. So barring any major gotchas, that piece should some a bit sooner 🤞.
There seem to be 2 issues here:
[Fixed] Issue 1 -
:app:kaptGenerateStubsDebugKotlinis UP-TO-DATE when an annotation is added/removed in:mylibraryAs explained in https://github.com/square/anvil/issues/693#issuecomment-1500455299, this is a regression in KGP 1.8.20 where the new IC (
kotlin.incremental.useClasspathSnapshot) is enabled by default.I’ve verified that the issue has been fixed in KGP 1.9.0-Beta (https://youtrack.jetbrains.com/issue/KT-58289). That is, in KGP 1.9.0-Beta,
:app:kaptGenerateStubsDebugKotlinis no longer UP-TO-DATE when an annotation is added/removed in:mylibrary.[Not Yet Fixed] Issue 2 - Anvil doesn’t generate
mylibrary/build/anvil/src-gen-debugwhen an annotation is added in:mylibrarySteps to reproduce:
mylibrary/src/main/java/com/matejdro/mylibrary/LibraryMultibinds.kt,@ContributesMultibinding(AppScope::class)is removed../gradlew clean :app:kaptGenerateStubsDebugKotlin(or./gradlew clean :mylibrary:compileDebugKotlinto focus on this task).mylibrary/src/main/java/com/matejdro/mylibrary/LibraryMultibinds.kt, add@ContributesMultibinding(AppScope::class)../gradlew :app:kaptGenerateStubsDebugKotlin(or./gradlew :mylibrary:compileDebugKotlin).mylibrary/build/anvil/src-gen-debugis empty.app/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.ktis still correctly re-generated (it containsLibraryMultibinds), but maybe it’s just lucky, let’s focus on the fact thatmylibrary/build/anvil/src-gen-debugis empty.mylibrary/build/anvil/src-gen-debugis deleted (as expected), so the bug doesn’t manifest in this scenario. (Again, this seems to be just by luck – see root cause below.)Root cause:
com.squareup.anvil.compiler.codegen.CodeGenerationExtension#analysisCompletedis called 4 times. That’s because the Kotlin incremental compiler runs in multiple rounds (see this while loop). In this example, there are 2 rounds: Each round calls the Kotlin compiler once, and each Kotlin compiler invocation calls thecom.squareup.anvil.compiler.codegen.CodeGenerationExtension#analysisCompletedmethod twice.mylibrary/build/anvil/src-gen-debugis correctly generated in the first round, but gets deleted in the second round, resulting in an empty directory when the task finishes.kotlin.incremental.useClasspathSnapshot), so it’s likely to be a bug in Anvil.To sum up: It seems that Anvil currently doesn’t work well with Kotlin incremental compilation (IC). Specifically, it doesn’t handle the fact that Kotlin IC works in multiple rounds with multiple calls to the Kotlin compiler with different data each time.
Anvil currently disables Kotlin IC only for the KaptGenerateStubsTask. Maybe it should do that for the
KotlinCompiletask too until it can support Kotlin IC properly?@svilen-ivanov can confirm, I received exactly the same issue when tried to launch :sample:app:assembleDebug.
Steps to reproduce:
:sample:app:assembleDebug@Inject-constructor.:sample:app:assembleDebugagainError will be returned during
:sample:app:kaptDebugKotlinexecution:Project
sample:libraryitself compiles without errors, butanvil/src-gen-debugdirectory is empty.The sample project was very useful! I found that when
@ContributesMultibinding(AppScope::class)is added toclass LibraryMultibindsin:mylibrary, the new IC doesn’t detect a change because the new IC relies on the Kotlin class metadata inLibraryMultibinds.classand in this case the Kotlin class metadata doesn’t change (whether this is a bug is TBD [1]).Because of this, the
:app:kaptGenerateStubsDebugKotlintask is UP-TO-DATE. (This seems correct for this task because its output shouldn’t depend on changes to annotations of classes on the classpath.)However, Anvil seems to rely on the
:app:kaptGenerateStubsDebugKotlintask being run in order to do some work (e.g., to generateapp/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.ktwhich contains references toAppMultibindsandLibraryMultibinds). This can be seen at this code comment.I guess we’ll need to figure out [1] first. From there, it will be clear whether the bug is in the new IC or in Anvil.
I can confirm the beta version works for my team after opting in to
trackSourceFiles! Changes to files with a (custom) code generator applied to them no longer give any incremental build issues.Updating my custom code generator to use
GeneratedFileWithSourceswas easy.Thanks to everyone who helped figure this out and resolved it. 👏
This seems fixed by
v2.5.0-beta01withtrackSourceFileson 🎉Thanks for the workaround!
After some testing, it appears the workaround breaks UP-TO-DATE checks for ksp tasks. All ksp tasks in modules where anvil is seem to always execute, even without changes. Info from build scan points to this workaround:
It seems that ksp task extends
KotlinCompileand thus causing this. I’ve managed to workaround it with yet another hack by excluding ksp task from the workaround, but I’m not sure what are the full implications of this:I believe the workaround is to set
kotlin.incremental=falseingradle.properties. This will avoid the incremental compilation issues completely because compilation will always be non-incremental. The downside is that it will slow down your build significantly.I wouldn’t recommend setting
kotlin.incremental.useClasspathSnapshot=falseinstead ofkotlin.incremental=false. The reason is that:kotlin.incremental.useClasspathSnapshot=falsewill not help (because the issue withkotlin.incremental.useClasspathSnapshot=true– Issue 1 in this comment – has been fixed in KGP 1.9.0+).kotlin.incremental.useClasspathSnapshot=falseonly fixes Issue 1 in this comment; Issue 2 in that comment, which is more fundamental, is still not fixed.kotlin.incremental.useClasspathSnapshot=falsecan be used if the performance penalty ofkotlin.incremental=falseis too much and you’re happy with reducing the number of incremental compilation issues rather than resolving all of them.I’ve filed https://youtrack.jetbrains.com/issue/KT-57919/Kotlin-class-metadata-doesnt-contain-info-about-class-annotations for the Kotlin team to take a look.
@RBusarow thanks for the workaround, I see when we add the new output in the task we are producing overlapping outputs: https://ge.solutions-team.gradle.com/s/pk6mtthejsday/timeline?details=j7bq5uuamihla I tested as well the #836 branch and I’m seeing the same problem.
If you use custom Anvil code generators under your own namespace, make sure to remove the
anvilnamespace from the end of the build directory path:layout.buildDirectory.dir("anvil/src-gen-$ssName")I’m using
java-test-fixturesplugin. Due to unknown reason to me it has undefinedsourceSetName. Here is my workaround:EDIT. After I brought up my project to working state, the workaround doesn’t work for me. The
anvil/src-gen-test/anvildirectory is still wiped out on the second build when a change (added constructor parameter) to a class that participates in multi-binding. 😦Does anyone found a workaround that allows updating Kotlin version without disabling incremental compilation?
Hi @JoelWilcox, just wanted to follow up if
multifileFacadeToPartsAPI mentioned in KT-51733 was helpful in any way?Also - as Dagger has just brought KSP support are there any gaps in Anvil that could be filled in by external contributors to help solving this issue?
Thanks for the update. Are the IC regressions in the recent versions (such as #710) also caused by the same issue?
Sorry folks for confusion I meant we tried this option
kotlin.incremental.useClasspathSnapshot=falsewith false (not true), and it didn’t work for us.I bet we saw this issue even when we add a new argument (dependency) to the inject constructor, its not just adding / removing annotation but even changing the dependencies.