[CP-beta][Gradle] Run DependencyVersionChecker before accessing Android DSL extensions in FlutterPlugin.apply (#195060) This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request) Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request. ### Issue Link: What is the link to the issue this cherry-pick is addressing? https://github.com/flutter/flutter/issues/195062 ### Impact Description: On 3.50 beta, building an Android app that uses Android Gradle Plugin (AGP) 8.x crashes during Gradle configuration with `java.lang.NoSuchMethodError: 'com.android.build.api.dsl.ApplicationDefaultConfig com.android.build.api.dsl.ApplicationExtension.getDefaultConfig()'`. The developer never sees Flutter's actionable message that their AGP version is below the minimum supported version (9.1.1) or how to upgrade. This affects development: the build fails with an internal-looking stack trace instead of upgrade guidance. Apps on AGP 9.1.1+ are not affected. ### Changelog Description: [flutter/195062] When building an Android app that uses an Android Gradle Plugin version below Flutter's minimum, the build crashes with a `NoSuchMethodError` instead of reporting the minimum supported version. ### Workaround: Is there a workaround for this issue? Upgrade the project's Android Gradle Plugin to 9.1.1 or later, which the build requires anyway. `--android-skip-build-dependency-validation` does not avoid the crash. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: 1. Create an app with `flutter create`, then set `com.android.application` to `8.11.1` in `android/settings.gradle(.kts)` and use Gradle 9.3.1. 2. Run `flutter build apk --debug`. 3. Before the fix, the build fails with `NoSuchMethodError … ApplicationExtension.getDefaultConfig()`. After the fix, it fails with "Your project's Android Gradle Plugin version (… 8.11.1) is lower than Flutter's minimum supported version of … 9.1.1". 4. Unit test: `FlutterPluginTest` › `FlutterPlugin apply checks dependency versions before configuring project`. The integration test `android_dependency_version_checking_test.dart` also covers the Kotlin, AGP and Gradle warnings on template apps.
diff --git a/packages/flutter_tools/gradle/AGENTS.md b/packages/flutter_tools/gradle/AGENTS.md index 058dc38..bed8600 100644 --- a/packages/flutter_tools/gradle/AGENTS.md +++ b/packages/flutter_tools/gradle/AGENTS.md
@@ -37,4 +37,4 @@ ## 7. Namespace & Environment Hygiene - **Namespace Internal Properties**: Prefix all internal Gradle properties, project extensions, and system properties with `flutter.internal.` (e.g., `flutter.internal.agpVersion`) to prevent collisions with customer app configurations. -- **Matrix Verification**: Verify compatibility against the full matrix of supported AGP versions (AGP 8.x through 9.x) and Gradle versions. +- **Matrix Verification**: Verify compatibility against the full matrix of supported AGP and Gradle versions defined in `DependencyVersionChecker.kt`.
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 77dc2e9..46cd1ba 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
@@ -45,6 +45,30 @@ override fun apply(project: Project) { this.project = project + FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project) + + // Validate that the provided Gradle, Java, AGP, and KGP versions are all within our + // supported range. This runs before any task registration or extension configuration so + // that unsupported dependency versions fail with DependencyVersionChecker's diagnostic + // rather than a runtime error when calling AGP or Gradle APIs. + val shouldSkipDependencyChecks: Boolean = + project.hasProperty("skipDependencyChecks") && + project.properties["skipDependencyChecks"]?.toString()?.toBoolean() == true + if (!shouldSkipDependencyChecks) { + try { + DependencyVersionChecker.checkDependencyVersions(project) + } catch (e: DependencyValidationException) { + throw e + } catch (e: Exception) { + // Possible bug in dependency checking code - warn and do not block build. + project.logger.error( + "Warning: Flutter was unable to detect project Gradle, Java, " + + "AGP, and KGP versions. Skipping dependency version checking. Error was: " + + e + ) + } + } + val rootProject = project.rootProject if (FlutterPluginUtils.isFlutterAppProject(project)) { addTaskForLockfileGeneration(rootProject) @@ -161,34 +185,6 @@ flutterExecutable = Paths.get(flutterRoot!!.absolutePath, "bin", flutterExecutableName).toFile() - // Validate that the provided Gradle, Java, AGP, and KGP versions are all within our - // supported range. - val shouldSkipDependencyChecks: Boolean = - project.hasProperty("skipDependencyChecks") && - ( - project.properties["skipDependencyChecks"].toString().toBoolean() - ) - if (!shouldSkipDependencyChecks) { - try { - DependencyVersionChecker.checkDependencyVersions(project) - } catch (e: Exception) { - if (!project.hasProperty("usesUnsupportedDependencyVersions") || - !(project.properties["usesUnsupportedDependencyVersions"] as Boolean) - ) { - // Possible bug in dependency checking code - warn and do not block build. - project.logger.error( - "Warning: Flutter was unable to detect project Gradle, Java, " + - "AGP, and KGP versions. Skipping dependency version checking. Error was: " + - e - ) - } else { - // If usesUnsupportedDependencyVersions is set, the exception was thrown by us - // in the dependency version checker plugin so re-throw it here. - throw e - } - } - } - BaseApplicationNameHandler.setBaseName(project) val flutterProguardRules = Paths @@ -357,9 +353,6 @@ projectToAddTasksTo, getPluginHandler(projectToAddTasksTo).getPluginList() ) - FlutterPluginUtils.detectApplyingKotlinGradlePlugin( - projectToAddTasksTo - ) return } // Flutter host module project (Add-to-app). @@ -369,9 +362,6 @@ projectToAddTasksTo, getPluginHandler(projectToAddTasksTo).getPluginList() ) - FlutterPluginUtils.detectApplyingKotlinGradlePlugin( - projectToAddTasksTo - ) } private fun getPluginHandler(project: Project): PluginHandler {
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt index 9953063..3ad329d 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
@@ -30,9 +30,11 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify +import io.mockk.verifyOrder import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.NamedDomainObjectContainer @@ -89,6 +91,66 @@ } @Test + fun `FlutterPlugin apply checks dependency versions before configuring project`( + @TempDir tempDir: Path + ) { + val env = setupTestProjectEnvironment(tempDir) + val project = env.project + val mockApplicationExtension = setupMockApplicationExtension(project) + val mockComponentsExtension = setupMockComponentsExtension(project) + setupMockNativePluginLoader(project, env.flutterExtension) + mockkStatic(FlutterPluginUtils::class) + mockkStatic(DependencyVersionChecker::class) + every { DependencyVersionChecker.checkDependencyVersions(project) } just Runs + + FlutterPlugin().apply(project) + + verifyOrder { + FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project) + DependencyVersionChecker.checkDependencyVersions(project) + project.tasks.register("javaVersion", PrintTask::class.java, any()) + mockComponentsExtension.finalizeDsl(any<(Any) -> Unit>()) + mockComponentsExtension.onVariants(any(), any<(Variant) -> Unit>()) + mockApplicationExtension.defaultConfig + mockApplicationExtension.buildTypes + } + } + + @Test + fun `FlutterPlugin apply rethrows DependencyValidationException and logs other exceptions`( + @TempDir tempDir: Path + ) { + val env = setupTestProjectEnvironment(tempDir) + val project = env.project + setupMockApplicationExtension(project) + setupMockComponentsExtension(project) + setupMockNativePluginLoader(project, env.flutterExtension) + mockkStatic(DependencyVersionChecker::class) + + val validationException = DependencyValidationException("Unsupported AGP version") + every { DependencyVersionChecker.checkDependencyVersions(project) } throws validationException + + val thrown = + assertThrows<DependencyValidationException> { + FlutterPlugin().apply(project) + } + assertEquals(validationException, thrown) + + val unexpectedException = IllegalStateException("Unexpected failure") + every { DependencyVersionChecker.checkDependencyVersions(project) } throws unexpectedException + + FlutterPlugin().apply(project) + + verify { + project.logger.error( + "Warning: Flutter was unable to detect project Gradle, Java, " + + "AGP, and KGP versions. Skipping dependency version checking. Error was: " + + unexpectedException + ) + } + } + + @Test fun `FlutterPlugin apply wires flutter embedding dependencies on all build types`( @TempDir tempDir: Path ) { @@ -768,6 +830,7 @@ every { project.rootProject } returns project every { project.state.failure as Throwable? } returns null every { project.configurations.named("api") } returns mockk() + every { project.gradle.gradleVersion } returns DependencyVersionChecker.warnGradleVersion.toString() val flutterExtension = FlutterExtension() every { project.extensions.create("flutter", any<Class<*>>()) } returns flutterExtension @@ -840,6 +903,9 @@ val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<Any, VariantBuilder, Variant>>(relaxed = true) every { + mockAndroidComponentsExtension.pluginVersion + } returns DependencyVersionChecker.warnAGPVersion + every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns (mockAndroidComponentsExtension as AndroidComponentsExtension<*, *, *>) every {