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>
This commit is contained in:
Gray Mackall 2025-03-21 18:34:05 -07:00 committed by GitHub
parent 8c5bbf2931
commit c9667e07ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1281 additions and 346 deletions

View File

@ -5,6 +5,7 @@
import com.android.build.OutputFile
import com.android.build.gradle.AbstractAppExtension
import com.android.tools.r8.P
import com.flutter.gradle.AppLinkSettings
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.PackageAndroidArtifact
@ -72,7 +73,7 @@ class FlutterPlugin implements Plugin<Project> {
this.project = project
Project rootProject = project.rootProject
if (isFlutterAppProject()) {
if (FlutterPluginUtils.isFlutterAppProject(project)) {
rootProject.tasks.register("generateLockfiles") {
doLast {
rootProject.subprojects.each { subproject ->
@ -135,7 +136,7 @@ class FlutterPlugin implements Plugin<Project> {
extension.flutterVersionName = localProperties.getProperty("flutter.versionName", "1.0")
this.addFlutterTasks(project)
forceNdkDownload(project, flutterRootPath)
FlutterPluginUtils.forceNdkDownload(project, flutterRootPath)
// By default, assembling APKs generates fat APKs if multiple platforms are passed.
// Configuring split per ABI allows to generate separate APKs for each abi.
@ -162,7 +163,7 @@ class FlutterPlugin implements Plugin<Project> {
}
}
getTargetPlatforms().each { targetArch ->
FlutterPluginUtils.getTargetPlatforms(project).each { targetArch ->
String abiValue = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
project.android {
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
@ -256,52 +257,6 @@ class FlutterPlugin implements Plugin<Project> {
project.android.buildTypes.all(this.&addFlutterDependencies)
}
private static Properties readPropertiesIfExist(File propertiesFile) {
Properties result = new Properties()
if (propertiesFile.exists()) {
propertiesFile.withReader("UTF-8") { reader -> result.load(reader) }
}
return result
}
// 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.
private static void addTaskForJavaVersion(Project project) {
// Warning: the name of this task is used by other code. Change with caution.
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`.
private static void addTaskForPrintBuildVariants(Project project) {
// Warning: The name of this task is used by `AndroidProject.getBuildVariants`.
project.tasks.register("printBuildVariants") {
description "Prints out all build variants for this Android project"
doLast {
project.android.applicationVariants.all { variant ->
println "BuildVariant: ${variant.name}"
}
}
}
}
// Add a task that can be called on Flutter projects that outputs app link related project
// settings into a json file.
//
@ -428,29 +383,7 @@ class FlutterPlugin implements Plugin<Project> {
* 2. libflutter.so
*/
void addFlutterDependencies(BuildType buildType) {
String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
if (!FlutterPluginUtils.supportsBuildMode(project, flutterBuildMode)) {
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.
List<Map<String, Object>> pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency = flutterBuildMode == "release" ? getPluginListWithoutDevDependencies(project) : getPluginList(project);
if (!isFlutterAppProject() || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.size() == 0) {
FlutterPluginUtils.addApiDependencies(project, buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
}
List<String> platforms = getTargetPlatforms().collect()
platforms.each { platform ->
String arch = FlutterPluginUtils.formatPlatformString(platform)
// Add the `libflutter.so` dependency.
FlutterPluginUtils.addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
}
FlutterPluginUtils.addFlutterDependencies(project, buildType, getPluginList(project), engineVersion)
}
/**
@ -462,8 +395,12 @@ class FlutterPlugin implements Plugin<Project> {
*/
private void configurePlugins(Project project) {
configureLegacyPluginEachProjects(project)
getPluginList(project).each(this.&configurePluginProject)
getPluginList(project).each(this.&configurePluginDependencies)
getPluginList(project).each { Map<String, Object> plugin ->
FlutterPluginUtils.configurePluginProject(project, plugin, engineVersion)
}
getPluginList(project).each {Map<String, Object> plugin ->
FlutterPluginUtils.configurePluginDependencies(project, plugin)
}
}
// TODO(54566, 48918): Can remove once the issues are resolved.
@ -516,7 +453,7 @@ class FlutterPlugin implements Plugin<Project> {
} else if (FlutterPluginUtils.pluginSupportsAndroidPlatform(pluginProject)) {
// Plugin has a functioning `android` folder and is included successfully, although it's not supported.
// It must be configured nonetheless, to not throw an "Unresolved reference" exception.
configurePluginProject(it)
FlutterPluginUtils.configurePluginProject(project, it, engineVersion)
/* groovylint-disable-next-line EmptyElseBlock */
} else {
// Plugin has no or an empty `android` folder. No action required.
@ -524,226 +461,6 @@ class FlutterPlugin implements Plugin<Project> {
}
}
/** Adds the plugin project dependency to the app project. */
private void configurePluginProject(Map<String, Object> pluginObject) {
assert(pluginObject.name instanceof String)
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
if (pluginProject == null) {
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)
// Add plugin dependency to the app project. We only want to add dependency
// for dev dependencies in non-release builds.
project.afterEvaluate {
project.android.buildTypes.all { buildType ->
if (!pluginObject.dev_dependency || buildType.name != 'release') {
project.dependencies.add("${buildType.name}Api", pluginProject)
}
}
}
Closure addEmbeddingDependencyToPlugin = { BuildType buildType ->
String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
// 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 (!FlutterPluginUtils.supportsBuildMode(project, 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.
pluginProject.android.buildTypes {
"${buildType.name}" {}
}
// 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.
FlutterPluginUtils.addApiDependencies(
pluginProject,
buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
)
}
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
}
project.android.buildTypes.all(addEmbeddingDependencyToPlugin)
}
}
private void forceNdkDownload(Project gradleProject, String flutterSdkRootPath) {
// If the project is already configuring a native build, we don't need to do anything.
Boolean forcingNotRequired = gradleProject.android.externalNativeBuild.cmake.path != null
if (forcingNotRequired) {
return
}
// Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings.
gradleProject.android {
externalNativeBuild {
cmake {
// Respect the existing configuration if it exists - the NDK will already be
// downloaded in this case.
path = flutterSdkRootPath + "/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt"
}
}
defaultConfig {
externalNativeBuild {
cmake {
// CMake will print warnings when you try to build an empty project.
// These arguments silence the warnings - our project is intentionally
// empty.
arguments("-Wno-dev", "--no-warn-unused-cli")
}
}
}
}
}
/** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
private void detectLowCompileSdkVersionOrNdkVersion() {
project.afterEvaluate {
// Default to int max if using a preview version to skip the sdk check.
int projectCompileSdkVersion = Integer.MAX_VALUE
// Stable versions use ints, legacy preview uses string.
if (getCompileSdkFromProject(project).isInteger()) {
projectCompileSdkVersion = getCompileSdkFromProject(project) as int
}
int maxPluginCompileSdkVersion = projectCompileSdkVersion
String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
String maxPluginNdkVersion = projectNdkVersion
int numProcessedPlugins = getPluginList(project).size()
List<Tuple2<String, String>> pluginsWithHigherSdkVersion = []
List<Tuple2<String, String>> pluginsWithDifferentNdkVersion = []
getPluginList(project).each { pluginObject ->
assert(pluginObject.name instanceof String)
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
if (pluginProject == null) {
return
}
pluginProject.afterEvaluate {
// Default to int min if using a preview version to skip the sdk check.
int pluginCompileSdkVersion = Integer.MIN_VALUE
// Stable versions use ints, legacy preview uses string.
if (getCompileSdkFromProject(pluginProject).isInteger()) {
pluginCompileSdkVersion = getCompileSdkFromProject(pluginProject) as int
}
maxPluginCompileSdkVersion = Math.max(pluginCompileSdkVersion, maxPluginCompileSdkVersion)
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
pluginsWithHigherSdkVersion.add(new Tuple(pluginProject.name, pluginCompileSdkVersion))
}
String pluginNdkVersion = pluginProject.android.ndkVersion ?: ndkVersionIfUnspecified
maxPluginNdkVersion = VersionUtils.mostRecentSemanticVersion(pluginNdkVersion, maxPluginNdkVersion)
if (pluginNdkVersion != projectNdkVersion) {
pluginsWithDifferentNdkVersion.add(new Tuple(pluginProject.name, pluginNdkVersion))
}
numProcessedPlugins--
if (numProcessedPlugins == 0) {
if (maxPluginCompileSdkVersion > projectCompileSdkVersion) {
project.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 (Tuple2<String, String> pluginToCompileSdkVersion : pluginsWithHigherSdkVersion) {
project.logger.error("- ${pluginToCompileSdkVersion.v1} compiles against Android SDK ${pluginToCompileSdkVersion.v2}")
}
File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger)
project.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}
...
}
""".stripIndent())
}
if (maxPluginNdkVersion != projectNdkVersion) {
project.logger.error("Your project is configured with Android NDK $projectNdkVersion, but the following plugin(s) depend on a different Android NDK version:")
for (Tuple2<String, String> pluginToNdkVersion : pluginsWithDifferentNdkVersion) {
project.logger.error("- ${pluginToNdkVersion.v1} requires Android NDK ${pluginToNdkVersion.v2}")
}
File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger)
project.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}\"
...
}
""".stripIndent())
}
}
}
}
}
}
/**
* Returns the portion of the compileSdkVersion string that corresponds to either the numeric
* or string version.
*/
private static String getCompileSdkFromProject(Project gradleProject) {
return gradleProject.android.compileSdkVersion.substring(8)
}
/**
* 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.
*/
private void configurePluginDependencies(Map<String, Object> pluginObject) {
assert(pluginObject.name instanceof String)
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
if (pluginProject == null) {
return
}
project.android.buildTypes.each { buildType ->
String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
if (flutterBuildMode == "release" && pluginObject.dev_dependency) {
// This plugin is a dev dependency will not be included in the
// release build, so no need to add its dependencies.
return
}
def dependencies = pluginObject.dependencies
assert(dependencies instanceof List<String>)
dependencies.each { pluginDependencyName ->
if (pluginDependencyName.empty) {
return
}
Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
if (dependencyProject == null) {
return
}
// Wait for the Android plugin to load and add the dependency to the plugin project.
pluginProject.afterEvaluate {
pluginProject.dependencies {
implementation(dependencyProject)
}
}
}
}
}
/**
* Gets the list of plugins (as map) that support the Android platform.
*
@ -758,24 +475,6 @@ class FlutterPlugin implements Plugin<Project> {
return pluginList
}
/**
* 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 List<Map<String, Object>> getPluginListWithoutDevDependencies(Project project) {
List<Map<String, Object>> pluginListWithoutDevDependencies = []
for (Map<String, Object> plugin in getPluginList(project)) {
if (!plugin.dev_dependency) {
pluginListWithoutDevDependencies += plugin
}
}
return pluginListWithoutDevDependencies
}
// TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
// https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
/** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
@ -794,28 +493,11 @@ class FlutterPlugin implements Plugin<Project> {
private String resolveProperty(String name, String defaultValue) {
if (localProperties == null) {
localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
localProperties = FlutterPluginUtils.readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
}
return project.findProperty(name) ?: localProperties?.getProperty(name, defaultValue)
}
private List<String> getTargetPlatforms() {
final String propTargetPlatform = "target-platform"
if (!project.hasProperty(propTargetPlatform)) {
return FlutterPluginConstants.DEFAULT_PLATFORMS
}
return project.property(propTargetPlatform).split(",").collect {
if (!FlutterPluginConstants.PLATFORM_ARCH_MAP[it]) {
throw new GradleException("Invalid platform: $it.")
}
return it
}
}
private boolean isFlutterAppProject() {
return project.android.hasProperty("applicationVariants")
}
private void addFlutterTasks(Project project) {
if (project.state.failure) {
return
@ -890,12 +572,12 @@ class FlutterPlugin implements Plugin<Project> {
if (project.hasProperty(propValidateDeferredComponents)) {
validateDeferredComponentsValue = project.property(propValidateDeferredComponents).toBoolean()
}
addTaskForJavaVersion(project)
if (isFlutterAppProject()) {
addTaskForPrintBuildVariants(project)
FlutterPluginUtils.addTaskForJavaVersion(project)
if (FlutterPluginUtils.isFlutterAppProject(project)) {
FlutterPluginUtils.addTaskForPrintBuildVariants(project)
addTasksForOutputsAppLinkSettings(project)
}
List<String> targetPlatforms = getTargetPlatforms()
List<String> targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project)
def addFlutterDeps = { variant ->
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
variant.outputs.each { output ->
@ -1058,7 +740,7 @@ class FlutterPlugin implements Plugin<Project> {
}
return copyFlutterAssetsTask
} // end def addFlutterDeps
if (isFlutterAppProject()) {
if (FlutterPluginUtils.isFlutterAppProject(project)) {
AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android")
android.applicationVariants.configureEach { variant ->
Task assembleTask = variant.assembleProvider.get()
@ -1110,7 +792,7 @@ class FlutterPlugin implements Plugin<Project> {
String nativeAssetsDir = "${project.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/"
android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
configurePlugins(project)
detectLowCompileSdkVersionOrNdkVersion()
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
return
}
// Flutter host module project (Add-to-app).
@ -1160,6 +842,6 @@ class FlutterPlugin implements Plugin<Project> {
}
}
configurePlugins(project)
detectLowCompileSdkVersionOrNdkVersion()
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
}
}

View File

@ -18,6 +18,7 @@ object FlutterPluginConstants {
const val INTERMEDIATES_DIR = "intermediates"
const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL"
const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config"
/** Maps platforms to ABI architectures. */
@JvmStatic val PLATFORM_ARCH_MAP =

View File

@ -1,19 +1,26 @@
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.
// 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"
@ -21,6 +28,7 @@ object FlutterPluginUtils {
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. -----------------
@ -89,6 +97,21 @@ object FlutterPluginUtils {
@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
@ -193,8 +216,6 @@ object FlutterPluginUtils {
)?.toString()
?.toBoolean() ?: false
// TODO(gmackall): @JvmStatic internal fun getCompileSdkFromProject(project: Project): String {}
/**
* TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560
*
@ -371,4 +392,466 @@ object FlutterPluginUtils {
// 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
)

View File

@ -1,19 +1,31 @@
package com.flutter.gradle
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.dsl.CmakeOptions
import com.android.build.gradle.internal.dsl.DefaultConfig
import com.android.builder.model.BuildType
import io.mockk.called
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.logging.Logger
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.nio.file.Path
import java.util.Properties
import kotlin.io.path.createDirectory
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ -138,7 +150,8 @@ class FlutterPluginUtilsTest {
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk())
val result =
FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk())
assertEquals(settingsGradle, result)
}
@ -157,7 +170,8 @@ class FlutterPluginUtilsTest {
val mockLogger = mockk<Logger>()
every { mockLogger.error(any()) } returns Unit
val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
val result =
FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
assertEquals(groovySettingsGradle, result)
verify { mockLogger.error(any()) }
}
@ -173,7 +187,8 @@ class FlutterPluginUtilsTest {
val buildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle")
buildGradle.createNewFile()
val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk())
val result =
FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk())
assertEquals(buildGradle, result)
}
@ -192,7 +207,8 @@ class FlutterPluginUtilsTest {
val mockLogger = mockk<Logger>()
every { mockLogger.error(any()) } returns Unit
val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
val result =
FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
assertEquals(groovyBuildGradle, result)
verify { mockLogger.error(any()) }
}
@ -358,7 +374,11 @@ class FlutterPluginUtilsTest {
val variantName = "debug"
val dependency = mockk<Any>()
every { project.configurations.named("api") } throws UnknownTaskException("message", mockk())
every { project.configurations.named("api") } throws
UnknownTaskException(
"message",
mockk()
)
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addApiDependencies(project, variantName, dependency)
@ -415,4 +435,750 @@ class FlutterPluginUtilsTest {
val result = FlutterPluginUtils.supportsBuildMode(project, "release")
assertEquals(false, result)
}
// getTargetPlatforms
@Test
fun `getTargetPlatforms the default if property is not set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns false
val result = FlutterPluginUtils.getTargetPlatforms(project)
assertEquals(listOf("android-arm", "android-arm64", "android-x64"), result)
}
@Test
fun `getTargetPlatforms the value if property is set`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-arm64,android-arm"
val result = FlutterPluginUtils.getTargetPlatforms(project)
assertEquals(listOf("android-arm64", "android-arm"), result)
}
@Test
fun `getTargetPlatforms throws GradleException if property is set to invalid value`() {
val project = mockk<Project>()
every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-invalid"
val gradleException: GradleException =
assertThrows<GradleException> {
FlutterPluginUtils.getTargetPlatforms(project)
}
assertContains(gradleException.message!!, "android-invalid")
}
// readPropertiesIfExist
@Test
fun `readPropertiesIfExist returns empty Properties when file does not exist`(
@TempDir tempDir: Path
) {
val propertiesFile = tempDir.resolve("file_that_doesnt_exist.properties")
val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile.toFile())
assertEquals(Properties(), result)
}
@Test
fun `readPropertiesIfExist returns Properties when file exists`(
@TempDir tempDir: Path
) {
val propertiesFile = tempDir.resolve("file_that_exists.properties").toFile()
propertiesFile.writeText(
"""
sdk.dir=/Users/someuser/Library/Android/sdk
flutter.sdk=/Users/someuser/development/flutter/flutter
flutter.buildMode=release
flutter.versionName=1.0.0
flutter.versionCode=1
""".trimIndent()
)
val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile)
assertEquals(5, result.size)
assertEquals("/Users/someuser/Library/Android/sdk", result["sdk.dir"])
assertEquals("/Users/someuser/development/flutter/flutter", result["flutter.sdk"])
assertEquals("release", result["flutter.buildMode"])
assertEquals("1.0.0", result["flutter.versionName"])
assertEquals("1", result["flutter.versionCode"])
}
// getCompileSdkFromProject
@Test
fun `getCompileSdkFromProject returns the compileSdk from the project`() {
val project = mockk<Project>()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val result = FlutterPluginUtils.getCompileSdkFromProject(project)
assertEquals("35", result)
}
// detectLowCompileSdkVersionOrNdkVersion
@Test
fun `detectLowCompileSdkVersionOrNdkVersion logs no warnings when no plugins have higher sdk or ndk`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("app").toFile()
val project = mockk<Project>()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
every { project.projectDir } returns projectDir
val cameraPluginProject = mockk<Project>()
val projectActionSlot = slot<Action<Project>>()
val cameraPluginProjectActionSlot = slot<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject
every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit
every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, listOf(cameraDependency))
verify { project.afterEvaluate(capture(projectActionSlot)) }
projectActionSlot.captured.execute(project)
verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) }
cameraPluginProjectActionSlot.captured.execute(cameraPluginProject)
verify { mockLogger wasNot called }
}
@Test
fun `detectLowCompileSdkVersionOrNdkVersion logs warnings when plugins have higher sdk and ndk`(
@TempDir tempDir: Path
) {
val buildGradleFile =
tempDir
.resolve("app")
.createDirectory()
.resolve("build.gradle")
.toFile()
buildGradleFile.createNewFile()
val projectDir = tempDir.resolve("app").toFile()
val project = mockk<Project>()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
every { mockLogger.error(any()) } returns Unit
every { project.projectDir } returns projectDir
val cameraPluginProject = mockk<Project>()
val flutterPluginAndroidLifecycleDependencyPluginProject = mockk<Project>()
val projectActionSlot = slot<Action<Project>>()
val cameraPluginProjectActionSlot = slot<Action<Project>>()
val flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot = slot<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-33"
every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "24.3.11579264"
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject
every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns
flutterPluginAndroidLifecycleDependencyPluginProject
every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit
every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
every {
flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate(
capture(
flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot
)
)
} returns Unit
every {
flutterPluginAndroidLifecycleDependencyPluginProject.extensions
.findByType(
BaseExtension::class.java
)!!
.compileSdkVersion
} returns "android-34"
every {
flutterPluginAndroidLifecycleDependencyPluginProject.extensions
.findByType(
BaseExtension::class.java
)!!
.ndkVersion
} returns "25.3.11579264"
val dependencyList: List<Map<String?, Any?>> =
listOf(cameraDependency, flutterPluginAndroidLifecycleDependency)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
project,
dependencyList
)
verify { project.afterEvaluate(capture(projectActionSlot)) }
projectActionSlot.captured.execute(project)
verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) }
cameraPluginProjectActionSlot.captured.execute(cameraPluginProject)
verify {
flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate(
capture(
flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot
)
)
}
flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot.captured.execute(
flutterPluginAndroidLifecycleDependencyPluginProject
)
verify {
mockLogger.error(
"Your project is configured to compile against Android SDK 33, but the " +
"following plugin(s) require to be compiled against a higher Android SDK version:"
)
}
verify {
mockLogger.error(
"- ${cameraDependency["name"]} compiles against Android SDK 35"
)
}
verify {
mockLogger.error(
"- ${flutterPluginAndroidLifecycleDependency["name"]} compiles against Android SDK 34"
)
}
verify {
mockLogger.error(
"""
Fix this issue by compiling against the highest Android SDK version (they are backward compatible).
Add the following to ${buildGradleFile.path}:
android {
compileSdk = 35
...
}
""".trimIndent()
)
}
verify {
mockLogger.error(
"Your project is configured with Android NDK 24.3.11579264, but the following plugin(s) depend on a different Android NDK version:"
)
}
verify {
mockLogger.error(
"- ${cameraDependency["name"]} requires Android NDK 26.3.11579264"
)
}
verify {
mockLogger.error(
"- ${flutterPluginAndroidLifecycleDependency["name"]} requires Android NDK 25.3.11579264"
)
}
verify {
mockLogger.error(
"""
Fix this issue by using the highest Android NDK version (they are backward compatible).
Add the following to ${buildGradleFile.path}:
android {
ndkVersion = "26.3.11579264"
...
}
""".trimIndent()
)
}
}
@Test
fun `detectLowCompileSdkVersionOrNdkVersion throws IllegalArgumentException when plugin has no name`() {
val project = mockk<Project>()
val projectActionSlot = slot<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
project,
listOf(pluginWithoutName)
)
verify { project.afterEvaluate(capture(projectActionSlot)) }
assertThrows<IllegalArgumentException> { projectActionSlot.captured.execute(project) }
}
// forceNdkDownload
@Test
fun `forceNdkDownload skips projects which are already configuring a native build`(
@TempDir tempDir: Path
) {
val fakeCmakeFile = tempDir.resolve("CMakeLists.txt").toFile()
fakeCmakeFile.createNewFile()
val project = mockk<Project>()
val mockCmakeOptions = mockk<CmakeOptions>()
val mockDefaultConfig = mockk<DefaultConfig>()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.externalNativeBuild.cmake
} returns mockCmakeOptions
every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
every { mockCmakeOptions.path } returns fakeCmakeFile
FlutterPluginUtils.forceNdkDownload(project, "ignored")
verify(exactly = 1) {
mockCmakeOptions.path
}
verify(exactly = 0) { mockCmakeOptions.setPath(any()) }
verify { mockDefaultConfig wasNot called }
}
@Test
fun `forceNdkDownload sets externalNativeBuild properties`() {
val project = mockk<Project>()
val mockCmakeOptions = mockk<CmakeOptions>()
val mockDefaultConfig = mockk<DefaultConfig>()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.externalNativeBuild.cmake
} returns mockCmakeOptions
every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
every { mockCmakeOptions.path } returns null
every { mockCmakeOptions.path(any()) } returns Unit
every { mockDefaultConfig.externalNativeBuild.cmake.arguments(any(), any()) } returns Unit
val basePath = "/base/path"
FlutterPluginUtils.forceNdkDownload(project, basePath)
verify(exactly = 1) {
mockCmakeOptions.path
}
verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt") }
verify(exactly = 1) {
mockDefaultConfig.externalNativeBuild.cmake.arguments(
"-Wno-dev",
"--no-warn-unused-cli"
)
}
}
// isFlutterAppProject skipped as it is a wrapper for a single getter that we would have to mock
// addFlutterDependencies
@Test
fun `addFlutterDependencies returns early if buildMode is not supported`() {
val project = mockk<Project>()
val buildType: BuildType = mockk<BuildType>()
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns true
every { project.hasProperty("local-engine-build-mode") } returns true
every { project.property("local-engine-build-mode") } returns "release"
every { project.logger.quiet(any()) } returns Unit
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginList = pluginListWithoutDevDependency,
engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
)
verify(exactly = 1) {
project.logger.quiet(
"Project does not support Flutter build mode: debug, " +
"skipping adding flutter dependencies"
)
}
}
@Test
fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() {
val project = mockk<Project>()
val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginList = pluginListWithoutDevDependency,
engineVersion = engineVersion
)
verify(exactly = 3) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"debugApi",
"io.flutter:armeabi_v7a_debug:$engineVersion"
)
}
verify { project.dependencies.add("debugApi", "io.flutter:arm64_v8a_debug:$engineVersion") }
verify { project.dependencies.add("debugApi", "io.flutter:x86_64_debug:$engineVersion") }
}
@Test
fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() {
val project = mockk<Project>()
val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion
every { buildType.name } returns "release"
every { buildType.isDebuggable } returns false
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
val pluginListWithSingleDevDependency = listOf(devDependency)
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginList = pluginListWithSingleDevDependency,
engineVersion = engineVersion
)
verify(exactly = 4) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:flutter_embedding_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:armeabi_v7a_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:arm64_v8a_release:$engineVersion"
)
}
verify {
project.dependencies.add(
"releaseApi",
"io.flutter:x86_64_release:$engineVersion"
)
}
}
@Test
fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() {
val project = mockk<Project>()
val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
every { project.hasProperty("target-platform") } returns false
every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk()
val pluginListWithSingleDevDependency = listOf(devDependency)
FlutterPluginUtils.addFlutterDependencies(
project = project,
buildType = buildType,
pluginList = pluginListWithSingleDevDependency,
engineVersion = engineVersion
)
verify(exactly = 3) { project.dependencies.add(any(), any()) }
verify {
project.dependencies.add(
"debugApi",
"io.flutter:armeabi_v7a_debug:$engineVersion"
)
}
verify {
project.dependencies.add(
"debugApi",
"io.flutter:arm64_v8a_debug:$engineVersion"
)
}
verify {
project.dependencies.add(
"debugApi",
"io.flutter:x86_64_debug:$engineVersion"
)
}
}
// configurePluginDependencies TODO
@Test
fun `configurePluginDependencies throws IllegalArgumentException when plugin has no name`() {
val project = mockk<Project>()
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithoutName
)
}
}
@Test
fun `configurePluginDependencies throws IllegalArgumentException when plugin has null dependencies`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val pluginWithNullDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithNullDependencies["dependencies"] = null
every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator()
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithNullDependencies
)
}
}
@Test
fun `configurePluginDependencies adds plugin dependencies`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val pluginDependencyProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val pluginWithDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithDependencies["dependencies"] =
listOf(flutterPluginAndroidLifecycleDependency["name"])
every { project.rootProject.findProject(":${pluginWithDependencies["name"]}") } returns pluginProject
every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator()
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
val captureActionSlot = slot<Action<Project>>()
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockDependencyHandler = mockk<DependencyHandler>()
every { pluginProject.dependencies } returns mockDependencyHandler
every { mockDependencyHandler.add(any(), any()) } returns mockk()
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithDependencies
)
verify { pluginProject.afterEvaluate(capture(captureActionSlot)) }
captureActionSlot.captured.execute(pluginDependencyProject)
verify { mockDependencyHandler.add("implementation", pluginDependencyProject) }
}
// configurePluginProject
@Test
fun `configurePluginProject throws IllegalArgumentException when plugin has no name`() {
val project = mockk<Project>()
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginProject(
project = project,
pluginObject = pluginWithoutName,
engineVersion = exampleEngineVersion
)
}
}
@Test
fun `configurePluginProject adds plugin project`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
every { pluginProject.hasProperty("local-engine-repo") } returns false
every { pluginProject.hasProperty("android") } returns true
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject
every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
val captureActionSlot = slot<Action<Project>>()
val capturePluginActionSlot = slot<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
val mockPluginProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
every { mockPluginProjectBuildTypes.addAll(any()) } returns true
every { pluginProject.configurations.named(any<String>()) } returns mockk()
every { pluginProject.dependencies.add(any(), any()) } returns mockk()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator() andThen
mutableListOf( // can't return the same iterator as it is stateful
mockBuildType
).iterator()
every { project.dependencies.add(any(), any()) } returns mockk()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
FlutterPluginUtils.configurePluginProject(
project = project,
pluginObject = cameraDependency,
engineVersion = exampleEngineVersion
)
verify { project.afterEvaluate(capture(captureActionSlot)) }
verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) }
captureActionSlot.captured.execute(project)
capturePluginActionSlot.captured.execute(pluginProject)
verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) }
verify {
pluginProject.dependencies.add(
"debugApi",
"io.flutter:flutter_embedding_debug:$exampleEngineVersion"
)
}
verify { project.dependencies.add("debugApi", pluginProject) }
verify { mockLogger wasNot called }
verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) }
}
// addTaskForJavaVersion
@Test
fun `addTaskForJavaVersion adds task for Java version`() {
val project = mockk<Project>()
every { project.tasks.register(any(), any<Action<Task>>()) } returns mockk()
val captureSlot = slot<Action<Task>>()
FlutterPluginUtils.addTaskForJavaVersion(project)
verify { project.tasks.register("javaVersion", capture(captureSlot)) }
val mockTask = mockk<Task>()
every { mockTask.description = any() } returns Unit
every { mockTask.doLast(any<Action<Task>>()) } returns mockk()
captureSlot.captured.execute(mockTask)
verify {
mockTask.description = "Print the current java version used by gradle. see: " +
"https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
}
}
// addTaskForPrintBuildVariants
@Test
fun `addTaskForPrintBuildVariants adds task for printing build variants`() {
val project = mockk<Project>()
every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockk()
every { project.tasks.register(any(), any<Action<Task>>()) } returns mockk()
val captureSlot = slot<Action<Task>>()
FlutterPluginUtils.addTaskForPrintBuildVariants(project)
verify { project.tasks.register("printBuildVariants", capture(captureSlot)) }
val mockTask = mockk<Task>()
every { mockTask.description = any() } returns Unit
every { mockTask.doLast(any<Action<Task>>()) } returns mockk()
captureSlot.captured.execute(mockTask)
verify {
mockTask.description = "Prints out all build variants for this Android project"
}
}
companion object {
val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
val devDependency: Map<String?, Any?> =
mapOf(
Pair("name", "grays_fun_dev_dependency"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/grays_fun_dev_dependency-1.1.1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", true)
)
val cameraDependency: Map<String?, Any?> =
mapOf(
Pair("name", "camera_android_camerax"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/camera_android_camerax-0.6.14+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
val flutterPluginAndroidLifecycleDependency: Map<String?, Any?> =
mapOf(
Pair("name", "flutter_plugin_android_lifecycle"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.27/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
val pluginListWithoutDevDependency: List<Map<String?, Any?>> =
listOf(
cameraDependency,
flutterPluginAndroidLifecycleDependency,
mapOf(
Pair("name", "in_app_purchase_android"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
)
val pluginListWithDevDependency: List<Map<String?, Any?>> =
listOf(
cameraDependency,
flutterPluginAndroidLifecycleDependency,
devDependency,
mapOf(
Pair("name", "in_app_purchase_android"),
Pair(
"path",
"/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
),
Pair("native_build", true),
Pair("dependencies", emptyList<String>()),
Pair("dev_dependency", false)
)
)
}
}

View File

@ -67,6 +67,7 @@ void main() {
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')
@ -103,6 +104,7 @@ void main() {
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')
@ -155,6 +157,7 @@ void main() {
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')