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

incremental compilation fixes #836

Merged
merged 4 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

### Custom Code Generator

- The `GeneratedFile` result type has been deprecated in favor of `GeneratedFileWithSources`. This new type allows for precise tracking of the generated files, which in turn drastically improves incremental compilation performance (#693).

### Other Notes & Contributions


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.squareup.anvil.conventions

import com.rickbusarow.kgx.buildDir
import com.rickbusarow.kgx.extras
import com.rickbusarow.kgx.fromInt
import com.rickbusarow.kgx.javaExtension
Expand Down Expand Up @@ -125,6 +126,7 @@ abstract class BasePlugin : Plugin<Project> {
* so that there is no shared mutable state.
*/
private fun configureBuildDirs(target: Project) {

when {
!target.isInAnvilBuild() -> return

Expand All @@ -136,6 +138,16 @@ abstract class BasePlugin : Plugin<Project> {
target.layout.buildDirectory.set(target.file("build/included-build"))
}
}

// Set the kase working directory ('<build directory>/kase/<test|gradleTest>') as a System property,
// so that it's in the right place for projects with relocated directories.
// https://github.com/rickbusarow/kase/blob/255db67f40d5ec83e31755bc9ce81b1a2b08cf11/kase/src/main/kotlin/com/rickbusarow/kase/files/HasWorkingDir.kt#L93-L96
target.tasks.withType(Test::class.java).configureEach { task ->
task.systemProperty(
"kase.baseWorkingDir",
target.buildDir().resolve("kase/${task.name}"),
)
}
}

/**
Expand Down
40 changes: 37 additions & 3 deletions compiler-api/api/compiler-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public abstract interface class com/squareup/anvil/compiler/api/AnvilContext {
public abstract fun getGenerateFactories ()Z
public abstract fun getGenerateFactoriesOnly ()Z
public abstract fun getModule ()Lorg/jetbrains/kotlin/descriptors/ModuleDescriptor;
public abstract fun getTrackSourceFiles ()Z
}

public abstract interface class com/squareup/anvil/compiler/api/CodeGenerator : com/squareup/anvil/compiler/api/AnvilApplicabilityChecker {
Expand All @@ -52,17 +53,50 @@ public abstract interface class com/squareup/anvil/compiler/api/CodeGenerator :

public final class com/squareup/anvil/compiler/api/CodeGeneratorKt {
public static final fun createGeneratedFile (Lcom/squareup/anvil/compiler/api/CodeGenerator;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/anvil/compiler/api/GeneratedFile;
public static final fun createGeneratedFile (Lcom/squareup/anvil/compiler/api/CodeGenerator;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/io/File;[Ljava/io/File;)Lcom/squareup/anvil/compiler/api/GeneratedFileWithSources;
public static final fun createGeneratedFile (Lcom/squareup/anvil/compiler/api/CodeGenerator;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)Lcom/squareup/anvil/compiler/api/GeneratedFileWithSources;
}

public final class com/squareup/anvil/compiler/api/GeneratedFile {
public abstract interface class com/squareup/anvil/compiler/api/FileWithContent : java/lang/Comparable {
public abstract fun compareTo (Lcom/squareup/anvil/compiler/api/FileWithContent;)I
public abstract fun component1 ()Ljava/io/File;
public abstract fun component2 ()Ljava/lang/String;
public abstract fun getContent ()Ljava/lang/String;
public abstract fun getFile ()Ljava/io/File;
}

public final class com/squareup/anvil/compiler/api/FileWithContent$DefaultImpls {
public static fun compareTo (Lcom/squareup/anvil/compiler/api/FileWithContent;Lcom/squareup/anvil/compiler/api/FileWithContent;)I
public static fun component1 (Lcom/squareup/anvil/compiler/api/FileWithContent;)Ljava/io/File;
public static fun component2 (Lcom/squareup/anvil/compiler/api/FileWithContent;)Ljava/lang/String;
}

public final class com/squareup/anvil/compiler/api/GeneratedFile : com/squareup/anvil/compiler/api/FileWithContent {
public fun <init> (Ljava/io/File;Ljava/lang/String;)V
public fun compareTo (Lcom/squareup/anvil/compiler/api/FileWithContent;)I
public synthetic fun compareTo (Ljava/lang/Object;)I
public final fun component1 ()Ljava/io/File;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/io/File;Ljava/lang/String;)Lcom/squareup/anvil/compiler/api/GeneratedFile;
public static synthetic fun copy$default (Lcom/squareup/anvil/compiler/api/GeneratedFile;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/anvil/compiler/api/GeneratedFile;
public fun equals (Ljava/lang/Object;)Z
public final fun getContent ()Ljava/lang/String;
public final fun getFile ()Ljava/io/File;
public fun getContent ()Ljava/lang/String;
public fun getFile ()Ljava/io/File;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/squareup/anvil/compiler/api/GeneratedFileWithSources : com/squareup/anvil/compiler/api/FileWithContent {
public fun <init> (Ljava/io/File;Ljava/lang/String;Ljava/util/Set;)V
public fun compareTo (Lcom/squareup/anvil/compiler/api/FileWithContent;)I
public synthetic fun compareTo (Ljava/lang/Object;)I
public fun component1 ()Ljava/io/File;
public fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/util/Set;
public fun equals (Ljava/lang/Object;)Z
public fun getContent ()Ljava/lang/String;
public fun getFile ()Ljava/io/File;
public final fun getSourceFiles ()Ljava/util/Set;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand Down
5 changes: 5 additions & 0 deletions compiler-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ dependencies {
api(libs.kotlin.compiler)

implementation(platform(libs.kotlin.bom))

testImplementation(libs.junit)
testImplementation(libs.kase)
testImplementation(libs.kotest.assertions.api)
testImplementation(libs.kotest.assertions.core.jvm)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public interface AnvilContext {
*/
public val disableComponentMerging: Boolean

/**
* Enables incremental compilation support.
*
* This is achieved by tracking the source files for each generated file,
* which allows for two new behaviors:
* - Generated code is "invalidated" and deleted when the source file is changed or deleted.
* - Generated code is cached in a way that Gradle understands,
* and will be restored from cache along with other build artifacts.
*/
public val trackSourceFiles: Boolean

/**
* The module of the current compilation.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,81 @@ public interface CodeGenerator : AnvilApplicabilityChecker {
codeGenDir: File,
module: ModuleDescriptor,
projectFiles: Collection<KtFile>,
): Collection<GeneratedFile>
): Collection<FileWithContent>
}

/**
* Write [content] into a new file for the given [packageName] and [fileName]. [fileName] usually
* refers to the class name.
*/
@ExperimentalAnvilApi
@Suppress("unused")
@Deprecated(
message = "Use the createGeneratedFile with a `sourceFiles` argument instead.",
replaceWith = ReplaceWith(
expression = "createGeneratedFile(codeGenDir, packageName, fileName, content, sourceFile = TODO())",
imports = ["com.squareup.anvil.compiler.api.createGeneratedFile"],
),
)
@Suppress("UnusedReceiverParameter", "unused", "Deprecation")
public fun CodeGenerator.createGeneratedFile(
codeGenDir: File,
packageName: String,
fileName: String,
content: String,
): GeneratedFile {
): GeneratedFile = GeneratedFile(
file = writeFileContent(
codeGenDir = codeGenDir,
packageName = packageName,
fileName = fileName,
content = content,
),
content = content,
)

public fun CodeGenerator.createGeneratedFile(
codeGenDir: File,
packageName: String,
fileName: String,
content: String,
sourceFile: File,
vararg additionalSourceFiles: File,
): GeneratedFileWithSources = createGeneratedFile(
codeGenDir = codeGenDir,
packageName = packageName,
fileName = fileName,
content = content,
sourceFiles = setOf(sourceFile, *additionalSourceFiles),
)

@Suppress("UnusedReceiverParameter")
public fun CodeGenerator.createGeneratedFile(
codeGenDir: File,
packageName: String,
fileName: String,
content: String,
sourceFiles: Set<File>,
): GeneratedFileWithSources = GeneratedFileWithSources(
file = writeFileContent(
codeGenDir = codeGenDir,
packageName = packageName,
fileName = fileName,
content = content,
),
content = content,
sourceFiles = sourceFiles,
)

private fun writeFileContent(
codeGenDir: File,
packageName: String,
fileName: String,
content: String,
): File {
val directory = File(codeGenDir, packageName.replace('.', File.separatorChar))
val file = File(directory, "$fileName.kt")
check(file.parentFile.exists() || file.parentFile.mkdirs()) {
"Could not generate package directory: ${file.parentFile}"
}
file.writeText(content)

return GeneratedFile(file, content)
return file
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,32 @@ import java.io.File
* @property file the [File] to generate.
* @property content the file contents to write to [file].
*/
public data class GeneratedFile(
val file: File,
val content: String,
@Deprecated(
message = "Use GeneratedFileWithSources instead.",
replaceWith = ReplaceWith(
expression = "GeneratedFileWithSources(file, content, setOf(TODO(\"Add some source files using <somePsiElement>.containingFileAsJavaFile()\")))",
imports = [
"com.squareup.anvil.compiler.api.GeneratedFileWithSources",
"com.squareup.anvil.compiler.internal.containingFileAsJavaFile",
],
),
level = DeprecationLevel.WARNING,
)
public data class GeneratedFile(
override val file: File,
override val content: String,
) : FileWithContent

/** Represents a generated file that Anvil should eventually write. */
public sealed interface FileWithContent : Comparable<FileWithContent> {
/** The file to generate. */
public val file: File

/** The text to write to [file]. */
public val content: String

public operator fun component1(): File = file
public operator fun component2(): String = content

override fun compareTo(other: FileWithContent): Int = file.compareTo(other.file)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.squareup.anvil.compiler.api

import java.io.File

/**
* Represents a generated file that Anvil should eventually write. The [sourceFiles] are the
* files that triggered the generation of this file or are referenced by the generated file.
* A modification to any of the [sourceFiles] will invalidate this generated file.
*
* All source files must be:
* - absolute paths
* - actual files (not directories)
* - existent in the file system
*
* @property file the [File] to generate.
* @property content the file contents to write to [file].
* @property sourceFiles the source files used to generate this file.
* @throws AnvilCompilationException if a [sourceFiles] is not an absolute file, doesn't exist, or is a directory
* @throw AnvilCompilationException if a source file is the same as the generated [file]
*/
public class GeneratedFileWithSources(
override val file: File,
override val content: String,
public val sourceFiles: Set<File>,
) : FileWithContent {

init {
fun require(condition: Boolean, lazyMessage: () -> String) {
if (!condition) {
throw AnvilCompilationException(
"""
|message:
|${lazyMessage()}
|
|generated file:
|$file
|
""".trimMargin(),
)
}
}
require(sourceFiles.none { it == file }) {
"""
|${this::class.simpleName} must not contain the generated file as a source file.
|
|source files:
|${sourceFiles.sorted().joinToString("\n")}
""".trimMargin()
}
require(sourceFiles.all { it.isFile && it.isAbsolute }) {

val notAbsolute = sourceFiles.filterNot { it.isAbsolute }.sorted()
val notFiles = sourceFiles.filterNot { it.isFile }.sorted()

buildString {
appendLine(
"""
All source files must be:
- absolute paths
- actual files (not directories)
- existent in the file system
""".trimIndent(),
)
if (notAbsolute.isNotEmpty()) {
append(
"""
|
|not absolute:
|${notAbsolute.joinToString("\n")}
""".trimMargin(),
)
}
if (notFiles.isNotEmpty()) {
append(
"""
|
|not files:
|${notFiles.joinToString("\n")}
""".trimMargin(),
)
}
}
}
}

public operator fun component3(): Set<File> = sourceFiles

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GeneratedFileWithSources) return false

if (file != other.file) return false
if (content != other.content) return false
if (sourceFiles != other.sourceFiles) return false

return true
}

override fun hashCode(): Int {
var result = file.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + sourceFiles.hashCode()
return result
}

override fun toString(): String = buildString {
appendLine("GeneratedFileWithSource(")
appendLine(" file: $file")
appendLine(" sourceFiles: ${sourceFiles.sorted()}")
appendLine(" content: $content")
appendLine(")")
}
}
Loading