flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
Gray Mackall c9667e07ac
More FlutterPlugin static method conversion (#165506)
Moves a lot more functionality from `flutter.groovy` to
`FlutterPluginUtils.kt` that could be made static.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Gray Mackall <mackall@google.com>
2025-03-22 01:34:05 +00:00

858 lines
35 KiB
Kotlin

package com.flutter.gradle
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.builder.model.BuildType
import groovy.lang.Closure
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.logging.Logger
import java.io.File
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_IS_FAST_START = "fast-start"
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"
// ----------------- 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()
// 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 `true` if the given project is a plugin project having an `android` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
@JvmStatic
@JvmName("pluginSupportsAndroidPlatform")
internal fun pluginSupportsAndroidPlatform(project: Project): Boolean {
val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle")
val buildGradleKts =
File(File(project.projectDir.parentFile, "android"), "build.gradle.kts")
return buildGradle.exists() || buildGradleKts.exists()
}
/**
* 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
/** Whether to build the debug app in "fast-start" mode. */
@JvmStatic
@JvmName("isProjectFastStart")
internal fun isProjectFastStart(project: Project): Boolean =
project
.findProperty(
PROP_IS_FAST_START
)?.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
}
private fun getAndroidExtension(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)!!
}
/**
* 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 = getAndroidExtension(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
}
}
private fun logPluginCompileSdkWarnings(
maxPluginCompileSdkVersion: Int,
projectCompileSdkVersion: Int,
logger: Logger,
pluginsWithHigherSdkVersion: List<PluginVersionPair>,
projectDirectory: File
) {
logger.error(
"Your project is configured to compile against Android SDK $projectCompileSdkVersion, but the following plugin(s) require to be compiled against a higher Android SDK version:"
)
for (pluginToCompileSdkVersion in pluginsWithHigherSdkVersion) {
logger.error(
"- ${pluginToCompileSdkVersion.name} compiles against Android SDK ${pluginToCompileSdkVersion.version}"
)
}
val buildGradleFile =
getBuildGradleFileFromProjectDir(
projectDirectory,
logger
)
logger.error(
"""
Fix this issue by compiling against the highest Android SDK version (they are backward compatible).
Add the following to ${buildGradleFile.path}:
android {
compileSdk = $maxPluginCompileSdkVersion
...
}
""".trimIndent()
)
}
private fun logPluginNdkWarnings(
maxPluginNdkVersion: String,
projectNdkVersion: String,
logger: Logger,
pluginsWithDifferentNdkVersion: List<PluginVersionPair>,
projectDirectory: File
) {
logger.error(
"Your project is configured with Android NDK $projectNdkVersion, but the following plugin(s) depend on a different Android NDK version:"
)
for (pluginToNdkVersion in pluginsWithDifferentNdkVersion) {
logger.error("- ${pluginToNdkVersion.name} requires Android NDK ${pluginToNdkVersion.version}")
}
val buildGradleFile =
getBuildGradleFileFromProjectDir(
projectDirectory,
logger
)
logger.error(
"""
Fix this issue by using the highest Android NDK version (they are backward compatible).
Add the following to ${buildGradleFile.path}:
android {
ndkVersion = "$maxPluginNdkVersion"
...
}
""".trimIndent()
)
}
/** 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?>>
) {
project.afterEvaluate {
// getCompileSdkFromProject returns a string if the project uses a preview compileSdkVersion
// so default to Int.MAX_VALUE in that case.
val projectCompileSdkVersion: Int =
getCompileSdkFromProject(project).toIntOrNull() ?: Int.MAX_VALUE
var maxPluginCompileSdkVersion = projectCompileSdkVersion
// TODO(gmackall): This should be updated to reflect newer templates.
// The default for AGP 4.1.0 used in old templates.
val ndkVersionIfUnspecified = "21.1.6352462"
val projectNdkVersion =
getAndroidExtension(project).ndkVersion ?: ndkVersionIfUnspecified
var maxPluginNdkVersion = projectNdkVersion
var numProcessedPlugins = pluginList.size
val pluginsWithHigherSdkVersion = mutableListOf<PluginVersionPair>()
val pluginsWithDifferentNdkVersion = mutableListOf<PluginVersionPair>()
pluginList.forEach { pluginObject ->
val pluginName: String =
requireNotNull(
pluginObject["name"] as? String
) { "Missing valid \"name\" property for plugin object: $pluginObject" }
val pluginProject: Project =
project.rootProject.findProject(":$pluginName") ?: return@forEach
pluginProject.afterEvaluate {
val pluginCompileSdkVersion: Int =
getCompileSdkFromProject(pluginProject).toIntOrNull() ?: Int.MAX_VALUE
maxPluginCompileSdkVersion =
maxOf(maxPluginCompileSdkVersion, pluginCompileSdkVersion)
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
pluginsWithHigherSdkVersion.add(
PluginVersionPair(
pluginName,
pluginCompileSdkVersion.toString()
)
)
}
val pluginNdkVersion: String =
getAndroidExtension(pluginProject).ndkVersion ?: ndkVersionIfUnspecified
maxPluginNdkVersion =
VersionUtils.mostRecentSemanticVersion(
pluginNdkVersion,
maxPluginNdkVersion
)
if (pluginNdkVersion != projectNdkVersion) {
pluginsWithDifferentNdkVersion.add(PluginVersionPair(pluginName, pluginNdkVersion))
}
numProcessedPlugins--
if (numProcessedPlugins == 0) {
if (maxPluginCompileSdkVersion > projectCompileSdkVersion) {
logPluginCompileSdkWarnings(
maxPluginCompileSdkVersion = maxPluginCompileSdkVersion,
projectCompileSdkVersion = projectCompileSdkVersion,
logger = project.logger,
pluginsWithHigherSdkVersion = pluginsWithHigherSdkVersion,
projectDirectory = project.projectDir
)
}
if (maxPluginNdkVersion != projectNdkVersion) {
logPluginNdkWarnings(
maxPluginNdkVersion = maxPluginNdkVersion,
projectNdkVersion = projectNdkVersion,
logger = project.logger,
pluginsWithDifferentNdkVersion = pluginsWithDifferentNdkVersion,
projectDirectory = project.projectDir
)
}
}
}
}
}
}
/**
* 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 the project is already configuring a native build, we don't need to do anything.
val gradleProjectAndroidExtension = getAndroidExtension(gradleProject)
val forcingNotRequired: Boolean =
gradleProjectAndroidExtension.externalNativeBuild.cmake.path != null
if (forcingNotRequired) {
return
}
// Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings.
gradleProjectAndroidExtension.externalNativeBuild.cmake.path(
"$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt"
)
// CMake will print warnings when you try to build an empty project.
// These arguments silence the warnings - our project is intentionally
// empty.
gradleProjectAndroidExtension.defaultConfig.externalNativeBuild.cmake
.arguments("-Wno-dev", "--no-warn-unused-cli")
}
@JvmStatic
@JvmName("isFlutterAppProject")
internal fun isFlutterAppProject(project: Project): Boolean = project.extensions.findByType(AbstractAppExtension::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,
pluginList: List<Map<String?, Any?>>,
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") {
getPluginListWithoutDevDependencies(
pluginList
)
} else {
pluginList
}
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"
)
}
}
/**
* Gets the list of plugins (as map) that support the Android platform and are dependencies of the
* Android project excluding dev dependencies.
*
* The map value contains either the plugins `name` (String),
* its `path` (String), or its `dependencies` (List<String>).
* See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
*/
private fun getPluginListWithoutDevDependencies(pluginList: List<Map<String?, Any?>>): List<Map<String?, Any?>> =
pluginList.filter { pluginObject -> pluginObject["dev_dependency"] == false }
/**
* Add the dependencies on other plugin projects to the plugin project.
* A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
* making the Gradle plugin project A depend on the Gradle plugin project B.
*/
@JvmStatic
@JvmName("configurePluginDependencies")
internal fun configurePluginDependencies(
project: Project,
pluginObject: Map<String?, Any?>
) {
val pluginName: String =
requireNotNull(pluginObject["name"] as? String) {
"Missing valid \"name\" property for plugin object: $pluginObject"
}
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
getAndroidExtension(project).buildTypes.forEach { buildType ->
val flutterBuildMode: String = buildModeFor(buildType)
if (flutterBuildMode == "release" && (pluginObject["dev_dependency"] as? Boolean == true)) {
// This plugin is a dev dependency will not be included in the
// release build, so no need to add its dependencies.
return@forEach
}
val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>)
dependencies.forEach innerForEach@{ pluginDependencyName ->
check(pluginDependencyName is String)
if (pluginDependencyName.isEmpty()) {
return@innerForEach
}
val dependencyProject =
project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach
pluginProject.afterEvaluate {
pluginProject.dependencies.add("implementation", dependencyProject)
}
}
}
}
/**
* Performs configuration related to the plugin's Gradle [Project], including
* 1. Adding the plugin itself as a dependency to the main project.
* 2. Adding the main project's build types to the plugin's build types.
* 3. Adding a dependency on the Flutter embedding to the plugin.
*
* Should only be called on plugins that support the Android platform.
*/
@JvmStatic
@JvmName("configurePluginProject")
internal fun configurePluginProject(
project: Project,
pluginObject: Map<String?, Any?>,
engineVersion: String
) {
// TODO(gmackall): should guard this with a pluginObject.contains().
val pluginName =
requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" }
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
// Apply the "flutter" Gradle extension to plugins so that they can use it's vended
// compile/target/min sdk values.
pluginProject.extensions.create("flutter", FlutterExtension::class.java)
// Add plugin dependency to the app project. We only want to add dependency
// for dev dependencies in non-release builds.
project.afterEvaluate {
getAndroidExtension(project).buildTypes.forEach { buildType ->
if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") {
project.dependencies.add("${buildType.name}Api", pluginProject)
}
}
}
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
val projectCompileSdkVersion: String = getCompileSdkFromProject(project)
val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject)
// TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong
// when comparing preview versions (against non preview, and also in the
// case of alphabet reset which happened with "Baklava".
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
project.logger.quiet("Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher.")
project.logger.quiet(
"For more information about build configuration, see ${FlutterPluginConstants.WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}."
)
}
getAndroidExtension(project).buildTypes.forEach { buildType ->
addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion)
}
}
}
private fun addEmbeddingDependencyToPlugin(
project: Project,
pluginProject: Project,
buildType: BuildType,
engineVersion: String
) {
val flutterBuildMode: String = buildModeFor(buildType)
// TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above
// 3.5. We should try to remove it.
// In AGP 3.5, the embedding must be added as an API implementation,
// so java8 features are desugared against the runtime classpath.
// For more, see https://github.com/flutter/flutter/issues/40126
if (!supportsBuildMode(pluginProject, flutterBuildMode)) {
return
}
if (!pluginProject.hasProperty("android")) {
return
}
// Copy build types from the app to the plugin.
// This allows to build apps with plugins and custom build types or flavors.
getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes)
// The embedding is API dependency of the plugin, so the AGP is able to desugar
// default method implementations when the interface is implemented by a plugin.
//
// See https://issuetracker.google.com/139821726, and
// https://github.com/flutter/flutter/issues/72185 for more details.
addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$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") {
description = "Print the current java version used by gradle. see: " +
"https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
doLast {
println(JavaVersion.current())
}
}
}
// 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) {
// Groovy was dynamically getting a different subtype here than our Kotlin getAndroidExtension method.
// TODO(gmackall): We should take another pass at the different types we are using in our conversion of
// the groovy `flutter.android` lines.
val androidExtension = project.extensions.getByType(AbstractAppExtension::class.java)
project.tasks.register("printBuildVariants") {
description = "Prints out all build variants for this Android project"
doLast {
androidExtension.applicationVariants.forEach { variant ->
println("BuildVariant: ${variant.name}")
}
}
}
}
}
private data class PluginVersionPair(
val name: String,
val version: String
)