Skip to content

Commit

Permalink
fix incremental behavior when the compilation target has IR merging b…
Browse files Browse the repository at this point in the history
…ut no KAPT stub generation

KGP's "classpath snapshot" incremental logic uses a previous build's `.class` files to track references and invalidate files.  Assume there are two projects: `:lib1` and `:lib2`.  `:lib2` depends on `:lib1`.  If a change is made to an existing type in `:lib1` since the last successful build, KGP will inspect the snapshot of `:lib2`'s class files to see if it references anything in `:lib1`.  If `:lib2` didn't already reference `:lib1`, then the `:lib2:compileKotlin` task will be treated as `UP_TO_DATE` and will not execute again.

Anvil's module and interface merging logic only changes the IR symbols.  This means that all merged references are added to the resultant binary, but not to class files.  Those changes are invisible to the classpath snapshots.  This is why we disable incremental compilation in KAPT stub generation tasks, but it's also a problem in non-KAPT modules that are using other merging annotations.

### The fix

If a compilation merges any interfaces or modules, a file is created with their names.  The `AnvilPlugin` checks for the existence of this file during task configuration, and disables incremental compilation for that task if it's present.  The file won't exist on a fresh build, regardless of whether there will be merging, but that's okay because the build wouldn't be incremental anyway.
  • Loading branch information
RBusarow committed Jul 12, 2024
1 parent f2aebe9 commit 87a85a2
Show file tree
Hide file tree
Showing 9 changed files with 765 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

### Fixed

- incremental compilation is automatically disabled for source sets that perform interface or module merging ([#1024](https://github.com/square/anvil/pull/1024))

### Security

### Custom Code Generator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public class AnvilCompilation internal constructor(
val anvilCommandLineProcessor = AnvilCommandLineProcessor()
commandLineProcessors = listOf(anvilCommandLineProcessor)

val buildDir = workingDir.resolve("build")
val anvilCacheDir = buildDir.resolve("anvil-cache")

pluginOptions = mutableListOf(
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
Expand All @@ -79,12 +82,21 @@ public class AnvilCompilation internal constructor(
optionName = "analysis-backend",
optionValue = mode.analysisBackend.name.lowercase(Locale.US),
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "ir-merges-file",
optionValue = anvilCacheDir.resolve("merges/ir-merges.txt").absolutePath,
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "track-source-files",
optionValue = (trackSourceFiles && mode is Embedded).toString(),
),
)

when (mode) {
is Embedded -> {
anvilComponentRegistrar.addCodeGenerators(mode.codeGenerators)
val buildDir = workingDir.resolve("build")
pluginOptions +=
listOf(
PluginOption(
Expand All @@ -105,7 +117,7 @@ public class AnvilCompilation internal constructor(
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "anvil-cache-dir",
optionValue = buildDir.resolve("anvil-cache").absolutePath,
optionValue = anvilCacheDir.absolutePath,
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
Expand All @@ -122,11 +134,6 @@ public class AnvilCompilation internal constructor(
optionName = "will-have-dagger-factories",
optionValue = (generateDaggerFactories || enableDaggerAnnotationProcessor).toString(),
),
PluginOption(
pluginId = anvilCommandLineProcessor.pluginId,
optionName = "track-source-files",
optionValue = trackSourceFiles.toString(),
),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ internal const val gradleBuildDirName = "gradle-build-dir"
internal val gradleBuildDirKey =
CompilerConfigurationKey.create<File>("anvil $gradleBuildDirName")

internal const val irMergesFileName = "ir-merges-file"
internal val irMergesFileKey =
CompilerConfigurationKey.create<File>("anvil $irMergesFileName")

internal const val generateDaggerFactoriesName = "generate-dagger-factories"
internal val generateDaggerFactoriesKey =
CompilerConfigurationKey.create<Boolean>("anvil $generateDaggerFactoriesName")
Expand Down Expand Up @@ -89,6 +93,13 @@ public class AnvilCommandLineProcessor : CommandLineProcessor {
required = false,
allowMultipleOccurrences = false,
),
CliOption(
optionName = irMergesFileName,
valueDescription = "<file-path>",
description = "Path of the file where Anvil records its merged module annotations and component/module interfaces",
required = false,
allowMultipleOccurrences = false,
),
CliOption(
optionName = generateDaggerFactoriesName,
valueDescription = "<true|false>",
Expand Down Expand Up @@ -157,6 +168,7 @@ public class AnvilCommandLineProcessor : CommandLineProcessor {
gradleBuildDirName -> configuration.put(gradleBuildDirKey, File(value))
srcGenDirName -> configuration.put(srcGenDirKey, File(value))
anvilCacheDirName -> configuration.put(anvilCacheDirKey, File(value))
irMergesFileName -> configuration.put(irMergesFileKey, File(value))
generateDaggerFactoriesName ->
configuration.put(generateDaggerFactoriesKey, value.toBoolean())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,21 @@ public class AnvilComponentRegistrar : ComponentRegistrar {
val moduleDescriptorFactory by lazy(NONE) {
RealAnvilModuleDescriptor.Factory()
}
val irMergesFile by lazy(NONE) { configuration.getNotNull(irMergesFileKey) }
val trackSourceFiles = configuration.getNotNull(trackSourceFilesKey)

val mergingEnabled =
!commandLineOptions.generateFactoriesOnly && !commandLineOptions.disableComponentMerging
if (mergingEnabled) {
if (commandLineOptions.componentMergingBackend == ComponentMergingBackend.IR) {
IrGenerationExtension.registerExtension(
project,
IrContributionMerger(scanner, moduleDescriptorFactory),
IrContributionMerger(
classScanner = scanner,
moduleDescriptorFactory = moduleDescriptorFactory,
trackSourceFiles = trackSourceFiles,
irMergesFile = irMergesFile,
),
)
} else {
// TODO in dagger-ksp support
Expand All @@ -68,7 +75,6 @@ public class AnvilComponentRegistrar : ComponentRegistrar {
val cacheDir = configuration.getNotNull(anvilCacheDirKey)
val projectDir = BaseDir.ProjectDir(configuration.getNotNull(gradleProjectDirKey))
val buildDir = BaseDir.BuildDir(configuration.getNotNull(gradleBuildDirKey))
val trackSourceFiles = configuration.getNotNull(trackSourceFilesKey)

val codeGenerators = loadCodeGenerators() +
manuallyAddedCodeGenerators +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import org.jetbrains.kotlin.ir.builders.irCallConstructor
import org.jetbrains.kotlin.ir.builders.irVararg
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrVararg
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.classOrFail
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.starProjectedType
Expand All @@ -35,6 +37,7 @@ import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import java.io.File

/**
* An [IrGenerationExtension] that performs the two types of merging Anvil supports.
Expand All @@ -49,6 +52,8 @@ import org.jetbrains.kotlin.name.Name
internal class IrContributionMerger(
private val classScanner: ClassScanner,
private val moduleDescriptorFactory: RealAnvilModuleDescriptor.Factory,
private val trackSourceFiles: Boolean,
private val irMergesFile: File,
) : IrGenerationExtension {
// https://youtrack.jetbrains.com/issue/KT-56635
override val shouldAlsoBeAppliedInKaptStubGenerationMode: Boolean get() = true
Expand All @@ -57,6 +62,10 @@ internal class IrContributionMerger(
moduleFragment: IrModuleFragment,
pluginContext: IrPluginContext,
) {

val mergedModules = mutableMapOf<FqName, List<FqName>>()
val mergedInterfaces = mutableMapOf<FqName, List<FqName>>()

moduleFragment.transform(
object : IrElementTransformerVoid() {
override fun visitClass(declaration: IrClass): IrStatement {
Expand All @@ -73,8 +82,10 @@ internal class IrContributionMerger(
val moduleMergerAnnotations = mergeComponentAnnotations + mergeModulesAnnotations

if (moduleMergerAnnotations.isNotEmpty()) {
pluginContext.irBuiltIns.createIrBuilder(declaration.symbol)
.generateDaggerAnnotation(

mergedModules[mergeAnnotatedClass.fqName] = pluginContext.irBuiltIns
.createIrBuilder(declaration.symbol)
.addMergedModules(
annotations = moduleMergerAnnotations,
moduleFragment = moduleFragment,
pluginContext = pluginContext,
Expand All @@ -96,27 +107,65 @@ internal class IrContributionMerger(
)
}

addContributedInterfaces(
mergeAnnotations = interfaceMergerAnnotations,
moduleFragment = moduleFragment,
pluginContext = pluginContext,
mergeAnnotatedClass = mergeAnnotatedClass,
)
// Add supertypes to this `mergeAnnotatedClass`
mergedInterfaces[mergeAnnotatedClass.fqName] = mergeAnnotatedClass
.addInterfaceSupertypes(
mergeAnnotations = interfaceMergerAnnotations,
moduleFragment = moduleFragment,
pluginContext = pluginContext,
)
}

return super.visitClass(declaration)
}
},
null,
)

if (trackSourceFiles) {
// If any IR changes were made, record them in the merges file. This is because those changes
// are not reflected in the .class files, which means they're invisible to Kotlin's incremental logic.
// The Anvil Gradle plugin will disable incremental logic for this task if this merges file is present.
if (mergedModules.isNotEmpty() || mergedInterfaces.isNotEmpty()) {
writeMergesFile(mergedModules, mergedInterfaces)
} else {
irMergesFile.delete()
}
}
}

private fun IrBuilderWithScope.generateDaggerAnnotation(
private fun writeMergesFile(
mergedModules: MutableMap<FqName, List<FqName>>,
mergedInterfaces: MutableMap<FqName, List<FqName>>,
) {
val mergedText = buildString {

mergedModules.entries
.sortedBy { it.key.asString() }
.forEach { (key, values) ->
appendLine("module - $key : $values")
}

mergedInterfaces.entries
.sortedBy { it.key.asString() }
.forEach { (key, values) ->
appendLine("interface - $key : $values")
}
}

check(irMergesFile.parentFile.mkdirs() || irMergesFile.parentFile.isDirectory) {
"Could not generate directory: ${irMergesFile.parentFile}"
}

irMergesFile.writeText(mergedText)
}

private fun IrBuilderWithScope.addMergedModules(
annotations: List<AnnotationReferenceIr>,
moduleFragment: IrModuleFragment,
pluginContext: IrPluginContext,
declaration: ClassReferenceIr,
) {
): List<FqName> {
val daggerAnnotationFqName = annotations[0].daggerAnnotationFqName

val scopes = annotations.map { it.scope }
Expand Down Expand Up @@ -294,10 +343,10 @@ internal class IrContributionMerger(

val contributedSubcomponentModules =
findContributedSubcomponentModules(
declaration,
scopes,
pluginContext,
moduleFragment,
declaration = declaration,
scopes = scopes,
pluginContext = pluginContext,
moduleFragment = moduleFragment,
)

val contributedModules = contributesAnnotations
Expand All @@ -311,7 +360,25 @@ internal class IrContributionMerger(
.distinct()
.map { it.clazz.owner }

val annotationConstructorCall = irCallConstructor(
// Since we are modifying the state of the code here, this does not need to be reflected in
// the associated [ClassReferenceIr] which is more of an initial snapshot.
declaration.clazz.owner.annotations += createDaggerAnnotation(
pluginContext = pluginContext,
daggerAnnotationFqName = daggerAnnotationFqName,
contributedModules = contributedModules,
annotations = annotations,
)

return contributedModules.mapTo(mutableListOf()) { it.fqName }
}

private fun IrBuilderWithScope.createDaggerAnnotation(
pluginContext: IrPluginContext,
daggerAnnotationFqName: FqName,
contributedModules: Sequence<IrClass>,
annotations: List<AnnotationReferenceIr>,
): IrConstructorCall {
return irCallConstructor(
callee = pluginContext
.referenceConstructors(daggerAnnotationFqName.classIdBestGuess())
.single { it.owner.isPrimary },
Expand Down Expand Up @@ -356,10 +423,6 @@ internal class IrContributionMerger(
copyArrayValue("subcomponents")
}
}

// Since we are modifying the state of the code here, this does not need to be reflected in
// the associated [ClassReferenceIr] which is more of an initial snapshot.
declaration.clazz.owner.annotations += annotationConstructorCall
}

private fun checkSameScope(
Expand Down Expand Up @@ -418,12 +481,11 @@ internal class IrContributionMerger(
}
}

private fun addContributedInterfaces(
private fun ClassReferenceIr.addInterfaceSupertypes(
mergeAnnotations: List<AnnotationReferenceIr>,
moduleFragment: IrModuleFragment,
pluginContext: IrPluginContext,
mergeAnnotatedClass: ClassReferenceIr,
) {
): List<FqName> {
val scopes = mergeAnnotations.map { it.scope }
val contributesAnnotations = mergeAnnotations
.flatMap { annotation ->
Expand Down Expand Up @@ -516,17 +578,17 @@ internal class IrContributionMerger(

if (!contributesToOurScope) {
throw AnvilCompilationExceptionClassReferenceIr(
message = "${mergeAnnotatedClass.fqName} with scopes " +
message = "$fqName with scopes " +
"${scopes.joinToString(prefix = "[", postfix = "]") { it.fqName.asString() }} " +
"wants to exclude ${excludedClass.fqName}, but the excluded class isn't " +
"contributed to the same scope.",
classReference = mergeAnnotatedClass,
classReference = this,
)
}
}
.toList()

val supertypes = mergeAnnotatedClass.clazz.superTypes()
val supertypes = clazz.superTypes()
if (excludedClasses.isNotEmpty()) {
val intersect = supertypes
.map { it.classOrFail.toClassReference(pluginContext) }
Expand All @@ -537,23 +599,23 @@ internal class IrContributionMerger(

if (intersect.isNotEmpty()) {
throw AnvilCompilationExceptionClassReferenceIr(
classReference = mergeAnnotatedClass,
message = "${mergeAnnotatedClass.fqName} excludes types that it implements or " +
classReference = this@addInterfaceSupertypes,
message = "$fqName excludes types that it implements or " +
"extends. These types cannot be excluded. Look at all the super types to find these " +
"classes: ${intersect.joinToString { it.fqName.asString() }}.",
)
}
}

val supertypesToAdd = contributesAnnotations
val toAdd = contributesAnnotations
.asSequence()
.map { it.declaringClass }
.filter { clazz ->
clazz !in replacedClasses && clazz !in excludedClasses
}
.plus(
findContributedSubcomponentParentInterfaces(
clazz = mergeAnnotatedClass,
clazz = this@addInterfaceSupertypes,
scopes = scopes,
pluginContext = pluginContext,
moduleFragment = moduleFragment,
Expand All @@ -566,7 +628,10 @@ internal class IrContributionMerger(

// Since we are modifying the state of the code here, this does not need to be reflected in
// the associated [ClassReferenceIr] which is more of an initial snapshot.
mergeAnnotatedClass.clazz.owner.superTypes += supertypesToAdd
clazz.owner.superTypes += toAdd

// Return the list of added supertypes
return toAdd.map { it.classFqName!! }.sortedBy { it.asString() }
}

private fun findContributedSubcomponentParentInterfaces(
Expand Down
Loading

0 comments on commit 87a85a2

Please sign in to comment.