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>
diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
index 2f3fb00..7d96fcb 100644
--- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
+++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
@@ -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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
* 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 @@
*/
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 @@
} 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 @@
}
}
- /** 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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
}
}
configurePlugins(project)
- detectLowCompileSdkVersionOrNdkVersion()
+ FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
}
}
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
index fca6b99..eefcd25 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
@@ -18,6 +18,7 @@
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 =
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
index 96bdef7..2e473f6 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
@@ -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 @@
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 @@
@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 @@
)?.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 @@
// 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
+)
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
index cbc3062..dcad62a 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
@@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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)
+ )
+ )
+ }
}
diff --git a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
index 02402ed..c203268 100644
--- a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
+++ b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
@@ -67,6 +67,7 @@
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
+ expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')
@@ -103,6 +104,7 @@
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
+ expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')
@@ -155,6 +157,7 @@
'apk',
'--debug',
], workingDirectory: exampleAppDir.path);
+ expect(result, const ProcessResultMatcher());
expect(
exampleAppDir
.childDirectory('build')