From f20bc39cc0051771e140fcc0b3ea993b300ddfaa Mon Sep 17 00:00:00 2001 From: Reid Baker <1063596+reidbaker@users.noreply.github.com> Date: Fri, 18 Apr 2025 16:32:08 +0000 Subject: [PATCH] Add kotlin compatability to build file validation (#167143) Fixes https://github.com/flutter/flutter/issues/161443 * adds a new gradle task like `javaVersion` named `kgpVersion` that prints the version of kgp. * adds gradle_utils.dart method for getting kgp version * * kgp method is moved to utilities and we attempt to use a plugin method before reflection. * adds methods or evaluating KGP + gradle and KGP + AGP compatibility. * * It turns out that we have been using incompatible versions that happen to work with the subset of kotlin we are using. * Uses new kgp methods in flutter analyze --suggestions as part of the existing agp/java/gradle compatibility matrix. * adds new tests for all new functionality * Adds comments to sections of the code I found could use them. * Modifies flutter gallery to use a compatible version of kotlin and update its lockfiles. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../android/buildscript-gradle.lockfile | 43 +-- .../android/project-integration_test.lockfile | 7 +- .../project-url_launcher_android.lockfile | 2 +- .../flutter_gallery/android/settings.gradle | 2 +- .../main/kotlin/DependencyVersionChecker.kt | 102 +----- .../gradle/src/main/kotlin/FlutterPlugin.kt | 1 + .../src/main/kotlin/FlutterPluginUtils.kt | 27 +- .../gradle/src/main/kotlin/VersionFetcher.kt | 121 +++++++ .../src/test/kotlin/FlutterPluginUtilsTest.kt | 20 +- .../src/test/kotlin/VersionFetcherTest.kt | 67 ++++ .../lib/src/android/gradle_utils.dart | 330 +++++++++++++++++- packages/flutter_tools/lib/src/project.dart | 57 ++- .../android/gradle_utils_test.dart | 265 +++++++++++++- .../test/general.shard/project_test.dart | 125 ++++++- .../analyze_suggestions_integration_test.dart | 18 +- 15 files changed, 1015 insertions(+), 172 deletions(-) create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt create mode 100644 packages/flutter_tools/gradle/src/test/kotlin/VersionFetcherTest.kt diff --git a/dev/integration_tests/flutter_gallery/android/buildscript-gradle.lockfile b/dev/integration_tests/flutter_gallery/android/buildscript-gradle.lockfile index 545abc59e92..4de84865dd9 100644 --- a/dev/integration_tests/flutter_gallery/android/buildscript-gradle.lockfile +++ b/dev/integration_tests/flutter_gallery/android/buildscript-gradle.lockfile @@ -113,35 +113,30 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=classpath org.glassfish.jaxb:txw2:2.3.2=classpath org.jdom:jdom2:2.0.6=classpath org.jetbrains.intellij.deps:trove4j:1.0.20200330=classpath -org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:1.8.10=classpath -org.jetbrains.kotlin:kotlin-android-extensions:1.8.10=classpath -org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.8.10=classpath -org.jetbrains.kotlin:kotlin-build-common:1.8.10=classpath -org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.10=classpath -org.jetbrains.kotlin:kotlin-compiler-runner:1.8.10=classpath -org.jetbrains.kotlin:kotlin-daemon-client:1.8.10=classpath -org.jetbrains.kotlin:kotlin-daemon-embeddable:1.8.10=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.8.10=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.8.10=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.8.10=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.8.10=classpath -org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10=classpath -org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.8.10=classpath -org.jetbrains.kotlin:kotlin-native-utils:1.8.10=classpath -org.jetbrains.kotlin:kotlin-project-model:1.8.10=classpath +org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:2.1.10=classpath +org.jetbrains.kotlin:kotlin-build-statistics:2.1.10=classpath +org.jetbrains.kotlin:kotlin-build-tools-api:2.1.10=classpath +org.jetbrains.kotlin:kotlin-compiler-runner:2.1.10=classpath +org.jetbrains.kotlin:kotlin-daemon-client:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10=classpath +org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.1.10=classpath +org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.1.10=classpath +org.jetbrains.kotlin:kotlin-native-utils:2.1.10=classpath org.jetbrains.kotlin:kotlin-reflect:1.9.20=classpath -org.jetbrains.kotlin:kotlin-scripting-common:1.8.10=classpath -org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.8.10=classpath -org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.8.10=classpath -org.jetbrains.kotlin:kotlin-scripting-jvm:1.8.10=classpath org.jetbrains.kotlin:kotlin-stdlib-common:1.9.20=classpath org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20=classpath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20=classpath org.jetbrains.kotlin:kotlin-stdlib:1.9.20=classpath -org.jetbrains.kotlin:kotlin-tooling-core:1.8.10=classpath -org.jetbrains.kotlin:kotlin-util-io:1.8.10=classpath -org.jetbrains.kotlin:kotlin-util-klib:1.8.10=classpath -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath +org.jetbrains.kotlin:kotlin-tooling-core:2.1.10=classpath +org.jetbrains.kotlin:kotlin-util-io:2.1.10=classpath +org.jetbrains.kotlin:kotlin-util-klib-metadata:2.1.10=classpath +org.jetbrains.kotlin:kotlin-util-klib:2.1.10=classpath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=classpath org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.0=classpath org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.0=classpath org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.0=classpath diff --git a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile index c8c0c921411..005d2033a2d 100644 --- a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile @@ -111,8 +111,8 @@ javax.annotation:javax.annotation-api:1.3.2=_internal-unified-test-platform-andr javax.inject:javax.inject:1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath junit:junit:4.12=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -net.bytebuddy:byte-buddy:1.12.22=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -144,8 +144,7 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4=_internal-unified-test-platf org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains:annotations:13.0=_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle org.jetbrains:annotations:23.0.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.mockito:mockito-core:5.1.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.mockito:mockito-inline:5.1.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.mockito:mockito-core:5.8.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.objenesis:objenesis:3.3=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.1=androidJacocoAnt org.ow2.asm:asm-commons:9.1=androidJacocoAnt diff --git a/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile b/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile index 2db9276cffe..f4a787f8540 100644 --- a/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-url_launcher_android.lockfile @@ -30,7 +30,7 @@ androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndro androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test.espresso:espresso-idling-resource:3.5.1=debugUnitTestRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.test:annotation:1.0.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:core:1.0.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:core:1.4.0=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:monitor:1.6.1=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath diff --git a/dev/integration_tests/flutter_gallery/android/settings.gradle b/dev/integration_tests/flutter_gallery/android/settings.gradle index 4a155b35ec3..6a18a4852e5 100644 --- a/dev/integration_tests/flutter_gallery/android/settings.gradle +++ b/dev/integration_tests/flutter_gallery/android/settings.gradle @@ -35,7 +35,7 @@ buildscript { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.7.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.10" apply false + id "org.jetbrains.kotlin.android" version "2.1.10" apply false } include ":app" diff --git a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt index 11cab0ea636..b177c058394 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt @@ -12,8 +12,13 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.logging.Logger import org.gradle.kotlin.dsl.extra -import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper +/** + * 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" @@ -114,12 +119,12 @@ object DependencyVersionChecker { @JvmStatic fun checkDependencyVersions(project: Project) { project.extra.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) - checkGradleVersion(getGradleVersion(project), project) - checkJavaVersion(getJavaVersion(), project) + checkGradleVersion(VersionFetcher.getGradleVersion(project), project) + checkJavaVersion(VersionFetcher.getJavaVersion(), project) configureMinSdkCheck(project) - val agpVersion: AndroidPluginVersion? = getAGPVersion(project) + val agpVersion: AndroidPluginVersion? = VersionFetcher.getAGPVersion(project) if (agpVersion != null) { checkAGPVersion(agpVersion, project) } else { @@ -129,7 +134,7 @@ object DependencyVersionChecker { ) } - val kgpVersion: Version? = getKGPVersion(project) + val kgpVersion: Version? = VersionFetcher.getKGPVersion(project) if (kgpVersion != null) { checkKGPVersion(kgpVersion, project) } @@ -176,7 +181,7 @@ object DependencyVersionChecker { project: Project, it: Variant ): MinSdkVersion { - val agpVersion: AndroidPluginVersion? = getAGPVersion(project) + val agpVersion: AndroidPluginVersion? = VersionFetcher.getAGPVersion(project) return if (agpVersion != null && agpVersion.major >= 8 && agpVersion.minor >= 1) { MinSdkVersion(it.name, it.minSdk.apiLevel) } else { @@ -184,54 +189,6 @@ object DependencyVersionChecker { } } - // 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 = 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, @@ -394,43 +351,6 @@ object DependencyVersionChecker { } } -// 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 = "$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( diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 60907b10a5d..60c724ffcc4 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -314,6 +314,7 @@ class FlutterPlugin : Plugin { } FlutterPluginUtils.addTaskForJavaVersion(projectToAddTasksTo) + FlutterPluginUtils.addTaskForKGPVersion(projectToAddTasksTo) if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) { FlutterPluginUtils.addTaskForPrintBuildVariants(projectToAddTasksTo) FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 785c2396ecb..71effc96b15 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -12,7 +12,6 @@ import com.flutter.gradle.plugins.PluginHandler import groovy.lang.Closure import groovy.util.Node 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 @@ -625,7 +624,7 @@ object FlutterPluginUtils { // build artifact, so we move it from that directory to within Flutter's build directory // to avoid polluting source directories with build artifacts. // - // AGP explicitely recommends not setting the buildStagingDirectory to be within a build + // AGP explicitly recommends not setting the buildStagingDirectory to be within a build // directory in // https://developer.android.com/reference/tools/gradle-api/8.3/null/com/android/build/api/dsl/Cmake#buildStagingDirectory(kotlin.Any), // but as we are not actually building anything (and are instead only tricking AGP into @@ -675,7 +674,7 @@ object FlutterPluginUtils { if (!supportsBuildMode(project, flutterBuildMode)) { project.logger.quiet( "Project does not support Flutter build mode: $flutterBuildMode, " + - "skipping adding flutter dependencies" + "skipping adding Flutter dependencies" ) return } @@ -714,7 +713,7 @@ object FlutterPluginUtils { // ------------------ 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. + // 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 @@ -726,7 +725,25 @@ object FlutterPluginUtils { 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()) + println(VersionFetcher.getJavaVersion()) + } + } + } + + // Add a task that can be called on Flutter projects that prints the KGP version used in + // the project. + // + // Format of the output of this task can be used in debugging what version of KGP a + // project is using. + // Not recommended for use in time sensitive commands like `flutter run` or `flutter build` as + // Gradle tasks are slower than we want. Particularly in light of https://github.com/flutter/flutter/issues/119196. + @JvmStatic + @JvmName("addTaskForKGPVersion") + internal fun addTaskForKGPVersion(project: Project) { + project.tasks.register("kgpVersion") { + description = "Print the current kgp version used by the project." + doLast { + println("KGP Version: " + VersionFetcher.getKGPVersion(project).toString()) } } } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt b/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt new file mode 100644 index 00000000000..2ed6192f069 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt @@ -0,0 +1,121 @@ +// 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 com.android.build.api.AndroidPluginVersion +import com.android.build.api.variant.AndroidComponentsExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper + +internal object VersionFetcher { + /** + * Returns the version of the JVM. + */ + internal fun getJavaVersion(): JavaVersion = JavaVersion.current() + + /** + * Returns the version of Gradle. + */ + internal fun getGradleVersion(project: Project): Version { + // https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.invocation/-gradle/index.html#-837060600%2FFunctions%2F-1793262594 + 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('-')) + } + + /** + * Returns the version of the Android Gradle plugin. + */ + internal fun getAGPVersion(project: Project): AndroidPluginVersion? { + val androidPluginVersion: AndroidPluginVersion? = + project.extensions + .findByType( + AndroidComponentsExtension::class.java + )?.pluginVersion + return androidPluginVersion + } + + /** + * Returns the version of the Kotlin Gradle plugin. + */ + internal fun getKGPVersion(project: Project): Version? { + // TODO(gmackall): AGP has a getKotlinAndroidPluginVersion(), and KGP has a + // getKotlinPluginVersion(). Consider replacing this implementation with one of + // those. + 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) + // Partial implementation of getKotlinPluginVersion from the comment above. + var versionString: String? = kotlinPlugin?.pluginVersion + if (!versionString.isNullOrEmpty()) { + return Version.fromString(versionString) + } + // Fall back to reflection. + val versionField = + kotlinPlugin?.javaClass?.kotlin?.members?.firstOrNull { + it.name == firstKotlinVersionFieldName || it.name == secondKotlinVersionFieldName + } + versionString = versionField?.call(kotlinPlugin) as String? + return if (versionString == null) { + null + } else { + Version.fromString(versionString) + } + } +} + +/** + * 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 equals(other: Any?): Boolean = other is Version && compareTo(other) == 0 + + override fun hashCode(): Int = major.hashCode() or minor.hashCode() or patch.hashCode() + + override fun toString(): String = "$major.$minor.$patch" +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 88db3d3537f..ed96ef0a71d 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -886,7 +886,7 @@ class FlutterPluginUtilsTest { verify(exactly = 1) { project.logger.quiet( "Project does not support Flutter build mode: debug, " + - "skipping adding flutter dependencies" + "skipping adding Flutter dependencies" ) } } @@ -1049,6 +1049,24 @@ class FlutterPluginUtilsTest { } } + // addTaskForKGPVersion + @Test + fun `addTaskForKGPVersion adds task for KGP version`() { + val project = mockk() + every { project.tasks.register(any(), any>()) } returns mockk() + val captureSlot = slot>() + FlutterPluginUtils.addTaskForKGPVersion(project) + verify { project.tasks.register("kgpVersion", 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 kgp version used by the project." + } + } + // addTaskForPrintBuildVariants @Test fun `addTaskForPrintBuildVariants adds task for printing build variants`() { diff --git a/packages/flutter_tools/gradle/src/test/kotlin/VersionFetcherTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/VersionFetcherTest.kt new file mode 100644 index 00000000000..9df55fa3579 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/VersionFetcherTest.kt @@ -0,0 +1,67 @@ +// 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 com.android.build.api.AndroidPluginVersion +import com.android.build.api.variant.AndroidComponentsExtension +import io.mockk.every +import io.mockk.mockk +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper +import kotlin.test.Test +import kotlin.test.assertEquals + +class VersionFetcherTest { + // getGradleVersion + @Test + fun `getGradleVersion returns version when gradleVersion is set`() { + val gradleVersion = Version(1, 9, 20) + val project = mockk() + every { project.gradle.gradleVersion } returns gradleVersion.toString() + assertEquals(VersionFetcher.getGradleVersion(project), gradleVersion) + } + + @Test + fun `getGradleVersion returns version when gradleVersion has hyphen`() { + val project = mockk() + every { project.gradle.gradleVersion } returns "2.1.20-2" + assertEquals(VersionFetcher.getGradleVersion(project), Version(2, 1, 20)) + } + + // getAGPVersion + @Test + fun `getAGPVersion returns version when agpVersion is set`() { + val agpVersion = AndroidPluginVersion(8, 3, 0) + val project = mockk() + val mockAndroidComponentsExtension = mockk>() + every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + every { mockAndroidComponentsExtension.pluginVersion } returns agpVersion + assertEquals(VersionFetcher.getAGPVersion(project).toString(), agpVersion.toString()) + } + + // getKGPVersion + @Test + fun `getKGPVersion returns version when kotlin_version is set`() { + val kgpVersion = Version(1, 9, 20) + val project = mockk() + every { project.hasProperty(eq("kotlin_version")) } returns true + every { project.properties["kotlin_version"] } returns kgpVersion.toString() + val result = VersionFetcher.getKGPVersion(project) + assertEquals(kgpVersion, result!!) + } + + @Test + fun `getKGPVersion returns version from KotlinAndroidPluginWrapper`() { + val kgpVersion = Version(1, 9, 20) + val project = mockk() + every { project.hasProperty(eq("kotlin_version")) } returns false + every { project.plugins.findPlugin(KotlinAndroidPluginWrapper::class.java) } returns + mockk { + every { pluginVersion } returns kgpVersion.toString() + } + val result = VersionFetcher.getKGPVersion(project) + assertEquals(kgpVersion, result!!) + } +} diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index a3d78c321b5..e1f865905bd 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -8,6 +8,7 @@ import 'package:unified_analytics/unified_analytics.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; @@ -62,6 +63,12 @@ const String oneMajorVersionHigherJavaVersion = '24'; // flutter analyze --suggestions and does not imply broader flutter support. const String maxKnownAndSupportedGradleVersion = '8.12'; +// Update this with new KGP versions come out including minor versions. +// +// Supported here means supported by the tooling for +// flutter analyze --suggestions and does not imply broader flutter support. +const String maxKnownAndSupportedKgpVersion = '2.1.20'; + // Update this when new versions of AGP come out. // // Supported here means tooling is aware of this version's Java <-> AGP @@ -72,12 +79,30 @@ const String maxKnownAndSupportedAgpVersion = '8.7.3'; // Update this when new versions of AGP come out. const String maxKnownAgpVersion = '8.7.3'; +// Supported here means tooling is aware of this versions +// Java <-> AGP compatibility and does not imply broader flutter support. +// For use in flutter see the code in: +// flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt +@visibleForTesting +const String oldestConsideredAgpVersion = '3.3.0'; + +// Supported here means tooling is aware of this versions +// gradle compatibility and does not imply broader flutter support. +@visibleForTesting +const String oldestConsideredGradleVersion = '4.10.1'; + +// Supported here means tooling is aware of this versions +// gradle/AGP compatibility and does not imply broader flutter support. +@visibleForTesting +const String oldestDocumentedKgpCompatabilityVersion = '1.6.20'; + // Oldest documented version of AGP that has a listed minimum // compatible Java version. const String oldestDocumentedJavaAgpCompatibilityVersion = '4.2'; // Constant used in [_buildAndroidGradlePluginRegExp] and -// [_settingsAndroidGradlePluginRegExp] to identify the version section. +// [_settingsAndroidGradlePluginRegExp] and [_kotlinGradlePluginRegExpFromId] +// to identify the version section. const String _versionGroupName = 'version'; // AGP can be defined in the dependencies block of [build.gradle] or [build.gradle.kts]. @@ -103,6 +128,17 @@ final RegExp _androidGradlePluginRegExpFromId = RegExp( multiLine: true, ); +// KGP is defined in several places this code only checks in plugins block +// of [settings.gradle] and [settings.gradle.kts]. +// Expected content: +// Groovy DSL - id "org.jetbrains.kotlin.android" version "{{kgpVersion}}" +// Kotlin DSL - id("org.jetbrains.kotlin.android") version "{{kgpVersion}}" +// ? is used to name the version group which helps with extraction. +final RegExp _kotlinGradlePluginRegExpFromId = RegExp( + r"""[^\/]*s*id\s*\(?['"]org\.jetbrains\.kotlin\.android['"]\)?\s+version\s+['"](?\d+(\.\d+){1,2})\)?""", + multiLine: true, +); + // Expected content format (with lines above and below). // Version can have 2 or 3 numbers. // 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip' @@ -125,7 +161,8 @@ final RegExp tooOldMinSdkVersionMatch = RegExp( ); // From https://docs.gradle.org/current/userguide/command_line_interface.html#command_line_interface -const String gradleVersionFlag = r'--version'; +// Flag to print the versions for gradle, kotlin dsl, groovy, etc. +const String gradleVersionsFlag = r'--version'; // Directory under android/ that gradle uses to store gradle information. // Regularly used with [gradleWrapperDirectory] and @@ -274,7 +311,7 @@ Future getGradleVersion( return gradleVersion; } else { // Did not find gradle zip url. Likely this is a bug in our parsing. - logger.printWarning(_formatParseWarning(wrapperFileContent)); + logger.printWarning(_formatParseWarning(wrapperFileContent, type: 'gradle')); } } else { // If no distributionUrl log then treat as if there was no propertiesFile. @@ -287,9 +324,10 @@ Future getGradleVersion( logger.printTrace('$propertiesFile does not exist falling back to system gradle'); } // System installed Gradle version. + // TODO(reidbaker): Modify this gradle execution to use gradlew. if (processManager.canRun('gradle')) { - final String gradleVersionVerbose = - (await processManager.run(['gradle', gradleVersionFlag])).stdout as String; + final String gradleVersionsVerbose = + (await processManager.run(['gradle', gradleVersionsFlag])).stdout as String; // Expected format: /* @@ -311,10 +349,10 @@ OS: Mac OS X 13.2.1 aarch64 // Outer parentheticals `Gradle (...)` denote a grouping used to extract // the version number. final RegExp gradleVersionRegex = RegExp(r'Gradle\s+(\d+\.\d+(?:\.\d+)?)'); - final RegExpMatch? version = gradleVersionRegex.firstMatch(gradleVersionVerbose); + final RegExpMatch? version = gradleVersionRegex.firstMatch(gradleVersionsVerbose); if (version == null) { // Most likely a bug in our parse implementation/regex. - logger.printWarning(_formatParseWarning(gradleVersionVerbose)); + logger.printWarning(_formatParseWarning(gradleVersionsVerbose, type: 'gradle')); return null; } return version.group(1); @@ -324,6 +362,80 @@ OS: Mac OS X 13.2.1 aarch64 } } +/// Returns the Kotlin Gradle Plugin (KGP) version that the current project +/// depends on if found, null otherwise. +/// [directory] should be an android directory with a build.gradle file. +Future getKgpVersion( + Directory androidDirectory, + Logger logger, + ProcessManager processManager, +) async { + // Maintainers of the kotlin dsl and the kotlin gradle plugin are different. + // + // Android Docs refer to the kotlin gradle plugin with either the full name or KGP. + // Kotlin docs refer to the kotlin gradle plugin as kotlin android plugin. + // + // gradle --version or ./gradlew --version will print the kotlin dsl version. + // This version normally changes with the version of gradle. + // https://github.com/gradle/gradle/blob/cefbee263181a924ac4efcaace6bda97a55bc0f7/platforms/core-runtime/gradle-cli/src/main/java/org/gradle/launcher/cli/DefaultCommandLineActionFactory.java#L260 + // This vesion is NOT the version of KGP that the project uses. + // + // Instead the kgpVersion task is a custom flutter task dynamiclly added that can + // print the kgp version if gradle can run successfuly. + + if (processManager.canRun('./gradlew', workingDirectory: androidDirectory.path)) { + final ProcessResult command = await processManager.run([ + './gradlew', + 'kgpVersion', + '-q', + ], workingDirectory: androidDirectory.path); + if (command.exitCode == 0) { + final String kgpVersionOutput = command.stdout as String; + + // See expected output defined in + // flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt addTaskForKGPVersion + final RegExp kotlinVersionRegex = RegExp(r'KGP Version:\s+(\d+\.\d+(?:\.\d+)?)'); + final RegExpMatch? version = kotlinVersionRegex.firstMatch(kgpVersionOutput); + if (version != null) { + return version.group(1); + } + // Most likely a bug in our parse implementation/regex. + logger.printWarning(_formatParseWarning(kgpVersionOutput, type: 'kotlin')); + } else { + logger.printTrace('Non zero exit code from gradle task kgpVersion.'); + } + } else { + logger.printTrace('Could not run gradle task kgpVersion.'); + } + + // Project valiation code is regularly run on projects that can not build. + // Because of that this code also attempts to search through known template + // locations for kotlin versions. + + logger.printTrace('Checking settings for kgp version.'); + File settingsFile = androidDirectory.childFile('settings.gradle'); + if (!settingsFile.existsSync()) { + settingsFile = androidDirectory.childFile('settings.gradle.kts'); + } + + if (settingsFile.existsSync()) { + final String settingsFileContent = settingsFile.readAsStringSync(); + final RegExpMatch? settingsMatch = _kotlinGradlePluginRegExpFromId.firstMatch( + settingsFileContent, + ); + + if (settingsMatch != null) { + final String? kgpVersion = settingsMatch.namedGroup(_versionGroupName); + logger.printTrace('$settingsFile provides KGP version: $kgpVersion'); + return kgpVersion; + } + } else { + logger.printTrace('No settings.gradle.kts'); + } + + return null; +} + /// Returns the Android Gradle Plugin (AGP) version that the current project /// depends on when found, null otherwise. /// @@ -378,14 +490,198 @@ String? getAgpVersion(Directory androidDirectory, Logger logger) { return null; } -String _formatParseWarning(String content) { - return 'Could not parse gradle version from: \n' +String _formatParseWarning(String content, {required String type}) { + return 'Could not parse $type version from: \n' '$content \n' 'If there is a version please look for an existing bug ' 'https://github.com/flutter/flutter/issues/' ' and if one does not exist file a new issue.'; } +// Validate that KGP and Gradle are compatible with each other. +// +// Returns true if versions are compatible. +// Null or empty Gradle or KGP version returns false. +// If compatibility cannot be evaluated returns false. +// If versions are newer than the max known version a warning is logged and true +// returned. +// +// Source of truth found here: +// https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin +bool validateGradleAndKGP(Logger logger, {required String? kgpV, required String? gradleV}) { + if (gradleV == null || kgpV == null || gradleV.isEmpty || kgpV.isEmpty) { + logger.printTrace('Gradle or KGP version unknown ($gradleV, $kgpV).'); + return false; + } + + if (isWithinVersionRange(gradleV, min: '0.0', max: oldestConsideredGradleVersion)) { + logger.printTrace( + 'Gradle version $gradleV older than oldest considered $oldestConsideredGradleVersion', + ); + return false; + } + + if (isWithinVersionRange( + kgpV, + min: maxKnownAndSupportedKgpVersion, + max: '100.100', + inclusiveMin: false, + )) { + logger.printTrace( + 'Newer than known KGP version ($kgpV), gradle ($gradleV).' + '\n Treating as valid configuration.', + ); + return true; + } + + // https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin + // Documenation is non continuous, past versions are known to the + // publishers of KGP. When covering version ranges beyond what is documented + // add a comment with the documented value. + // Continuous KGP version handling is prefered in case an emergency patch to a + // past release is shipped this code will assume the version range that is closest. + if (isWithinVersionRange(kgpV, min: '2.1.20', max: '2.1.20')) { + // Documented max is 8.11, using 8.12 non inclusive covers patch versions. + return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.12', inclusiveMax: false); + } + if (isWithinVersionRange(kgpV, min: '2.1.0', max: '2.1.10')) { + // Documented max is 8.10, using 8.11 non inclusive covers patch versions. + return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.11', inclusiveMax: false); + } + // Documented max is 2.0.21. + if (isWithinVersionRange(kgpV, min: '2.0.20', max: '2.1', inclusiveMax: false)) { + // Documented max is 8.5, using 8.9 non inclusive covers patch versions. + // Kotlin Multiplatform can throw warnings on 8.8. + return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.9', inclusiveMax: false); + } + if (isWithinVersionRange(kgpV, min: '2.0', max: '2.0.20', inclusiveMax: false)) { + // Documented max is 8.5, using 8.6 non inclusive covers patch versions. + return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.6', inclusiveMax: false); + } + // Documented max is 1.9.25. + if (isWithinVersionRange(kgpV, min: '1.9.20', max: '2.0', inclusiveMax: false)) { + return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.1.1'); + } + // Documented max is 1.9.10. + if (isWithinVersionRange(kgpV, min: '1.8.20', max: '1.9.20', inclusiveMax: false)) { + return isWithinVersionRange(gradleV, min: '6.8.3', max: '7.6.0'); + } + // Documented max is 1.8.11. + if (isWithinVersionRange(kgpV, min: '1.8.0', max: '1.8.20', inclusiveMax: false)) { + return isWithinVersionRange(gradleV, min: '6.8.3', max: '7.3.3'); + } + // Documented max is 1.7.22. + if (isWithinVersionRange(kgpV, min: '1.7.20', max: '1.8.0', inclusiveMax: false)) { + return isWithinVersionRange(gradleV, min: '6.7.1', max: '7.1.1'); + } + // Documented max is 1.7.10. + if (isWithinVersionRange(kgpV, min: '1.7.0', max: '1.7.20', inclusiveMax: false)) { + return isWithinVersionRange(gradleV, min: '6.7.1', max: '7.0.2'); + } + // Documented max is 1.6.21. + if (isWithinVersionRange( + kgpV, + min: oldestDocumentedKgpCompatabilityVersion, + max: '1.7.0', + inclusiveMax: false, + )) { + return isWithinVersionRange(gradleV, min: '6.1.1', max: '7.0.2'); + } + + logger.printTrace('Unknown KGP-Gradle compatibility, KGP: $kgpV, Gradle: $gradleV'); + return false; +} + +// Validate that KGP and AGP are compatible with each other. +// +// Returns true if versions are compatible. +// Null or empty KGP or AGP version returns false. +// If compatibility cannot be evaluated returns false. +// If versions are newer than the max known version a warning is logged and true +// returned. +// +// Source of truth found here: +// https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin +bool validateAgpAndKgp(Logger logger, {required String? kgpV, required String? agpV}) { + if (agpV == null || kgpV == null || agpV.isEmpty || kgpV.isEmpty) { + logger.printTrace('KGP or AGP version unknown ($kgpV, $agpV).'); + return false; + } + + if (isWithinVersionRange(agpV, min: '0.0', max: oldestConsideredAgpVersion)) { + logger.printTrace( + 'AGP version ($agpV) older than oldest supported $oldestConsideredAgpVersion.', + ); + } + const String maxKnownAgpVersionWithFullKotinSupport = '8.7.2'; + + if (isWithinVersionRange( + kgpV, + min: maxKnownAndSupportedKgpVersion, + max: '100.100', + inclusiveMin: false, + ) || + isWithinVersionRange( + agpV, + min: maxKnownAgpVersionWithFullKotinSupport, + max: '100.100', + inclusiveMin: false, + )) { + logger.printTrace( + 'Newer than known KGP version ($kgpV), AGP ($agpV).' + '\n Treating as valid configuration.', + ); + return true; + } + + // https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin + // Documenation is non continuous, past versions are known to the + // publishers of KGP. When covering version ranges beyond what is documented + // add a comment with the documented value. + // Continuous KGP version handling is prefered in case an emergency patch to a + // past release is shipped this code will assume the version range that is closest. + if (isWithinVersionRange(kgpV, min: '2.1.0', max: '2.1.20')) { + return isWithinVersionRange(agpV, min: '7.3.1', max: '8.7.2'); + } + // Documented max is 2.0.21 + if (isWithinVersionRange(kgpV, min: '2.0.20', max: '2.1.0', inclusiveMax: false)) { + // Documented max is 8.5. + return isWithinVersionRange(agpV, min: '7.1.3', max: '8.6', inclusiveMax: false); + } + // Documented max is 2.0.0. + if (isWithinVersionRange(kgpV, min: '2.0.0', max: '2.0.20', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '7.1.3', max: '8.3.1'); + } + // Documented max is 1.9.25 + if (isWithinVersionRange(kgpV, min: '1.9.20', max: '2.0.0', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '4.2.2', max: '8.1.0'); + } + // Documented max is 1.9.10 + if (isWithinVersionRange(kgpV, min: '1.9.0', max: '1.9.20', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '4.2.2', max: '7.4.0'); + } + // Documented max is 1.8.22 + if (isWithinVersionRange(kgpV, min: '1.8.20', max: '1.9', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '4.1.3', max: '7.4.0'); + } + // Documented max is 1.8.11 + if (isWithinVersionRange(kgpV, min: '1.8.0', max: '1.8.20', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '4.1.3', max: '7.2.1'); + } + // Documented max is 1.7.22 + if (isWithinVersionRange(kgpV, min: '1.7.20', max: '1.8.0', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '3.6.4', max: '7.0.4'); + } + // Documented max is 1.7.10 + // Documented gap between 1.6.21 and 1.7.0. + if (isWithinVersionRange(kgpV, min: '1.6.20', max: '1.7.20', inclusiveMax: false)) { + return isWithinVersionRange(agpV, min: '3.4.3', max: '7.0.2'); + } + + logger.printTrace('Unknown KGP-Gradle compatibility, KGP: $kgpV, AGP: $agpV'); + return false; +} + // Validate that Gradle version and AGP are compatible with each other. // // Returns true if versions are compatible. @@ -399,23 +695,25 @@ String _formatParseWarning(String content) { // AGP has a minimum version of gradle required but no max starting at // AGP version 2.3.0+. bool validateGradleAndAgp(Logger logger, {required String? gradleV, required String? agpV}) { - const String oldestSupportedAgpVersion = '3.3.0'; - const String oldestSupportedGradleVersion = '4.10.1'; - if (gradleV == null || agpV == null) { logger.printTrace('Gradle version or AGP version unknown ($gradleV, $agpV).'); return false; } // First check if versions are too old. - if (isWithinVersionRange(agpV, min: '0.0', max: oldestSupportedAgpVersion, inclusiveMax: false)) { + if (isWithinVersionRange( + agpV, + min: '0.0', + max: oldestConsideredAgpVersion, + inclusiveMax: false, + )) { logger.printTrace('AGP Version: $agpV is too old.'); return false; } if (isWithinVersionRange( gradleV, min: '0.0', - max: oldestSupportedGradleVersion, + max: oldestConsideredGradleVersion, inclusiveMax: false, )) { logger.printTrace('Gradle Version: $gradleV is too old.'); @@ -499,7 +797,7 @@ bool validateGradleAndAgp(Logger logger, {required String? gradleV, required Str /// https://docs.gradle.org/current/userguide/compatibility.html#java bool validateJavaAndGradle(Logger logger, {required String? javaV, required String? gradleV}) { // https://docs.gradle.org/current/userguide/compatibility.html#java - const String oldestSupportedJavaVersion = '1.8'; + const String oldestConsideredJavaVersion = '1.8'; const String oldestDocumentedJavaGradleCompatibility = '2.0'; // Begin Java <-> Gradle validation. @@ -513,7 +811,7 @@ bool validateJavaAndGradle(Logger logger, {required String? javaV, required Stri if (isWithinVersionRange( javaV, min: '1.1', - max: oldestSupportedJavaVersion, + max: oldestConsideredJavaVersion, inclusiveMax: false, )) { logger.printTrace('Java Version: $javaV is too old.'); diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 4ef0d270790..3774d35a874 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -442,9 +442,9 @@ abstract class FlutterProjectPlatform { class AndroidProject extends FlutterProjectPlatform { AndroidProject._(this.parent); - // User facing string when java/gradle/agp versions are compatible. + // User facing string when java/gradle/agp/kgp versions are compatible. @visibleForTesting - static const String validJavaGradleAgpString = 'compatible java/gradle/agp'; + static const String validJavaGradleAgpKgpString = 'compatible java/gradle/agp/kgp'; // User facing link that describes compatibility between gradle and // android gradle plugin. @@ -456,6 +456,11 @@ class AndroidProject extends FlutterProjectPlatform { static const String javaGradleCompatUrl = 'https://docs.gradle.org/current/userguide/compatibility.html#java'; + // User facing link that describes compatibility between KGP and Gradle + // and AGP. + static const String kgpCompatUrl = + 'https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin'; + // User facing link that describes instructions for downloading // the latest version of Android Studio. static const String installAndroidStudioUrl = 'https://developer.android.com/studio/install'; @@ -681,7 +686,7 @@ class AndroidProject extends FlutterProjectPlatform { // Constructing ProjectValidatorResult happens here and not in // flutter_tools/lib/src/project_validator.dart because of the additional // Complexity of variable status values and error string formatting. - const String visibleName = 'Java/Gradle/Android Gradle Plugin'; + const String visibleName = 'Java/Gradle/KGP/Android Gradle Plugin'; final CompatibilityResult validJavaGradleAgpVersions = await hasValidJavaGradleAgpVersions(); return ProjectValidatorResult( @@ -695,8 +700,8 @@ class AndroidProject extends FlutterProjectPlatform { } /// Ensures Java SDK is compatible with the project's Gradle version and - /// the project's Gradle version is compatible with the AGP version used - /// in build.gradle. + /// the project's Gradle version is compatible with the AGP version and + /// kotlin version used in build.gradle. Future hasValidJavaGradleAgpVersions() async { final String? gradleVersion = await gradle.getGradleVersion( hostAppGradleRoot, @@ -705,9 +710,14 @@ class AndroidProject extends FlutterProjectPlatform { ); final String? agpVersion = gradle.getAgpVersion(hostAppGradleRoot, globals.logger); final String? javaVersion = versionToParsableString(globals.java?.version); + final String? kgpVersion = await gradle.getKgpVersion( + hostAppGradleRoot, + globals.logger, + globals.processManager, + ); // Assume valid configuration. - String description = validJavaGradleAgpString; + String description = validJavaGradleAgpKgpString; final bool compatibleGradleAgp = gradle.validateGradleAndAgp( globals.logger, @@ -721,6 +731,18 @@ class AndroidProject extends FlutterProjectPlatform { gradleV: gradleVersion, ); + final bool compatibleKgpGradle = gradle.validateGradleAndKGP( + globals.logger, + gradleV: gradleVersion, + kgpV: kgpVersion, + ); + + final bool compatibleAgpKgp = gradle.validateAgpAndKgp( + globals.logger, + agpV: agpVersion, + kgpV: kgpVersion, + ); + // Begin description formatting. if (!compatibleGradleAgp) { final String gradleDescription = @@ -745,7 +767,28 @@ See the link below for more information: $javaGradleCompatUrl '''; } - return CompatibilityResult(compatibleJavaGradle && compatibleGradleAgp, description); + if (!compatibleKgpGradle) { + description = ''' +${compatibleGradleAgp ? '' : description} +Incompatible KGP/Gradle versions. +Gradle Version: $gradleVersion, Kotlin Version: $kgpVersion\n +See the link below for more information: + $kgpCompatUrl +'''; + } + if (!compatibleAgpKgp) { + description = ''' +${compatibleGradleAgp ? '' : description} +Incompatible AGP/KGP versions. +AGP Version: $agpVersion, KGP Version: $kgpVersion\n +See the link below for more information: + $kgpCompatUrl +'''; + } + return CompatibilityResult( + compatibleJavaGradle && compatibleGradleAgp && compatibleKgpGradle && compatibleAgpKgp, + description, + ); } bool get isUsingGradle { diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart index 271d1bf302c..9c56ccf43aa 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart @@ -332,7 +332,10 @@ OS: Mac OS X 13.2.1 aarch64 final Directory androidDirectory = fileSystem.directory('/android')..createSync(); final ProcessManager processManager = FakeProcessManager.empty()..addCommand( - const FakeCommand(command: ['gradle', gradleVersionFlag], stdout: gradleOutput), + const FakeCommand( + command: ['gradle', gradleVersionsFlag], + stdout: gradleOutput, + ), ); expect( @@ -347,7 +350,10 @@ OS: Mac OS X 13.2.1 aarch64 final Directory androidDirectory = fileSystem.directory('/android')..createSync(); final ProcessManager processManager = FakeProcessManager.empty()..addCommand( - const FakeCommand(command: ['gradle', gradleVersionFlag], stdout: gradleOutput), + const FakeCommand( + command: ['gradle', gradleVersionsFlag], + stdout: gradleOutput, + ), ); expect( @@ -708,6 +714,247 @@ dependencies { } }); + FakeCommand createKgpVersionCommand(String kgpV) { + return FakeCommand( + command: const ['./gradlew', 'kgpVersion', '-q'], + stdout: ''' + KGP Version: $kgpV + ''', + ); + } + + testWithoutContext('returns the KGP fetched from kgpVersion gradle task', () async { + final Directory androidDirectory = fileSystem.directory('/android')..createSync(); + // Three numbered versions. + const String kgpV2 = '1.8.22'; + final FakeProcessManager processManager2 = FakeProcessManager.list([ + createKgpVersionCommand(kgpV2), + ]); + expect(await getKgpVersion(androidDirectory, BufferLogger.test(), processManager2), kgpV2); + // 2 numbered versions + const String kgpV3 = '1.9'; + final FakeProcessManager processManager3 = FakeProcessManager.list([ + createKgpVersionCommand(kgpV3), + ]); + expect(await getKgpVersion(androidDirectory, BufferLogger.test(), processManager3), kgpV3); + final FakeProcessManager processManagerNoGradle = FakeProcessManager.empty(); + processManagerNoGradle.excludedExecutables = {'./gradlew'}; + expect( + await getKgpVersion(androidDirectory, BufferLogger.test(), processManagerNoGradle), + null, + ); + }); + + testWithoutContext('returns the KGP version when in Kotlin settings as plugin', () async { + final Directory androidDirectory = fileSystem.directory('/android')..createSync(); + // File must exist and cannot have kgp defined. + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(r''); + androidDirectory.childFile('settings.gradle.kts').writeAsStringSync(r''' +pluginManagement { + plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + // Decoy value to ensure we ignore commented out lines. + // id("org.jetbrains.kotlin.android") version "6.1.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false + } +} +'''); + final FakeProcessManager processManager = FakeProcessManager.empty(); + processManager.excludedExecutables = {'./gradlew'}; + + expect(await getKgpVersion(androidDirectory, BufferLogger.test(), processManager), '1.8.22'); + }); + + group('validates kgp/gradle versions', () { + final List testData = [ + // Values too new. + GradleKgpTestData(true, kgpVersion: '3.0', gradleVersion: '99.99'), + + // Template versions of Gradle/AGP. + GradleKgpTestData( + true, + kgpVersion: templateKotlinGradlePluginVersion, + // TODO(reidbaker): replace with templateDefaultGradleVersion. + gradleVersion: '8.10', + ), + + // Kotlin version at the edge of support window. + GradleKgpTestData(true, kgpVersion: '2.1.20', gradleVersion: '8.1'), + GradleKgpTestData(true, kgpVersion: '2.1.10', gradleVersion: '8.3'), + GradleKgpTestData(true, kgpVersion: '2.0.21', gradleVersion: '7.6.3'), + GradleKgpTestData(true, kgpVersion: '2.0.20', gradleVersion: '7.6.3'), + GradleKgpTestData(true, kgpVersion: '2.0', gradleVersion: '8.5'), + GradleKgpTestData(true, kgpVersion: '1.9.25', gradleVersion: '8.1.1'), + GradleKgpTestData(true, kgpVersion: '1.9.20', gradleVersion: '6.8.3'), + GradleKgpTestData(true, kgpVersion: '1.9.10', gradleVersion: '6.8.3'), + GradleKgpTestData(true, kgpVersion: '1.9.0', gradleVersion: '7.6.0'), + GradleKgpTestData(true, kgpVersion: '1.8.22', gradleVersion: '7.6.0'), + GradleKgpTestData(true, kgpVersion: '1.8.20', gradleVersion: '6.8.3'), + GradleKgpTestData(true, kgpVersion: '1.8.11', gradleVersion: '7.3.3'), + GradleKgpTestData(true, kgpVersion: '1.8.0', gradleVersion: '7.3.3'), + GradleKgpTestData(true, kgpVersion: '1.7.22', gradleVersion: '7.1.1'), + GradleKgpTestData(true, kgpVersion: '1.7.20', gradleVersion: '6.7.1'), + GradleKgpTestData(true, kgpVersion: '1.7.10', gradleVersion: '7.0.2'), + GradleKgpTestData(true, kgpVersion: '1.7.0', gradleVersion: '6.7.1'), + GradleKgpTestData(true, kgpVersion: '1.6.21', gradleVersion: '6.1.1'), + GradleKgpTestData(true, kgpVersion: '1.6.20', gradleVersion: '7.0.2'), + // Gradle versions inspired by + // https://developer.android.com/build/releases/gradle-plugin#expandable-1 + GradleKgpTestData(true, kgpVersion: '2.1.20', gradleVersion: '8.11.1'), + GradleKgpTestData(true, kgpVersion: '2.1.10', gradleVersion: '8.10.2'), + GradleKgpTestData(true, kgpVersion: '2.1.10', gradleVersion: '8.9'), + GradleKgpTestData(true, kgpVersion: '2.1.5', gradleVersion: '8.7'), + GradleKgpTestData(true, kgpVersion: '2.1.0', gradleVersion: '8.7'), + GradleKgpTestData(true, kgpVersion: '2.0.20', gradleVersion: '8.6'), + GradleKgpTestData(true, kgpVersion: '2.0.1', gradleVersion: '8.4'), + GradleKgpTestData(true, kgpVersion: '2.0.0', gradleVersion: '8.2'), + GradleKgpTestData(true, kgpVersion: '1.9.25', gradleVersion: '8.0'), + GradleKgpTestData(true, kgpVersion: '1.9.10', gradleVersion: '7.6.0'), + GradleKgpTestData(true, kgpVersion: '1.9.7', gradleVersion: '7.5'), + GradleKgpTestData(true, kgpVersion: '1.8.21', gradleVersion: '7.4'), + GradleKgpTestData(true, kgpVersion: '1.9.0', gradleVersion: '7.3.3'), + GradleKgpTestData(true, kgpVersion: '1.8.0', gradleVersion: '7.2'), + GradleKgpTestData(true, kgpVersion: '1.7.0', gradleVersion: '7.0'), + GradleKgpTestData(true, kgpVersion: '2.0.21', gradleVersion: '7.0'), + GradleKgpTestData(true, kgpVersion: '1.7.22', gradleVersion: '6.7.1'), + GradleKgpTestData(true, kgpVersion: '1.6.21', gradleVersion: '6.7.1'), + GradleKgpTestData(true, kgpVersion: '1.6.21', gradleVersion: '6.5'), + // Kotlin newer than max known. + GradleKgpTestData(true, kgpVersion: '2.1.21', gradleVersion: '8.12.1'), + // Kotlin too new for gradle version. + GradleKgpTestData(false, kgpVersion: '2.1.20', gradleVersion: '7.6.2'), + GradleKgpTestData(false, kgpVersion: '2.1.0', gradleVersion: '7.6.2'), + GradleKgpTestData(false, kgpVersion: '2.0.20', gradleVersion: '6.8.2'), + GradleKgpTestData(false, kgpVersion: '1.9.0', gradleVersion: '6.8.2'), + GradleKgpTestData(false, kgpVersion: '1.8.0', gradleVersion: '6.8.2'), + GradleKgpTestData(false, kgpVersion: '1.7.22', gradleVersion: '6.7.0'), + GradleKgpTestData(false, kgpVersion: '1.7.0', gradleVersion: '6.1.1'), + // Kotlin too old for gradle version. + GradleKgpTestData(false, kgpVersion: '2.1.10', gradleVersion: '8.11.1'), + GradleKgpTestData(false, kgpVersion: '2.1.0', gradleVersion: '8.11'), + GradleKgpTestData(false, kgpVersion: '2.0.0', gradleVersion: '8.6'), + GradleKgpTestData(false, kgpVersion: '1.9.20', gradleVersion: '8.2'), + GradleKgpTestData(false, kgpVersion: '1.9.0', gradleVersion: '7.7'), + GradleKgpTestData(false, kgpVersion: '1.8.20', gradleVersion: '7.7'), + GradleKgpTestData(false, kgpVersion: '1.8.0', gradleVersion: '7.4'), + GradleKgpTestData(false, kgpVersion: '1.7.20', gradleVersion: '7.2'), + GradleKgpTestData(false, kgpVersion: '1.7.0', gradleVersion: '7.0.3'), + GradleKgpTestData(false, kgpVersion: '1.6.20', gradleVersion: '7.0.3'), + // Kotlin older than oldest supported. + GradleKgpTestData(false, kgpVersion: '1.6.19', gradleVersion: '7.0.3'), + // Gradle older than oldest supported. + GradleKgpTestData(false, kgpVersion: '1.6.20', gradleVersion: '4.10'), + // Null values: + // ignore: avoid_redundant_argument_values + GradleKgpTestData(false, kgpVersion: null, gradleVersion: '7.2'), + // ignore: avoid_redundant_argument_values + GradleKgpTestData(false, kgpVersion: '2.1', gradleVersion: null), + // ignore: avoid_redundant_argument_values + GradleKgpTestData(false, kgpVersion: '', gradleVersion: ''), + // ignore: avoid_redundant_argument_values + GradleKgpTestData(false, kgpVersion: null, gradleVersion: null), + ]; + for (final GradleKgpTestData data in testData) { + test('(KGP, Gradle): (${data.kgpVersion}, ${data.gradleVersion})', () { + expect( + validateGradleAndKGP( + BufferLogger.test(), + gradleV: data.gradleVersion, + kgpV: data.kgpVersion, + ), + data.validPair ? isTrue : isFalse, + reason: 'KGP: ${data.kgpVersion}, G: ${data.gradleVersion}', + ); + }); + } + }); + + group('validates KGP/AGP versions', () { + final List testData = [ + // Values too new. + KgpAgpTestData(true, kgpVersion: '3.0', agpVersion: '99.99'), + + // Template versions of Gradle/AGP. + KgpAgpTestData( + true, + kgpVersion: templateKotlinGradlePluginVersion, + // TODO(reidbaker): Replace with templateAndroidGradlePluginVersion + agpVersion: '8.7.2', + ), + + // Kotlin version at the edge of support window. + KgpAgpTestData(true, kgpVersion: '2.1.20', agpVersion: '8.7.2'), + KgpAgpTestData(true, kgpVersion: '2.1.20', agpVersion: '7.3.1'), + // AGP Versions not "fully supported" by kotlin + KgpAgpTestData(true, kgpVersion: '2.1.20', agpVersion: '8.9'), + KgpAgpTestData(true, kgpVersion: '2.1.20', agpVersion: '8.8'), + // Gradle versions inspired by + // https://developer.android.com/build/releases/gradle-plugin#expandable-1 + KgpAgpTestData(true, kgpVersion: '2.1.5', agpVersion: '8.7'), + KgpAgpTestData(true, kgpVersion: '2.1.10', agpVersion: '8.6'), + KgpAgpTestData(true, kgpVersion: '2.0.21', agpVersion: '8.5'), + KgpAgpTestData(true, kgpVersion: '2.0.20', agpVersion: '8.4'), + KgpAgpTestData(true, kgpVersion: '2.0', agpVersion: '8.3.1'), + KgpAgpTestData(true, kgpVersion: '2.1.5', agpVersion: '8.2'), + KgpAgpTestData(true, kgpVersion: '1.9.25', agpVersion: '8.1'), + KgpAgpTestData(true, kgpVersion: '1.9.20', agpVersion: '8.0'), + KgpAgpTestData(true, kgpVersion: '1.9.10', agpVersion: '7.4'), + KgpAgpTestData(true, kgpVersion: '1.8.20', agpVersion: '7.4'), + KgpAgpTestData(true, kgpVersion: '1.8.21', agpVersion: '7.3'), + KgpAgpTestData(true, kgpVersion: '1.8.11', agpVersion: '7.2.1'), + KgpAgpTestData(true, kgpVersion: '1.8.0', agpVersion: '7.2.1'), + KgpAgpTestData(true, kgpVersion: '1.8.0', agpVersion: '7.1'), + KgpAgpTestData(true, kgpVersion: '1.7.20', agpVersion: '7.0.4'), + KgpAgpTestData(true, kgpVersion: '1.7.22', agpVersion: '7.0'), + KgpAgpTestData(true, kgpVersion: '1.8.22', agpVersion: '4.2.0'), + KgpAgpTestData(true, kgpVersion: '1.6.20', agpVersion: '4.1.0'), + // Kotlin newer than max known. + KgpAgpTestData(true, kgpVersion: '2.1.21', agpVersion: '8.7.2'), + // Kotlin too new for AGP version. + KgpAgpTestData(false, kgpVersion: '2.1.20', agpVersion: '7.3.0'), + KgpAgpTestData(false, kgpVersion: '2.1.10', agpVersion: '7.3.0'), + KgpAgpTestData(false, kgpVersion: '2.0.21', agpVersion: '7.1.2'), + KgpAgpTestData(false, kgpVersion: '2.0.0', agpVersion: '7.1.2'), + KgpAgpTestData(false, kgpVersion: '1.9.25', agpVersion: '4.2.1'), + KgpAgpTestData(false, kgpVersion: '1.8.20', agpVersion: '4.1.2'), + // Kotlin too old for gradle version. + KgpAgpTestData(false, kgpVersion: '2.0.20', agpVersion: '8.7.2'), + KgpAgpTestData(false, kgpVersion: '2.0.20', agpVersion: '8.6'), + KgpAgpTestData(false, kgpVersion: '2.0.0', agpVersion: '8.4'), + KgpAgpTestData(false, kgpVersion: '1.9.20', agpVersion: '8.2'), + KgpAgpTestData(false, kgpVersion: '1.9.0', agpVersion: '7.5'), + KgpAgpTestData(false, kgpVersion: '1.8.20', agpVersion: '7.5'), + KgpAgpTestData(false, kgpVersion: '1.8.1', agpVersion: '7.3'), + KgpAgpTestData(false, kgpVersion: '1.7.20', agpVersion: '7.1'), + KgpAgpTestData(false, kgpVersion: '1.7.0', agpVersion: '7.0.3'), + KgpAgpTestData(false, kgpVersion: '1.6.19', agpVersion: '7.0.3'), + // Unknown values. + KgpAgpTestData( + false, + kgpVersion: oldestDocumentedKgpCompatabilityVersion, + agpVersion: oldestConsideredAgpVersion, + ), + // Null values: + // ignore: avoid_redundant_argument_values + KgpAgpTestData(false, kgpVersion: null, agpVersion: '7.2'), + // ignore: avoid_redundant_argument_values + KgpAgpTestData(false, kgpVersion: '2.1', agpVersion: null), + // ignore: avoid_redundant_argument_values + KgpAgpTestData(false, kgpVersion: '', agpVersion: ''), + // ignore: avoid_redundant_argument_values + KgpAgpTestData(false, kgpVersion: null, agpVersion: null), + ]; + for (final KgpAgpTestData data in testData) { + test('(KGP, AGP): (${data.kgpVersion}, ${data.agpVersion})', () { + expect( + validateAgpAndKgp(BufferLogger.test(), agpV: data.agpVersion, kgpV: data.kgpVersion), + data.validPair ? isTrue : isFalse, + reason: 'KGP: ${data.kgpVersion}, AGP: ${data.agpVersion}', + ); + }); + } + }); + group('Parse gradle version from distribution url', () { testWithoutContext('null distribution url returns null version', () { expect(parseGradleVersionFromDistributionUrl(null), null); @@ -1373,6 +1620,20 @@ class GradleAgpTestData { final bool validPair; } +class GradleKgpTestData { + GradleKgpTestData(this.validPair, {this.gradleVersion, this.kgpVersion}); + final String? gradleVersion; + final String? kgpVersion; + final bool validPair; +} + +class KgpAgpTestData { + KgpAgpTestData(this.validPair, {this.agpVersion, this.kgpVersion}); + final String? agpVersion; + final String? kgpVersion; + final bool validPair; +} + class JavaGradleTestData { JavaGradleTestData(this.validPair, {this.javaVersion, this.gradleVersion}); final String? gradleVersion; diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 8df65c1a143..5ad22fec1a2 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -576,7 +576,7 @@ dependencies { final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); java = FakeJava(version: Version(17, 0, 2)); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.list([createKgpVersionCommand('1.9.20')]); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); @@ -604,7 +604,7 @@ dependencies { final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); java = FakeJava(version: const Version.withText(1, 8, 0, '1.8.0_242')); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.list([createKgpVersionCommand('1.7.20')]); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); @@ -632,7 +632,7 @@ dependencies { final AndroidStudio androidStudio; final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.list([createKgpVersionCommand('1.9.1')]); java = FakeJava(version: Version(11, 0, 14)); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); @@ -658,13 +658,14 @@ dependencies { const String javaV = '17.0.2'; const String gradleV = '6.7.3'; const String agpV = '7.2.0'; + const String kgpV = '2.1.0'; final FakeProcessManager processManager; final Java java; final AndroidStudio androidStudio; final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.list([createKgpVersionCommand(kgpV)]); java = FakeJava(version: Version.parse(javaV)); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); @@ -682,7 +683,7 @@ dependencies { // Should not have the valid string expect( value.description, - isNot(contains(RegExp(AndroidProject.validJavaGradleAgpString))), + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), ); // On gradle/agp error print help url and gradle and agp versions. expect(value.description, contains(RegExp(AndroidProject.gradleAgpCompatUrl))); @@ -692,6 +693,15 @@ dependencies { expect(value.description, contains(RegExp(AndroidProject.javaGradleCompatUrl))); expect(value.description, contains(RegExp(javaV))); expect(value.description, contains(RegExp(gradleV))); + // On kgp/gradle eror print help url and kgp versions + expect(value.description, contains(RegExp(kgpV))); + expect(value.description, contains(RegExp('KGP/Gradle'))); + expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl))); + // On agp/kgp error print help url and agp and kgp versions + expect(value.description, contains(RegExp(agpV))); + expect(value.description, contains(RegExp(kgpV))); + expect(value.description, contains(RegExp('AGP/KGP'))); + expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl))); }, java: java, androidStudio: androidStudio, @@ -703,13 +713,14 @@ dependencies { const String javaV = '17.0.2'; const String gradleV = '6.7.3'; const String agpV = '4.2.0'; + const String kgpV = '1.7.22'; final FakeProcessManager processManager; final Java java; final AndroidStudio androidStudio; final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.list([createKgpVersionCommand(kgpV)]); java = FakeJava(version: Version(17, 0, 2)); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); @@ -727,7 +738,7 @@ dependencies { // Should not have the valid string. expect( value.description, - isNot(contains(RegExp(AndroidProject.validJavaGradleAgpString))), + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), ); // On gradle/agp error print help url and java and gradle versions. expect(value.description, contains(RegExp(AndroidProject.javaGradleCompatUrl))); @@ -747,7 +758,7 @@ dependencies { final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); java = FakeJava(version: Version(11, 0, 2)); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.any(); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); @@ -766,7 +777,7 @@ dependencies { // Should not have the valid string. expect( value.description, - isNot(contains(RegExp(AndroidProject.validJavaGradleAgpString))), + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), ); // On gradle/agp error print help url and gradle and agp versions. expect(value.description, contains(RegExp(AndroidProject.gradleAgpCompatUrl))); @@ -779,6 +790,89 @@ dependencies { androidSdk: androidSdk, ); }); + group('_', () { + const String gradleV = '8.11'; + const String agpV = '8.7.2'; + const String kgpV = '2.1.10'; + + final FakeProcessManager processManager; + final Java java; + final AndroidStudio androidStudio; + final FakeAndroidSdkWithDir androidSdk; + final FileSystem fileSystem = getFileSystemForPlatform(); + processManager = FakeProcessManager.list([createKgpVersionCommand(kgpV)]); + java = FakeJava(version: Version(17, 0, 2)); + androidStudio = FakeAndroidStudio(); + androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); + fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); + _testInMemory( + 'incompatible kgp/gradle only', + () async { + final FlutterProject? project = await configureGradleAgpForTest( + gradleV: gradleV, + agpV: agpV, + ); + final CompatibilityResult value = + await project!.android.hasValidJavaGradleAgpVersions(); + expect(value.success, isFalse); + // Should not have the valid string. + expect( + value.description, + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), + ); + // On gradle/agp error print help url and java and gradle versions. + expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl))); + expect(value.description, contains(RegExp(kgpV))); + expect(value.description, contains(RegExp(gradleV))); + }, + java: java, + androidStudio: androidStudio, + processManager: processManager, + androidSdk: androidSdk, + ); + }); + group('_', () { + const String gradleV = '8.9'; + const String agpV = '8.7.2'; + const String kgpV = '2.0.20'; + + final FakeProcessManager processManager; + final Java java; + final AndroidStudio androidStudio; + final FakeAndroidSdkWithDir androidSdk; + final FileSystem fileSystem = getFileSystemForPlatform(); + processManager = FakeProcessManager.list([createKgpVersionCommand(kgpV)]); + java = FakeJava(version: Version(17, 0, 2)); + androidStudio = FakeAndroidStudio(); + androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); + fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); + _testInMemory( + 'incompatible agp/kgp only', + () async { + final FlutterProject? project = await configureGradleAgpForTest( + gradleV: gradleV, + agpV: agpV, + ); + final CompatibilityResult value = + await project!.android.hasValidJavaGradleAgpVersions(); + expect(value.success, isFalse); + // Should not have the valid string. + expect( + value.description, + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), + ); + // On gradle/agp error print help url and java and gradle versions. + expect(value.description, contains(RegExp(kgpV))); + expect(value.description, contains(RegExp(agpV))); + expect(value.description, contains(RegExp('AGP/KGP'))); + expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl))); + }, + java: java, + androidStudio: androidStudio, + processManager: processManager, + androidSdk: androidSdk, + ); + }); group('_', () { final FakeProcessManager processManager; final Java java; @@ -786,7 +880,7 @@ dependencies { final FakeAndroidSdkWithDir androidSdk; final FileSystem fileSystem = getFileSystemForPlatform(); java = FakeJava(version: Version(11, 0, 2)); - processManager = FakeProcessManager.empty(); + processManager = FakeProcessManager.any(); androidStudio = FakeAndroidStudio(); androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory); fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync(); @@ -804,7 +898,7 @@ dependencies { // Should not have the valid string. expect( value.description, - isNot(contains(RegExp(AndroidProject.validJavaGradleAgpString))), + isNot(contains(RegExp(AndroidProject.validJavaGradleAgpKgpString))), ); // On gradle/agp error print help url null value for agp. expect(value.description, contains(RegExp(AndroidProject.gradleAgpCompatUrl))); @@ -1979,6 +2073,15 @@ flutter: return FlutterProject.fromDirectory(directory); } +FakeCommand createKgpVersionCommand(String kgpV) { + return FakeCommand( + command: const ['./gradlew', 'kgpVersion', '-q'], + stdout: ''' +KGP Version: $kgpV +''', + ); +} + /// Executes the [testMethod] in a context where the file system /// is in memory. @isTest diff --git a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart index 7dcb34a9361..4446f3e09e3 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart @@ -51,15 +51,15 @@ void main() { const String expected = '\n' - '┌───────────────────────────────────────────────────────────────────┐\n' - '│ General Info │\n' - '│ [✓] App Name: flutter_gallery │\n' - '│ [✓] Supported Platforms: android, ios, web, macos, linux, windows │\n' - '│ [✓] Is Flutter Package: yes │\n' - '│ [✓] Uses Material Design: yes │\n' - '│ [✓] Is Plugin: no │\n' - '│ [✓] Java/Gradle/Android Gradle Plugin: ${AndroidProject.validJavaGradleAgpString} │\n' - '└───────────────────────────────────────────────────────────────────┘\n'; + '┌───────────────────────────────────────────────────────────────────────────┐\n' + '│ General Info │\n' + '│ [✓] App Name: flutter_gallery │\n' + '│ [✓] Supported Platforms: android, ios, web, macos, linux, windows │\n' + '│ [✓] Is Flutter Package: yes │\n' + '│ [✓] Uses Material Design: yes │\n' + '│ [✓] Is Plugin: no │\n' + '│ [✓] Java/Gradle/KGP/Android Gradle Plugin: ${AndroidProject.validJavaGradleAgpKgpString} │\n' + '└───────────────────────────────────────────────────────────────────────────┘\n'; expect(loggerTest.statusText, contains(expected)); });