diff --git a/packages/flutter_tools/gradle/README.md b/packages/flutter_tools/gradle/README.md index bcf71abdee..acad1c7bdd 100644 --- a/packages/flutter_tools/gradle/README.md +++ b/packages/flutter_tools/gradle/README.md @@ -34,3 +34,6 @@ e.g `./gradlew test --tests "com.flutter.gradle.BaseApplicationNameHandlerTest.s Sometimes changing a test name and then running it will cause an IDE error. To get Android Studio back to a good state on Mac, run `Help > "Repair IDE"`, and then in the popup window `"Rescan project indexes > Everything works now."` + +To add a new test, add a class under `src/test/kotlin`, with methods annotated with `@Test`. +These tests will get automatically run on CI by `packages/flutter_tools/test/integration.shard/android_run_flutter_gradle_plugin_tests_test.dart`. diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 56d4629b9b..554500d51f 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -59,6 +59,8 @@ kotlin { } dependencies { + compileOnly("androidx.annotation:annotation-jvm:1.9.1") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10") // When bumping, also update: // * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/groovy/flutter.groovy // * AGP version in the buildscript block in packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts @@ -68,4 +70,5 @@ dependencies { testImplementation(kotlin("test")) testImplementation("com.android.tools.build:gradle:8.7.3") testImplementation("org.mockito:mockito-core:4.8.0") + testImplementation("io.mockk:mockk:1.13.16") } diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 734fb2e23e..c247e4430e 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -6,6 +6,7 @@ import com.android.build.OutputFile import com.flutter.gradle.BaseApplicationNameHandler import com.flutter.gradle.Deeplink +import com.flutter.gradle.DependencyVersionChecker import com.flutter.gradle.IntentFilterCheck import groovy.json.JsonGenerator import groovy.xml.QName @@ -300,10 +301,7 @@ class FlutterPlugin implements Plugin { final Boolean shouldSkipDependencyChecks = project.hasProperty("skipDependencyChecks") && project.getProperty("skipDependencyChecks") if (!shouldSkipDependencyChecks) { try { - final String dependencyCheckerPluginPath = Paths.get(flutterRoot.absolutePath, - "packages", "flutter_tools", "gradle", "src", "main", "kotlin_scripts", - "dependency_version_checker.gradle.kts") - project.apply from: dependencyCheckerPluginPath + DependencyVersionChecker.checkDependencyVersions(project) } catch (Exception e) { if (!project.hasProperty("usesUnsupportedDependencyVersions") || !project.usesUnsupportedDependencyVersions) { // Possible bug in dependency checking code - warn and do not block build. diff --git a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt new file mode 100644 index 0000000000..580203cc4a --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt @@ -0,0 +1,331 @@ +package com.flutter.gradle + +import androidx.annotation.VisibleForTesting +import com.android.build.api.AndroidPluginVersion +import com.android.build.api.variant.AndroidComponentsExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra +import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper + +object DependencyVersionChecker { + @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" + + // 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 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 { + return "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=`. \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 { + return "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:'\".\n" + } + + @VisibleForTesting internal fun getPotentialKGPFix(projectDirectory: String): String { + return "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" + } + + // 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(7, 3, 1) + + @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) + + /** + * 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(getGradleVersion(project), project) + checkJavaVersion(getJavaVersion(), project) + val agpVersion: AndroidPluginVersion? = 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? = 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. + } + + // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.invocation/-gradle/index.html#-837060600%2FFunctions%2F-1793262594 + @VisibleForTesting internal fun getGradleVersion(project: Project): Version { + val untrimmedGradleVersion: String = project.gradle.gradleVersion + // Trim to handle candidate gradle versions (example 7.6-rc-4). This means we treat all + // candidate versions of gradle as the same as their base version + // (i.e., "7.6"="7.6-rc-4"). + return Version.fromString(untrimmedGradleVersion.substringBefore('-')) + } + + // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api/-java-version/index.html#-1790786897%2FFunctions%2F-1793262594 + @VisibleForTesting internal fun getJavaVersion(): JavaVersion { + return JavaVersion.current() + } + + @VisibleForTesting internal fun getAGPVersion(project: Project): AndroidPluginVersion? { + val androidPluginVersion: AndroidPluginVersion? = + project.extensions.findByType( + AndroidComponentsExtension::class.java + )?.pluginVersion + return androidPluginVersion + } + + // TODO(gmackall): AGP has a getKotlinAndroidPluginVersion(), and KGP has a + // getKotlinPluginVersion(). Consider replacing this implementation with one of + // those. + @VisibleForTesting internal fun getKGPVersion(project: Project): Version? { + val kotlinVersionProperty = "kotlin_version" + val firstKotlinVersionFieldName = "pluginVersion" + val secondKotlinVersionFieldName = "kotlinPluginVersion" + // This property corresponds to application of the Kotlin Gradle plugin in the + // top-level build.gradle file. + if (project.hasProperty(kotlinVersionProperty)) { + return Version.fromString(project.properties[kotlinVersionProperty] as String) + } + val kotlinPlugin = + project.plugins + .findPlugin(KotlinAndroidPluginWrapper::class.java) + val versionField = + kotlinPlugin?.javaClass?.kotlin?.members?.first { + it.name == firstKotlinVersionFieldName || it.name == secondKotlinVersionFieldName + } + val versionString = versionField?.call(kotlinPlugin) + return if (versionString == null) { + null + } else { + Version.fromString(versionString as String) + } + } + + @VisibleForTesting internal fun getErrorMessage( + dependencyName: String, + versionString: String, + errorVersion: String, + potentialFix: String + ): String { + return "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 { + return "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 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) + } + } +} + +// Helper class to parse the versions that are provided as plain strings (Gradle, Kotlin) and +// perform easy comparisons. All versions will have a major, minor, and patch value. These values +// default to 0 when they are not provided or are otherwise unparseable. +// For example the version strings "8.2", "8.2.2hfd", and "8.2.0" would parse to the same version. +internal class Version(val major: Int, val minor: Int, val patch: Int) : Comparable { + companion object { + fun fromString(version: String): Version { + val asList: List = version.split(".") + val convertedToNumbers: List = asList.map { it.toIntOrNull() ?: 0 } + return Version( + major = convertedToNumbers.getOrElse(0) { 0 }, + minor = convertedToNumbers.getOrElse(1) { 0 }, + patch = convertedToNumbers.getOrElse(2) { 0 } + ) + } + } + + override fun compareTo(other: Version): Int { + if (major != other.major) { + return major - other.major + } + if (minor != other.minor) { + return minor - other.minor + } + if (patch != other.patch) { + return patch - other.patch + } + return 0 + } + + override fun toString(): String { + return "$major.$minor.$patch" + } +} + +// 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) diff --git a/packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts b/packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts deleted file mode 100644 index d785fb43b5..0000000000 --- a/packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts +++ /dev/null @@ -1,370 +0,0 @@ -// 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. - -import org.gradle.api.JavaVersion -import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper - -// This buildscript block supplies dependencies for this file's own import -// declarations above. It exists solely for compatibility with projects that -// have not migrated to declaratively apply the Flutter Gradle Plugin; -// for those that have, FGP's `build.gradle.kts` takes care of this. -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - // When bumping, also update: - // * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/groovy/flutter.groovy - // * AGP version in the buildscript block in packages/flutter_tools/gradle/src/main/groovy/flutter.groovy - // * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart - // * AGP version in dependencies block in packages/flutter_tools/gradle/build.gradle.kts - classpath("com.android.tools.build:gradle:7.3.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10") - } -} - -apply() - -class FlutterDependencyCheckerPlugin : Plugin { - override fun apply(project: Project) { - DependencyVersionChecker.checkDependencyVersions(project) - } -} - -class DependencyVersionChecker { - companion object { - private const val GRADLE_NAME: String = "Gradle" - private const val JAVA_NAME: String = "Java" - private const val AGP_NAME: String = "Android Gradle Plugin" - private const val KGP_NAME: String = "Kotlin" - - // 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. - private const val OUT_OF_SUPPORT_RANGE_PROPERTY = "usesUnsupportedDependencyVersions" - - // 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. - private fun getPotentialGradleFix(projectDirectory: String): String { - return "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. - private const val POTENTIAL_JAVA_FIX: String = - "The Java version used by Flutter can be " + - "set with `flutter config --jdk-dir=`. \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" - - private fun getPotentialAGPFix(projectDirectory: String): String { - return "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:'\".\n" - } - - private fun getPotentialKGPFix(projectDirectory: String): String { - return "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" - } - - // 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. - val warnGradleVersion: Version = Version(7, 4, 2) - val errorGradleVersion: Version = Version(7, 0, 2) - - val warnJavaVersion: JavaVersion = JavaVersion.VERSION_11 - val errorJavaVersion: JavaVersion = JavaVersion.VERSION_1_1 - - val warnAGPVersion: Version = Version(7, 3, 1) - val errorAGPVersion: Version = Version(7, 0, 0) - - val warnKGPVersion: Version = Version(1, 8, 10) - val errorKGPVersion: Version = Version(1, 7, 0) - - /** - * 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. - */ - fun checkDependencyVersions(project: Project) { - project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) - var agpVersion: Version? - var kgpVersion: Version? - - checkGradleVersion(getGradleVersion(project), project) - checkJavaVersion(getJavaVersion(), project) - agpVersion = 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." - ) - } - - kgpVersion = 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. - } - - // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.invocation/-gradle/index.html#-837060600%2FFunctions%2F-1793262594 - fun getGradleVersion(project: Project): Version { - val untrimmedGradleVersion: String = project.gradle.getGradleVersion() - // Trim to handle candidate gradle versions (example 7.6-rc-4). This means we treat all - // candidate versions of gradle as the same as their base version - // (i.e., "7.6"="7.6-rc-4"). - return Version.fromString(untrimmedGradleVersion.substringBefore('-')) - } - - // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api/-java-version/index.html#-1790786897%2FFunctions%2F-1793262594 - fun getJavaVersion(): JavaVersion { - return JavaVersion.current() - } - - // This approach is taken from AGP's own version checking plugin: - // https://android.googlesource.com/platform/tools/base/+/1839aa23b8dc562005e2f0f0cc8e8b4c5caa37d0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/utils/agpVersionChecker.kt#58. - fun getAGPVersion(project: Project): Version? { - val agpPluginName: String = "com.android.base" - val agpVersionFieldName: String = "ANDROID_GRADLE_PLUGIN_VERSION" - var agpVersion: Version? - try { - agpVersion = - Version.fromString( - project.plugins.getPlugin(agpPluginName)::class.java.classLoader.loadClass( - com.android.Version::class.java.name - ).fields.find { it.name == agpVersionFieldName }!! - .get(null) as String - ) - } catch (ignored: ClassNotFoundException) { - // Use deprecated Version class as it exists in older AGP (com.android.Version) does - // not exist in those versions. - @Suppress("deprecation") - agpVersion = - Version.fromString( - project.plugins.getPlugin(agpPluginName)::class.java.classLoader.loadClass( - com.android.builder.model.Version::class.java.name - ).fields.find { it.name == agpVersionFieldName }!! - .get(null) as String - ) - } - return agpVersion - } - - fun getKGPVersion(project: Project): Version? { - val kotlinVersionProperty: String = "kotlin_version" - val firstKotlinVersionFieldName: String = "pluginVersion" - val secondKotlinVersionFieldName: String = "kotlinPluginVersion" - // This property corresponds to application of the Kotlin Gradle plugin in the - // top-level build.gradle file. - if (project.hasProperty(kotlinVersionProperty)) { - return Version.fromString(project.properties.get(kotlinVersionProperty) as String) - } - val kotlinPlugin = - project.getPlugins() - .findPlugin(KotlinAndroidPluginWrapper::class.java) - val versionfield = - kotlinPlugin?.javaClass?.kotlin?.members?.first { - it.name == firstKotlinVersionFieldName || it.name == secondKotlinVersionFieldName - } - val versionString = versionfield?.call(kotlinPlugin) - if (versionString == null) { - return null - } else { - return Version.fromString(versionString as String) - } - } - - private fun getErrorMessage( - dependencyName: String, - versionString: String, - errorVersion: String, - potentialFix: String - ): String { - return "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" - } - - private fun getWarnMessage( - dependencyName: String, - versionString: String, - warnVersion: String, - potentialFix: String - ): String { - return "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" - } - - fun checkGradleVersion( - version: Version, - project: Project - ) { - if (version < errorGradleVersion) { - val errorMessage: String = - getErrorMessage( - GRADLE_NAME, - version.toString(), - errorGradleVersion.toString(), - getPotentialGradleFix(project.getRootDir().getPath()) - ) - 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.getRootDir().getPath()) - ) - project.logger.error(warnMessage) - } - } - - 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) - } - } - - fun checkAGPVersion( - version: Version, - project: Project - ) { - if (version < errorAGPVersion) { - val errorMessage: String = - getErrorMessage( - AGP_NAME, - version.toString(), - errorAGPVersion.toString(), - getPotentialAGPFix(project.getRootDir().getPath()) - ) - project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) - throw DependencyValidationException(errorMessage) - } else if (version < warnAGPVersion) { - val warnMessage: String = - getWarnMessage( - AGP_NAME, - version.toString(), - warnAGPVersion.toString(), - getPotentialAGPFix(project.getRootDir().getPath()) - ) - project.logger.error(warnMessage) - } - } - - fun checkKGPVersion( - version: Version, - project: Project - ) { - if (version < errorKGPVersion) { - val errorMessage: String = - getErrorMessage( - KGP_NAME, - version.toString(), - errorKGPVersion.toString(), - getPotentialKGPFix(project.getRootDir().getPath()) - ) - 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.getRootDir().getPath()) - ) - project.logger.error(warnMessage) - } - } - } -} - -// Helper class to parse the versions that are provided as plain strings (Gradle, Kotlin) and -// perform easy comparisons. All versions will have a major, minor, and patch value. These values -// default to 0 when they are not provided or are otherwise unparseable. -// For example the version strings "8.2", "8.2.2hfd", and "8.2.0" would parse to the same version. -class Version(val major: Int, val minor: Int, val patch: Int) : Comparable { - companion object { - fun fromString(version: String): Version { - val asList: List = version.split(".") - val convertedToNumbers: List = asList.map { it.toIntOrNull() ?: 0 } - return Version( - major = convertedToNumbers.getOrElse(0, { 0 }), - minor = convertedToNumbers.getOrElse(1, { 0 }), - patch = convertedToNumbers.getOrElse(2, { 0 }) - ) - } - } - - override fun compareTo(other: Version): Int { - if (major != other.major) { - return major - other.major - } - if (minor != other.minor) { - return minor - other.minor - } - if (patch != other.patch) { - return patch - other.patch - } - return 0 - } - - override fun toString(): String { - return major.toString() + "." + minor.toString() + "." + patch.toString() - } -} - -// Custom error for when the dependency_version_checker.kts script finds a dependency out of -// the defined support range. -class DependencyValidationException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { - constructor(cause: Throwable) : this(null, cause) -} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt new file mode 100644 index 0000000000..942d4b1e8f --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt @@ -0,0 +1,259 @@ +package com.flutter.gradle + +import com.android.build.api.AndroidPluginVersion +import com.android.build.api.variant.AndroidComponentsExtension +import com.flutter.gradle.DependencyVersionChecker.AGP_NAME +import com.flutter.gradle.DependencyVersionChecker.GRADLE_NAME +import com.flutter.gradle.DependencyVersionChecker.JAVA_NAME +import com.flutter.gradle.DependencyVersionChecker.KGP_NAME +import com.flutter.gradle.DependencyVersionChecker.OUT_OF_SUPPORT_RANGE_PROPERTY +import com.flutter.gradle.DependencyVersionChecker.POTENTIAL_JAVA_FIX +import com.flutter.gradle.DependencyVersionChecker.errorAGPVersion +import com.flutter.gradle.DependencyVersionChecker.errorGradleVersion +import com.flutter.gradle.DependencyVersionChecker.errorKGPVersion +import com.flutter.gradle.DependencyVersionChecker.getErrorMessage +import com.flutter.gradle.DependencyVersionChecker.getPotentialAGPFix +import com.flutter.gradle.DependencyVersionChecker.getPotentialGradleFix +import com.flutter.gradle.DependencyVersionChecker.getPotentialKGPFix +import com.flutter.gradle.DependencyVersionChecker.getWarnMessage +import com.flutter.gradle.DependencyVersionChecker.warnAGPVersion +import com.flutter.gradle.DependencyVersionChecker.warnGradleVersion +import com.flutter.gradle.DependencyVersionChecker.warnJavaVersion +import com.flutter.gradle.DependencyVersionChecker.warnKGPVersion +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.gradle.api.plugins.ExtraPropertiesExtension +import org.gradle.internal.extensions.core.extra +import kotlin.test.Test +import kotlin.test.assertFailsWith + +const val FAKE_PROJECT_ROOT_DIR = "/fake/root/dir" + +// The following values will need to be modified when the corresponding "warn$DepName" versions +// are updated in DependencyVersionChecker.kt +const val SUPPORTED_GRADLE_VERSION: String = "7.4.2" +val SUPPORTED_JAVA_VERSION: JavaVersion = JavaVersion.VERSION_11 +val SUPPORTED_AGP_VERSION: AndroidPluginVersion = AndroidPluginVersion(7, 3, 1) +const val SUPPORTED_KGP_VERSION: String = "1.8.10" + +class DependencyVersionCheckerTest { + @Test + fun `AGP version in error range results in DependencyValidationException`() { + val exampleErrorAgpVersion = AndroidPluginVersion(4, 2, 0) + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleErrorAgpVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit + + val dependencyValidationException = + assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } + assert( + dependencyValidationException.message == + getErrorMessage( + AGP_NAME, + exampleErrorAgpVersion.toString(), + errorAGPVersion.toString(), + getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + @Test + fun `AGP version in warn range results in warning logs`() { + val exampleWarnAgpVersion = AndroidPluginVersion(7, 1, 0) + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleWarnAgpVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit + val mockLogger = mockProject.logger + every { mockLogger.error(any()) } returns Unit + + DependencyVersionChecker.checkDependencyVersions(mockProject) + verify { + mockLogger.error( + getWarnMessage( + AGP_NAME, + exampleWarnAgpVersion.toString(), + warnAGPVersion.toString(), + getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + } + verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + @Test + fun `KGP version in error range results in DependencyValidationException`() { + val exampleErrorKgpVersion = "1.6.0" + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleErrorKgpVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit + + val dependencyValidationException = + assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } + + println(dependencyValidationException.message) + assert( + dependencyValidationException.message == + getErrorMessage( + KGP_NAME, + exampleErrorKgpVersion, + errorKGPVersion.toString(), + getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + @Test + fun `KGP version in warn range results in warning logs`() { + val exampleWarnKgpVersion = "1.8.0" + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleWarnKgpVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit + val mockLogger = mockProject.logger + every { mockLogger.error(any()) } returns Unit + + DependencyVersionChecker.checkDependencyVersions(mockProject) + verify { + mockLogger.error( + getWarnMessage( + KGP_NAME, + exampleWarnKgpVersion, + warnKGPVersion.toString(), + getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + } + verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + // No test for Java version in error range, as the lowest supported Java version is also the + // lowest possible. + + @Test + fun `Java version in warn range results in warning logs`() { + val exampleWarnJavaVersion = JavaVersion.VERSION_1_8 + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(javaVersion = exampleWarnJavaVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit + val mockLogger = mockProject.logger + every { mockLogger.error(any()) } returns Unit + + DependencyVersionChecker.checkDependencyVersions(mockProject) + verify { + mockLogger.error( + getWarnMessage( + JAVA_NAME, + exampleWarnJavaVersion.toString(), + warnJavaVersion.toString(), + POTENTIAL_JAVA_FIX + ) + ) + } + verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + @Test + fun `Gradle version in error range results in DependencyValidationException`() { + val exampleErrorGradleVersion = "7.0.0" + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleErrorGradleVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit + + val dependencyValidationException = + assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } + + assert( + dependencyValidationException.message == + getErrorMessage( + GRADLE_NAME, + exampleErrorGradleVersion, + errorGradleVersion.toString(), + getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } + + @Test + fun `Gradle version in warn range results in warning logs`() { + val exampleWarnGradleVersion = "7.4.0" + val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleWarnGradleVersion) + + val mockExtraPropertiesExtension = mockProject.extra + every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit + val mockLogger = mockProject.logger + every { mockLogger.error(any()) } returns Unit + + DependencyVersionChecker.checkDependencyVersions(mockProject) + verify { + mockLogger.error( + getWarnMessage( + GRADLE_NAME, + exampleWarnGradleVersion, + warnGradleVersion.toString(), + getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) + ) + ) + } + verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } + } +} + +// There isn't a way to create a real org.gradle.api.Project object for testing unfortunately, so +// these tests rely heavily on mocking. +// +// TODO(gmackall): We should consider adding functional tests built on top of a testing framework +// perhaps like +// https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit +// as a way to fill this gap in testing (combined with moving functionality to individual tasks +// that can be tested independently). +object MockProjectFactory { + fun createMockProjectWithSpecifiedDependencyVersions( + javaVersion: JavaVersion = SUPPORTED_JAVA_VERSION, + gradleVersion: String = SUPPORTED_GRADLE_VERSION, + agpVersion: AndroidPluginVersion = SUPPORTED_AGP_VERSION, + kgpVersion: String = SUPPORTED_KGP_VERSION + ): Project { + // Java + mockkStatic(JavaVersion::class) + every { JavaVersion.current() } returns javaVersion + + // Gradle + val mockProject = mockk() + every { mockProject.gradle.gradleVersion } returns gradleVersion + + // AGP + val mockAndroidComponentsExtension = mockk>() + every { mockProject.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + every { mockAndroidComponentsExtension.pluginVersion } returns agpVersion + + // KGP + every { mockProject.hasProperty(eq("kotlin_version")) } returns true + every { mockProject.properties["kotlin_version"] } returns kgpVersion + + // Logger + val mockLogger = mockk() + every { mockProject.logger } returns mockLogger + + // Extra properties extension + val mockExtraPropertiesExtension = mockk() + every { mockProject.extra } returns mockExtraPropertiesExtension + + // Project path + every { mockProject.rootDir.path } returns FAKE_PROJECT_ROOT_DIR + + return mockProject + } +}