diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 2f3fb00744c..7d96fcb1c03 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 @@ class FlutterPlugin implements Plugin { 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 { 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 { } } - 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.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 { * 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> pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency = flutterBuildMode == "release" ? getPluginListWithoutDevDependencies(project) : getPluginList(project); - if (!isFlutterAppProject() || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.size() == 0) { - FlutterPluginUtils.addApiDependencies(project, buildType.name, - "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") - } - List 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 { */ private void configurePlugins(Project project) { configureLegacyPluginEachProjects(project) - getPluginList(project).each(this.&configurePluginProject) - getPluginList(project).each(this.&configurePluginDependencies) + getPluginList(project).each { Map plugin -> + FlutterPluginUtils.configurePluginProject(project, plugin, engineVersion) + } + getPluginList(project).each {Map plugin -> + FlutterPluginUtils.configurePluginDependencies(project, plugin) + } } // TODO(54566, 48918): Can remove once the issues are resolved. @@ -516,7 +453,7 @@ class FlutterPlugin implements Plugin { } 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 { } } - /** Adds the plugin project dependency to the app project. */ - private void configurePluginProject(Map 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> pluginsWithHigherSdkVersion = [] - List> 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 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 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 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) - 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 { 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). - * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy - */ - private List> getPluginListWithoutDevDependencies(Project project) { - List> pluginListWithoutDevDependencies = [] - for (Map 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 { 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 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 { 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 targetPlatforms = getTargetPlatforms() + List targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project) def addFlutterDeps = { variant -> if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { variant.outputs.each { output -> @@ -1058,7 +740,7 @@ class FlutterPlugin implements Plugin { } 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 { 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 { } } 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 fca6b99a39d..eefcd25be73 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 @@ 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 = diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 96bdef7c693..2e473f63451 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 @@ 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 { + 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, + 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, + 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> + ) { + 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() + val pluginsWithDifferentNdkVersion = mutableListOf() + 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>, + 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> = + 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 = 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). + * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy + */ + private fun getPluginListWithoutDevDependencies(pluginList: List>): List> = + 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 + ) { + 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, + 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 cbc30624af6..dcad62a7ea4 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 @@ 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() 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() 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() - 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() + 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() + 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() + every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true + every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-invalid" + val gradleException: GradleException = + assertThrows { + 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() + 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() + val mockLogger = mockk() + every { project.logger } returns mockLogger + every { project.projectDir } returns projectDir + val cameraPluginProject = mockk() + val projectActionSlot = slot>() + val cameraPluginProjectActionSlot = slot>() + every { project.afterEvaluate(any>()) } 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() + val mockLogger = mockk() + every { project.logger } returns mockLogger + every { mockLogger.error(any()) } returns Unit + every { project.projectDir } returns projectDir + val cameraPluginProject = mockk() + val flutterPluginAndroidLifecycleDependencyPluginProject = mockk() + val projectActionSlot = slot>() + val cameraPluginProjectActionSlot = slot>() + val flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot = slot>() + every { project.afterEvaluate(any>()) } 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> = + 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() + val projectActionSlot = slot>() + every { project.afterEvaluate(any>()) } 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 = cameraDependency.toMutableMap() + pluginWithoutName.remove("name") + + FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion( + project, + listOf(pluginWithoutName) + ) + verify { project.afterEvaluate(capture(projectActionSlot)) } + assertThrows { 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() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + 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() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + 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() + val buildType: BuildType = mockk() + 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() + val buildType: BuildType = mockk() + 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() + 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() + val buildType: BuildType = mockk() + 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() + 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() + val buildType: BuildType = mockk() + 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() + 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() + val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() + pluginWithoutName.remove("name") + assertThrows { + FlutterPluginUtils.configurePluginDependencies( + project = project, + pluginObject = pluginWithoutName + ) + } + } + + @Test + fun `configurePluginDependencies throws IllegalArgumentException when plugin has null dependencies`() { + val project = mockk() + val pluginProject = mockk() + val mockBuildType = mockk() + val pluginWithNullDependencies: MutableMap = 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 { + FlutterPluginUtils.configurePluginDependencies( + project = project, + pluginObject = pluginWithNullDependencies + ) + } + } + + @Test + fun `configurePluginDependencies adds plugin dependencies`() { + val project = mockk() + val pluginProject = mockk() + val pluginDependencyProject = mockk() + val mockBuildType = mockk() + val pluginWithDependencies: MutableMap = 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>() + every { pluginProject.afterEvaluate(any>()) } returns Unit + val mockDependencyHandler = mockk() + 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() + val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() + pluginWithoutName.remove("name") + + assertThrows { + FlutterPluginUtils.configurePluginProject( + project = project, + pluginObject = pluginWithoutName, + engineVersion = exampleEngineVersion + ) + } + } + + @Test + fun `configurePluginProject adds plugin project`() { + val project = mockk() + val pluginProject = mockk() + val mockBuildType = mockk() + val mockLogger = mockk() + 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>()) } returns mockk() + val captureActionSlot = slot>() + val capturePluginActionSlot = slot>() + every { project.afterEvaluate(any>()) } returns Unit + every { pluginProject.afterEvaluate(any>()) } returns Unit + + val mockProjectBuildTypes = + mockk>() + val mockPluginProjectBuildTypes = + mockk>() + 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()) } 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() + every { project.tasks.register(any(), any>()) } returns mockk() + val captureSlot = slot>() + FlutterPluginUtils.addTaskForJavaVersion(project) + verify { project.tasks.register("javaVersion", capture(captureSlot)) } + + val mockTask = mockk() + every { mockTask.description = any() } returns Unit + every { mockTask.doLast(any>()) } 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() + every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockk() + every { project.tasks.register(any(), any>()) } returns mockk() + val captureSlot = slot>() + + FlutterPluginUtils.addTaskForPrintBuildVariants(project) + + verify { project.tasks.register("printBuildVariants", capture(captureSlot)) } + val mockTask = mockk() + every { mockTask.description = any() } returns Unit + every { mockTask.doLast(any>()) } 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 = + 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()), + Pair("dev_dependency", true) + ) + + val cameraDependency: Map = + 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()), + Pair("dev_dependency", false) + ) + + val flutterPluginAndroidLifecycleDependency: Map = + 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()), + Pair("dev_dependency", false) + ) + + val pluginListWithoutDevDependency: List> = + 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()), + Pair("dev_dependency", false) + ) + ) + + val pluginListWithDevDependency: List> = + 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()), + 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 02402ed6383..c2032685bce 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 @@ 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')