| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package com.flutter.gradle |
| |
| import androidx.annotation.VisibleForTesting |
| import com.android.build.api.AndroidPluginVersion |
| import com.android.build.api.variant.AndroidComponentsExtension |
| import com.android.build.api.variant.Variant |
| import org.gradle.api.JavaVersion |
| import org.gradle.api.Project |
| import org.gradle.api.logging.Logger |
| import org.gradle.kotlin.dsl.extra |
| |
| /** |
| * Warns or errors on version ranges of dependencies required to build a Flutter Android app. |
| * |
| * For code that evaluates if dependencies are compatible with each other see |
| * packages/flutter_tools/lib/src/android/gradle_utils.dart. |
| */ |
| object DependencyVersionChecker { |
| // Logging constants. |
| @VisibleForTesting internal const val GRADLE_NAME: String = "Gradle" |
| |
| @VisibleForTesting internal const val JAVA_NAME: String = "Java" |
| |
| @VisibleForTesting internal const val AGP_NAME: String = "Android Gradle Plugin" |
| |
| @VisibleForTesting internal const val KGP_NAME: String = "Kotlin" |
| |
| @VisibleForTesting internal const val MIN_SDK_NAME: String = "minimum Android SDK" |
| |
| // String constant that defines the name of the Gradle extra property that we set when |
| // detecting that the project is using versions outside of Flutter's support range. |
| // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api/-project/index.html#-2107180640%2FProperties%2F-1867656071. |
| @VisibleForTesting internal const val OUT_OF_SUPPORT_RANGE_PROPERTY = "usesUnsupportedDependencyVersions" |
| |
| // The task prefix for assemble builds. |
| @VisibleForTesting |
| internal const val ASSEMBLE_PREFIX = "assemble" |
| |
| // The task postfix to use when checking the minimum SDK version for each flavor. |
| internal const val MIN_SDK_CHECK_TASK_POSTFIX = "MinSdkCheck" |
| |
| // The following messages represent best effort guesses at where a Flutter developer should |
| // look to upgrade a dependency that is below the corresponding threshold. Developers can |
| // change some of these locations, so they are not guaranteed to be accurate. |
| @VisibleForTesting internal fun getPotentialGradleFix(projectDirectory: String): String = |
| "Your project's gradle version is typically " + |
| "defined in the gradle wrapper file. By default, this can be found at " + |
| "$projectDirectory/gradle/wrapper/gradle-wrapper.properties. \n" + |
| "For more information, see https://docs.gradle.org/current/userguide/gradle_wrapper.html.\n" |
| |
| // The potential java fix does not make use of the project directory, |
| // so it left as a constant. |
| @VisibleForTesting internal const val POTENTIAL_JAVA_FIX: String = |
| "The Java version used by Flutter can be " + |
| "set with `flutter config --jdk-dir=<path>`. \nFor more information about how Flutter " + |
| "chooses which version of Java to use, see the --jdk-dir section of the " + |
| "output of `flutter config -h`.\n" |
| |
| @VisibleForTesting internal fun getPotentialAGPFix(projectDirectory: String): String = |
| "Your project's AGP version is typically " + |
| "defined in the plugins block of the `settings.gradle` file " + |
| "($projectDirectory/settings.gradle), by a plugin with the id of " + |
| "com.android.application. \nIf you don't see a plugins block, your project " + |
| "was likely created with an older template version. In this case it is most " + |
| "likely defined in the top-level build.gradle file " + |
| "($projectDirectory/build.gradle) by the following line in the dependencies" + |
| " block of the buildscript: \"classpath 'com.android.tools.build:gradle:<version>'\".\n" |
| |
| @VisibleForTesting internal fun getPotentialKGPFix(projectDirectory: String): String = |
| "Your project's KGP version is typically " + |
| "defined in the plugins block of the `settings.gradle` file " + |
| "($projectDirectory/settings.gradle), by a plugin with the id of " + |
| "org.jetbrains.kotlin.android. \nIf you don't see a plugins block, your project " + |
| "was likely created with an older template version, in which case it is most " + |
| "likely defined in the top-level build.gradle file " + |
| "($projectDirectory/build.gradle) by the ext.kotlin_version property.\n" |
| |
| @VisibleForTesting internal fun getPotentialSDKFix(projectDirectory: String): String = |
| "Your project's minimum Android SDK version is typically " + |
| "defined in the android block of the app-level `build.gradle(.kts)` file " + |
| "($projectDirectory/app/build.gradle(.kts))." |
| |
| // The following versions define our support policy for Gradle, Java, AGP, and KGP. |
| // Before updating any "error" version, ensure that you have updated the corresponding |
| // "warn" version for a full release to provide advanced warning. See |
| // flutter.dev/go/android-dependency-versions for more. |
| @VisibleForTesting internal val warnGradleVersion: Version = Version(7, 4, 2) |
| |
| @VisibleForTesting internal val errorGradleVersion: Version = Version(7, 0, 2) |
| |
| @VisibleForTesting internal val warnJavaVersion: JavaVersion = JavaVersion.VERSION_11 |
| |
| @VisibleForTesting internal val errorJavaVersion: JavaVersion = JavaVersion.VERSION_1_1 |
| |
| @VisibleForTesting internal val warnAGPVersion: AndroidPluginVersion = AndroidPluginVersion(8, 3, 0) |
| |
| @VisibleForTesting internal val errorAGPVersion: AndroidPluginVersion = AndroidPluginVersion(7, 0, 0) |
| |
| @VisibleForTesting internal val warnKGPVersion: Version = Version(1, 8, 10) |
| |
| @VisibleForTesting internal val errorKGPVersion: Version = Version(1, 7, 0) |
| |
| // If this value is changed, then make sure to change the documentation on https://docs.flutter.dev/reference/supported-platforms |
| @VisibleForTesting |
| internal val warnMinSdkVersion: Int = 21 |
| |
| @VisibleForTesting |
| internal val errorMinSdkVersion: Int = 1 |
| |
| /** |
| * Checks if the project's Android build time dependencies are each within the respective |
| * version range that we support. When we can't find a version for a given dependency |
| * we treat it as within the range for the purpose of this check. |
| */ |
| @JvmStatic fun checkDependencyVersions(project: Project) { |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) |
| |
| checkGradleVersion(VersionFetcher.getGradleVersion(project), project) |
| checkJavaVersion(VersionFetcher.getJavaVersion(), project) |
| |
| configureMinSdkCheck(project) |
| |
| val agpVersion: AndroidPluginVersion? = VersionFetcher.getAGPVersion(project) |
| if (agpVersion != null) { |
| checkAGPVersion(agpVersion, project) |
| } else { |
| project.logger.error( |
| "Warning: unable to detect project AGP version. Skipping " + |
| "version checking. \nThis may be because you have applied AGP after the Flutter Gradle Plugin." |
| ) |
| } |
| |
| val kgpVersion: Version? = VersionFetcher.getKGPVersion(project) |
| if (kgpVersion != null) { |
| checkKGPVersion(kgpVersion, project) |
| } |
| // KGP is not required, so don't log any warning if we can't find the version. |
| } |
| |
| private fun configureMinSdkCheck(project: Project) { |
| val androidComponents = |
| project.extensions.findByType(AndroidComponentsExtension::class.java) |
| |
| androidComponents?.onVariants( |
| androidComponents.selector().all() |
| ) { |
| val taskName = generateMinSdkCheckTaskName(it) |
| val minSdkCheckTask = |
| project.tasks.register(taskName) { |
| doLast { |
| val minSdkVersion = getMinSdkVersion(project, it) |
| try { |
| checkMinSdkVersion(minSdkVersion, project.rootDir.path, project.logger) |
| } catch (e: DependencyValidationException) { |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) |
| throw e |
| } |
| } |
| } |
| |
| project.afterEvaluate { |
| // Make assemble task depend on minSdkCheckTask for this variant. |
| project.tasks |
| .named(generateAssembleTaskName(it)) |
| .configure { |
| dependsOn(minSdkCheckTask) |
| } |
| } |
| } |
| } |
| |
| private fun generateAssembleTaskName(it: Variant) = "$ASSEMBLE_PREFIX${FlutterPluginUtils.capitalize(it.name)}" |
| |
| private fun generateMinSdkCheckTaskName(it: Variant) = "${FlutterPluginUtils.capitalize(it.name)}$MIN_SDK_CHECK_TASK_POSTFIX" |
| |
| private fun getMinSdkVersion( |
| project: Project, |
| it: Variant |
| ): MinSdkVersion { |
| val agpVersion: AndroidPluginVersion? = VersionFetcher.getAGPVersion(project) |
| return if (agpVersion != null && agpVersion.major >= 8 && agpVersion.minor >= 1) { |
| MinSdkVersion(it.name, it.minSdk.apiLevel) |
| } else { |
| MinSdkVersion(it.name, it.minSdkVersion.apiLevel) |
| } |
| } |
| |
| @VisibleForTesting internal fun getErrorMessage( |
| dependencyName: String, |
| versionString: String, |
| errorVersion: String, |
| potentialFix: String |
| ): String = |
| "Error: Your project's $dependencyName version ($versionString) is lower " + |
| "than Flutter's minimum supported version of $errorVersion. Please upgrade " + |
| "your $dependencyName version. \nAlternatively, use the flag " + |
| "\"--android-skip-build-dependency-validation\" to bypass this check.\n\n" + |
| "Potential fix: $potentialFix" |
| |
| @VisibleForTesting internal fun getWarnMessage( |
| dependencyName: String, |
| versionString: String, |
| warnVersion: String, |
| potentialFix: String |
| ): String = |
| "Warning: Flutter support for your project's $dependencyName version " + |
| "($versionString) will soon be dropped. Please upgrade your $dependencyName " + |
| "version to a version of at least $warnVersion soon." + |
| "\nAlternatively, use the flag \"--android-skip-build-dependency-validation\"" + |
| " to bypass this check.\n\nPotential fix: $potentialFix" |
| |
| @VisibleForTesting |
| internal fun getFlavorSpecificMessage( |
| flavorName: String?, |
| dependencyName: String |
| ): String = dependencyName + (if (flavorName != null) " (flavor='$flavorName')" else "") |
| |
| @VisibleForTesting internal fun checkGradleVersion( |
| version: Version, |
| project: Project |
| ) { |
| if (version < errorGradleVersion) { |
| val errorMessage: String = |
| getErrorMessage( |
| GRADLE_NAME, |
| version.toString(), |
| errorGradleVersion.toString(), |
| getPotentialGradleFix(project.rootDir.path) |
| ) |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) |
| throw DependencyValidationException(errorMessage) |
| } else if (version < warnGradleVersion) { |
| val warnMessage: String = |
| getWarnMessage( |
| GRADLE_NAME, |
| version.toString(), |
| warnGradleVersion.toString(), |
| getPotentialGradleFix(project.rootDir.path) |
| ) |
| project.logger.error(warnMessage) |
| } |
| } |
| |
| @VisibleForTesting internal fun checkJavaVersion( |
| version: JavaVersion, |
| project: Project |
| ) { |
| if (version < errorJavaVersion) { |
| val errorMessage: String = |
| getErrorMessage( |
| JAVA_NAME, |
| version.toString(), |
| errorJavaVersion.toString(), |
| POTENTIAL_JAVA_FIX |
| ) |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) |
| throw DependencyValidationException(errorMessage) |
| } else if (version < warnJavaVersion) { |
| val warnMessage: String = |
| getWarnMessage( |
| JAVA_NAME, |
| version.toString(), |
| warnJavaVersion.toString(), |
| POTENTIAL_JAVA_FIX |
| ) |
| project.logger.error(warnMessage) |
| } |
| } |
| |
| @VisibleForTesting internal fun checkAGPVersion( |
| androidPluginVersion: AndroidPluginVersion, |
| project: Project |
| ) { |
| if (androidPluginVersion < errorAGPVersion) { |
| val errorMessage: String = |
| getErrorMessage( |
| AGP_NAME, |
| androidPluginVersion.toString(), |
| errorAGPVersion.toString(), |
| getPotentialAGPFix(project.rootDir.path) |
| ) |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) |
| throw DependencyValidationException(errorMessage) |
| } else if (androidPluginVersion < warnAGPVersion) { |
| val warnMessage: String = |
| getWarnMessage( |
| AGP_NAME, |
| androidPluginVersion.toString(), |
| warnAGPVersion.toString(), |
| getPotentialAGPFix(project.rootDir.path) |
| ) |
| project.logger.error(warnMessage) |
| } |
| } |
| |
| @VisibleForTesting internal fun checkKGPVersion( |
| version: Version, |
| project: Project |
| ) { |
| if (version < errorKGPVersion) { |
| val errorMessage: String = |
| getErrorMessage( |
| KGP_NAME, |
| version.toString(), |
| errorKGPVersion.toString(), |
| getPotentialKGPFix(project.rootDir.path) |
| ) |
| project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) |
| throw DependencyValidationException(errorMessage) |
| } else if (version < warnKGPVersion) { |
| val warnMessage: String = |
| getWarnMessage( |
| KGP_NAME, |
| version.toString(), |
| warnKGPVersion.toString(), |
| getPotentialKGPFix(project.rootDir.path) |
| ) |
| project.logger.error(warnMessage) |
| } |
| } |
| |
| @VisibleForTesting internal fun checkMinSdkVersion( |
| minSdkVersion: MinSdkVersion, |
| projectDirectory: String, |
| logger: Logger |
| ) { |
| // For Android SDK, only the major version is relevant, no need to do a full version check. |
| if (minSdkVersion.version < errorMinSdkVersion) { |
| val errorMessage: String = |
| getErrorMessage( |
| getFlavorSpecificMessage(minSdkVersion.flavor, MIN_SDK_NAME), |
| minSdkVersion.version.toString(), |
| errorMinSdkVersion.toString(), |
| getPotentialSDKFix(projectDirectory) |
| ) |
| throw DependencyValidationException(errorMessage) |
| } else if (minSdkVersion.version < warnMinSdkVersion) { |
| val warnMessage: String = |
| getWarnMessage( |
| getFlavorSpecificMessage(minSdkVersion.flavor, MIN_SDK_NAME), |
| minSdkVersion.version.toString(), |
| warnMinSdkVersion.toString(), |
| getPotentialSDKFix(projectDirectory) |
| ) |
| logger.error(warnMessage) |
| } |
| } |
| } |
| |
| // Custom error for when the dependency_version_checker.kts script finds a dependency out of |
| // the defined support range. |
| @VisibleForTesting internal class DependencyValidationException( |
| message: String? = null, |
| cause: Throwable? = null |
| ) : Exception(message, cause) |
| |
| /** |
| * Represents the minimum Android SDK version for a specific product flavor. |
| * |
| * @param flavor The product flavor name, or null for the default configuration. |
| * @param version The minimum Android SDK version (API level). |
| */ |
| @VisibleForTesting internal class MinSdkVersion( |
| val flavor: String, |
| val version: Int |
| ) |