forked from firka/flutter
This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? There was no associated issue, but the problem is that https://github.com/flutter/flutter/pull/166727 broke Flutter Android builds on apps that use plugins with AGP versions less than 8.2. ### Changelog Description: Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples Fixes Flutter Android builds for apps which use plugins with old Android Gradle Plugin versions. ### Impact Description: What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch) The impact is a crash in the build process. ### Workaround: Is there a workaround for this issue? No ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? Build an app that uses AGP < 8.2.
968 lines
41 KiB
Kotlin
968 lines
41 KiB
Kotlin
// 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.gradle.AbstractAppExtension
|
|
import com.android.build.gradle.BaseExtension
|
|
import com.android.build.gradle.tasks.ProcessAndroidResources
|
|
import com.android.builder.model.BuildType
|
|
import com.flutter.gradle.plugins.PluginHandler
|
|
import groovy.lang.Closure
|
|
import groovy.util.Node
|
|
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 {
|
|
private const val MANIFEST_NAME_KEY = "android:name"
|
|
private const val MANIFEST_VALUE_KEY = "android:value"
|
|
private const val MANIFEST_VALUE_TRUE = "true"
|
|
|
|
// 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()
|
|
|
|
// Kotlin's toLowerCase function is deprecated, but the suggested replacement is not supported
|
|
// by the minimum version of Kotlin that we support. Centralize the use to one place, so that
|
|
// when our minimum version does support the replacement we can replace by changing a single
|
|
// line.
|
|
@Suppress("DEPRECATION")
|
|
internal fun lowercase(string: String): String = string.toLowerCase()
|
|
|
|
// 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
|
|
|
|
/** 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
|
|
}
|
|
|
|
internal 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)!!
|
|
}
|
|
|
|
// TODO: Use find by type and AbstractAppExtension instead. Or delete in favor of getAndroidExtension.
|
|
// see https://github.com/flutter/flutter/issues/165882
|
|
private fun getAndroidAppExtensionOrNull(project: Project): AbstractAppExtension? =
|
|
project.extensions.findByName("android") as? AbstractAppExtension
|
|
|
|
/**
|
|
* 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"
|
|
|
|
// TODO(gmackall): We can remove this elvis when our minimum AGP is >= 8.2.
|
|
// This value (ndkVersion) is nullable on AGP versions below that.
|
|
// See https://developer.android.com/reference/tools/gradle-api/8.1/com/android/build/api/dsl/CommonExtension#ndkVersion().
|
|
@Suppress("USELESS_ELVIS")
|
|
val projectNdkVersion: String =
|
|
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()
|
|
)
|
|
)
|
|
}
|
|
|
|
// TODO(gmackall): We can remove this elvis when our minimum AGP is >= 8.2.
|
|
// This value (ndkVersion) is nullable on AGP versions below that.
|
|
// See https://developer.android.com/reference/tools/gradle-api/8.1/com/android/build/api/dsl/CommonExtension#ndkVersion().
|
|
@Suppress("USELESS_ELVIS")
|
|
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/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 explicitely 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.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,
|
|
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") {
|
|
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}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(gmackall): Migrate to AGPs variant api.
|
|
// https://github.com/flutter/flutter/issues/166550
|
|
@Suppress("DEPRECATION")
|
|
private fun findProcessResources(baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput): ProcessAndroidResources =
|
|
baseVariantOutput.processResourcesProvider?.get() ?: baseVariantOutput.processResources
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* An example json:
|
|
* {
|
|
* applicationId: "com.example.app",
|
|
* deeplinks: [
|
|
* {"scheme":"http", "host":"example.com", "path":".*"},
|
|
* {"scheme":"https","host":"example.com","path":".*"}
|
|
* ]
|
|
* }
|
|
* 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 android = getAndroidAppExtensionOrNull(project)
|
|
if (android == null) {
|
|
project.logger.info("addTasksForOutputsAppLinkSettings called on project without android extension.")
|
|
return
|
|
}
|
|
android.applicationVariants.configureEach {
|
|
val variant = this
|
|
project.tasks.register("output${capitalize(variant.name)}AppLinkSettings") {
|
|
val task: Task = this
|
|
task.description =
|
|
"stores app links settings for the given build variant of this Android project into a json file."
|
|
variant.outputs.configureEach {
|
|
// TODO(gmackall): Migrate to AGPs variant api.
|
|
// https://github.com/flutter/flutter/issues/166550
|
|
@Suppress("DEPRECATION")
|
|
val baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput = this
|
|
// Deeplinks are defined in AndroidManifest.xml and is only available after
|
|
// processResourcesProvider.
|
|
dependsOn(findProcessResources(baseVariantOutput))
|
|
}
|
|
doLast {
|
|
// We are configuring the same object before a doLast and in a doLast.
|
|
// without a clear reason why. That is not good.
|
|
variant.outputs.configureEach {
|
|
val appLinkSettings = createAppLinkSettings(variant, this)
|
|
File(project.property("outputPath").toString()).writeText(
|
|
appLinkSettings.toJson().toString()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts app deeplink information from the Android manifest file of a variant then returns
|
|
* an AppLinkSettings object.
|
|
*
|
|
* @param BaseVariantOutput The output of a specific build variant (e.g., debug, release).
|
|
* @param variant The application variant being processed.
|
|
*/
|
|
@Suppress("KDocUnresolvedReference")
|
|
private fun createAppLinkSettings(
|
|
// TODO(gmackall): Migrate to AGPs variant api.
|
|
// https://github.com/flutter/flutter/issues/166550
|
|
@Suppress("DEPRECATION") variant: com.android.build.gradle.api.ApplicationVariant,
|
|
@Suppress("DEPRECATION") baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput
|
|
): AppLinkSettings {
|
|
val appLinkSettings = AppLinkSettings(variant.applicationId)
|
|
|
|
// XmlParser is not namespace aware because it makes querying nodes cumbersome.
|
|
// TODO(gmackall): Migrate to AGPs variant api.
|
|
// https://github.com/flutter/flutter/issues/166550
|
|
@Suppress("DEPRECATION")
|
|
val manifest: Node =
|
|
groovy.xml.XmlParser(false, false).parse(findProcessResources(baseVariantOutput).manifestFile)
|
|
val applicationNode: Node? =
|
|
manifest.children().find { node ->
|
|
node is Node && node.name() == "application"
|
|
} as Node?
|
|
if (applicationNode == null) {
|
|
return appLinkSettings
|
|
}
|
|
val activities: List<Node> =
|
|
applicationNode.children().filterIsInstance<Node>().filter { item ->
|
|
item.name() == "activity"
|
|
}
|
|
|
|
activities.forEach { activity ->
|
|
val metaDataItems: List<Node> =
|
|
activity.children().filterIsInstance<Node>().filter { metaItem ->
|
|
metaItem.name() == "meta-data"
|
|
}
|
|
metaDataItems.forEach { metaDataItem ->
|
|
val nameAttribute: Boolean =
|
|
metaDataItem.attribute(MANIFEST_NAME_KEY) == "flutter_deeplinking_enabled"
|
|
val valueAttribute: Boolean =
|
|
metaDataItem.attribute(MANIFEST_VALUE_KEY) == MANIFEST_VALUE_TRUE
|
|
if (nameAttribute && valueAttribute) {
|
|
appLinkSettings.deeplinkingFlagEnabled = true
|
|
}
|
|
}
|
|
val intentFilterItems: List<Node> =
|
|
activity.children().filterIsInstance<Node>().filter { filterItem ->
|
|
filterItem.name() == "intent-filter"
|
|
}
|
|
intentFilterItems.forEach { appLinkIntent ->
|
|
// Print out the host attributes in data tags.
|
|
val schemes: MutableSet<String?> = mutableSetOf()
|
|
val hosts: MutableSet<String?> = mutableSetOf()
|
|
val paths: MutableSet<String?> = mutableSetOf()
|
|
val intentFilterCheck = IntentFilterCheck()
|
|
if (appLinkIntent.attribute("android:autoVerify") == MANIFEST_VALUE_TRUE) {
|
|
intentFilterCheck.hasAutoVerify = true
|
|
}
|
|
|
|
val actionItems: List<Node> =
|
|
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
|
|
item.name() == "action"
|
|
}
|
|
// Any action item causes intentFilterCheck to always be true
|
|
// and we keep looping instead of exiting out early.
|
|
// TODO: Exit out early per intent filter action view.
|
|
actionItems.forEach { action ->
|
|
if (action.attribute(MANIFEST_NAME_KEY) == "android.intent.action.VIEW") {
|
|
intentFilterCheck.hasActionView = true
|
|
}
|
|
}
|
|
val categoryItems: List<Node> =
|
|
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
|
|
item.name() == "category"
|
|
}
|
|
categoryItems.forEach { category ->
|
|
// TODO: Exit out early per intent filter default category.
|
|
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.DEFAULT") {
|
|
intentFilterCheck.hasDefaultCategory = true
|
|
}
|
|
// TODO: Exit out early per intent filter browsable category.
|
|
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.BROWSABLE") {
|
|
intentFilterCheck.hasBrowsableCategory =
|
|
true
|
|
}
|
|
}
|
|
val dataItems: List<Node> =
|
|
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
|
|
item.name() == "data"
|
|
}
|
|
dataItems.forEach { data ->
|
|
data.attributes().forEach { entry ->
|
|
when (entry.key) {
|
|
"android:scheme" -> schemes.add(entry.value.toString())
|
|
"android:host" -> hosts.add(entry.value.toString())
|
|
// All path patterns add to paths.
|
|
"android:pathAdvancedPattern" ->
|
|
paths.add(
|
|
entry.value.toString()
|
|
)
|
|
|
|
"android:pathPattern" -> paths.add(entry.value.toString())
|
|
"android:path" -> paths.add(entry.value.toString())
|
|
"android:pathPrefix" -> paths.add(entry.value.toString() + ".*")
|
|
"android:pathSuffix" -> paths.add(".*" + entry.value.toString())
|
|
}
|
|
}
|
|
}
|
|
if (hosts.isNotEmpty() || paths.isNotEmpty()) {
|
|
if (schemes.isEmpty()) {
|
|
schemes.add(null)
|
|
}
|
|
if (hosts.isEmpty()) {
|
|
hosts.add(null)
|
|
}
|
|
if (paths.isEmpty()) {
|
|
paths.add(".*")
|
|
}
|
|
// Sets are not ordered this could produce a bug.
|
|
schemes.forEach { scheme ->
|
|
hosts.forEach { host ->
|
|
paths.forEach { path ->
|
|
appLinkSettings.deeplinks.add(
|
|
Deeplink(
|
|
scheme,
|
|
host,
|
|
path,
|
|
intentFilterCheck
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return appLinkSettings
|
|
}
|
|
}
|
|
|
|
private data class PluginVersionPair(
|
|
val name: String,
|
|
val version: String
|
|
)
|