Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions packages/flutter_tools/gradle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
}
}

// The AGP version the Flutter Gradle Plugin compiles and tests against. CI additionally runs
// this build with -PagpVersion=<AGP 9 line> to catch public-DSL binary incompatibilities
// between the AGP major versions the plugin supports (see AgpCommonExtensionWrapper).
val agpVersion: String = providers.gradleProperty("agpVersion").getOrElse("8.11.1")

dependencies {
// Versions available https://mvnrepository.com/artifact/androidx.annotation/annotation-jvm.
// Version release notes https://developer.android.com/jetpack/androidx/releases/annotation
Expand All @@ -61,14 +66,59 @@ dependencies {
// All kotlinx implementation dependencies must work with the oldest kotlin supported versions.
// Defined in packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0")
// When bumping, also update:
// When bumping the default agpVersion above, also update:
// * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart
// * ndkVersion constant in packages/flutter_tools/lib/src/android/gradle_utils.dart
// * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt
compileOnly("com.android.tools.build:gradle:8.11.1")
compileOnly("com.android.tools.build:gradle:$agpVersion")

testImplementation(kotlin("test"))
testImplementation("com.android.tools.build:gradle:8.11.1")
testImplementation("com.android.tools.build:gradle:$agpVersion")
testImplementation("org.mockito:mockito-core:5.8.0")
testImplementation("io.mockk:mockk:1.13.16")
}

// CommonExtension is binary-incompatible between AGP 8 and 9, which is why DSL access is
// routed through AgpCommonExtensionWrapper. If the compiler emits a reference to
// CommonExtension as the owner of a call, the plugin breaks on one of the two AGP lines at
// runtime even though it compiles on both. Fail fast if such a reference appears in the
// main bytecode (test bytecode intentionally references AGP types directly).
val validateNoCommonExtensionInBytecode by tasks.registering {
description =
"Checks that no compiled main class references com.android.build.api.dsl.CommonExtension."
dependsOn(tasks.named("compileKotlin"))
val classesDir = layout.buildDirectory.dir("classes/kotlin/main")
doLast {
val needle = "com/android/build/api/dsl/CommonExtension".toByteArray(Charsets.ISO_8859_1)
val offenders =
classesDir
.get()
.asFile
.walkTopDown()
.filter { it.isFile && it.extension == "class" }
.filter { file ->
val bytes = file.readBytes()
(0..bytes.size - needle.size).any { start ->
needle.indices.all { i -> bytes[start + i] == needle[i] }
}
}.map { it.name }
.toList()
if (offenders.isNotEmpty()) {
throw GradleException(
"CommonExtension must not be referenced from Flutter Gradle Plugin bytecode " +
"(it is binary-incompatible between AGP 8 and 9). Route DSL access through " +
"AgpCommonExtensionWrapper instead. Offending classes: $offenders"
)
}
}
}

// Attached to `test` (not just `check`) because CI drives this build through `gradlew test`
// (see packages/flutter_tools/test/integration.shard/android_run_flutter_gradle_plugin_tests_test.dart).
tasks.test {
dependsOn(validateNoCommonExtensionInBytecode)
}

tasks.named("check") {
dependsOn(validateNoCommonExtensionInBytecode)
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,12 @@ class FlutterPlugin : Plugin<Project> {
}
localEngineHost = engineHostOut.name
}
FlutterPluginUtils.getLegacyAndroidExtension(project).buildTypes.all {
FlutterPluginUtils.getAndroidExtension(project).buildTypes.all {
addFlutterDependencies(this)
}
}

private fun addFlutterDependencies(buildType: com.android.builder.model.BuildType) {
private fun addFlutterDependencies(buildType: BuildType) {
FlutterPluginUtils.addFlutterDependencies(
project!!,
buildType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package com.flutter.gradle

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationBuildType
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.DynamicFeatureBuildType
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
Expand All @@ -28,6 +30,7 @@ import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Properties
import com.android.build.api.dsl.BuildType as DslBuildType

/**
* A collection of static utility functions used by the Flutter Gradle Plugin.
Expand Down Expand Up @@ -469,15 +472,50 @@ object FlutterPluginUtils {
*/
@JvmStatic
@JvmName("buildModeFor")
internal fun buildModeFor(buildType: BuildType): String {
if (buildType.name == "profile") {
internal fun buildModeFor(buildType: BuildType): String = buildModeFor(buildType.name, buildType.isDebuggable)

/**
* Returns a Flutter build mode for a build type identified by [buildTypeName] and its
* [isDebuggable] flag.
*
* Variant-scope callers must pass the public `Component.debuggable` flag so that custom
* debuggable build types (e.g. a host app's `staging`) map to the debug engine artifacts.
*
* @return "debug", "profile", or "release" (fall-back).
*/
@JvmStatic
@JvmName("buildModeFor")
internal fun buildModeFor(
buildTypeName: String,
isDebuggable: Boolean
): String {
if (buildTypeName == "profile") {
return "profile"
} else if (buildType.isDebuggable) {
} else if (isDebuggable) {
return "debug"
}
return "release"
}

/**
* Returns a Flutter build mode for a new-DSL [buildType].
*
* Application and dynamic-feature build types expose a public `isDebuggable` flag.
* Library build types do not, so for them the conventional "debug" name is the only
* public signal available at DSL scope.
*/
@JvmStatic
@JvmName("buildModeFor")
internal fun buildModeFor(buildType: DslBuildType): String {
val isDebuggable =
when (buildType) {
is ApplicationBuildType -> buildType.isDebuggable
is DynamicFeatureBuildType -> buildType.isDebuggable
else -> buildType.name == "debug"
}
return buildModeFor(buildType.name, isDebuggable)
}

/**
* Returns true if the build mode is supported by the current call to Gradle.
* This only relevant when using a local engine. Because the engine
Expand Down Expand Up @@ -986,7 +1024,7 @@ object FlutterPluginUtils {
@JvmName("addFlutterDependencies")
internal fun addFlutterDependencies(
project: Project,
buildType: BuildType,
buildType: DslBuildType,
pluginHandler: PluginHandler,
engineVersion: String
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

package com.flutter.gradle.plugins

import com.android.builder.model.BuildType
import com.android.build.api.dsl.BuildType
import com.flutter.gradle.CompileSdkVersion
import com.flutter.gradle.FlutterExtension
import com.flutter.gradle.FlutterPluginUtils
import com.flutter.gradle.FlutterPluginUtils.addApiDependencies
import com.flutter.gradle.FlutterPluginUtils.buildModeFor
import com.flutter.gradle.FlutterPluginUtils.getAndroidExtension
import com.flutter.gradle.FlutterPluginUtils.getCompileSdkFromProject
import com.flutter.gradle.FlutterPluginUtils.getLegacyAndroidExtension
import com.flutter.gradle.FlutterPluginUtils.isBuiltAsApp
Expand Down Expand Up @@ -112,7 +113,7 @@ class PluginHandler(
// Add plugin dependency to the app project. We only want to add dependency
// for dev dependencies in non-release builds.
project.afterEvaluate {
getLegacyAndroidExtension(project).buildTypes.forEach { buildType ->
getAndroidExtension(project).buildTypes.forEach { buildType ->
if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") {
project.dependencies.add("${buildType.name}Api", pluginProject)
}
Expand All @@ -133,7 +134,7 @@ class PluginHandler(
)
}

getLegacyAndroidExtension(project).buildTypes.forEach { buildType ->
getAndroidExtension(project).buildTypes.forEach { buildType ->
addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion)
}
}
Expand Down Expand Up @@ -213,7 +214,7 @@ class PluginHandler(
}
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return

getLegacyAndroidExtension(project).buildTypes.forEach { buildType ->
getAndroidExtension(project).buildTypes.forEach { buildType ->
val flutterBuildMode: String = buildModeFor(buildType)
if (flutterBuildMode == "release" && (pluginObject["dev_dependency"] as? Boolean == true)) {
// This plugin is a dev dependency will not be included in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
package com.flutter.gradle

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.dsl.ApplicationBuildType
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryBuildType
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.android.build.api.variant.VariantBuilder
Expand Down Expand Up @@ -528,6 +530,34 @@ class FlutterPluginUtilsTest {
assertEquals("release", result)
}

@Test
fun `buildModeFor with a name and debuggable flag prefers the profile name over debuggability`() {
assertEquals("profile", FlutterPluginUtils.buildModeFor("profile", isDebuggable = true))
assertEquals("debug", FlutterPluginUtils.buildModeFor("staging", isDebuggable = true))
assertEquals("release", FlutterPluginUtils.buildModeFor("staging", isDebuggable = false))
}

@Test
fun `buildModeFor reads isDebuggable from new-DSL application build types`() {
val buildType = mockk<ApplicationBuildType>()
every { buildType.name } returns "staging"
every { buildType.isDebuggable } returns true

assertEquals("debug", FlutterPluginUtils.buildModeFor(buildType))
}

@Test
fun `buildModeFor falls back to the conventional debug name for new-DSL library build types`() {
// LibraryBuildType has no public isDebuggable flag, so the name is the only signal.
val debugBuildType = mockk<LibraryBuildType>()
every { debugBuildType.name } returns "debug"
assertEquals("debug", FlutterPluginUtils.buildModeFor(debugBuildType))

val customBuildType = mockk<LibraryBuildType>()
every { customBuildType.name } returns "staging"
assertEquals("release", FlutterPluginUtils.buildModeFor(customBuildType))
}

// supportsBuildMode
@Test
fun `supportsBuildMode returns true if project should not use local engine`() {
Expand Down Expand Up @@ -2552,7 +2582,7 @@ class FlutterPluginUtilsTest {
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>()
val buildType = mockk<ApplicationBuildType>()
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns true
Expand Down Expand Up @@ -2584,7 +2614,7 @@ class FlutterPluginUtilsTest {
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>()
val buildType = mockk<ApplicationBuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
Expand Down Expand Up @@ -2622,7 +2652,7 @@ class FlutterPluginUtilsTest {
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>()
val buildType = mockk<ApplicationBuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "release"
every { buildType.isDebuggable } returns false
Expand Down Expand Up @@ -2676,7 +2706,7 @@ class FlutterPluginUtilsTest {
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>()
val buildType = mockk<ApplicationBuildType>()
val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true
Expand Down
Loading