From 14b0beda3fe3a5a8fd1cc8eb05436e12829cd4cf Mon Sep 17 00:00:00 2001 From: Mathew Henson Date: Wed, 18 Jun 2025 12:05:00 -0700 Subject: [PATCH] Use excludedSources in Android KAPT Workaround excludedSources prevents KSP tasks from taking dependencies on any task that is added to the list in an effort to prevent circular dependencies. However, if an output is added via Android Gradle Plugin's APIs, KSP can still end up with a dependency on the task that generates the output, potentially leading to a circular dependency. This change modifies the filter logic for adding outputs of Android projects to respect outputs added to `excludedSources`, which can be used to break the circular dependency at the cost of KSP ignoring the added source. --- .../ksp/gradle/AndroidPluginIntegration.kt | 6 +- .../devtools/ksp/test/AndroidComponentsIT.kt | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 integration-tests/src/test/kotlin/com/google/devtools/ksp/test/AndroidComponentsIT.kt diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt index ee73e3b00b..bc08ac8e06 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/AndroidPluginIntegration.kt @@ -76,13 +76,17 @@ object AndroidPluginIntegration { val kaptProvider: TaskProvider? = project.locateTask(kotlinCompilation.compileTaskProvider.kaptTaskName) + val kspExtension = project.extensions.getByType(KspExtension::class.java) val sources = kotlinCompilation.androidVariant.getSourceFolders(SourceKind.JAVA) kspTaskProvider.configure { task -> // this is workaround for KAPT generator that prevents circular dependency val filteredSources = Callable { val destinationProperty = (kaptProvider?.get() as? KaptTask)?.destinationDir val dir = destinationProperty?.get()?.asFile - sources.filter { dir?.isParentOf(it.dir) != true } + sources.filter { source -> + dir?.isParentOf(source.dir) != true && + source.dir !in kspExtension.excludedSources + } } when (task) { is KspTaskJvm -> { diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/AndroidComponentsIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/AndroidComponentsIT.kt new file mode 100644 index 0000000000..9ddd8547c1 --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/AndroidComponentsIT.kt @@ -0,0 +1,78 @@ +package com.google.devtools.ksp.test + +import org.gradle.testkit.runner.GradleRunner +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.File + +@RunWith(Parameterized::class) +class AndroidComponentsIT(useKSP2: Boolean) { + + @Rule + @JvmField + val project: TemporaryTestProject = TemporaryTestProject("playground-android-multi", "playground", useKSP2) + + @Test + fun testDependencyResolutionCheck() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root).withGradleVersion("8.12.1") + + File(project.root, "gradle.properties").appendText("\nagpVersion=8.9.0") + gradleRunner.withArguments(":workload:compileDebugKotlin").build().let { result -> + Assert.assertFalse(result.output.contains("was resolved during configuration time.")) + } + } + + @Test + fun testBreakingCircularTaskDependencyWithAndroidComponentGeneratedCode() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root).withGradleVersion("8.12.1") + + File(project.root, "gradle.properties").appendText("\nagpVersion=8.9.0") + File(project.root, "workload/build.gradle.kts").appendText( + """ + android { + flavorDimensions += listOf("tier") + productFlavors { + create("free") { + dimension = "tier" + } + } + } + configurations.matching { it.name.startsWith("ksp") && !it.name.endsWith("ProcessorClasspath") }.all { + // Make sure ksp configs are not empty. + project.dependencies.add(name, "androidx.room:room-compiler:2.4.2") + } + androidComponents { + onVariants { variant -> + val task = project.tasks.register(variant.name + "Gen", GenTask::class) { + dependsOn(project.tasks.named("ksp" + variant.name.replaceFirstChar(Char::uppercase) + "Kotlin")) + getOutputDirectory().set(project.layout.buildDirectory.dir("generated/" + variant.name)) + } + ksp.excludedSources.from(task) // This breaks the circular dependency + variant.sources.java?.addGeneratedSourceDirectory(task, GenTask::getOutputDirectory) + } + } + abstract class GenTask : DefaultTask() { + @OutputDirectory + abstract fun getOutputDirectory(): DirectoryProperty + + @TaskAction + fun generateCode() { } + } + """.trimIndent() + ) + + gradleRunner.withArguments(":workload:assemble", "--dry-run", "--stacktrace").build().let { result -> + val outputs = result.output.lines().joinToString("\n") + println(outputs) + } + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "KSP2={0}") + fun params() = listOf(arrayOf(true), arrayOf(false)) + } +}