diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index d5b8a42195f12..62be799b3371e 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -533,41 +533,39 @@ object FlutterPluginUtils { @JvmStatic @JvmName("detectApplyingKotlinGradlePlugin") internal fun detectApplyingKotlinGradlePlugin(project: Project) { + val gradlePropertiesFile = project.rootProject.file("gradle.properties") + val properties = readPropertiesIfExist(gradlePropertiesFile) + val isBuiltInKotlinEnabled = + properties.getProperty("android.builtInKotlin")?.lowercase()?.toBoolean() ?: false + + if (isBuiltInKotlinEnabled) { + val allSubprojectsDoNotApplyKgp = + project.rootProject.subprojects.all { subproject -> + val pluginState = getSubprojectPluginState(subproject) + if (pluginState == null || (!pluginState.hasAppPlugin && !pluginState.hasLibPlugin)) { + true + } else { + !pluginState.hasKgpPlugin + } + } + if (allSubprojectsDoNotApplyKgp) { + return + } + } + val pluginsWithKGPAppliedList = mutableListOf() var shouldLogForApp = false project.rootProject.subprojects { - // Accounts for Add-to-app scenarios where the Flutter Module ephemeral .android/ directory should not be adjusted and by default does not apply KGP - if (!buildFile.exists() || buildFile.absolutePath.contains(".android")) return@subprojects - - val scriptText: String = - if (buildFile.absolutePath.contains("app/build.gradle")) { - getBuildGradleFileFromProjectDir(this.projectDir, this.logger).readText() - } else { - buildFile.readText() - } - - val (hasKgpPlugin, hasAppPlugin, hasLibPlugin) = - if (buildFile.extension == "kts") { - Triple( - kgpRegexKotlin.containsMatchIn(scriptText), - appPluginRegexKotlin.containsMatchIn(scriptText), - libPluginRegexKotlin.containsMatchIn(scriptText) - ) - } else { - Triple( - kgpRegexGroovy.containsMatchIn(scriptText), - appPluginRegexGroovy.containsMatchIn(scriptText), - libPluginRegexGroovy.containsMatchIn(scriptText) - ) - } + val pluginState = getSubprojectPluginState(this) ?: return@subprojects // Ensures applying AGP exists in the build file configuration. - if (!hasAppPlugin && !hasLibPlugin) return@subprojects + if (!pluginState.hasAppPlugin && !pluginState.hasLibPlugin) return@subprojects - if (!hasKgpPlugin) { + if (!pluginState.hasKgpPlugin) { try { pluginManager.apply("kotlin-android") + println("applied KGP in FGP") } catch (_: Exception) { logger.quiet( """ @@ -581,11 +579,11 @@ object FlutterPluginUtils { } // Apply AGP exists and Apply KGP also exists in build.gradle - if (hasAppPlugin) { + if (pluginState.hasAppPlugin) { shouldLogForApp = true } - if (hasLibPlugin) { + if (pluginState.hasLibPlugin) { pluginsWithKGPAppliedList.add(name) } } @@ -617,6 +615,46 @@ object FlutterPluginUtils { } } + private data class SubprojectPluginState( + val hasKgpPlugin: Boolean, + val hasAppPlugin: Boolean, + val hasLibPlugin: Boolean + ) + + private fun getSubprojectPluginState(subproject: Project): SubprojectPluginState? { + val buildFile = subproject.buildFile + if (!buildFile.exists() || buildFile.absolutePath.contains(".android")) { + return null + } + + val scriptText: String = + if (buildFile.absolutePath.contains("app/build.gradle")) { + getBuildGradleFileFromProjectDir( + subproject.projectDir, + subproject.logger + ).readText() + } else { + buildFile.readText() + } + + val (hasKgpPlugin, hasAppPlugin, hasLibPlugin) = + if (buildFile.extension == "kts") { + Triple( + kgpRegexKotlin.containsMatchIn(scriptText), + appPluginRegexKotlin.containsMatchIn(scriptText), + libPluginRegexKotlin.containsMatchIn(scriptText) + ) + } else { + Triple( + kgpRegexGroovy.containsMatchIn(scriptText), + appPluginRegexGroovy.containsMatchIn(scriptText), + libPluginRegexGroovy.containsMatchIn(scriptText) + ) + } + + return SubprojectPluginState(hasKgpPlugin, hasAppPlugin, hasLibPlugin) + } + /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */ @JvmStatic @JvmName("detectLowCompileSdkVersionOrNdkVersion") diff --git a/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt index 1b779b21fae50..4fce003030fcd 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt @@ -4,7 +4,7 @@ package com.flutter.gradle -import org.gradle.internal.impldep.org.junit.Assert.assertThrows +import org.junit.jupiter.api.assertThrows import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertFalse @@ -41,7 +41,7 @@ class DeeplinkTest { val deeplink1 = Deeplink("scheme1", "host1", "path1", IntentFilterCheck()) val deeplink2 = null - assertThrows(NullPointerException::class.java) { deeplink1.equals(deeplink2) } + assertThrows { deeplink1.equals(deeplink2) } } @Test diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index ab51a4f5de016..8bc0d8cedf978 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -34,8 +34,8 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.invocation.Gradle import org.gradle.api.logging.Logger import org.gradle.api.plugins.PluginManager -import org.gradle.internal.impldep.junit.framework.TestCase.assertFalse -import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue +import kotlin.test.assertFalse +import kotlin.test.assertTrue import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows @@ -48,6 +48,23 @@ import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse +private data class SubprojectConfig( + val name: String, + val plugins: List = emptyList(), + val legacyPlugins: List = emptyList() +) + +private class TestEnvironment( + val appProject: Project, + val appPluginManager: PluginManager, + val plugins: List>, + val subprojectsActionSlot: io.mockk.CapturingSlot> = io.mockk.slot(), + val projectsEvaluatedActionSlot: io.mockk.CapturingSlot> = io.mockk.slot() +) { + val firstPluginProject: Project get() = plugins[0].first + val firstPluginManager: PluginManager get() = plugins[0].second +} + class FlutterPluginUtilsTest { companion object { const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" @@ -924,611 +941,443 @@ class FlutterPluginUtilsTest { @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() + private val mockGradle = mockk() + private val mockLogger = mockk(relaxed = true) + + private fun writeGradleProperties(rootDir: File, content: String) { + File(rootDir, "gradle.properties").apply { + createNewFile() + writeText(content) + } + } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.library") - } - """.trimIndent() - ) - } + private fun createSubproject( + tempDir: Path, + projectName: String, + plugins: List = emptyList(), + legacyPlugins: List = emptyList() + ): Pair { + val rootDir = tempDir.toFile() + every { rootProject.file("gradle.properties") } returns File(rootDir, "gradle.properties") + every { rootProject.projectDir } returns rootDir + + val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() } + val buildGradleFile = File(projectDir, "build.gradle").apply { + createNewFile() + val pluginsBlock = if (plugins.isNotEmpty()) { + "plugins {\n" + plugins.joinToString("\n") { " id(\"$it\")" } + "\n}\n" + } else "" + val legacyBlock = if (legacyPlugins.isNotEmpty()) { + legacyPlugins.joinToString("\n") { "apply plugin: '$it'" } + "\n" + } else "" + writeText(pluginsBlock + legacyBlock) + } + val pluginManager = mockk(relaxed = true) + val project = createMockSubproject( + tempDir = tempDir, + buildFile = buildGradleFile, + projectName = projectName, + mockLogger = mockLogger, + rootProjectMock = rootProject, + gradleMock = mockGradle, + pluginManager = pluginManager + ) + return Pair(project, pluginManager) + } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) + private fun setupTest( + tempDir: Path, + builtInKotlin: String? = null, + appConfig: SubprojectConfig = SubprojectConfig("app", plugins = listOf("com.android.application")), + pluginConfigs: List = listOf(SubprojectConfig("plugin", plugins = listOf("com.android.library"))), + captureActions: Boolean = true + ): TestEnvironment { + val rootDir = tempDir.toFile() + if (builtInKotlin != null) { + writeGradleProperties(rootDir, "android.builtInKotlin=$builtInKotlin\n") + } else { + writeGradleProperties(rootDir, "") + } - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) + val (appProject, appPluginManager) = createSubproject( + tempDir = tempDir, + projectName = appConfig.name, + plugins = appConfig.plugins, + legacyPlugins = appConfig.legacyPlugins + ) - val pluginProject = - createMockSubproject( + val pluginProjects = pluginConfigs.map { config -> + createSubproject( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager + projectName = config.name, + plugins = config.plugins, + legacyPlugins = config.legacyPlugins ) + } - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - 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) + val allProjects = setOf(appProject) + pluginProjects.map { it.first } + every { rootProject.subprojects } returns allProjects - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + val env = TestEnvironment(appProject, appPluginManager, pluginProjects) - 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() - ) + if (captureActions) { + every { rootProject.subprojects(capture(env.subprojectsActionSlot)) } returns Unit + every { mockGradle.projectsEvaluated(capture(env.projectsEvaluatedActionSlot)) } returns Unit } - 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") } + return env } - @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() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) + private fun executeDetectApplyingKotlinGradlePlugin(env: TestEnvironment) { + detectApplyingKotlinGradlePlugin(env.appProject) - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) + verify { rootProject.subprojects(capture(env.subprojectsActionSlot)) } + env.subprojectsActionSlot.captured.execute(env.appProject) + for (plugin in env.plugins) { + env.subprojectsActionSlot.captured.execute(plugin.first) + } - val appProject = - createMockSubproject( + verify { mockGradle.projectsEvaluated(capture(env.projectsEvaluatedActionSlot)) } + env.projectsEvaluatedActionSlot.captured.execute(mockGradle) + } + @Nested + inner class BuiltInKotlinIsEnabled { + @Test + fun `exits early when no subproject applies KGP and property is true`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager + builtInKotlin = "true", + captureActions = false ) - val pluginProject = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager - ) + detectApplyingKotlinGradlePlugin(env.appProject) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + verify(exactly = 0) { rootProject.subprojects(any>()) } + verify(exactly = 0) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { env.firstPluginManager.apply("kotlin-android") } + } - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + @Test + fun `exits early when no subproject applies KGP and property is TRUE`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "TRUE", + captureActions = false + ) - detectApplyingKotlinGradlePlugin(appProject) + detectApplyingKotlinGradlePlugin(env.appProject) - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + verify(exactly = 0) { rootProject.subprojects(any>()) } + verify(exactly = 0) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { env.firstPluginManager.apply("kotlin-android") } + } - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + @Test + fun `does not exit early when a subproject applies KGP and property is true`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "true", + pluginConfigs = listOf( + SubprojectConfig( + "plugin", + plugins = listOf("com.android.library", "kotlin-android") + ) + ) + ) - 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. + executeDetectApplyingKotlinGradlePlugin(env) - 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 + verify(exactly = 0) { + mockLogger.error(match { it.contains("Your Android app project") }) + } + verify { + mockLogger.error(match { it.contains("Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin") }) + } + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { env.firstPluginManager.apply("kotlin-android") } + } + } - If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS - """.trimIndent() + @Nested + inner class BuiltInKotlinIsDisabled { + @Test + fun `does not exit early when property is false`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "false" ) - } - 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") } - } + executeDetectApplyingKotlinGradlePlugin(env) - @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 { - createNewFile() - writeText( - """ - plugins { - id("com.android.application") - id("kotlin-android") - } - """.trimIndent() - ) - } + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { env.firstPluginManager.apply("kotlin-android") } + } - 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() - ) - } + @Test + fun `does not exit early when property is FALSE`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "FALSE" + ) - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) + executeDetectApplyingKotlinGradlePlugin(env) - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + } - val appProject = - createMockSubproject( + @Test + fun `does not exit early when property is invalid`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager + builtInKotlin = "5" ) - val pluginProject = - createMockSubproject( + executeDetectApplyingKotlinGradlePlugin(env) + + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { env.firstPluginManager.apply("kotlin-android") } + } + + @Test + fun `does not log when migrated to Built-in Kotlin`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager + builtInKotlin = "false", + appConfig = SubprojectConfig("app", legacyPlugins = listOf("com.android.application")), + pluginConfigs = listOf(SubprojectConfig("plugin", legacyPlugins = listOf("com.android.library"))) ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + executeDetectApplyingKotlinGradlePlugin(env) - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + verify(exactly = 0) { + mockLogger.error(any()) + } - detectApplyingKotlinGradlePlugin(appProject) + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { env.firstPluginManager.apply("kotlin-android") } + } - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + @Test + fun `logs app warning when KGP is only applied in app`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "false", + appConfig = SubprojectConfig("app", plugins = listOf("com.android.application", "kotlin-android")) + ) - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + executeDetectApplyingKotlinGradlePlugin(env) - verify { - mockLogger.error( - """ - WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath} + val expectedAppWarning = """ + WARNING: Your Android app project: app located at: ${env.appProject.buildFile.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. + verify { + mockLogger.error(expectedAppWarning) + } - 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 + verify(exactly = 0) { + mockLogger.error(match { it.contains("Your app uses the following plugins") }) + } + verify(exactly = 0) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { env.firstPluginManager.apply("kotlin-android") } + } - If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS - """.trimIndent() + @Test + fun `logs plugin warning when KGP is only applied in one plugin`( + @TempDir tempDir: Path + ) { + val env = setupTest( + tempDir = tempDir, + builtInKotlin = "false", + pluginConfigs = listOf(SubprojectConfig("plugin", plugins = listOf("com.android.library", "kotlin-android"))) ) - } - verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") } - } + executeDetectApplyingKotlinGradlePlugin(env) - @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( + verify { + mockLogger.error( """ - apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - """.trimIndent() - ) - } + 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. - 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' + 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() ) } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectOnePluginManager = mockk(relaxed = true) - val pluginProjectTwoPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) - - val pluginProjectOne = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin1", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectOnePluginManager - ) + verify(exactly = 0) { + mockLogger.error(match { it.contains("Your Android app project") }) + } + verify(exactly = 1) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { env.firstPluginManager.apply("kotlin-android") } + } - val pluginProjectTwo = - createMockSubproject( + @Test + fun `logs app and plugin warning when KGP is applied in both app and plugins`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin2", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectTwoPluginManager + builtInKotlin = "false", + appConfig = SubprojectConfig("app", plugins = listOf("com.android.application", "kotlin-android")), + pluginConfigs = listOf(SubprojectConfig("plugin", plugins = listOf("com.android.library", "kotlin-android"))) ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - 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) + executeDetectApplyingKotlinGradlePlugin(env) - verify { - mockLogger.error( - """ - WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath} + val expectedAppWarning = """ + WARNING: Your Android app project: app located at: ${env.appProject.buildFile.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() - ) + verify { + mockLogger.error(expectedAppWarning) } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( + verify { + mockLogger.error( """ - apply plugin: 'com.android.library' + 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() ) } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) + verify(exactly = 0) { env.appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { env.firstPluginManager.apply("kotlin-android") } + } - val pluginProject = - createMockSubproject( + @Test + fun `logs app and plugin warning when legacy KGP configuration is applied in both app and plugins`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager + builtInKotlin = "false", + appConfig = SubprojectConfig("app", legacyPlugins = listOf("com.android.application", "kotlin-android")), + pluginConfigs = listOf( + SubprojectConfig("plugin1", legacyPlugins = listOf("com.android.library", "kotlin-android")), + SubprojectConfig("plugin2", legacyPlugins = listOf("com.android.library", "kotlin-android")) + ) ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + executeDetectApplyingKotlinGradlePlugin(env) - detectApplyingKotlinGradlePlugin(appProject) - - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + val expectedAppWarning = """ + WARNING: Your Android app project: app located at: ${env.appProject.buildFile.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 - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + """.trimIndent() + verify { + mockLogger.error(expectedAppWarning) + } - verify(exactly = 0) { - mockLogger.error(any()) - } + 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. - verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") } - } + 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 - @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") - } + If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS """.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) { env.appPluginManager.apply("kotlin-android") } + for (plugin in env.plugins) { + verify(exactly = 0) { plugin.second.apply("kotlin-android") } } + } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) - - val pluginProject = - createMockSubproject( + @Test + fun `logs quiet warning when KGP application fails`( + @TempDir tempDir: Path + ) { + val env = setupTest( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager + builtInKotlin = "false" ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - 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) + every { env.appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") + every { env.firstPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + executeDetectApplyingKotlinGradlePlugin(env) - verify(exactly = 0) { - mockLogger.error(any()) - } + verify(exactly = 0) { + mockLogger.error(any()) + } - 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() - ) + 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() + ) + } } } - } - 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() } - - val project = mockk() - 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 - - if (rootProjectMock != null) every { project.rootProject } returns rootProjectMock - if (gradleMock != null) every { project.gradle } returns gradleMock - - return project + 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() } + + val project = mockk() + 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 + + if (rootProjectMock != null) every { project.rootProject } returns rootProjectMock + if (gradleMock != null) every { project.gradle } returns gradleMock + + return project + } } }