blob: 50779e6d0b8d0eddcd25c2368f218878946a5ede [file]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.builder.model.BuildType
import com.flutter.gradle.plugins.PluginHandler
import com.flutter.gradle.tasks.DeepLinkJsonFromManifestTask
import com.flutter.gradle.tasks.PrintTask
import com.flutter.gradle.tasks.ValidateCompileSdkVersionTask
import groovy.lang.Closure
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.logging.Logger
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.process.ExecOperations
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Properties
/**
* A collection of static utility functions used by the Flutter Gradle Plugin.
*/
object FlutterPluginUtils {
// Gradle properties. These must correspond to the values used in
// flutter/packages/flutter_tools/lib/src/android/gradle.dart, and therefore it is not
// recommended to use these const values in tests.
internal const val PROP_SHOULD_SHRINK_RESOURCES = "shrink"
internal const val PROP_SPLIT_PER_ABI = "split-per-abi"
internal const val PROP_LOCAL_ENGINE_REPO = "local-engine-repo"
internal const val PROP_IS_VERBOSE = "verbose"
internal const val PROP_TARGET = "target"
internal const val PROP_LOCAL_ENGINE_BUILD_MODE = "local-engine-build-mode"
internal const val PROP_TARGET_PLATFORM = "target-platform"
internal const val PROP_DISABLE_ABI_FILTERING = "disable-abi-filtering"
internal const val PROP_SDK_MANAGER_PATH = "flutter.sdkManagerPath"
internal const val PROP_ANDROID_SDK_ROOT = "flutter.androidSdkRoot"
internal const val PROP_INSTALLED_NDK_VERSIONS = "flutter.installedNdkVersions"
internal const val TASK_PRINT_NDK_VERSION = "printNdkVersion"
internal const val NDK_VERSION_OUTPUT_PREFIX = "NdkVersion: "
private data class ToolNdkProvisioningProperties(
val androidSdkRoot: String,
val installedNdkVersions: Set<String>,
val sdkManagerPath: String?
)
internal const val PROP_FORCE_VERSION_CODE_IGNORING_ABI = "force-version-code-ignoring-abi"
/**
* The URL for documentation for general information on migration to built-in Kotlin.
*/
internal const val BUILT_IN_KOTLIN_DOCS =
"https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin"
/**
* The URL for documentation instructing app developers how to migrate their app to built-in Kotlin.
*/
internal const val BUILT_IN_KOTLIN_DOCS_FOR_APPS =
"https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers"
/**
* The URL for documentation instructing plugin authors how to migrate to their plugin to built-in Kotlin.
*/
internal const val BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS =
"https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-plugin-authors"
/**
* The URL for documentation instructing app developers on how to report incompatible KGP usage to plugin authors.
*/
internal const val BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS =
"https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers#report-incompatible-kotlin-gradle-plugin-usage-to-plugin-authors"
/**
* Matches the AGP application plugin declaration in Kotlin DSL (`build.gradle.kts`).
* Targets `id("com.android.application")` or `alias(libs.plugins.android.application)`
* within a `plugins { ... }` block.
*/
internal val appPluginRegexKotlin =
"""(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)[ \t]*\(\s*(['"]com\.android\.application['"]|libs\.plugins\.android\.application)\s*\)(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
/**
* Matches the AGP library plugin declaration in Kotlin DSL (`build.gradle.kts`).
* Targets `id("com.android.library")` or `alias(libs.plugins.android.library)`
* within a `plugins { ... }` block.
*/
internal val libPluginRegexKotlin =
"""(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)[ \t]*\(\s*(['"]com\.android\.library['"]|libs\.plugins\.android\.library)\s*\)(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
/**
* Matches the KGP declaration in Kotlin DSL (`build.gradle.kts`).
* Targets `kotlin-android`, `org.jetbrains.kotlin.android`, or version catalog aliases
* for Kotlin Android within a `plugins { ... }` block.
*/
internal val kgpRegexKotlin =
"""(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)[ \t]*\(\s*(['"](?:kotlin-android|org\.jetbrains\.kotlin\.android)['"]|libs\.plugins\.(?:android|kotlin)\.android)\s*\)(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
/**
* Matches the AGP application plugin in Groovy DSL (`build.gradle`).
* Supports both the legacy `apply plugin: 'com.android.application'` syntax and
* the modern `plugins { id 'com.android.application' }` syntax (with or without parentheses).
*/
internal val appPluginRegexGroovy =
"""(?m)^[ \t]*apply[ \t]+plugin[ \t]*:[ \t]*(['"])com\.android\.application\1|(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)(?:[ \t]*\(\s*|[ \t]+)(['"]com\.android\.application['"]|libs\.plugins\.android\.application)(?:\s*\))?(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
/**
* Matches the AGO library plugin in Groovy DSL (`build.gradle`).
* Supports both the legacy `apply plugin: 'com.android.library'` syntax and
* the modern `plugins { id 'com.android.library' }` syntax (with or without parentheses).
*/
internal val libPluginRegexGroovy =
"""(?m)^[ \t]*apply[ \t]+plugin[ \t]*:[ \t]*(['"])com\.android\.library\1|(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)(?:[ \t]*\(\s*|[ \t]+)(['"]com\.android\.library['"]|libs\.plugins\.android\.library)(?:\s*\))?(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
/**
* Matches the KGP in Groovy DSL (`build.gradle`).
* Supports both legacy `apply plugin` and modern `plugins {}` blocks for
* `kotlin-android` or `org.jetbrains.kotlin.android`.
*/
internal val kgpRegexGroovy =
"""(?m)^[ \t]*apply[ \t]+plugin[ \t]*:[ \t]*(['"])(?:kotlin-android|org\.jetbrains\.kotlin\.android)\1|(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)(?:[ \t]*\(\s*|[ \t]+)(['"](?:kotlin-android|org\.jetbrains\.kotlin\.android)['"]|libs\.plugins\.(?:android|kotlin)\.android)(?:\s*\))?(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
// ----------------- Methods for string manipulation and comparison. -----------------
@JvmStatic
fun toCamelCase(parts: List<String>): String {
if (parts.isEmpty()) {
return ""
}
return parts[0] +
parts.drop(1).joinToString("") { capitalize(it) }
}
// Kotlin's capitalize function is deprecated, but the suggested replacement uses syntax that
// our minimum version doesn't support yet. Centralize the use to one place, so that when our
// minimum version does support the replacement we can replace by changing a single line.
@JvmStatic
@Suppress("DEPRECATION")
internal fun capitalize(string: String): String = string.capitalize()
@OptIn(ExperimentalStdlibApi::class)
internal fun lowercase(string: String): String = string.lowercase()
// compareTo implementation of version strings in the format of ints and periods
// Will not crash on RC candidate strings but considers all RC candidates the same version.
// Returns -1 if firstString < secondString, 0 if firstString == secondString, 1 if firstString > secondString
@JvmStatic
@JvmName("compareVersionStrings")
internal fun compareVersionStrings(
firstString: String,
secondString: String
): Int {
val firstVersion = firstString.split(".")
val secondVersion = secondString.split(".")
val commonIndices = minOf(firstVersion.size, secondVersion.size)
for (i in 0 until commonIndices) {
var firstAtIndex = firstVersion[i]
var secondAtIndex = secondVersion[i]
var firstInt = 0
var secondInt = 0
// Strip any chars after "-". For example "8.6-rc-2"
firstAtIndex = firstAtIndex.substringBefore("-")
try {
firstInt = firstAtIndex.toInt()
} catch (nfe: NumberFormatException) {
println(nfe)
}
secondAtIndex = secondAtIndex.substringBefore("-")
try {
secondInt = secondAtIndex.toInt()
} catch (nfe: NumberFormatException) {
println(nfe)
}
val comparisonResult = firstInt.compareTo(secondInt)
if (comparisonResult != 0) {
return comparisonResult
}
}
// If we got this far then all the common indices are identical, so whichever version is longer must be more recent
return firstVersion.size.compareTo(secondVersion.size)
}
@JvmStatic
@JvmName("formatPlatformString")
fun formatPlatformString(platform: String): String = FlutterPluginConstants.PLATFORM_ARCH_MAP[platform]!!.replace("-", "_")
@JvmStatic
@JvmName("readPropertiesIfExist")
internal fun readPropertiesIfExist(propertiesFile: File): Properties {
val result = Properties()
if (propertiesFile.exists()) {
propertiesFile
.reader(StandardCharsets.UTF_8)
.use { reader ->
// Use Kotlin's reader with UTF-8 and 'use' for auto-closing
result.load(reader)
}
}
return result
}
// ----------------- Methods that interact primarily with the Gradle project. -----------------
@JvmStatic
@JvmName("shouldShrinkResources")
fun shouldShrinkResources(project: Project): Boolean {
if (project.hasProperty(PROP_SHOULD_SHRINK_RESOURCES)) {
val propertyValue = project.property(PROP_SHOULD_SHRINK_RESOURCES)
return propertyValue.toString().toBoolean()
}
return true
}
// TODO(54566): Can remove this function and its call sites once resolved.
/**
* Returns the Gradle settings script for the build. When both Groovy and
* Kotlin variants exist, then Groovy (settings.gradle) is preferred over
* Kotlin (settings.gradle.kts). This is the same behavior as Gradle 8.5.
*/
@JvmStatic
@JvmName("getSettingsGradleFileFromProjectDir")
internal fun getSettingsGradleFileFromProjectDir(
projectDirectory: File,
logger: Logger
): File {
val settingsGradle = File(projectDirectory.parentFile, "settings.gradle")
val settingsGradleKts = File(projectDirectory.parentFile, "settings.gradle.kts")
if (settingsGradle.exists() && settingsGradleKts.exists()) {
logger.error(
"""
Both settings.gradle and settings.gradle.kts exist, so
settings.gradle.kts is ignored. This is likely a mistake.
""".trimIndent()
)
}
return if (settingsGradle.exists()) settingsGradle else settingsGradleKts
}
/**
* Returns the Gradle build script for the build. When both Groovy and
* Kotlin variants exist, then Groovy (build.gradle) is preferred over
* Kotlin (build.gradle.kts). This is the same behavior as Gradle 8.5.
*/
@JvmStatic
@JvmName("getBuildGradleFileFromProjectDir")
internal fun getBuildGradleFileFromProjectDir(
projectDirectory: File,
logger: Logger
): File {
val buildGradle = File(File(projectDirectory.parentFile, "app"), "build.gradle")
val buildGradleKts = File(File(projectDirectory.parentFile, "app"), "build.gradle.kts")
if (buildGradle.exists() && buildGradleKts.exists()) {
logger.error(
"""
Both build.gradle and build.gradle.kts exist, so
build.gradle.kts is ignored. This is likely a mistake.
""".trimIndent()
)
}
return if (buildGradle.exists()) buildGradle else buildGradleKts
}
@JvmStatic
@JvmName("shouldProjectSplitPerAbi")
internal fun shouldProjectSplitPerAbi(project: Project): Boolean =
project
.findProperty(
PROP_SPLIT_PER_ABI
)?.toString()
?.toBoolean() ?: false
@JvmStatic
@JvmName("shouldProjectUseLocalEngine")
internal fun shouldProjectUseLocalEngine(project: Project): Boolean = project.hasProperty(PROP_LOCAL_ENGINE_REPO)
@JvmStatic
@JvmName("isProjectVerbose")
internal fun isProjectVerbose(project: Project): Boolean = project.findProperty(PROP_IS_VERBOSE)?.toString()?.toBoolean() ?: false
/**
* Developers can set this value by passing `-P disable-abi-filtering=true`
* to flutter build. Where "disable-abi-filtering" comes from
* PROP_DISABLE_ABI_FILTERING.
*/
@JvmStatic
@JvmName("shouldProjectDisableAbiFiltering")
internal fun shouldProjectDisableAbiFiltering(project: Project): Boolean =
project.findProperty(PROP_DISABLE_ABI_FILTERING)?.toString()?.toBoolean() ?: false
/**
* Developers can set this value by passing `-P force-version-code-ignoring-abi=true`
* to flutter build. Where "force-version-code-ignoring-abi" comes from
* PROP_FORCE_VERSION_CODE_IGNORING_ABI.
*/
@JvmStatic
@JvmName("shouldForceVersionCodeIgnoringAbi")
internal fun shouldForceVersionCodeIgnoringAbi(project: Project): Boolean =
project.findProperty(PROP_FORCE_VERSION_CODE_IGNORING_ABI)?.toString()?.toBoolean() ?: false
/**
* TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560
*
* In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`.
* When building APKs, this causes an issue where building release requires the debug JAR,
* but Gradle won't build debug.
*
* To workaround this issue, only configure the JAR task that is required given the task
* from the command line.
*
* The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the
* time of adding this code. Once released, this can be removed. However, after updating to
* AGP/Gradle 7.2.0/7.5, removing this hack still causes build failures. Further
* investigation necessary to remove this.
*
* Tested cases:
* * `./gradlew assembleRelease`
* * `./gradlew app:assembleRelease.`
* * `./gradlew assemble{flavorName}Release`
* * `./gradlew app:assemble{flavorName}Release`
* * `./gradlew assemble.`
* * `./gradlew app:assemble.`
* * `./gradlew bundle.`
* * `./gradlew bundleRelease.`
* * `./gradlew app:bundleRelease.`
*
* Related issues:
* https://issuetracker.google.com/issues/158060799
* https://issuetracker.google.com/issues/158753935
*/
@JvmStatic
@JvmName("shouldConfigureFlutterTask")
internal fun shouldConfigureFlutterTask(
project: Project,
assembleTask: Task
): Boolean {
val cliTasksNames = project.gradle.startParameter.taskNames
if (cliTasksNames.size != 1 || !cliTasksNames.first().contains("assemble")) {
return true
}
val taskName = cliTasksNames.first().split(":").last()
if (taskName == "assemble") {
return true
}
if (taskName == assembleTask.name) {
return true
}
if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) {
return true
}
if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) {
return true
}
if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) {
return true
}
return false
}
private fun getFlutterExtensionOrNull(project: Project): FlutterExtension? = project.extensions.findByType(FlutterExtension::class.java)
/**
* Gets the directory that contains the Flutter source code.
* This is the directory containing the `android/` directory.
*/
@JvmStatic
@JvmName("getFlutterSourceDirectory")
internal fun getFlutterSourceDirectory(project: Project): File {
val flutterExtension = getFlutterExtensionOrNull(project)
// TODO(gmackall): clean up this NPE that is still around from the Groovy conversion.
if (flutterExtension!!.source == null) {
throw GradleException("Flutter source directory not set.")
}
return project.file(flutterExtension.source!!)
}
/**
* Gets the target file. This is typically `lib/main.dart`.
*
* Returns
* 1. the value of the `target` property, if it exists
* 2. the target value set in the FlutterExtension, if it exists
* 3. `lib/main.dart` otherwise
*/
@JvmStatic
@JvmName("getFlutterTarget")
internal fun getFlutterTarget(project: Project): String {
if (project.hasProperty(PROP_TARGET)) {
return project.property(PROP_TARGET).toString()
}
val target: String = getFlutterExtensionOrNull(project)!!.target ?: "lib/main.dart"
return target
}
@JvmStatic
@JvmName("isBuiltAsApp")
internal fun isBuiltAsApp(project: Project): Boolean {
// Projects are built as applications when the they use the `com.android.application`
// plugin.
return project.plugins.hasPlugin("com.android.application")
}
// Optional parameters don't work when Groovy makes calls into Kotlin, so provide an additional
// signature for the 3 argument version.
@JvmStatic
@JvmName("addApiDependencies")
internal fun addApiDependencies(
project: Project,
variantName: String,
dependency: Any
) {
addApiDependencies(project, variantName, dependency, null)
}
@JvmStatic
@JvmName("addApiDependencies")
internal fun addApiDependencies(
project: Project,
variantName: String,
dependency: Any,
config: Closure<Any>?
) {
var configuration: String
try {
project.configurations.named("api")
configuration = "${variantName}Api"
} catch (ignored: UnknownTaskException) {
// TODO(gmackall): The docs say the above should actually be an UnknownDomainObjectException.
configuration = "${variantName}Compile"
}
if (config == null) {
project.dependencies.add(
configuration,
dependency
)
} else {
project.dependencies.add(configuration, dependency, config)
}
}
/**
* Returns a Flutter build mode suitable for the specified Android buildType.
*
* @return "debug", "profile", or "release" (fall-back).
*/
@JvmStatic
@JvmName("buildModeFor")
internal fun buildModeFor(buildType: BuildType): String {
if (buildType.name == "profile") {
return "profile"
} else if (buildType.isDebuggable) {
return "debug"
}
return "release"
}
/**
* Returns true if the build mode is supported by the current call to Gradle.
* This only relevant when using a local engine. Because the engine
* is built for a specific mode, the call to Gradle must match that mode.
*/
@JvmStatic
@JvmName("supportsBuildMode")
internal fun supportsBuildMode(
project: Project,
flutterBuildMode: String
): Boolean {
if (!shouldProjectUseLocalEngine(project)) {
return true
}
check(project.hasProperty(PROP_LOCAL_ENGINE_BUILD_MODE)) { "Project must have property '$PROP_LOCAL_ENGINE_BUILD_MODE'" }
// Don't configure dependencies for a build mode that the local engine
// doesn't support.
return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode
}
/**
* Returns BaseExtension for the project. Used for compatibility.
*
* From BaseExtension docs:
* "Don't use this extension directly Instead, use one of the following:
* ApplicationExtension, LibraryExtension, TestExtension, DynamicFeatureExtension"
*
* For ApplicationExtension use `getAndroidApplicationExtension`.
* For LibraryExtension use `getAndroidLibraryExtension`.
*/
internal fun getLegacyAndroidExtension(project: Project): BaseExtension {
// Common supertype of the android extension types.
// But maybe this should be https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/TestedExtension.
return project.extensions.findByType(BaseExtension::class.java)!!
}
internal fun getAndroidExtension(project: Project): AgpCommonExtensionWrapper {
// Look up by name to completely avoid importing or resolving CommonExtension
val androidExtension =
project.extensions.findByName("android")
?: throw IllegalStateException("The Android plugin must be applied before accessing the Android extension.")
return AgpCommonExtensionWrapper(androidExtension)
}
internal fun getAndroidLibraryExtension(project: Project): LibraryExtension = project.extensions.getByType(LibraryExtension::class.java)
internal fun getAndroidApplicationExtension(project: Project): ApplicationExtension =
project.extensions.getByType(ApplicationExtension::class.java)
internal fun getConfiguredNdkVersion(project: Project): String? =
project.extensions.findByType(ApplicationExtension::class.java)?.ndkVersion
?: getLegacyAndroidExtension(project).ndkVersion
/**
* Expected format of getAndroidExtension(project).compileSdkVersion is a string of the form
* `android-` followed by either the numeric version, e.g. `android-35`, or a preview version,
* e.g. `android-UpsideDownCake`.
*/
@JvmStatic
@JvmName("getCompileSdkFromProject")
internal fun getCompileSdkFromProject(project: Project): String = getLegacyAndroidExtension(project).compileSdkVersion!!.substring(8)
/**
* Returns:
* The default platforms if the `target-platform` property is not set.
* The requested platforms after verifying they are supported by the Flutter plugin, otherwise.
* Throws a GradleException if any of the requested platforms are not supported.
*/
@JvmStatic
@JvmName("getTargetPlatforms")
internal fun getTargetPlatforms(project: Project): List<String> {
if (!project.hasProperty(PROP_TARGET_PLATFORM)) {
return FlutterPluginConstants.DEFAULT_PLATFORMS
}
val platformsString = project.property(PROP_TARGET_PLATFORM) as String
return platformsString.split(",").map { platform ->
if (!FlutterPluginConstants.PLATFORM_ARCH_MAP.containsKey(platform)) {
throw GradleException("Invalid platform: $platform")
}
platform
}
}
/** Prints error message for usage of KGP. */
@JvmStatic
@JvmName("detectApplyingKotlinGradlePlugin")
internal fun detectApplyingKotlinGradlePlugin(project: Project) {
val pluginsWithKGPAppliedList = mutableListOf<String>()
val agpVersion = VersionFetcher.getAGPVersion(project)
var shouldLogForApp = false
project.rootProject.subprojects {
val pluginState = getSubprojectPluginState(this) ?: return@subprojects
// Ensures applying AGP exists in the build file configuration.
if (!pluginState.hasAppPlugin && !pluginState.hasLibPlugin) return@subprojects
if (!isBuiltInKotlinEnabled(project, agpVersion) && !pluginState.hasKgpPlugin) {
try {
pluginManager.apply("kotlin-android")
} catch (_: Exception) {
logger.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()
)
}
}
// Apply AGP exists and Apply KGP also exists in build.gradle
if (pluginState.hasAppPlugin && pluginState.hasKgpPlugin) {
shouldLogForApp = true
}
if (pluginState.hasLibPlugin && pluginState.hasKgpPlugin) {
pluginsWithKGPAppliedList.add(name)
}
}
// If no imperative apply KGP declarations were found, there is nothing to log.
if (!shouldLogForApp && pluginsWithKGPAppliedList.isEmpty()) {
return
}
project.gradle.projectsEvaluated {
if (agpVersion == null || agpVersion.major < 9) {
return@projectsEvaluated
}
if (shouldLogForApp) {
project.logger.error(
"""
WARNING: Your Android app project: ${project.name} located at: ${project.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()
)
}
if (pluginsWithKGPAppliedList.isEmpty()) return@projectsEvaluated
project.logger.error(
"""
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): ${pluginsWithKGPAppliedList.joinToString()}
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()
)
}
}
/**
* 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 (`build.gradle` or `build.gradle.kts`) of Flutter Android app modules and Flutter plugin
* modules to detect declarations of Kotlin Gradle Plugin, Android Gradle Plugin (for applications),
* and Android Gradle Plugin (for libraries).
*
* This inspects build script files directly via regex rather than querying Gradle plugin
* state at runtime. Evaluating Kotlin Gradle Plugin dynamically at runtime to conditionally apply Kotlin Gradle Plugin
* during configuration leads to lifecycle and ordering issues (see https://github.com/gradle/gradle/issues/36953).
*
* 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)
}
/**
* 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")
internal fun detectLowCompileSdkVersionOrNdkVersion(
project: Project,
pluginList: List<Map<String?, Any?>>
) {
val validateTask =
project.tasks.register("validateCompileSdkVersion", ValidateCompileSdkVersionTask::class.java) {
this.projectDir.set(project.layout.projectDirectory)
}
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.finalizeDsl { _ ->
val projectAndroidExtension = getAndroidExtension(project)
validateTask.configure {
projectCompileSdk.set(projectAndroidExtension.compileSdk ?: Int.MAX_VALUE)
projectNdkVersion.set(projectAndroidExtension.ndkVersion)
val pluginSdksMap = mutableMapOf<String, Int>()
val pluginNdksMap = mutableMapOf<String, String>()
pluginList.forEach { plugin ->
val name = requireNotNull(plugin["name"] as? String) { "Missing valid \"name\" property for plugin object: $plugin" }
val pluginProject = project.rootProject.findProject(":$name")
if (pluginProject != null) {
val pluginAndroidExtension =
try {
getAndroidExtension(pluginProject)
} catch (e: IllegalStateException) {
null
}
pluginSdksMap[name] = pluginAndroidExtension?.compileSdk ?: Int.MAX_VALUE
val ndkVersion = pluginAndroidExtension?.ndkVersion ?: projectAndroidExtension.ndkVersion
if (ndkVersion != null) {
pluginNdksMap[name] = ndkVersion
}
}
}
pluginCompileSdks.set(pluginSdksMap)
pluginNdkVersions.set(pluginNdksMap)
}
}
// Wire the task to run before compilation.
project.tasks.named("preBuild").configure {
dependsOn(validateTask)
}
}
/**
* Forces the project to download the NDK by configuring properties that makes AGP think the
* project actually requires the NDK.
*/
@JvmStatic
@JvmName("forceNdkDownload")
internal fun forceNdkDownload(
gradleProject: Project,
flutterSdkRootPath: String
) {
if (isFlutterAppProject(gradleProject) && isInvokingMetadataNdkVersionTask(gradleProject)) {
return
}
// If the project is already configuring a native build, we don't need to do anything.
val gradleProjectAndroidExtension = getLegacyAndroidExtension(gradleProject)
val externalNativeBuild = gradleProjectAndroidExtension.externalNativeBuild
val forcingNotRequired: Boolean =
externalNativeBuild?.cmake?.path != null ||
externalNativeBuild?.ndkBuild?.path != null
if (forcingNotRequired) {
return
}
val toolNdkProvisioningProperties = getToolNdkProvisioningProperties(gradleProject)
if (toolNdkProvisioningProperties != null) {
val androidComponents =
gradleProject.extensions.findByType(AndroidComponentsExtension::class.java)
if (androidComponents == null) {
configureSyntheticExternalNativeBuildFallback(
gradleProject = gradleProject,
flutterSdkRootPath = flutterSdkRootPath
)
return
}
androidComponents.finalizeDsl { _ ->
if (gradleProjectAndroidExtension.externalNativeBuild.cmake.path != null) {
return@finalizeDsl
}
val configuredNdkVersion = getConfiguredNdkVersion(gradleProject)
if (
!configuredNdkVersion.isNullOrBlank() &&
toolNdkProvisioningProperties.installedNdkVersions.contains(
configuredNdkVersion
)
) {
return@finalizeDsl
}
if (
toolNdkProvisioningProperties.sdkManagerPath == null ||
gradleProject.gradle.startParameter.isOffline
) {
configureSyntheticExternalNativeBuildFallback(
gradleProject = gradleProject,
flutterSdkRootPath = flutterSdkRootPath
)
return@finalizeDsl
}
val handledByToolProvisioning =
maybeHandleToolNdkProvisioning(
gradleProject = gradleProject,
toolNdkProvisioningProperties = toolNdkProvisioningProperties
)
if (!handledByToolProvisioning) {
configureSyntheticExternalNativeBuildFallback(
gradleProject = gradleProject,
flutterSdkRootPath = flutterSdkRootPath
)
}
}
return
}
configureSyntheticExternalNativeBuildFallback(
gradleProject = gradleProject,
flutterSdkRootPath = flutterSdkRootPath
)
}
private fun getToolNdkProvisioningProperties(project: Project): ToolNdkProvisioningProperties? {
val androidSdkRoot = project.findProperty(PROP_ANDROID_SDK_ROOT)?.toString() ?: return null
val installedNdkVersions =
project
.findProperty(PROP_INSTALLED_NDK_VERSIONS)
?.toString()
?.split(",")
?.map(String::trim)
?.filter(String::isNotEmpty)
?.toSet() ?: return null
val sdkManagerPath = project.findProperty(PROP_SDK_MANAGER_PATH)?.toString()
return ToolNdkProvisioningProperties(
androidSdkRoot = androidSdkRoot,
installedNdkVersions = installedNdkVersions,
sdkManagerPath = sdkManagerPath
)
}
private fun maybeHandleToolNdkProvisioning(
gradleProject: Project,
toolNdkProvisioningProperties: ToolNdkProvisioningProperties
): Boolean {
val configuredNdkVersion = getConfiguredNdkVersion(gradleProject)
if (configuredNdkVersion.isNullOrBlank()) {
return false
}
if (toolNdkProvisioningProperties.installedNdkVersions.contains(configuredNdkVersion)) {
return true
}
val sdkManagerPath = toolNdkProvisioningProperties.sdkManagerPath ?: return false
val execOps = gradleProject.serviceOf<ExecOperations>()
execOps
.exec {
commandLine(
listOf(
sdkManagerPath,
"--sdk_root=${toolNdkProvisioningProperties.androidSdkRoot}",
"--install",
"ndk;$configuredNdkVersion"
)
)
}.assertNormalExitValue()
val installedNdkMarker =
File(
toolNdkProvisioningProperties.androidSdkRoot,
"ndk/$configuredNdkVersion/source.properties"
)
if (!installedNdkMarker.exists()) {
throw GradleException(
"Android sdkmanager did not install NDK $configuredNdkVersion into ${toolNdkProvisioningProperties.androidSdkRoot}."
)
}
return true
}
private fun configureSyntheticExternalNativeBuildFallback(
gradleProject: Project,
flutterSdkRootPath: String
) {
val gradleProjectAndroidExtension = getLegacyAndroidExtension(gradleProject)
gradleProjectAndroidExtension.externalNativeBuild.cmake.path(
"$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt"
)
// AGP defaults to outputting build artifacts in `android/app/.cxx`. This directory is a
// build artifact, so we move it from that directory to within Flutter's build directory
// to avoid polluting source directories with build artifacts.
//
// AGP explicitly recommends not setting the buildStagingDirectory to be within a build
// directory in
// https://developer.android.com/reference/tools/gradle-api/8.3/null/com/android/build/api/dsl/Cmake#buildStagingDirectory(kotlin.Any),
// but as we are not actually building anything (and are instead only tricking AGP into
// downloading the NDK), it is acceptable for the buildStagingDirectory to be removed
// and rebuilt when running clean builds.
gradleProjectAndroidExtension.externalNativeBuild.cmake.buildStagingDirectory(
gradleProject.layout.buildDirectory
.dir("../.cxx")
.get()
.asFile.path
)
// CMake will print warnings when you try to build an empty project.
// These arguments silence the warnings - our project is intentionally
// empty.
gradleProjectAndroidExtension.buildTypes.forEach { buildType ->
buildType.externalNativeBuild.cmake.arguments(
"-Wno-dev",
"--no-warn-unused-cli",
"-DCMAKE_BUILD_TYPE=${buildType.name}"
)
}
}
@JvmStatic
@JvmName("isInvokingMetadataNdkVersionTask")
internal fun isInvokingMetadataNdkVersionTask(project: Project): Boolean =
project.gradle.startParameter.taskNames.any { taskName ->
taskName == TASK_PRINT_NDK_VERSION || taskName.endsWith(":$TASK_PRINT_NDK_VERSION")
}
@JvmStatic
@JvmName("isFlutterAppProject")
internal fun isFlutterAppProject(project: Project): Boolean =
project.extensions.findByType(
ApplicationExtension::class.java
) != null
/**
* Ensures that the dependencies required by the Flutter project are available.
* This includes:
* 1. The embedding
* 2. libflutter.so
*
* Should only be called on the main gradle [Project] for this application
* of the [FlutterPlugin].
*/
@JvmStatic
@JvmName("addFlutterDependencies")
internal fun addFlutterDependencies(
project: Project,
buildType: BuildType,
pluginHandler: PluginHandler,
engineVersion: String
) {
val flutterBuildMode: String = buildModeFor(buildType)
if (!supportsBuildMode(project, flutterBuildMode)) {
project.logger.quiet(
"Project does not support Flutter build mode: $flutterBuildMode, " +
"skipping adding Flutter dependencies"
)
return
}
// The embedding is set as an API dependency in a Flutter plugin.
// Therefore, don't make the app project depend on the embedding if there are Flutter
// plugin dependencies. In release mode, dev dependencies are stripped, so we do not
// consider those in the check.
// This prevents duplicated classes when using custom build types. That is, a custom build
// type like profile is used, and the plugin and app projects have API dependencies on the
// embedding.
val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List<Map<String?, Any?>> =
if (flutterBuildMode == "release") {
pluginHandler.getPluginListWithoutDevDependencies()
} else {
pluginHandler.getPluginList()
}
if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) {
addApiDependencies(
project,
buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
)
}
val platforms: List<String> = getTargetPlatforms(project)
platforms.forEach { platform ->
val arch: String = formatPlatformString(platform)
// Add the `libflutter.so` dependency.
addApiDependencies(
project,
buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion"
)
}
}
// ------------------ Task adders (a subset of the above category)
// Add a task that can be called on Flutter projects that prints the Java version used in Gradle.
//
// Format of the output of this task can be used in debugging what version of Java Gradle is using.
// Not recommended for use in time sensitive commands like `flutter run` or `flutter build` as
// Gradle is slower than we want. Particularly in light of https://github.com/flutter/flutter/issues/119196.
@JvmStatic
@JvmName("addTaskForJavaVersion")
internal fun addTaskForJavaVersion(project: Project) {
project.tasks.register("javaVersion", PrintTask::class.java) {
description = "Print the current java version used by gradle. see: " +
"https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
message.set(VersionFetcher.getJavaVersion().toString())
}
}
// Add a task that can be called on Flutter projects that prints the KGP version used in
// the project.
//
// Format of the output of this task can be used in debugging what version of KGP a
// project is using.
// Not recommended for use in time sensitive commands like `flutter run` or `flutter build` as
// Gradle tasks are slower than we want. Particularly in light of https://github.com/flutter/flutter/issues/119196.
@JvmStatic
@JvmName("addTaskForKGPVersion")
internal fun addTaskForKGPVersion(project: Project) {
project.tasks.register("kgpVersion", PrintTask::class.java) {
description = "Print the current kgp version used by the project."
val version = VersionFetcher.getKGPVersion(project)?.toString() ?: "null"
message.set("KGP Version: $version")
}
}
// Add a task that can be called on Flutter projects that prints the available build variants
// in Gradle.
//
// This task prints variants in this format:
//
// BuildVariant: debug
// BuildVariant: release
// BuildVariant: profile
//
// Format of the output of this task is used by `AndroidProject.getBuildVariants`.
@JvmStatic
@JvmName("addTaskForPrintBuildVariants")
internal fun addTaskForPrintBuildVariants(project: Project) {
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
val variantsList = project.objects.listProperty(String::class.java)
// Collect variant names during configuration phase to avoid lifecycle violations
androidComponents.onVariants { variant ->
variantsList.add(variant.name)
}
project.tasks.register("printBuildVariants", PrintTask::class.java) {
description = "Prints out all build variants for this Android project"
message.set(variantsList.map { list -> list.joinToString("\n") { name -> "BuildVariant: $name" } })
}
}
// Add a task that can be called on Flutter projects that prints the effective ndkVersion
// configured for the Android app.
//
// This task prints the version in this format:
//
// NdkVersion: 28.2.13676358
//
// Format of the output of this task is kept for diagnostics and targeted testing.
@JvmStatic
@JvmName("addTaskForPrintNdkVersion")
internal fun addTaskForPrintNdkVersion(project: Project) {
project.tasks.register(TASK_PRINT_NDK_VERSION, PrintTask::class.java) {
description = "Prints out the configured ndkVersion for this Android project"
message.set(
project.provider {
"$NDK_VERSION_OUTPUT_PREFIX${getConfiguredNdkVersion(project)}"
}
)
}
}
/**
* Adds required tasks for the AppLinkSettings feature.
*
* Should only be called if the build target is an app, as opposed to an aar/module.
*
* Add a task that can be called on Flutter projects that outputs app link related project
* settings into a json file.
* See https://developer.android.com/training/app-links/ for more information about app link.
* The json will be saved in path stored in "outputPath" parameter or in the projects build
* directory with the file deeplink.json if not specified.
*
* See DeepLinkJsonFromManifestTask for the structure of the json.
*
* The output file is parsed and used by devtool.
*/
@JvmStatic
@JvmName("addTasksForOutputsAppLinkSettings")
internal fun addTasksForOutputsAppLinkSettings(project: Project) {
// Integration test for AppLinkSettings task defined in
// flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val manifestUpdater =
project.tasks.register("output${capitalize(variant.name)}AppLinkSettings", DeepLinkJsonFromManifestTask::class.java) {
namespace.set(variant.namespace)
// Flutter should always use project.layout.buildDirectory.file("deeplink.json")
// instead of relying on passing in a path.
if (project.hasProperty("outputPath")) {
deepLinkJson.set(
File(project.property("outputPath").toString())
)
} else {
deepLinkJson.set(project.layout.buildDirectory.file("deeplink.json"))
}
}
// This task does not modify the manifest despite using an api
// designed for modification. The task is responsible for an exact copy of the input
// manifest being used for the output manifest.
variant.artifacts
.use(manifestUpdater)
.wiredWithFiles(
DeepLinkJsonFromManifestTask::manifestFile,
DeepLinkJsonFromManifestTask::updatedManifest
).toTransform(SingleArtifact.MERGED_MANIFEST) // (3) Indicate the artifact and operation type.
}
}
}