[CP-Stable] Ensure Warnings Do Not Log When AGP Is Below 9 (#188716)
This was a manual cherry pick of https://github.com/flutter/flutter/pull/188373.
### Issue Link:
What is the link to the issue this cherry-pick is addressing?
https://github.com/flutter/flutter/issues/188718
### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)?
Does it impact development (ex. flutter doctor crashes when Android Studio is installed),
or the shipping of production apps (the app crashes on launch).
This information is for domain experts and release engineers to understand the consequences of saying yes or no to the cherry pick.
All existing Flutter Android apps and add-to-app scenarios that use AGP < 9 and apply KGP.
### Changelog Description:
Explain this cherry pick:
* In one line that is accessible to most Flutter developers.
* That describes the state prior to the fix.
* That includes which platforms are impacted.
See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples.
< Replace with changelog description here >
[flutter/188718](https://github.com/flutter/flutter/issues/186100): When building Flutter Android apps and add-to-app modules using AGP < 9 with KGP, the tool incorrectly logs Built-in Kotlin migration warnings.
### Workaround:
Is there a workaround for this issue?
This change must be made. Built-in Kotlin only exists starting AGP 9, therefore we should not be informing app developers to migrate when their apps are below AGP 9.
### Risk:
What is the risk level of this cherry-pick?
### Test Coverage:
Are you confident that your fix is well-tested by automated tests?
### Validation Steps:
What are the steps to validate that this fix works?
- navigate to plugin camera_android_camerax in the packages repo
- change versions to AGP: 8.11.1, Gradle: 8.14, KGP: 2.2.20 in settings.gradle.kts
- add KGP id("kotlin-android") to the example app build.gradle.kts and the plugin module's build.gradle.kts
- `flutter build apk` and the build is successful and you see no logs. Without this change, you will see logs both for app developers and for plugin authors.diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
index 4c5e2f9..94376ca 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
@@ -656,6 +656,11 @@
}
project.gradle.projectsEvaluated {
+ // Safe to query AGP version after all projects are evaluated.
+ val agpVersion = VersionFetcher.getAGPVersion(project)
+ if (agpVersion == null || agpVersion.major < 9) {
+ return@projectsEvaluated
+ }
if (shouldLogForApp) {
project.logger.error(
"""
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
index 28bc9c0..bf282ba 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
@@ -4,6 +4,7 @@
package com.flutter.gradle
+import com.android.build.api.AndroidPluginVersion
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
@@ -21,6 +22,7 @@
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
+import io.mockk.unmockkObject
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.GradleException
@@ -35,6 +37,8 @@
import org.gradle.internal.impldep.junit.framework.TestCase.assertFalse
import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue
import org.jetbrains.kotlin.gradle.plugin.extraProperties
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
@@ -46,6 +50,33 @@
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+/**
+ * Configuration for a mock Gradle subproject.
+ *
+ * @property name The name of the subproject.
+ * @property declarativelyAppliedPlugins Plugins applied via the modern Gradle `plugins {}` block.
+ * For more details, see [Gradle Plugins Block Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:plugins_block).
+ * @property imperativelyAppliedPlugins Plugins applied via the legacy Gradle `apply plugin:` statement.
+ * For more details, see [Gradle Old Plugin Application Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:old_plugin_application).
+ */
+private data class SubprojectConfig(
+ val name: String,
+ val declarativelyAppliedPlugins: List<String> = emptyList(),
+ val imperativelyAppliedPlugins: List<String> = emptyList()
+)
+
+private class TestEnvironment(
+ val appProject: Project,
+ val plugins: List<Project>,
+ val subprojectsActionSlot: io.mockk.CapturingSlot<Action<Project>> = slot(),
+ val projectsEvaluatedActionSlot: io.mockk.CapturingSlot<Action<Gradle>> = slot()
+) {
+ val appPluginManager: PluginManager get() = appProject.pluginManager
+ val plugin1Manager: PluginManager get() = plugins[0].pluginManager
+ val plugin2Manager: PluginManager get() = plugins[1].pluginManager
+}
class FlutterPluginUtilsTest {
companion object {
@@ -998,611 +1029,529 @@
@Nested
inner class DetectApplyingKotlinGradlePluginTests {
- @Test
- fun `logs app warning when KGP is only applied in app`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.application")
- id("kotlin-android")
- }
- """.trimIndent()
- )
- }
+ private val rootProject = mockk<Project>()
+ private val mockGradle = mockk<Gradle>()
+ private val mockLogger = mockk<Logger>(relaxed = true)
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.library")
- }
- """.trimIndent()
- )
- }
+ // This AGP version will should match the Flutter create template values.
+ // In //packages/flutter_tools/lib/src/android/gradle_utils.dart
+ private val templateAgpVersion = AndroidPluginVersion(9, 0, 1)
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
+ private val errorAgpVersion = DependencyVersionChecker.errorAGPVersion
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
-
- val appProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
- )
-
- val pluginProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProject)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify {
- mockLogger.error(
- """
- WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
- applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
- Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
-
- """.trimIndent()
- )
- }
-
- verify(exactly = 0) {
- mockLogger.error(match { it.contains("Your app uses the following plugins") })
- }
- verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
- verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") }
+ @BeforeEach
+ fun setUp() {
+ mockkObject(VersionFetcher)
}
- @Test
- fun `logs plugin warning when KGP is only applied in one plugin`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.application")
- }
- """.trimIndent()
- )
- }
-
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.library")
- id("kotlin-android")
- }
- """.trimIndent()
- )
- }
-
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
-
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
-
- val appProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
- )
-
- val pluginProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProject)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify {
- mockLogger.error(
- """
- WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin
- Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
-
- Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
- If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
- an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
-
- If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
- """.trimIndent()
- )
- }
-
- verify(exactly = 0) {
- mockLogger.error(match { it.contains("Your Android app project") })
- }
- verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") }
- verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") }
+ @AfterEach
+ fun tearDown() {
+ unmockkObject(VersionFetcher)
}
- @Test
- fun `logs app and plugin warning when KGP is applied in both app and plugins`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
+ private fun createSubproject(
+ tempDir: Path,
+ projectName: String,
+ declarativelyAppliedPlugins: List<String> = emptyList(),
+ imperativelyAppliedPlugins: List<String> = emptyList()
+ ): Project {
+ val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() }
+ val buildGradleFile =
+ File(projectDir, "build.gradle").apply {
createNewFile()
- writeText(
- """
- plugins {
- id("com.android.application")
- id("kotlin-android")
+ // Expected output of declarativeBlock if declarativelyAppliedPlugins contains ["kotlin-android"]:
+ // plugins {
+ // id("kotlin-android")
+ // }
+ val declarativeBlock =
+ if (declarativelyAppliedPlugins.isNotEmpty()) {
+ "plugins {\n" + declarativelyAppliedPlugins.joinToString("\n") { " id(\"$it\")" } + "\n}\n"
+ } else {
+ ""
}
- """.trimIndent()
- )
- }
-
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.library")
- id("kotlin-android")
+ // Expected output of imperativeBlock if imperativelyAppliedPlugins contains ["kotlin-android"]:
+ // apply plugin: 'kotlin-android'
+ val imperativeBlock =
+ if (imperativelyAppliedPlugins.isNotEmpty()) {
+ imperativelyAppliedPlugins.joinToString("\n") { "apply plugin: '$it'" } + "\n"
+ } else {
+ ""
}
- """.trimIndent()
- )
+ writeText(declarativeBlock + imperativeBlock)
}
+ val pluginManager = mockk<PluginManager>(relaxed = true)
+ val project = mockk<Project>()
+ every { project.name } returns projectName
+ every { project.projectDir } returns projectDir
+ every { project.buildFile } returns buildGradleFile
+ every { project.logger } returns mockLogger
+ every { project.pluginManager } returns pluginManager
+ every { project.rootProject } returns rootProject
+ every { project.gradle } returns mockGradle
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
+ val extensions = mockk<org.gradle.api.plugins.ExtensionContainer>()
+ every { extensions.findByType(any<Class<*>>()) } returns null
+ every { project.extensions } returns extensions
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
-
- val appProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
- )
-
- val pluginProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProject)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify {
- mockLogger.error(
- """
- WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
- applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
- Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
-
- """.trimIndent()
- )
- }
-
- verify {
- mockLogger.error(
- """
- WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin
- Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
-
- Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
- If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
- an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
-
- If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
- """.trimIndent()
- )
- }
-
- verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
- verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") }
+ return project
}
- @Test
- fun `logs app and plugin warning when legacy KGP configuration is applied in both app and plugins`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- apply plugin: 'com.android.application'
- apply plugin: 'kotlin-android'
- """.trimIndent()
- )
- }
-
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- apply plugin: 'com.android.library'
- apply plugin: 'kotlin-android'
- """.trimIndent()
- )
- }
-
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
-
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectOnePluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectTwoPluginManager = mockk<PluginManager>(relaxed = true)
+ private fun setupTest(
+ tempDir: Path,
+ agpVersion: AndroidPluginVersion,
+ appConfig: SubprojectConfig,
+ pluginConfigs: List<SubprojectConfig>
+ ): TestEnvironment {
+ every { VersionFetcher.getAGPVersion(any()) } returns agpVersion
val appProject =
- createMockSubproject(
+ createSubproject(
tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
+ projectName = appConfig.name,
+ declarativelyAppliedPlugins = appConfig.declarativelyAppliedPlugins,
+ imperativelyAppliedPlugins = appConfig.imperativelyAppliedPlugins
)
- val pluginProjectOne =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin1",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectOnePluginManager
- )
-
- val pluginProjectTwo =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin2",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectTwoPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProjectOne)
- subprojectsActionSlot.captured.execute(pluginProjectTwo)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify {
- mockLogger.error(
- """
- WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath}
- applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter.
- Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS
-
- """.trimIndent()
- )
- }
-
- verify {
- mockLogger.error(
- """
- WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin1, plugin2
- Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
-
- Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin.
- If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing
- an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS
-
- If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS
- """.trimIndent()
- )
- }
-
- verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") }
- verify(exactly = 0) { pluginProjectOnePluginManager.apply("kotlin-android") }
- verify(exactly = 0) { pluginProjectTwoPluginManager.apply("kotlin-android") }
- }
-
- @Test
- fun `does not log when migrated to Built-in Kotlin`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- apply plugin: 'com.android.application'
- """.trimIndent()
+ val pluginProjects =
+ pluginConfigs.map { config ->
+ createSubproject(
+ tempDir = tempDir,
+ projectName = config.name,
+ declarativelyAppliedPlugins = config.declarativelyAppliedPlugins,
+ imperativelyAppliedPlugins = config.imperativelyAppliedPlugins
)
}
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- apply plugin: 'com.android.library'
- """.trimIndent()
- )
- }
+ val allProjects = setOf(appProject) + pluginProjects
+ every { rootProject.subprojects } returns allProjects
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
+ val testProject = TestEnvironment(appProject, pluginProjects)
+ every { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } returns Unit
+ every { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } returns Unit
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
-
- val appProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
- )
-
- val pluginProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProject)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify(exactly = 0) {
- mockLogger.error(any())
- }
-
- verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") }
- verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") }
+ return testProject
}
- @Test
- fun `logs when KGP is applied but fails to apply`(
- @TempDir tempDir: Path
- ) {
- val appDir = tempDir.resolve("app").toFile().apply { mkdirs() }
- val appBuildGradleFile =
- File(appDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.application")
+ private fun executeDetectApplyingKotlinGradlePlugin(testProject: TestEnvironment) {
+ detectApplyingKotlinGradlePlugin(testProject.appProject)
+
+ verify { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) }
+ testProject.subprojectsActionSlot.captured.execute(testProject.appProject)
+ for (plugin in testProject.plugins) {
+ testProject.subprojectsActionSlot.captured.execute(plugin)
+ }
+
+ verify { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) }
+ testProject.projectsEvaluatedActionSlot.captured.execute(mockGradle)
+ }
+
+ @Nested
+ inner class TestLogWarningsWhenAGPis9OrHigher {
+ @Test
+ fun `logs app warning when KGP is only applied in app`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your Android app project") &&
+ it.contains("applies the Kotlin Gradle Plugin") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS)
}
- """.trimIndent()
)
}
- val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() }
- val pluginBuildGradleFile =
- File(pluginDir, "build.gradle").apply {
- createNewFile()
- writeText(
- """
- plugins {
- id("com.android.library")
- }
- """.trimIndent()
- )
+ verify(exactly = 0) {
+ mockLogger.error(match { it.contains("WARNING: Your app uses the following plugins") })
}
-
- val rootProject = mockk<Project>()
- val mockGradle = mockk<Gradle>()
- val mockLogger = mockk<Logger>(relaxed = true)
-
- val appProjectPluginManager = mockk<PluginManager>(relaxed = true)
- val pluginProjectPluginManager = mockk<PluginManager>(relaxed = true)
-
- val appProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = appBuildGradleFile,
- projectName = "app",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = appProjectPluginManager
- )
-
- val pluginProject =
- createMockSubproject(
- tempDir = tempDir,
- buildFile = pluginBuildGradleFile,
- projectName = "plugin",
- mockLogger = mockLogger,
- rootProjectMock = rootProject,
- gradleMock = mockGradle,
- pluginManager = pluginProjectPluginManager
- )
-
- val subprojectsActionSlot = slot<Action<Project>>()
- val projectsEvaluatedActionSlot = slot<Action<Gradle>>()
-
- every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit
- every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit
-
- every { appProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
- every { pluginProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
-
- detectApplyingKotlinGradlePlugin(appProject)
-
- verify { rootProject.subprojects(capture(subprojectsActionSlot)) }
- subprojectsActionSlot.captured.execute(appProject)
- subprojectsActionSlot.captured.execute(pluginProject)
-
- verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) }
- projectsEvaluatedActionSlot.captured.execute(mockGradle)
-
- verify(exactly = 0) {
- mockLogger.error(any())
+ verify(exactly = 0) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 1) { plugin1Manager.apply("kotlin-android") }
}
- verify {
- mockLogger.quiet(
- """
- Applying the Kotlin Android Plugin (KGP) was unsuccessful. KGP was not found on the classpath.
- If your project uses Kotlin, ensure KGP is declared in the root plugins block.
- For more details check: $BUILT_IN_KOTLIN_DOCS
- """.trimIndent()
- )
+ @Test
+ fun `logs plugin warning when KGP is only applied in one plugin`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your app uses the following plugins") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS)
+ }
+ )
+ }
+
+ verify(exactly = 0) {
+ mockLogger.error(match { it.contains("WARNING: Your Android app project") })
+ }
+ verify(exactly = 1) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 0) { plugin1Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `logs app and plugin warning when KGP is applied in both app and plugins`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your Android app project") &&
+ it.contains("applies the Kotlin Gradle Plugin") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS)
+ }
+ )
+ }
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your app uses the following plugins") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS)
+ }
+ )
+ }
+
+ verify(exactly = 0) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 0) { plugin1Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `logs app and plugin warning when imperative KGP configuration is applied in both app and plugins`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ imperativelyAppliedPlugins =
+ listOf(
+ "com.android.application",
+ "kotlin-android"
+ )
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin1",
+ imperativelyAppliedPlugins =
+ listOf(
+ "com.android.library",
+ "kotlin-android"
+ )
+ ),
+ SubprojectConfig(
+ "plugin2",
+ imperativelyAppliedPlugins =
+ listOf(
+ "com.android.library",
+ "kotlin-android"
+ )
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+ val plugin2Manager = testProject.plugin2Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your Android app project") &&
+ it.contains("applies the Kotlin Gradle Plugin") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS)
+ }
+ )
+ }
+
+ verify {
+ mockLogger.error(
+ match {
+ it.contains("WARNING: Your app uses the following plugins") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) &&
+ it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS)
+ }
+ )
+ }
+
+ verify(exactly = 0) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 0) { plugin1Manager.apply("kotlin-android") }
+ verify(exactly = 0) { plugin2Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `does not log when migrated to Built-in Kotlin`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ imperativelyAppliedPlugins = listOf("com.android.application")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ imperativelyAppliedPlugins = listOf("com.android.library")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify(exactly = 0) {
+ mockLogger.error(any())
+ }
+
+ verify(exactly = 1) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 1) { plugin1Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `logs KGP warning when KGP attempts to but fails to apply`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = templateAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
+ every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath")
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify(exactly = 0) {
+ mockLogger.error(any())
+ }
+
+ verify {
+ mockLogger.quiet(
+ match {
+ it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") &&
+ it.contains("ensure KGP is declared in the root plugins block") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS)
+ }
+ )
+ }
}
}
- }
- private fun createMockSubproject(
- tempDir: Path,
- buildFile: File,
- projectName: String,
- mockLogger: Logger,
- rootProjectMock: Project? = null,
- gradleMock: Gradle? = null,
- pluginManager: PluginManager
- ): Project {
- val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() }
+ @Nested
+ inner class TestLogWarningsWhenAGPIsLessThan9 {
+ @Test
+ fun `does not log warnings when migrated to built-in kotlin`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = errorAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library")
+ )
+ )
+ )
- val project = mockk<Project>()
- every { project.name } returns projectName
- every { project.projectDir } returns projectDir
- every { project.buildFile } returns buildFile
- every { project.logger } returns mockLogger
- every { project.pluginManager } returns pluginManager
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
- if (rootProjectMock != null) every { project.rootProject } returns rootProjectMock
- if (gradleMock != null) every { project.gradle } returns gradleMock
+ executeDetectApplyingKotlinGradlePlugin(testProject)
- return project
+ // No warnings should be logged because AGP version is < 9.
+ verify(exactly = 0) { mockLogger.error(any()) }
+
+ // KGP is still applied in case the entire project has been migrated.
+ verify(exactly = 1) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 1) { plugin1Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `does not log warnings when KGP is applied`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = errorAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ // No warnings should be logged because AGP version is < 9.
+ verify(exactly = 0) { mockLogger.error(any()) }
+
+ verify(exactly = 0) { appPluginManager.apply("kotlin-android") }
+ verify(exactly = 0) { plugin1Manager.apply("kotlin-android") }
+ }
+
+ @Test
+ fun `logs KGP warning when KGP attempts to but fails to apply`(
+ @TempDir tempDir: Path
+ ) {
+ val testProject =
+ setupTest(
+ tempDir = tempDir,
+ agpVersion = errorAgpVersion,
+ appConfig =
+ SubprojectConfig(
+ "app",
+ declarativelyAppliedPlugins = listOf("com.android.application")
+ ),
+ pluginConfigs =
+ listOf(
+ SubprojectConfig(
+ "plugin",
+ declarativelyAppliedPlugins = listOf("com.android.library")
+ )
+ )
+ )
+
+ val appPluginManager = testProject.appPluginManager
+ val plugin1Manager = testProject.plugin1Manager
+
+ every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath")
+ every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath")
+
+ executeDetectApplyingKotlinGradlePlugin(testProject)
+
+ verify(exactly = 0) {
+ mockLogger.error(any())
+ }
+
+ verify {
+ mockLogger.quiet(
+ match {
+ it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") &&
+ it.contains("ensure KGP is declared in the root plugins block") &&
+ it.contains(BUILT_IN_KOTLIN_DOCS)
+ }
+ )
+ }
+ }
+ }
}
}
diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart
index 817317d..bd271d5 100644
--- a/packages/flutter_tools/lib/src/android/gradle_utils.dart
+++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart
@@ -39,6 +39,7 @@
// * AGP version constants in packages/flutter_tools/gradle/build.gradle.kts
// * AGP warn version in packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt
// * AGP test constants in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt
+// * AGP test constants in packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
// See https://mvnrepository.com/artifact/com.android.tools.build/gradle
const templateAndroidGradlePluginVersion = '9.0.1';
const templateAndroidGradlePluginVersionForModule = '9.0.1';