Skip to content
Open
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
148 changes: 119 additions & 29 deletions packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.flutter.gradle

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
Expand All @@ -22,6 +23,7 @@ import org.gradle.api.UnknownTaskException
import org.gradle.api.logging.Logger
import org.gradle.kotlin.dsl.register
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Properties

Expand Down Expand Up @@ -536,38 +538,18 @@ object FlutterPluginUtils {
@JvmName("detectApplyingKotlinGradlePlugin")
internal fun detectApplyingKotlinGradlePlugin(project: Project) {
val pluginsWithKGPAppliedList = mutableListOf<String>()

val agpVersion = VersionFetcher.getAGPVersion(project)
if (isBuiltInKotlinEnabled(project, agpVersion) && hasNoSubprojectsApplyingKgp(project)) {
return
}
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")
} catch (_: Exception) {
Expand All @@ -583,11 +565,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)
}
}
Expand Down Expand Up @@ -624,6 +606,114 @@ object FlutterPluginUtils {
}
}

/**
* Represents whether Kotlin Gradle Plugin, Android Gradle Plugin (for applications), and the
* Android Gradle Plugin (for libraries) are declared in a subproject's build script.
*
* @property hasKgpPlugin `true` if the Kotlin Gradle Plugin (KGP) is declared in the subproject's build script.
* @property hasAppPlugin `true` if the Android Gradle Plugin (AGP) for applications is declared in the subproject's build script.
* @property hasLibPlugin `true` if the Android Gradle Plugin (AGP) for libraries is declared in the subproject's build script.
*/
internal data class SubprojectPluginState(
val hasKgpPlugin: Boolean,
val hasAppPlugin: Boolean,
val hasLibPlugin: Boolean
)

/**
* Scans the build script of the subproject to detect declarations of Kotlin Gradle Plugin,
* Android Gradle Plugin (for applications), and the Android Gradle Plugin (for libraries).
*
* Returns null if the build script does not exist, is inside an ephemeral `.android/` directory,
* or fails to read due to an [IOException].
*/
internal fun getSubprojectPluginState(subproject: Project): SubprojectPluginState? {
val buildFile = subproject.buildFile

// 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 null
}

val scriptText: String =
try {
if (buildFile.absolutePath.contains("app/build.gradle")) {
getBuildGradleFileFromProjectDir(
subproject.projectDir,
subproject.logger
).readText()
} else {
buildFile.readText()
}
} catch (e: IOException) {
subproject.logger.error("Failed to read build file: ${buildFile.absolutePath}", e)
return null
}

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)
}

/**
* Checks if all subprojects within the root project have migrated away from the legacy
* Kotlin Gradle Plugin (KGP) to Built-in Kotlin.
*/
@JvmStatic
@JvmName("hasNoSubprojectsApplyingKgp")
internal fun hasNoSubprojectsApplyingKgp(project: Project): Boolean =
project.rootProject.subprojects.all { subproject ->
val subprojectPluginState = getSubprojectPluginState(subproject)

// Non-Android subprojects (those not applying com.android.application or com.android.library)
// are ignored as they do not affect the Android Kotlin build configuration.
// For Android subprojects, we verify they do not apply the legacy KGP.
if (subprojectPluginState == null || (!subprojectPluginState.hasAppPlugin && !subprojectPluginState.hasLibPlugin)) {
true
} else {
!subprojectPluginState.hasKgpPlugin
}
}

/**
* Determines if the Gradle property `android.builtInKotlin` is enabled globally across the multi-project Gradle build.
*
* Evaluates the `android.builtInKotlin` Gradle property, supporting any [standard Gradle
* configuration source](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) (such as the root project's `gradle.properties` file or command-line `-P` flags).
*
* Defaults to `true` for AGP 9.0+ unless `android.builtInKotlin` is explicitly configured
* to `false`. Always returns `false` if the AGP version is below `9.0.0` (or null).
* See [Android Migration Guide](https://developer.android.com/build/migrate-to-built-in-kotlin).
*/
@JvmStatic
@JvmName("isBuiltInKotlinEnabled")
internal fun isBuiltInKotlinEnabled(
project: Project,
agpVersion: AndroidPluginVersion?
): Boolean {
if (agpVersion == null || agpVersion.major < 9) {
return false
}
return project.providers
.gradleProperty("android.builtInKotlin")
.orNull
?.toBoolean() ?: true
}

/** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
@JvmStatic
@JvmName("detectLowCompileSdkVersionOrNdkVersion")
Expand Down
Loading