diff --git a/README.md b/README.md index 1b5782859d..36ad256bb8 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,19 @@ Please refer to the [KSP2 introduction](docs/ksp2.md) for further details. When applying KSP in your Gradle project, place symbol processor dependencies into the appropriate configuration inside the `dependencies { ... }` block of your `build.gradle.kts` (or `build.gradle`) file based on your target platforms, source sets, and build variants. +### Global (`ksp`) Configuration Behavior & `ksp.allow.all.target.configuration` + +The behavior of the `ksp` dependency configuration differs depending on whether your project is single-platform or multiplatform: + +- **Single-Platform Android (`com.android.application`, `com.android.library`)**: The global `ksp` configuration is **enabled by default** across all scopes and build variants. Declaring a dependency using `ksp("...")` automatically applies to all main build variants (`debug`, `release`, flavors), unit tests (`kspTest`), and instrumentation tests (`kspAndroidTest`). If you wish to disable inheritance into test scopes, set `ksp.allow.all.target.configuration=false` in `gradle.properties`. +- **Single-Platform JVM (`kotlin("jvm")`)**: The global `ksp` configuration is **enabled by default** (`ksp.allow.all.target.configuration = true`), meaning `ksp("...")` applies to both the `main` (`src/main`) and `test` (`src/test`) source sets. If you wish to restrict `ksp` strictly to `main`, set `ksp.allow.all.target.configuration=false` in `gradle.properties`. +- **Kotlin Multiplatform (`kotlin("multiplatform")`, including KMP Android libraries)**: To prevent unintentional cross-compilation contamination across diverse platforms (e.g., applying a JVM-only processor to iOS or JS targets), the global `ksp` configuration is **disallowed by default** (`ksp.allow.all.target.configuration = false`). Declaring `ksp("...")` in a KMP project will throw an exception (`InvalidUserCodeException`). You should instead use target-specific configurations (`kspJvm`, `kspAndroid`, `kspJs`, `kspIosArm64`, etc.). If you explicitly want to allow a global `ksp` configuration across all KMP targets and compilations, opt in by adding `-Pksp.allow.all.target.configuration=true` in your `gradle.properties`. + ### Single-Platform (JVM & Android) | Configuration Name / Pattern | Project Type / Target | Source Set / Scope | Usage Example (`build.gradle.kts`) | Details & Behavior | | --- | --- | --- | --- | --- | -| `ksp` | Single-target JVM / Android | Main source set (`src/main`) | `ksp("com.example:processor:1.0")` | Applied to default JVM compilation and single-platform Android main source set. Deprecated in KMP unless `ksp.allow.all.target.configuration=true`. | +| `ksp` | Single-target JVM / Android | All scopes by default (`src/main`, `src/test`, variants) | `ksp("com.example:processor:1.0")` | Global catch-all configuration enabled by default across production and test scopes (`ksp.allow.all.target.configuration = true`). Disallowed by default in KMP. | | `kspTest` | Single-target JVM / Android | Unit tests (`src/test`) | `kspTest("com.example:test-processor:1.0")` | Runs symbol processing exclusively for unit test sources. | | `ksp` | Single-target JVM | Custom JVM source set | `add("kspIntegrationTest", "...")` | Generates sources for custom source sets like `integrationTest`. | | `ksp` | Android (Single-platform) | Specific build type (e.g., debug, release) | `add("kspDebug", "...")` | Runs processor only when compiling the specified Android build variant. | @@ -88,6 +96,7 @@ When applying KSP in your Gradle project, place symbol processor dependencies in | `kspCommonMainMetadata` | Kotlin Multiplatform | Common Main metadata compilation | `add("kspCommonMainMetadata", "...")` | Target-specific configuration for KMP `commonMain` metadata processing. | + ## Nightly Builds Nightly builds of KSP for the latest Kotlin stable releases are published here: diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt index aafb830bc4..b6c36969ec 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt @@ -20,11 +20,11 @@ class KspConfigurations(private val project: Project) { private const val PREFIX = "ksp" } - internal val allowAllTargetConfiguration = - project.providers.gradleProperty("ksp.allow.all.target.configuration") + internal val allowAllTargetConfiguration: Boolean + get() = project.providers.gradleProperty("ksp.allow.all.target.configuration") .orNull ?.toBoolean() - ?: false + ?: !isMppProject() // The "ksp" configuration, applied to every compilation. private val configurationForAll = project.configurations.create(PREFIX).apply { @@ -127,8 +127,7 @@ class KspConfigurations(private val project: Project) { if (!reported) { reported = true val msg = "The 'ksp' configuration is deprecated in Kotlin Multiplatform projects. " + - "Please use target-specific configurations like 'kspJvm' instead." - + "Please use target-specific configurations like 'kspJvm' instead. " if (allowAllTargetConfiguration) { project.logger.warn(msg) } else { diff --git a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/ProcessorClasspathConfigurationsTest.kt b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/ProcessorClasspathConfigurationsTest.kt index d98db9e630..55deba41bd 100644 --- a/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/ProcessorClasspathConfigurationsTest.kt +++ b/gradle-plugin/src/test/kotlin/com/google/devtools/ksp/gradle/ProcessorClasspathConfigurationsTest.kt @@ -91,7 +91,7 @@ class ProcessorClasspathConfigurationsTest(isExperimentalPsiResolution: Boolean) val main = configurations["kspKotlinProcessorClasspath"] val test = configurations["kspTestKotlinProcessorClasspath"] require(main.extendsFrom.map { it.name } == listOf("ksp")) - require(test.extendsFrom.map { it.name } == listOf("kspTest")) + require(test.extendsFrom.map { it.name } == listOf("kspTest", "ksp")) } } """.trimIndent() @@ -101,6 +101,33 @@ class ProcessorClasspathConfigurationsTest(isExperimentalPsiResolution: Boolean) .build() } + @Test + fun testConfigurationsForSinglePlatformAppDisallowAll() { + testRule.setupAppAsJvmApp() + testRule.appModule.addSource("Foo.kt", "class Foo") + testRule.appModule.buildFileAdditions.add( + """ + $kspConfigs.all { + // Make sure ksp configs are not empty. + project.dependencies.add(name, "androidx.room:room-compiler:2.4.2") + } + tasks.register("testConfigurations") { + // Resolve all tasks to trigger classpath config creation + dependsOn(tasks["tasks"]) + doLast { + val main = configurations["kspKotlinProcessorClasspath"] + val test = configurations["kspTestKotlinProcessorClasspath"] + require(main.extendsFrom.map { it.name } == listOf("ksp")) + require(test.extendsFrom.map { it.name } == listOf("kspTest")) + } + } + """.trimIndent() + ) + testRule.runner() + .withArguments(":app:testConfigurations", "-Pksp.allow.all.target.configuration=false") + .build() + } + @Test fun testConfigurationsForAndroidAppAllowAll() { testRule.setupAppAsAndroidApp() @@ -229,6 +256,7 @@ class ProcessorClasspathConfigurationsTest(isExperimentalPsiResolution: Boolean) ) val testFreeUsDebugParentConfigs = setOf( + "ksp", "kspTest", "kspTestDebug", "kspTestFree", @@ -238,6 +266,7 @@ class ProcessorClasspathConfigurationsTest(isExperimentalPsiResolution: Boolean) ) val androidTestFreeUsDebugParentConfigs = setOf( + "ksp", "kspAndroidTest", "kspAndroidTestDebug", "kspAndroidTestFree", @@ -266,6 +295,90 @@ class ProcessorClasspathConfigurationsTest(isExperimentalPsiResolution: Boolean) .build() } + @Test + fun testConfigurationsForAndroidAppDisallowAll() { + testRule.setupAppAsAndroidApp() + testRule.appModule.addSource("Foo.kt", "class Foo") + testRule.appModule.buildFileAdditions.add( + """ + android { + flavorDimensions += listOf("tier", "region") + + productFlavors { + create("free") { + dimension = "tier" + } + create("premium") { + dimension = "tier" + } + create("us") { + dimension = "region" + } + create("eu") { + dimension = "region" + } + } + } + $kspConfigs.all { + // Make sure ksp configs are not empty. + project.dependencies.add(name, "androidx.room:room-compiler:2.4.2") + } + tasks.register("testConfigurations") { + // Resolve all tasks to trigger classpath config creation + dependsOn(tasks["tasks"]) + doLast { + val freeUsDebugConfig = configurations["kspFreeUsDebugKotlinProcessorClasspath"] + val testFreeUsDebugConfig = configurations["kspFreeUsDebugUnitTestKotlinProcessorClasspath"] + val androidTestFreeUsDebugConfig = + configurations["kspFreeUsDebugAndroidTestKotlinProcessorClasspath"] + val freeUsDebugParentConfigs = + setOf( + "ksp", + "kspDebug", + "kspFree", + "kspUs", + "kspFreeUs", + "kspFreeUsDebug" + ) + val testFreeUsDebugParentConfigs = + setOf( + "kspTest", + "kspTestDebug", + "kspTestFree", + "kspTestUs", + "kspTestFreeUs", + "kspTestFreeUsDebug" + ) + val androidTestFreeUsDebugParentConfigs = + setOf( + "kspAndroidTest", + "kspAndroidTestDebug", + "kspAndroidTestFree", + "kspAndroidTestUs", + "kspAndroidTestFreeUs", + "kspAndroidTestFreeUsDebug" + ) + val actualFreeUsDebug = freeUsDebugConfig.extendsFrom.map { it.name }.toSet() + require(actualFreeUsDebug == freeUsDebugParentConfigs) { + "freeUsDebugConfig: expected ${'$'}freeUsDebugParentConfigs but got ${'$'}actualFreeUsDebug" + } + val actualTestFreeUsDebug = testFreeUsDebugConfig.extendsFrom.map { it.name }.toSet() + require(actualTestFreeUsDebug == testFreeUsDebugParentConfigs) { + "testFreeUsDebugConfig: expected ${'$'}testFreeUsDebugParentConfigs but got ${'$'}actualTestFreeUsDebug" + } + val actualAndroidTestFreeUsDebug = androidTestFreeUsDebugConfig.extendsFrom.map { it.name }.toSet() + require(actualAndroidTestFreeUsDebug == androidTestFreeUsDebugParentConfigs) { + "androidTestFreeUsDebugConfig: expected ${'$'}androidTestFreeUsDebugParentConfigs but got ${'$'}actualAndroidTestFreeUsDebug" + } + } + } + """.trimIndent() + ) + testRule.runner() + .withArguments(":app:testConfigurations", "-Pksp.allow.all.target.configuration=false") + .build() + } + @Test fun testConfigurationsForMultiPlatformApp() { testRule.setupAppAsMultiplatformApp(