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
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,27 @@ projects. That opt-out dies with AGP 10.
it is seeded with the merged value; set `abiOffset * 1000 + current`,
avoiding a self-referential `.map`. Fall back to a snapshot only if
read-then-set is impossible; record the outcome here.
- *Spike result:* _pending (P6)_.
- *Spike result:* read-then-set implemented in P6 (`VariantOutput.versionCode.orNull`
read at onVariants time, then `set(abiOffset * 1000 + base)`); the sandbox could not
execute builds, so the split-per-abi × flavor-defined-versionCode apkanalyzer check in
CI is the confirming gate. The `finalizeDsl` snapshot fallback remains unimplemented.
- *Behavioral shift for custom `onVariants` build scripts:* Under modern AGP 9
`androidComponents.onVariants`, plugins applied at line 25 of `build.gradle.kts`
execute their callbacks before app-level `onVariants` blocks at line 70 (FIFO
callback ordering). Therefore, when building `--split-per-abi`, FGP seeds
`output.versionCode` (`abiOffset * 1000 + base`) before app-level callbacks run.
Any custom app-level `onVariants` block that reads `output.versionCode.get()` will
observe the offset value rather than the base `versionCode`. For standard apps, no
change is needed. Even if an app script transforms `versionCode` (e.g. via
multiplication or addition), monotonic ABI ordering (`armeabi-v7a` < `arm64-v8a` <
`x86_64`) and Google Play Store uniqueness are preserved. If an app requires an exact
literal numeric formula on base versionCodes, it can subtract `abiOffset` before its
transformation and re-add it afterwards.
- *flutter-apk copy outputs:* the copy task declares individual predictable
`@OutputFiles` (from target platforms × flavor × build mode) instead of the shared
`outputs/flutter-apk` directory, because a shared `@OutputDirectory` would overlap
between variants by construction. A runtime warning reports produced names outside
the predicted set.
6. **P3 pre-spike (afterEvaluate DSL mutation under newDsl).** The planned scratch-app
spike (AGP 9.1 + `newDsl=true` + custom build type, verifying that build-type
creation from `pluginProject.afterEvaluate` still works) could not run in the
Expand Down
187 changes: 92 additions & 95 deletions packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@

package com.flutter.gradle

import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.BuildType
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
import com.android.build.api.variant.FilterConfiguration
import com.android.build.api.variant.Variant
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.ApkVariant
import com.android.build.gradle.tasks.PackageAndroidArtifact
import com.flutter.gradle.FlutterPluginConstants.PLATFORM_ABI_LIST
import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist
import com.flutter.gradle.plugins.PluginHandler
import com.flutter.gradle.tasks.CopyFlutterApksTask
import com.flutter.gradle.tasks.CopyFlutterAssetsTask
import com.flutter.gradle.tasks.CopyFlutterJniLibsTask
import com.flutter.gradle.tasks.FlutterTask
Expand All @@ -23,7 +25,6 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.file.Directory
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.internal.os.OperatingSystem
Expand Down Expand Up @@ -367,6 +368,72 @@ class FlutterPlugin : Plugin<Project> {
copyFlutterAssetsTaskProvider,
CopyFlutterAssetsTask::destinationDir
)

// Per-ABI versionCode for --split-per-abi builds: read the merged versionCode
// AGP seeded on each variant output and offset it by the ABI constant.
// (Read-then-set with a plain get, not a self-referential lazy map.)
val appliedAbiVersionCodes = mutableMapOf<String, Int>()
if (FlutterPluginUtils.shouldProjectSplitPerAbi(projectToAddTasksTo) &&
!FlutterPluginUtils.shouldForceVersionCodeIgnoringAbi(projectToAddTasksTo)
) {
(variant as? ApplicationVariant)?.outputs?.forEach { output ->
val abi: String? =
output.filters
.find { it.filterType == FilterConfiguration.FilterType.ABI }
?.identifier
val abiVersionCode: Int? = FlutterPluginConstants.ABI_VERSION[abi]
val baseVersionCode: Int? = output.versionCode.orNull
if (abi != null && abiVersionCode != null && baseVersionCode != null) {
val newVersionCode = abiVersionCode * 1000 + baseVersionCode
output.versionCode.set(newVersionCode)
appliedAbiVersionCodes[abi] = newVersionCode
}
}
}

// Copy the output APKs into a known location, so `flutter run` or
// `flutter build apk` can discover them. By default, this is
// `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`, where the
// filename is `app<-abi>?<-flavor-name>?-<build-mode>.apk` (unchanged from
// the pre-migration assemble.doLast copy).
val variantBuildModeName =
FlutterPluginUtils.buildModeFor(variant.buildType ?: variant.name, variant.debuggable)
val flutterApkDir =
projectToAddTasksTo.layout.buildDirectory.dir("outputs/flutter-apk")
val apkOutputFileNames =
flutterApkOutputFileNames(projectToAddTasksTo, variant.flavorName, variantBuildModeName)
val copyFlutterApksTaskProvider: TaskProvider<CopyFlutterApksTask> =
projectToAddTasksTo.tasks.register(
"copyFlutterApks$capitalizeVariantName",
CopyFlutterApksTask::class.java
) {
apkDirectory.set(variant.artifacts.get(SingleArtifact.APK))
builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
buildModeName.set(variantBuildModeName)
flavorName.set(variant.flavorName ?: "")
destinationDir.set(flutterApkDir)
outputApks.from(apkOutputFileNames.map { name -> flutterApkDir.map { dir -> dir.file(name) } })
expectedVersionCodes.set(appliedAbiVersionCodes)
}
// The flutter-apk copy must run whenever the variant is assembled. The
// assemble task does not exist yet at variant-API time, so match it by name,
// and assert after evaluation that it was found: a silent no-match would mean
// `flutter run`/`flutter build apk` no longer find their APKs.
val assembleTaskName = "assemble$capitalizeVariantName"
projectToAddTasksTo.tasks
.matching { it.name == assembleTaskName }
.configureEach { finalizedBy(copyFlutterApksTaskProvider) }
projectToAddTasksTo.gradle.projectsEvaluated {
if (projectToAddTasksTo.tasks.findByName(assembleTaskName) == null) {
throw GradleException(
"Flutter expected the Android Gradle Plugin to create the " +
"'$assembleTaskName' task, but it does not exist, so the " +
"flutter-apk copy for variant '${variant.name}' could not be " +
"attached. Please file an issue at " +
"https://github.com/flutter/flutter/issues."
)
}
}
}

val copyJniLibsTaskProvider: TaskProvider<CopyFlutterJniLibsTask> =
Expand Down Expand Up @@ -401,63 +468,6 @@ class FlutterPlugin : Plugin<Project> {
if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) {
val appExtension = FlutterPluginUtils.getAndroidApplicationExtension(projectToAddTasksTo)
configureAbis(projectToAddTasksTo, appExtension)
val android: AbstractAppExtension =
projectToAddTasksTo.extensions.findByName("android") as AbstractAppExtension
android.applicationVariants.configureEach {
val variant = this
val assembleTask = variant.assembleProvider.get()
if (!FlutterPluginUtils.shouldConfigureFlutterTask(
projectToAddTasksTo,
assembleTask
)
) {
return@configureEach
}
// Per-ABI versionCode override; migrates to VariantOutput.versionCode.
// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
configureLegacyAbiVersionCodeOverride(variant, projectToAddTasksTo)

// Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
// can discover them. By default, this is `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`.
//
// The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.apk`.
// Where:
// * `abi` can be `armeabi-v7a|arm64-v8a|x86_64` only if the flag `split-per-abi` is set.
// * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called.
// * `build-mode` can be `release|debug|profile`.
variant.outputs.forEach { output ->
assembleTask.doLast {
// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
@Suppress("DEPRECATION")
output as com.android.build.gradle.api.ApkVariantOutput
val packageApplicationProvider: PackageAndroidArtifact =
variant.packageApplicationProvider.get()
val outputDirectory: Directory =
packageApplicationProvider.outputDirectory.get()
val outputDirectoryStr: String = outputDirectory.toString()
var filename = "app"

// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
@Suppress("DEPRECATION")
val abi = output.getFilter(com.android.build.VariantOutput.FilterType.ABI)
if (abi != null && abi.isNotEmpty()) {
filename += "-$abi"
}
if (variant.flavorName != null && variant.flavorName.isNotEmpty()) {
filename += "-${FlutterPluginUtils.lowercase(variant.flavorName)}"
}
filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}"
projectToAddTasksTo.copy {
from(File("$outputDirectoryStr/${output.outputFileName}"))
into(projectToAddTasksTo.layout.buildDirectory.dir("outputs/flutter-apk"))
rename { "$filename.apk" }
}
}
}
}
getPluginHandler(projectToAddTasksTo).configurePlugins(engineVersion!!)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
projectToAddTasksTo,
Expand Down Expand Up @@ -579,6 +589,28 @@ class FlutterPlugin : Plugin<Project> {
private fun flutterCompileTaskName(variantName: String): String =
FlutterPluginUtils.toCamelCase(listOf("compile", FLUTTER_BUILD_PREFIX, variantName))

/**
* The flutter-apk file names a variant build produces, predictable at configuration
* time: one per target ABI for `--split-per-abi` builds (universal APKs are disabled
* for those, see [configureAbis]), or the single fat-APK name otherwise.
*/
private fun flutterApkOutputFileNames(
project: Project,
flavorName: String?,
buildModeName: String
): List<String> {
val flavorPart =
if (flavorName.isNullOrEmpty()) "" else "-${FlutterPluginUtils.lowercase(flavorName)}"
return if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
FlutterPluginUtils.getTargetPlatforms(project).map { platform ->
val abi = FlutterPluginConstants.PLATFORM_ARCH_MAP[platform]
"app-$abi$flavorPart-$buildModeName.apk"
}
} else {
listOf("app$flavorPart-$buildModeName.apk")
}
}

/**
* Configures flutter default abi support respecting flutter command line flags.
*/
Expand Down Expand Up @@ -723,41 +755,6 @@ class FlutterPlugin : Plugin<Project> {
}
}

// Per-ABI versionCode override for --split-per-abi builds. Last legacy-variant-API
// consumer on the application path besides the flutter-apk copy; both migrate to the
// variant API (VariantOutput.versionCode / SingleArtifact.APK) in the next phase.
// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
private fun configureLegacyAbiVersionCodeOverride(
@Suppress("DEPRECATION") variant: com.android.build.gradle.api.BaseVariant,
project: Project
) {
if (!FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
return
}
variant.outputs.forEach { output ->
// need to force this as the API does not return the right thing for our use.
// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
@Suppress("DEPRECATION")
output as com.android.build.gradle.api.ApkVariantOutput
val versionCodeIfPresent: Int? = if (variant is ApkVariant) variant.versionCode else null

// TODO(gmackall): Migrate to AGPs variant api.
// https://github.com/flutter/flutter/issues/166550
@Suppress("DEPRECATION")
val filterIdentifier: String? =
output.getFilter(com.android.build.VariantOutput.FilterType.ABI)
val abiVersionCode: Int? = FlutterPluginConstants.ABI_VERSION[filterIdentifier]
if (abiVersionCode != null && !FlutterPluginUtils.shouldForceVersionCodeIgnoringAbi(project)) {
output.versionCodeOverride = abiVersionCode * 1000 + (
versionCodeIfPresent
?: variant.mergedFlavor.versionCode as Int
)
}
}
}

// Add-to-app module (library) path. Still entirely on the legacy variant API; the
// whole path is rewired to the variant API when add-to-app migrates
// (https://github.com/flutter/flutter/issues/166550).
Expand Down
Loading