anvil: Compiler doesn't pick incremental changes to multibindings in some circumstances
When:
- We have classes with
@ContributesMultibinding
annotation in a library module - Said library module includes Jetpack Compose compiler
- New incremental compilation is enabled (
kotlin.incremental.useClasspathSnapshot=true
ingradle.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
@ContributesMultibinding
annotation - 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
KotlinCompile
task’s outputs. This is why incremental compilation (and compilation from a remote build cache) is so hit-or-miss.Assume you have a single
:lib
kotlin-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
compileKotlin
task 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.kt
and runcompileKotlin
again, 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 compileKotlin
MyClass.kt
again./gradlew compileKotlin -i
The 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
KotlinCompile
task. Simply adding the directory to outputs is enough to fix the behavior.Permanent Fix?
I’ll do a quick update to
AnvilPlugin
and 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:kaptGenerateStubsDebugKotlin
is UP-TO-DATE when an annotation is added/removed in:mylibrary
As 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:kaptGenerateStubsDebugKotlin
is 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-debug
when an annotation is added in:mylibrary
Steps to reproduce:
mylibrary/src/main/java/com/matejdro/mylibrary/LibraryMultibinds.kt
,@ContributesMultibinding(AppScope::class)
is removed../gradlew clean :app:kaptGenerateStubsDebugKotlin
(or./gradlew clean :mylibrary:compileDebugKotlin
to 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-debug
is empty.app/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.kt
is still correctly re-generated (it containsLibraryMultibinds
), but maybe it’s just lucky, let’s focus on the fact thatmylibrary/build/anvil/src-gen-debug
is empty.mylibrary/build/anvil/src-gen-debug
is 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#analysisCompleted
is 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#analysisCompleted
method twice.mylibrary/build/anvil/src-gen-debug
is 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
KotlinCompile
task 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:assembleDebug
againError will be returned during
:sample:app:kaptDebugKotlin
execution:Project
sample:library
itself compiles without errors, butanvil/src-gen-debug
directory is empty.The sample project was very useful! I found that when
@ContributesMultibinding(AppScope::class)
is added toclass LibraryMultibinds
in:mylibrary
, the new IC doesn’t detect a change because the new IC relies on the Kotlin class metadata inLibraryMultibinds.class
and in this case the Kotlin class metadata doesn’t change (whether this is a bug is TBD [1]).Because of this, the
:app:kaptGenerateStubsDebugKotlin
task 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:kaptGenerateStubsDebugKotlin
task being run in order to do some work (e.g., to generateapp/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.kt
which contains references toAppMultibinds
andLibraryMultibinds
). 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
GeneratedFileWithSources
was easy.Thanks to everyone who helped figure this out and resolved it. 👏
This seems fixed by
v2.5.0-beta01
withtrackSourceFiles
on 🎉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
KotlinCompile
and 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=false
ingradle.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=false
instead ofkotlin.incremental=false
. The reason is that:kotlin.incremental.useClasspathSnapshot=false
will 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=false
only fixes Issue 1 in this comment; Issue 2 in that comment, which is more fundamental, is still not fixed.kotlin.incremental.useClasspathSnapshot=false
can be used if the performance penalty ofkotlin.incremental=false
is 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
anvil
namespace from the end of the build directory path:layout.buildDirectory.dir("anvil/src-gen-$ssName")
I’m using
java-test-fixtures
plugin. 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/anvil
directory 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
multifileFacadeToParts
API 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=false
with 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.