Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler doesn't pick incremental changes to multibindings in some circumstances #693

Closed
matejdro opened this issue Feb 27, 2023 · 44 comments · Fixed by #836
Closed

Compiler doesn't pick incremental changes to multibindings in some circumstances #693

matejdro opened this issue Feb 27, 2023 · 44 comments · Fixed by #836
Labels
bug Something isn't working

Comments

@matejdro
Copy link

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 in gradle.properties)

Changes on those annotations may not get picked up when making incremental builds.

Steps to reproduce:

  1. Download and open Project.zip
  2. Run project
  3. Check logcat
  4. Note that logs will print two entries (as it should)
  5. Open LibraryMultibinds.kt
  6. Comment out @ContributesMultibinding annotation
  7. Run again and check logcat
  8. Logs print two entries again, even though we have commented one
@vRallev vRallev added the bug Something isn't working label Feb 27, 2023
@vRallev
Copy link
Collaborator

vRallev commented Feb 27, 2023

I've seen this under similar circumstances too, but only for code removals like you pointed out. In some cases Anvil's generated code stayed around and referenced deleted code, which caused compilation failures.

@matejdro
Copy link
Author

matejdro commented Apr 4, 2023

New incremental compilation is now enabled by default in Kotlin 1.8.20: https://kotlinlang.org/docs/whatsnew1820.html#new-jvm-incremental-compilation-by-default-in-gradle

And since many Android projects include Jetpack Compose compiler, I suspect this issue will now get much more widespread.

@hungvietnguyen
Copy link

The sample project was very useful! I found that when @ContributesMultibinding(AppScope::class) is added to class LibraryMultibinds in :mylibrary, the new IC doesn't detect a change because the new IC relies on the Kotlin class metadata in LibraryMultibinds.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 generate app/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.kt which contains references to AppMultibinds and LibraryMultibinds). 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.

@hungvietnguyen
Copy link

in this case the Kotlin class metadata doesn't change (whether this is a bug is TBD [1]).

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.

@vRallev
Copy link
Collaborator

vRallev commented Apr 11, 2023

Thanks!

@sav007
Copy link

sav007 commented Apr 21, 2023

Hey team is there any workaround for this issue? We found that every time we touch any class with ContributesMultibinding we have to run with --rerun-tasks but it's not great experience. Is there any other possible workaround?

Also is it the root cause because of this https://kotlinlang.org/docs/whatsnew1820.html#new-jvm-incremental-compilation-by-default-in-gradle?

If yes does it mean turning it somehow off should address the issue?

Btw we are on 1.8.20 so this issue persist not only in 1.8.10.

Update: disabling incremental compilation kotlin.incremental=false helps to resolve the issue as well :(

@matejdro
Copy link
Author

matejdro commented Apr 22, 2023

Can you try turning new incremental compilation off with kotlin.incremental.useClasspathSnapshot=true?

@ZacSweers
Copy link
Collaborator

The old IC uses metadata too iirc, don't think changing the IC will help

@sav007
Copy link

sav007 commented Apr 22, 2023

otlin.incremental.useClasspathSnapshot=true the first what we tried, and it didn't work for us. As mentioned earlier kotlin.incremental=false only works in our case.

@matejdro
Copy link
Author

The old IC uses metadata too iirc, don't think changing the IC will help

For us, issue only occurs when new IC is turned on (on 1.8.10, we did not update to 1.8.20 yet).

otlin.incremental.useClasspathSnapshot=true the first what we tried

Did you try setting it to false?

@hungvietnguyen
Copy link

I wanted to provide a bit more context on why only the new IC has this issue.

Both the new IC and the old IC rely on Kotlin class metadata to detect a change. Because the Kotlin class metadata currently doesn't contain info about class annotations (https://youtrack.jetbrains.com/issue/KT-57919), if we add/remove a class annotation, both the new and old IC will not detect a change.

The difference is that the new IC supports compile avoidance, which means that the task (in this case, :app:kaptGenerateStubsDebugKotlin) will be UP-TO-DATE, whereas with the old IC, that task will still run, then think that there are no changes, and won't recompile any files.

As long as the :app:kaptGenerateStubsDebugKotlin task runs, Anvil will be able to do its work as seen at this code comment.

Therefore, the new IC make its easier for this bug to show up, especially when used with Anvil.

@hungvietnguyen
Copy link

hungvietnguyen commented Apr 24, 2023

@sav007

We found that every time we touch any class with ContributesMultibinding we have to run with --rerun-tasks but it's not great experience.

This bug should only happen if you add/remove the ContributesMultibinding annotation. It should not happen if you "touch any class with ContributesMultibinding" but do not add/remove that annotation.

Btw, the workaround is to set kotlin.incremental.useClasspathSnapshot=false (note: false not true). Setting kotlin.incremental=false will not be great for performance.

If you don't find the above to be the case, then that is probably a different bug.

@matejdro
Copy link
Author

As long as the :app:kaptGenerateStubsDebugKotlin task runs, Anvil will be able to do its work as seen at this code comment.

Would the workaround be to just set upToDateWhen { false } on the stubs task? Presumably it would not cause that big of a deal if only app module contains kapt, since it's compiled incrementally anyway (just not avoided)?

@hungvietnguyen
Copy link

Would the workaround be to just set upToDateWhen { false } on the stubs task?

Yes, I think that's another workaround. However, there will be some performance impact: Even if the task can run fast, it runs for all builds even when we don't make any changes.

@sav007
Copy link

sav007 commented Apr 24, 2023

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.

This bug should only happen if you add/remove the ContributesMultibinding annotation. It should not happen if you "touch any class with ContributesMultibinding" but do not add/remove that annotation.

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.

@hungvietnguyen
Copy link

That sounds like a different bug. It would be great if you could file another bug with a sample project.

@ZacSweers
Copy link
Collaborator

@hungvietnguyen we're still seeing this issue fail in Kotlin 1.9.0-Beta with the fix in IC

@hungvietnguyen
Copy link

hungvietnguyen commented Jun 10, 2023

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 #693 (comment), 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:

  1. Use the sample project in comment 1 (which uses KGP 1.8.0).
  2. Make sure that in mylibrary/src/main/java/com/matejdro/mylibrary/LibraryMultibinds.kt, @ContributesMultibinding(AppScope::class) is removed.
  3. Run ./gradlew clean :app:kaptGenerateStubsDebugKotlin (or ./gradlew clean :mylibrary:compileDebugKotlin to focus on this task).
  4. In mylibrary/src/main/java/com/matejdro/mylibrary/LibraryMultibinds.kt, add @ContributesMultibinding(AppScope::class).
  5. Run ./gradlew :app:kaptGenerateStubsDebugKotlin (or ./gradlew :mylibrary:compileDebugKotlin).
  6. Observe that mylibrary/build/anvil/src-gen-debug is empty.
    • Side note:
      • For some reason, app/build/anvil/src-gen-debug/anvil/module/com/matejdro/multibindsanvildemo/AppComponent.kt is still correctly re-generated (it contains LibraryMultibinds), but maybe it's just lucky, let's focus on the fact that mylibrary/build/anvil/src-gen-debug is empty.
      • When the annotation is removed, 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:

  • In an incremental build where the annotation is added or removed (step 5 above), 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 the com.squareup.anvil.compiler.codegen.CodeGenerationExtension#analysisCompleted method twice.
  • The data sent for each round is different, and Anvil doesn't seem to be able to handle that. In this case, 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.
  • This happens with KGP 1.8.0, with or without the new IC (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?

ZacSweers added a commit to ZacSweers/anvil that referenced this issue Jun 20, 2023
@ZacSweers
Copy link
Collaborator

Thank you @hungvietnguyen!! I've taken a shot at a possible fix in #720, would be curious for your thoughts.

Re: this

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?

Anvil currently does this due to this.

Disable incremental compilation for the stub generating task. Trigger the compiler
plugin if any dependencies in the compile classpath have changed. This will make sure
that we pick up any change from a dependency when merging all the classes. Without
this workaround we could make changes in any library, but these changes wouldn't be
contributed to the Dagger graph, because incremental compilation tricked us.

I'd be curious if there's a way we can get around that limitation in a way that is IC friendly?

@hungvietnguyen
Copy link

Sure, I've left a comment on #720.

Regarding an IC-compatible solution, I have no idea, but I think we can start by refining what "incremental compilation tricked us" means. Once we've tracked that down, usually it's either a bug in the IC or some missing features for compiler plugins that the Kotlin compiler should provide. In both cases, it will be a bug/request for Kotlin team, and we can go from there?

@Programistich
Copy link

Programistich commented Jul 6, 2023

image

https://github.com/JetBrains/kotlin/releases/tag/v1.9.0

Well, now Kotlin fix issue 1, what need for issue 2? #693 (comment)

@JoelWilcox
Copy link
Member

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.

@matejdro
Copy link
Author

matejdro commented Aug 9, 2023

Thanks for the update. Are the IC regressions in the recent versions (such as #710) also caused by the same issue?

@hungvietnguyen
Copy link

hungvietnguyen commented Aug 9, 2023

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.

I believe the workaround is to set kotlin.incremental=false in gradle.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 of kotlin.incremental=false. The reason is that:

  • For KGP 1.9.0+, kotlin.incremental.useClasspathSnapshot=false will not help (because the issue with kotlin.incremental.useClasspathSnapshot=true -- Issue 1 in this comment -- has been fixed in KGP 1.9.0+).
  • For KGP 1.8.22 and earlier, kotlin.incremental.useClasspathSnapshot=false only fixes Issue 1 in this comment; Issue 2 in that comment, which is more fundamental, is still not fixed.
    • That said, maybe kotlin.incremental.useClasspathSnapshot=false can be used if the performance penalty of kotlin.incremental=false is too much and you're happy with reducing the number of incremental compilation issues rather than resolving all of them.

@JoelWilcox
Copy link
Member

Thanks for the update. Are the IC regressions in the recent versions (such as #710) also caused by the same issue?

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.

That said, maybe kotlin.incremental.useClasspathSnapshot=false can be used if the performance penalty of kotlin.incremental=false is too much and you're happy with reducing the number of incremental compilation issues rather than resolving all of them.

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).

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.

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 🤞.

@scana
Copy link

scana commented Aug 31, 2023

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?

@JoelWilcox
Copy link
Member

Hi @JoelWilcox, just wanted to follow up if multifileFacadeToParts API mentioned in KT-51733 was helpful in any way?

👋 No updates to share on this currently.

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 asking! We always appreciate contributions from the community, but for now I think it's easier to keep it as an internal effort, at least until we're farther along in the initial support. I'll continue to provide updates here if that changes, as well as when there's other major milestones to report.

@guuilp
Copy link

guuilp commented Sep 25, 2023

Does anyone found a workaround that allows updating Kotlin version without disabling incremental compilation?

@RBusarow
Copy link
Member

RBusarow commented Oct 3, 2023

Does anyone found a workaround that allows updating Kotlin version without disabling incremental compilation?

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)
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
  // Kapt tasks are also KotlinCompile tasks, but we don't care about them
  if (this !is org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs) {

    val anvilSrcGenDir = layout.buildDirectory.dir(sourceSetName.map{ "anvil/src-gen-$it/anvil" })

    // adds the Anvil directory to the task's outputs
    this.outputs.dir(anvilSrcGenDir)
  }
}
Groovy DSL (build.gradle)
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
  // Kapt tasks are also KotlinCompile tasks, but we don't care about them
  if (!(this instanceof org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs)) {

    def anvilSrcGenDir = layout.buildDirectory.dir(sourceSetName.map { "anvil/src-gen-$it/anvil" })

    // adds the Anvil directory to the task's outputs
    this.outputs.dir(anvilSrcGenDir)
  }
}

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:

package com.foo

import com.squareup.anvil.annotations.ContributesMultibinding

@ContributesMultibinding(String::class)
class MyClass : MyInterface

interface MyInterface

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).

./gradlew compileKotlin
rm -rf lib/build
./gradlew compileKotlin -i

The second compileKotlin task will be FROM-CACHE, because Gradle does know that it needs to run something. The console output will be:

> Task :lib:compileKotlin FROM-CACHE
[...]
Build cache key for task ':lib:compileKotlin' is 8f3471179429d249f958ea6ad1380a62
Task ':lib:compileKotlin' is not up-to-date because:
  Output property 'classpathSnapshotProperties.classpathSnapshotDir' file [...]/lib/build/kotlin/compileKotlin/classpath-snapshot has been removed.
  Output property 'classpathSnapshotProperties.classpathSnapshotDir' file [...]/lib/build/kotlin/compileKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin has been removed.
  Output property 'destinationDirectory' file [...]/lib/build/classes/kotlin/main has been removed.
Loaded cache entry for task ':lib:compileKotlin' with cache key 8f3471179429d249f958ea6ad1380a62

Note that there are only three outputs listed, and none of them have anything to do with Anvil:

build/kotlin/compileKotlin/classpath-snapshot
build/kotlin/compileKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin
build/classes/kotlin/main

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:

> Task :lib:compileKotlin FROM-CACHE
[...]
Task ':lib:compileKotlin' is not up-to-date because:
  Output property '$1' file [...]/lib/build/anvil/src-gen-main/anvil has been removed.
  Output property '$1' file [...]/lib/build/anvil/src-gen-main/anvil/hint has been removed.
  Output property '$1' file [...]/lib/build/anvil/src-gen-main/anvil/hint/multibinding has been removed.
Loaded cache entry for task ':lib:compileKotlin' with cache key 0fae2f2f63a340d0025ad0be3edba1fc

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 run compileKotlin again, the console output will be:

(full output)
> Task :lib:compileKotlin
Transforming annotations-2.4.8-1-8.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-jdk8-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-jdk7-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-common-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming annotations-13.0.jar with ClasspathEntrySnapshotTransform
Build cache key for task ':lib:compileKotlin' is d803204ae4acb428508c261acc25ed20
Task ':lib:compileKotlin' is not up-to-date because:
  Input property 'sources' file [...]/lib/src/main/kotlin/com/foo/MyClass.kt has changed.
file or directory '[...]/lib/build/kotlin/compileKotlin/local-state', not found
file or directory '[...]/lib/src/main/java', not found
file or directory '[...]/lib/src/main/java', not found
file or directory '[...]/lib/src/main/java', not found
Using Kotlin/JVM incremental compilation
Kotlin source files: [...]/lib/src/main/kotlin/com/foo/MyClass.kt
Java source files: 
Script source files: 
Script file extensions: 
[KOTLIN] Kotlin compilation 'jdkHome' argument: [...]/azul-17-ARM64/zulu-17.jdk/Contents/Home
i: found daemon on port 17040 (1855766 ms old), trying to connect
i: connected to the daemon
Options for KOTLIN DAEMON: IncrementalCompilationOptions(super=CompilationOptions(compilerMode=INCREMENTAL_COMPILER, targetPlatform=JVM, reportCategories=[0, 3], reportSeverity=2, requestedCompilationResults=[0], kotlinScriptExtensions=[]), areFileChangesKnown=true, modifiedFiles=[[...]/lib/src/main/kotlin/com/foo/MyClass.kt], deletedFiles=[], classpathChanges=NoChanges, workingDir=[...]/lib/build/kotlin/compileKotlin/cacheable, multiModuleICSettings=MultiModuleICSettings(buildHistoryFile=[...]/lib/build/kotlin/compileKotlin/local-state/build-history.bin, useModuleDetection=false), usePreciseJavaTracking=true, outputFiles=[[...]/lib/build/classes/kotlin/main, [...]/lib/build/kotlin/compileKotlin/cacheable, [...]/lib/build/kotlin/compileKotlin/local-state])
Stored cache entry for task ':lib:compileKotlin' with cache key d803204ae4acb428508c261acc25ed20

Here's the important part:

IncrementalCompilationOptions(
  [...] 
  outputFiles=[
    [...]/lib/build/classes/kotlin/main,
    [...]/lib/build/kotlin/compileKotlin/cacheable,
    [...]/lib/build/kotlin/compileKotlin/local-state
  ]
)

Kotlin is not watching the Anvil-generated code as part of its incremental compilation logic. That's why we're here.

Now if I:

  1. apply the fix above
  2. restore the annotation in MyClass.kt
  3. re-run ./gradlew compileKotlin
  4. comment out the annotation in MyClass.kt again
  5. re-run ./gradlew compileKotlin -i

The output is now:

(full output)
> Task :lib:compileKotlin
Transforming annotations-2.4.8-1-8.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-jdk8-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-jdk7-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming kotlin-stdlib-common-1.8.22.jar with ClasspathEntrySnapshotTransform
Transforming annotations-13.0.jar with ClasspathEntrySnapshotTransform
Build cache key for task ':lib:compileKotlin' is fe802a676b1a8030fe43e7a00c11c09c
Task ':lib:compileKotlin' is not up-to-date because:
  Input property 'sources' file [...]/lib/src/main/kotlin/com/foo/MyClass.kt has changed.
file or directory '[...]/lib/build/kotlin/compileKotlin/local-state', not found
file or directory '[...]/lib/src/main/java', not found
file or directory '[...]/lib/src/main/java', not found
file or directory '[...]/lib/src/main/java', not found
Using Kotlin/JVM incremental compilation
Kotlin source files: [...]/lib/src/main/kotlin/com/foo/MyClass.kt
Java source files: 
Script source files: 
Script file extensions: 
[KOTLIN] Kotlin compilation 'jdkHome' argument: [...]/azul-17-ARM64/zulu-17.jdk/Contents/Home
i: found daemon on port 17040 (2456952 ms old), trying to connect
i: connected to the daemon
Options for KOTLIN DAEMON: IncrementalCompilationOptions(super=CompilationOptions(compilerMode=INCREMENTAL_COMPILER, targetPlatform=JVM, reportCategories=[0, 3], reportSeverity=2, requestedCompilationResults=[0], kotlinScriptExtensions=[]), areFileChangesKnown=true, modifiedFiles=[[...]/lib/src/main/kotlin/com/foo/MyClass.kt], deletedFiles=[], classpathChanges=NoChanges, workingDir=[...]/lib/build/kotlin/compileKotlin/cacheable, multiModuleICSettings=MultiModuleICSettings(buildHistoryFile=[...]/lib/build/kotlin/compileKotlin/local-state/build-history.bin, useModuleDetection=false), usePreciseJavaTracking=true, outputFiles=[[...]/lib/build/anvil/src-gen-main/anvil, [...]/lib/build/classes/kotlin/main, [...]/lib/build/kotlin/compileKotlin/cacheable, [...]/lib/build/kotlin/compileKotlin/local-state])
Stored cache entry for task ':lib:compileKotlin' with cache key fe802a676b1a8030fe43e7a00c11c09c

And here's that same important part:

IncrementalCompilationOptions(
  [...] 
  outputFiles=[
    [...]/lib/build/anvil/src-gen-main/anvil,
    [...]/lib/build/classes/kotlin/main,
    [...]/lib/build/kotlin/compileKotlin/cacheable,
    [...]/lib/build/kotlin/compileKotlin/local-state
  ]
)

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.

@vRallev
Copy link
Collaborator

vRallev commented Oct 3, 2023

Thanks Rick, I'll give this a try!

I want to give some background why the output directory with generated files never was added. The idea is that the main input files are the source of truth. Based on the main input Anvil generates source files, which then get compiled to class files. There's the mapping of the main source code to this modified set of class files. Anvil's generated source code is only a by-product that doesn't need to be restored from cache. The important piece that needs to be restored in the output is the class files and not the generated source code.

I'm afraid that this will only mask the fundamental gaps with incremental compilation, but I may be the wrong. I can also see a scenario happening where after making an incremental change kotlinc will have to run twice before the output is cached. Test cases for that will be a big help, so I'm looking forward to that.

@matejdro
Copy link
Author

matejdro commented Oct 5, 2023

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:

2023-10-05T07:41:39,937623531+02:00

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:

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    if (this !is KspTaskJvm && this !is KaptGenerateStubsTask) {
        val ssName = sourceSetName.get()
        val anvilSrcGenDir = layout.buildDirectory.dir("anvil/src-gen-$ssName/anvil")
        // adds the Anvil directory to the task's outputs
        outputs.dir(anvilSrcGenDir)
    }
}

@vRallev
Copy link
Collaborator

vRallev commented Oct 5, 2023

The workaround does not need to be applied to the KSP task. Anvil doesn't run in the KSP task. Your solution seems valid.

@svilen-ivanov
Copy link

svilen-ivanov commented Oct 6, 2023

I'm using java-test-fixtures plugin. Due to unknown reason to me it has undefined sourceSetName. Here is my workaround:

   // ....
    val ssName = when {
        sourceSetName.isPresent -> sourceSetName.get()
        name == "compileTestFixturesKotlin" -> "testFixtures"
        else -> error("Cannot determine sourceSetName for task $name")
    }
   // ....

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. :(

@midery
Copy link

midery commented Oct 6, 2023

@svilen-ivanov can confirm, I received exactly the same issue when tried to launch :sample:app:assembleDebug.

Steps to reproduce:

  1. Apply the changes proposed by @RBusarow both to library and app module
  2. Run :sample:app:assembleDebug
  3. Replace RealFatherProvider's object provision with @Inject-constructor.
  4. Run :sample:app:assembleDebug again

Error will be returned during :sample:app:kaptDebugKotlin execution:

error: [Dagger/MissingBinding] com.squareup.anvil.sample.father.FatherProvider cannot be provided without an @Provides-annotated method.

Project sample:library itself compiles without errors, but anvil/src-gen-debug directory is empty.

@ZacSweers
Copy link
Collaborator

The workaround does not need to be applied to the KSP task. Anvil doesn't run in the KSP task. Your solution seems valid.

I think the problem might be deeper than that as the KspTask doesn't extend KotlinCompile, so I'm not sure why it's triggering here 🤔

@Laimiux
Copy link

Laimiux commented Oct 6, 2023

I think the problem lies with how Anvil always clears the generated code (link). Even with @RBusarow solution, there just isn't any Anvil code to reference after incremental compilation. I ran into a similar issue #754 and the following patch fixes it #755. I don't have a strong understanding of how compiler plugins work, so it would be good for someone with more context to evaluate this workaround.

@matejdro
Copy link
Author

matejdro commented Oct 7, 2023

I think the problem might be deeper than that as the KspTask doesn't extend KotlinCompile, so I'm not sure why it's triggering here 🤔

KspTaskJvm, which is the issue here, does extend KotlinCompile: https://github.com/google/ksp/blob/2b1044a28a48b00adc1a3064442b97cc53ee84b7/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinFactories.kt#L185

@ursusursus
Copy link

lurker here; so we're unable to fix this & are waiting for dagger ksp+anvil support of that?

@apramana
Copy link

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")

@agrosner
Copy link

agrosner commented Nov 20, 2023

adding the code above to add it as an output + custom code generator adjustment worked like a charm!
but upon upgrading to 1.9.10 of kotlin, this issue seems to be back in full force. Anyone else notice this?

@cdsap
Copy link

cdsap commented Jan 10, 2024

@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.

@matejdro
Copy link
Author

This seems fixed by v2.5.0-beta01 with trackSourceFiles on 🎉

@bddckr
Copy link
Contributor

bddckr commented Feb 16, 2024

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. 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet