Add our own wrapper for `CommonExtension` due to change in signature from 8.x->9.0 (#184433)
We can't reference this type directly because of:
https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/CommonExtension
https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/CommonExtension
---------
Co-authored-by: Gray Mackall <mackall@google.com>
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt b/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt
new file mode 100644
index 0000000..db0943a
--- /dev/null
+++ b/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt
@@ -0,0 +1,111 @@
+// 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.dsl.ApplicationExtension
+import com.android.build.api.dsl.BuildType
+import com.android.build.api.dsl.DynamicFeatureExtension
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.dsl.TestExtension
+import org.gradle.api.NamedDomainObjectContainer
+import java.io.File
+
+/**
+ * A wrapper to bypass binary incompatibilities in AGP's CommonExtension between 8.x and 9.x.
+ * * CRITICAL: Do not import or reference `com.android.build.api.dsl.CommonExtension`
+ * anywhere in this file, or the compiler may weave the broken type into the bytecode.
+ */
+class AgpCommonExtensionWrapper(
+ private val backingExtension: Any
+) {
+ var compileSdk: Int?
+ get() =
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.compileSdk
+ is LibraryExtension -> backingExtension.compileSdk
+ is DynamicFeatureExtension -> backingExtension.compileSdk
+ is TestExtension -> backingExtension.compileSdk
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ set(value) {
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.compileSdk = value
+ is LibraryExtension -> backingExtension.compileSdk = value
+ is DynamicFeatureExtension -> backingExtension.compileSdk = value
+ is TestExtension -> backingExtension.compileSdk = value
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ }
+
+ var namespace: String?
+ get() =
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.namespace
+ is LibraryExtension -> backingExtension.namespace
+ is DynamicFeatureExtension -> backingExtension.namespace
+ is TestExtension -> backingExtension.namespace
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ set(value) {
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.namespace = value
+ is LibraryExtension -> backingExtension.namespace = value
+ is DynamicFeatureExtension -> backingExtension.namespace = value
+ is TestExtension -> backingExtension.namespace = value
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ }
+
+ var ndkVersion: String
+ get() =
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.ndkVersion
+ is LibraryExtension -> backingExtension.ndkVersion
+ is DynamicFeatureExtension -> backingExtension.ndkVersion
+ is TestExtension -> backingExtension.ndkVersion
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ set(value) {
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.ndkVersion = value
+ is LibraryExtension -> backingExtension.ndkVersion = value
+ is DynamicFeatureExtension -> backingExtension.ndkVersion = value
+ is TestExtension -> backingExtension.ndkVersion = value
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ }
+
+ val buildTypes: NamedDomainObjectContainer<out BuildType>
+ get() =
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.buildTypes
+ is LibraryExtension -> backingExtension.buildTypes
+ is DynamicFeatureExtension -> backingExtension.buildTypes
+ is TestExtension -> backingExtension.buildTypes
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+
+ fun getDefaultProguardFile(fileName: String): File =
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.getDefaultProguardFile(fileName)
+ is LibraryExtension -> backingExtension.getDefaultProguardFile(fileName)
+ is DynamicFeatureExtension -> backingExtension.getDefaultProguardFile(fileName)
+ is TestExtension -> backingExtension.getDefaultProguardFile(fileName)
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+
+ // Example of wrapping a method rather than a property
+ fun compileOptions(action: (Any) -> Unit) {
+ when (backingExtension) {
+ is ApplicationExtension -> backingExtension.compileOptions { action(this) }
+ is LibraryExtension -> backingExtension.compileOptions { action(this) }
+ is DynamicFeatureExtension -> backingExtension.compileOptions { action(this) }
+ is TestExtension -> backingExtension.compileOptions { action(this) }
+ else -> throw IllegalArgumentException(unsupportedMessage())
+ }
+ }
+
+ private fun unsupportedMessage() = "Unsupported Android extension type: ${backingExtension.javaClass.name}"
+}
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
index 660c6a5..eba58ee 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
@@ -6,7 +6,6 @@
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationExtension
-import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
@@ -409,8 +408,14 @@
return project.extensions.findByType(BaseExtension::class.java)!!
}
- internal fun getAndroidExtension(project: Project): CommonExtension<*, *, *, *, *, *> =
- project.extensions.findByType(CommonExtension::class.java)!!
+ internal fun getAndroidExtension(project: Project): AgpCommonExtensionWrapper {
+ // Look up by name to completely avoid importing or resolving CommonExtension
+ val androidExtension =
+ project.extensions.findByName("android")
+ ?: throw IllegalStateException("The Android plugin must be applied before accessing the Android extension.")
+
+ return AgpCommonExtensionWrapper(androidExtension)
+ }
internal fun getAndroidLibraryExtension(project: Project): LibraryExtension = project.extensions.getByType(LibraryExtension::class.java)
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
index a182517..c1cbdce 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
@@ -1,5 +1,6 @@
package com.flutter.gradle
+import com.android.build.api.dsl.ApplicationBuildType
import com.android.build.api.dsl.ApplicationDefaultConfig
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
@@ -49,7 +50,11 @@
val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM)
val project = mockk<Project>(relaxed = true)
- val mockAbstractAppExtension = mockk<AbstractAppExtension>(relaxed = true)
+ val mockAbstractAppExtension =
+ mockk<AbstractAppExtension>(
+ moreInterfaces = arrayOf(ApplicationExtension::class),
+ relaxed = true
+ )
val mockLibraryExtension = mockk<LibraryExtension>(relaxed = true)
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
@@ -69,19 +74,35 @@
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
val mockBaseExtension = mockk<BaseExtension>(relaxed = true)
val mockCommonExtension = mockk<CommonExtension<*, *, *, *, *, *>>(relaxed = true)
- val mockDebugBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
- val mockReleaseBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
+ val mockDebugBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+ val mockReleaseBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+
+ // Cast our multi-interface mock instead of creating a brand new one
+ val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension
+
+ // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them
+ every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
+ every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
+ // Keep the CommonExtension mocks just in case other parts of the plugin look for it
every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension
every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension
- val mockApplicationExtension = mockk<ApplicationExtension>(relaxed = true)
+
+ // Pass the dual-purpose mock for any ApplicationExtension lookups
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension
- val mockApplicationDefaultConfig = mockk<ApplicationDefaultConfig>(relaxed = true)
+
+ val mockApplicationDefaultConfig =
+ mockk<com.android.build.gradle.internal.dsl.DefaultConfig>(
+ moreInterfaces = arrayOf(ApplicationDefaultConfig::class),
+ relaxed = true
+ )
every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
every { project.rootProject } returns project
- every { project.state.failure } returns null
+ every { project.state.failure as Throwable? } returns null
val mockDirectory = mockk<Directory>(relaxed = true)
every { project.layout.buildDirectory.get() } returns mockDirectory
val mockAndroidSourceSet = mockk<com.android.build.gradle.api.AndroidSourceSet>(relaxed = true)
@@ -120,7 +141,11 @@
val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM)
val project = mockk<Project>(relaxed = true)
- val mockAbstractAppExtension = mockk<AbstractAppExtension>(relaxed = true)
+ val mockAbstractAppExtension =
+ mockk<AbstractAppExtension>(
+ moreInterfaces = arrayOf(ApplicationExtension::class),
+ relaxed = true
+ )
every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
every { project.extensions.findByName("android") } returns mockAbstractAppExtension
@@ -138,19 +163,35 @@
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
val mockBaseExtension = mockk<BaseExtension>(relaxed = true)
val mockCommonExtension = mockk<CommonExtension<*, *, *, *, *, *>>(relaxed = true)
- val mockDebugBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
- val mockReleaseBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
+ val mockDebugBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+ val mockReleaseBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+
+ // Cast our multi-interface mock instead of creating a brand new one
+ val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension
+
+ // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them
+ every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
+ every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
+ // Keep the CommonExtension mocks just in case other parts of the plugin look for it
every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension
every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension
- val mockApplicationExtension = mockk<ApplicationExtension>(relaxed = true)
+
+ // Pass the dual-purpose mock for any ApplicationExtension lookups
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension
- val mockApplicationDefaultConfig = mockk<ApplicationDefaultConfig>(relaxed = true)
+
+ val mockApplicationDefaultConfig =
+ mockk<com.android.build.gradle.internal.dsl.DefaultConfig>(
+ moreInterfaces = arrayOf(ApplicationDefaultConfig::class),
+ relaxed = true
+ )
every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
every { project.rootProject } returns project
- every { project.state.failure } returns null
+ every { project.state.failure as Throwable? } returns null
val mockDirectory = mockk<Directory>(relaxed = true)
every { project.layout.buildDirectory.get() } returns mockDirectory
val mockAndroidSourceSet = mockk<com.android.build.gradle.api.AndroidSourceSet>(relaxed = true)