[reland] Convert the Flutter Gradle Plugin entirely to Kotlin source (#166676)

Relands https://github.com/flutter/flutter/pull/166114.

The original PR failed this postsubmit

https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8718287794116896097/+/u/run_engine_dependency_proxy_test/stdout

Because 
```
Task result:
{
  "success": false,
  "reason": "Task failed: Expected Android engine maven dependency URL to resolve to https://storage.googleapis.com/download.flutter.io. Got https://storage.googleapis.com//download.flutter.io instead"
}
```
which was because apparently in Groovy
```groovy
String foo = ""
if (foo) { 
    // branch
}
```
Evaluates to false (i.e. does not take the branch). So we need to check
if the `engineRealm` string is empty, which is what the additional
commit does.

## 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.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Gray Mackall <mackall@google.com>
This commit is contained in:
Gray Mackall 2025-04-07 09:36:26 -07:00 committed by GitHub
parent a333757dcf
commit 56e11aed71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1747 additions and 1121 deletions

View File

@ -34,7 +34,7 @@ gradlePlugin {
// The "flutterPlugin" name isn't used anywhere. // The "flutterPlugin" name isn't used anywhere.
create("flutterPlugin") { create("flutterPlugin") {
id = "dev.flutter.flutter-gradle-plugin" id = "dev.flutter.flutter-gradle-plugin"
implementationClass = "FlutterPlugin" implementationClass = "com.flutter.gradle.FlutterPlugin"
} }
// The "flutterAppPluginLoaderPlugin" name isn't used anywhere. // The "flutterAppPluginLoaderPlugin" name isn't used anywhere.
create("flutterAppPluginLoaderPlugin") { create("flutterAppPluginLoaderPlugin") {

View File

@ -1,722 +0,0 @@
/* groovylint-disable LineLength, UnnecessaryGString, UnnecessaryGetter */
// 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.
import com.android.build.OutputFile
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.PackageAndroidArtifact
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType
import com.flutter.gradle.BaseApplicationNameHandler
import com.flutter.gradle.DependencyVersionChecker
import com.flutter.gradle.FlutterExtension
import com.flutter.gradle.FlutterPluginConstants
import com.flutter.gradle.FlutterTask
import com.flutter.gradle.FlutterPluginUtils
import com.flutter.gradle.NativePluginLoaderReflectionBridge
import org.gradle.api.file.Directory
import java.nio.file.Paths
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.Task
import org.gradle.api.UnknownTaskException
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.internal.os.OperatingSystem
class FlutterPlugin implements Plugin<Project> {
private final static String propLocalEngineRepo = "local-engine-repo"
/**
* The name prefix for flutter builds. This is used to identify gradle tasks
* where we expect the flutter tool to provide any error output, and skip the
* standard Gradle error output in the FlutterEventLogger. If you change this,
* be sure to change any instances of this string in symbols in the code below
* to match.
*/
static final String FLUTTER_BUILD_PREFIX = "flutterBuild"
private Project project
private File flutterRoot
private File flutterExecutable
private String localEngine
private String localEngineHost
private String localEngineSrcPath
private Properties localProperties
private String engineVersion
private String engineRealm
private List<Map<String, Object>> pluginList
private List<Map<String, Object>> pluginDependencies
/**
* Flutter Docs Website URLs for help messages.
*/
private final String kWebsiteDeploymentAndroidBuildConfig = "https://flutter.dev/to/review-gradle-config"
@Override
void apply(Project project) {
this.project = project
Project rootProject = project.rootProject
if (FlutterPluginUtils.isFlutterAppProject(project)) {
rootProject.tasks.register("generateLockfiles") {
doLast {
rootProject.subprojects.each { subproject ->
String gradlew = (OperatingSystem.current().isWindows()) ?
"${rootProject.projectDir}/gradlew.bat" : "${rootProject.projectDir}/gradlew"
rootProject.exec {
workingDir(rootProject.projectDir)
executable(gradlew)
args(":${subproject.name}:dependencies", "--write-locks")
}
}
}
}
}
String flutterRootPath = resolveProperty("flutter.sdk", System.getenv("FLUTTER_ROOT"))
if (flutterRootPath == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
}
flutterRoot = project.file(flutterRootPath)
if (!flutterRoot.isDirectory()) {
throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
}
engineVersion = FlutterPluginUtils.shouldProjectUseLocalEngine(project)
? "+" // Match any version since there's only one.
: "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "cache", "engine.stamp").toFile().text.trim()
engineRealm = Paths.get(flutterRoot.absolutePath, "bin", "cache", "engine.realm").toFile().text.trim()
if (engineRealm) {
engineRealm += "/"
}
// Configure the Maven repository.
String hostedRepository = System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL) ?: FlutterPluginConstants.DEFAULT_MAVEN_HOST
String repository = FlutterPluginUtils.shouldProjectUseLocalEngine(project)
? project.property(propLocalEngineRepo)
: "$hostedRepository/${engineRealm}download.flutter.io"
rootProject.allprojects {
repositories {
maven {
url(repository)
}
}
}
// Load shared gradle functions
project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "scripts", "native_plugin_loader.gradle.kts")
FlutterExtension extension = project.extensions.create("flutter", FlutterExtension)
Properties localProperties = new Properties()
File localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
extension.flutterVersionCode = localProperties.getProperty("flutter.versionCode", "1")
extension.flutterVersionName = localProperties.getProperty("flutter.versionName", "1.0")
this.addFlutterTasks(project)
FlutterPluginUtils.forceNdkDownload(project, flutterRootPath)
// By default, assembling APKs generates fat APKs if multiple platforms are passed.
// Configuring split per ABI allows to generate separate APKs for each abi.
// This is a noop when building a bundle.
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
project.android {
splits {
abi {
// Enables building multiple APKs per ABI.
enable(true)
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies that we do not want to also generate a universal APK that includes all ABIs.
universalApk(false)
}
}
}
}
final String propDeferredComponentNames = "deferred-component-names"
if (project.hasProperty(propDeferredComponentNames)) {
String[] componentNames = project.property(propDeferredComponentNames).split(",").collect {":${it}"}
project.android {
dynamicFeatures = componentNames
}
}
FlutterPluginUtils.getTargetPlatforms(project).each { targetArch ->
String abiValue = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
project.android {
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
splits {
abi {
include(abiValue)
}
}
}
}
}
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile()
// Validate that the provided Gradle, Java, AGP, and KGP versions are all within our
// supported range.
// TODO(gmackall) Dependency version checking is currently implemented as an additional
// Gradle plugin because we can't import it from Groovy code. As part of the Groovy
// -> Kotlin migration, we should remove this complexity and perform the checks inside
// of the main Flutter Gradle Plugin.
// See https://github.com/flutter/flutter/issues/121541#issuecomment-1920363687.
final Boolean shouldSkipDependencyChecks = project.hasProperty("skipDependencyChecks") && project.getProperty("skipDependencyChecks")
if (!shouldSkipDependencyChecks) {
try {
DependencyVersionChecker.checkDependencyVersions(project)
} catch (Exception e) {
if (!project.hasProperty("usesUnsupportedDependencyVersions") || !project.usesUnsupportedDependencyVersions) {
// 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
}
}
}
// Use Kotlin source to handle baseApplicationName logic due to Groovy dynamic dispatch bug.
BaseApplicationNameHandler.setBaseName(project)
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
"gradle", "flutter_proguard_rules.pro")
project.android.buildTypes {
// Add profile build type.
profile {
initWith(debug)
if (it.hasProperty("matchingFallbacks")) {
matchingFallbacks = ["debug", "release"]
}
}
// TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet.
// This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove
// this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see
// increased app size due to this.
if (FlutterPluginUtils.shouldShrinkResources(project)) {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled(true)
// Enables resource shrinking, which is performed by the Android Gradle plugin.
// The resource shrinker can't be used for libraries.
shrinkResources(FlutterPluginUtils.isBuiltAsApp(project))
// Fallback to `android/app/proguard-rules.pro`.
// This way, custom Proguard rules can be configured as needed.
proguardFiles(project.android.getDefaultProguardFile("proguard-android-optimize.txt"), flutterProguardRules, "proguard-rules.pro")
}
}
}
if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
// This is required to pass the local engine to flutter build aot.
String engineOutPath = project.property("local-engine-out")
File engineOut = project.file(engineOutPath)
if (!engineOut.isDirectory()) {
throw new GradleException("local-engine-out must point to a local engine build")
}
localEngine = engineOut.name
localEngineSrcPath = engineOut.parentFile.parent
String engineHostOutPath = project.property("local-engine-host-out")
File engineHostOut = project.file(engineHostOutPath)
if (!engineHostOut.isDirectory()) {
throw new GradleException("local-engine-host-out must point to a local engine host build")
}
localEngineHost = engineHostOut.name
}
project.android.buildTypes.all(this.&addFlutterDependencies)
}
/**
* Adds the dependencies required by the Flutter project.
* This includes:
* 1. The embedding
* 2. libflutter.so
*/
void addFlutterDependencies(BuildType buildType) {
FlutterPluginUtils.addFlutterDependencies(project, buildType, getPluginList(project), engineVersion)
}
/**
* Configures the Flutter plugin dependencies.
*
* The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
* the tool generates a `.flutter-plugins-dependencies` file, which contains a map to each plugin location.
* Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject.
*/
private void configurePlugins(Project project) {
configureLegacyPluginEachProjects(project)
getPluginList(project).each { Map<String, Object> plugin ->
FlutterPluginUtils.configurePluginProject(project, plugin, engineVersion)
}
getPluginList(project).each {Map<String, Object> plugin ->
FlutterPluginUtils.configurePluginDependencies(project, plugin)
}
}
// TODO(54566, 48918): Can remove once the issues are resolved.
// This means all references to `.flutter-plugins` are then removed and
// apps only depend exclusively on the `plugins` property in `.flutter-plugins-dependencies`.
/**
* Workaround to load non-native plugins for developers who may still use an
* old `settings.gradle` which includes all the plugins from the
* `.flutter-plugins` file, even if not made for Android.
* The settings.gradle then:
* 1) tries to add the android plugin implementation, which does not
* exist at all, but is also not included successfully
* (which does not throw an error and therefore isn't a problem), or
* 2) includes the plugin successfully as a valid android plugin
* directory exists, even if the surrounding flutter package does not
* support the android platform (see e.g. apple_maps_flutter: 1.0.1).
* So as it's included successfully it expects to be added as API.
* This is only possible by taking all plugins into account, which
* only appear on the `dependencyGraph` and in the `.flutter-plugins` file.
* So in summary the plugins are currently selected from the `dependencyGraph`
* and filtered then with the [doesSupportAndroidPlatform] method instead of
* just using the `plugins.android` list.
*/
static private void configureLegacyPluginEachProjects(Project project) {
try {
// Read the contents of the settings.gradle file.
// Remove block/line comments
String settingsText = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).text
settingsText = settingsText.replaceAll(/(?s)\/\*.*?\*\//, '').replaceAll(/(?m)\/\/.*$/, '')
if (!settingsText.contains("'.flutter-plugins'")) {
return
}
} catch (FileNotFoundException ignored) {
throw new GradleException("settings.gradle/settings.gradle.kts does not exist: " +
"${FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).absolutePath}")
}
// TODO(matanlurey): https://github.com/flutter/flutter/issues/48918.
project.logger.quiet("Warning: This project is still reading the deprecated '.flutter-plugins. file.")
project.logger.quiet("In an upcoming stable release support for this file will be completely removed and your build will fail.")
project.logger.quiet("See https:/flutter.dev/to/flutter-plugins-configuration.")
List<Map<String, Object>> deps = getPluginDependencies(project)
List<String> plugins = getPluginList(project).collect { it.name as String }
deps.removeIf { plugins.contains(it.name) }
deps.each {
Project pluginProject = project.rootProject.findProject(":${it.name}")
if (pluginProject == null) {
// Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`.
project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle/settings.gradle.kts.")
} else if (FlutterPluginUtils.pluginSupportsAndroidPlatform(pluginProject)) {
// Plugin has a functioning `android` folder and is included successfully, although it's not supported.
// It must be configured nonetheless, to not throw an "Unresolved reference" exception.
FlutterPluginUtils.configurePluginProject(project, it, engineVersion)
/* groovylint-disable-next-line EmptyElseBlock */
} else {
// Plugin has no or an empty `android` folder. No action required.
}
}
}
/**
* Gets the list of plugins (as map) that support the Android platform.
*
* The map value contains either the plugins `name` (String),
* its `path` (String), or its `dependencies` (List<String>).
* See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts
*/
private List<Map<String, Object>> getPluginList(Project project) {
if (pluginList == null) {
pluginList = NativePluginLoaderReflectionBridge.getPlugins(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project))
}
return pluginList
}
// TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
// https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
/** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
private List<Map<String, Object>> getPluginDependencies(Project project) {
if (pluginDependencies == null) {
Map meta = NativePluginLoaderReflectionBridge.getDependenciesMetadata(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project))
if (meta == null) {
pluginDependencies = []
} else {
assert(meta.dependencyGraph instanceof List<Map>)
pluginDependencies = meta.dependencyGraph as List<Map<String, Object>>
}
}
return pluginDependencies
}
private String resolveProperty(String name, String defaultValue) {
if (localProperties == null) {
localProperties = FlutterPluginUtils.readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
}
return project.findProperty(name) ?: localProperties?.getProperty(name, defaultValue)
}
private void addFlutterTasks(Project project) {
if (project.state.failure) {
return
}
String[] fileSystemRootsValue = null
final String propFileSystemRoots = "filesystem-roots"
if (project.hasProperty(propFileSystemRoots)) {
fileSystemRootsValue = project.property(propFileSystemRoots).split("\\|")
}
String fileSystemSchemeValue = null
final String propFileSystemScheme = "filesystem-scheme"
if (project.hasProperty(propFileSystemScheme)) {
fileSystemSchemeValue = project.property(propFileSystemScheme)
}
Boolean trackWidgetCreationValue = true
final String propTrackWidgetCreation = "track-widget-creation"
if (project.hasProperty(propTrackWidgetCreation)) {
trackWidgetCreationValue = project.property(propTrackWidgetCreation).toBoolean()
}
String frontendServerStarterPathValue = null
final String propFrontendServerStarterPath = "frontend-server-starter-path"
if (project.hasProperty(propFrontendServerStarterPath)) {
frontendServerStarterPathValue = project.property(propFrontendServerStarterPath)
}
String extraFrontEndOptionsValue = null
final String propExtraFrontEndOptions = "extra-front-end-options"
if (project.hasProperty(propExtraFrontEndOptions)) {
extraFrontEndOptionsValue = project.property(propExtraFrontEndOptions)
}
String extraGenSnapshotOptionsValue = null
final String propExtraGenSnapshotOptions = "extra-gen-snapshot-options"
if (project.hasProperty(propExtraGenSnapshotOptions)) {
extraGenSnapshotOptionsValue = project.property(propExtraGenSnapshotOptions)
}
String splitDebugInfoValue = null
final String propSplitDebugInfo = "split-debug-info"
if (project.hasProperty(propSplitDebugInfo)) {
splitDebugInfoValue = project.property(propSplitDebugInfo)
}
Boolean dartObfuscationValue = false
final String propDartObfuscation = "dart-obfuscation"
if (project.hasProperty(propDartObfuscation)) {
dartObfuscationValue = project.property(propDartObfuscation).toBoolean()
}
Boolean treeShakeIconsOptionsValue = false
final String propTreeShakeIcons = "tree-shake-icons"
if (project.hasProperty(propTreeShakeIcons)) {
treeShakeIconsOptionsValue = project.property(propTreeShakeIcons).toBoolean()
}
String dartDefinesValue = null
final String propDartDefines = "dart-defines"
if (project.hasProperty(propDartDefines)) {
dartDefinesValue = project.property(propDartDefines)
}
String performanceMeasurementFileValue
final String propPerformanceMeasurementFile = "performance-measurement-file"
if (project.hasProperty(propPerformanceMeasurementFile)) {
performanceMeasurementFileValue = project.property(propPerformanceMeasurementFile)
}
String codeSizeDirectoryValue
final String propCodeSizeDirectory = "code-size-directory"
if (project.hasProperty(propCodeSizeDirectory)) {
codeSizeDirectoryValue = project.property(propCodeSizeDirectory)
}
Boolean deferredComponentsValue = false
final String propDeferredComponents = "deferred-components"
if (project.hasProperty(propDeferredComponents)) {
deferredComponentsValue = project.property(propDeferredComponents).toBoolean()
}
Boolean validateDeferredComponentsValue = true
final String propValidateDeferredComponents = "validate-deferred-components"
if (project.hasProperty(propValidateDeferredComponents)) {
validateDeferredComponentsValue = project.property(propValidateDeferredComponents).toBoolean()
}
FlutterPluginUtils.addTaskForJavaVersion(project)
if (FlutterPluginUtils.isFlutterAppProject(project)) {
FlutterPluginUtils.addTaskForPrintBuildVariants(project)
FlutterPluginUtils.addTasksForOutputsAppLinkSettings(project)
}
List<String> targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project)
def addFlutterDeps = { variant ->
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
variant.outputs.each { output ->
// Assigns the new version code to versionCodeOverride, which changes the version code
// for only the output APK, not for the variant itself. Skipping this step simply
// causes Gradle to use the value of variant.versionCode for the APK.
// For more, see https://developer.android.com/studio/build/configure-apk-splits
Integer abiVersionCode = FlutterPluginConstants.ABI_VERSION[output.getFilter(OutputFile.ABI)]
if (abiVersionCode != null) {
output.versionCodeOverride =
abiVersionCode * 1000 + variant.versionCode
}
}
}
// Build an AAR when this property is defined.
boolean isBuildingAar = project.hasProperty("is-plugin")
// In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
// `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR.
Task packageAssets
Task cleanPackageAssets
try {
packageAssets = project.tasks.named("package${variant.name.capitalize()}Assets").get()
} catch (UnknownTaskException ignored) {
packageAssets = null
}
try {
cleanPackageAssets = project.tasks.named("cleanPackage${variant.name.capitalize()}Assets").get()
} catch (UnknownTaskException ignored) {
cleanPackageAssets = null
}
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
String variantBuildMode = FlutterPluginUtils.buildModeFor(variant.buildType)
String flavorValue = variant.getFlavorName()
String taskName = FlutterPluginUtils.toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
// Be careful when configuring task below, Groovy has bizarre
// scoping rules: writing `verbose isVerbose()` means calling
// `isVerbose` on the task itself - which would return `verbose`
// original value. You either need to hoist the value
// into a separate variable `verbose verboseValue` or prefix with
// `this` (`verbose this.isVerbose()`).
TaskProvider<FlutterTask> compileTaskProvider = project.tasks.register(taskName , FlutterTask) {
flutterRoot(this.flutterRoot)
flutterExecutable(this.flutterExecutable)
buildMode(variantBuildMode)
minSdkVersion(variant.mergedFlavor.minSdkVersion.apiLevel)
localEngine(this.localEngine)
localEngineHost(this.localEngineHost)
localEngineSrcPath(this.localEngineSrcPath)
targetPath(FlutterPluginUtils.getFlutterTarget(project))
verbose(FlutterPluginUtils.isProjectVerbose(project))
fastStart(FlutterPluginUtils.isProjectFastStart(project))
fileSystemRoots(fileSystemRootsValue)
fileSystemScheme(fileSystemSchemeValue)
trackWidgetCreation(trackWidgetCreationValue)
targetPlatformValues = targetPlatforms
sourceDir(FlutterPluginUtils.getFlutterSourceDirectory(project))
intermediateDir(project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/")))
frontendServerStarterPath(frontendServerStarterPathValue)
extraFrontEndOptions(extraFrontEndOptionsValue)
extraGenSnapshotOptions(extraGenSnapshotOptionsValue)
splitDebugInfo(splitDebugInfoValue)
treeShakeIcons(treeShakeIconsOptionsValue)
dartObfuscation(dartObfuscationValue)
dartDefines(dartDefinesValue)
performanceMeasurementFile(performanceMeasurementFileValue)
codeSizeDirectory(codeSizeDirectoryValue)
deferredComponents(deferredComponentsValue)
validateDeferredComponents(validateDeferredComponentsValue)
flavor(flavorValue)
}
Task compileTask = compileTaskProvider.get()
File libJar = project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar"))
TaskProvider<Jar> packJniLibsTaskProvider = project.tasks.register("packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", Jar) {
destinationDirectory = libJar.parentFile
archiveFileName = libJar.name
dependsOn(compileTask)
targetPlatforms.each { targetPlatform ->
String abi = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
from("${compileTask.intermediateDir}/${abi}") {
include("*.so")
// Move `app.so` to `lib/<abi>/libapp.so`
rename { String filename ->
return "lib/${abi}/lib${filename}"
}
}
// Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble.
// The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'.
String buildDir = "${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build"
String nativeAssetsDir = "${buildDir}/native_assets/android/jniLibs/lib"
from("${nativeAssetsDir}/${abi}") {
include("*.so")
rename { String filename ->
return "lib/${abi}/${filename}"
}
}
}
}
Task packJniLibsTask = packJniLibsTaskProvider.get()
FlutterPluginUtils.addApiDependencies(project, variant.name, project.files {
packJniLibsTask
})
TaskProvider<Copy> copyFlutterAssetsTaskProvider = project.tasks.register(
"copyFlutterAssets${variant.name.capitalize()}" , Copy
) {
dependsOn(compileTask)
with(compileTask.assets)
String currentGradleVersion = project.getGradle().getGradleVersion()
// See https://docs.gradle.org/current/javadoc/org/gradle/api/file/ConfigurableFilePermissions.html
// See https://github.com/flutter/flutter/pull/50047
if (FlutterPluginUtils.compareVersionStrings(currentGradleVersion, "8.3") >= 0) {
filePermissions {
user {
read = true
write = true
}
}
} else {
// See https://docs.gradle.org/8.2/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:fileMode
// See https://github.com/flutter/flutter/pull/50047
fileMode(0644)
}
if (isUsedAsSubproject) {
dependsOn(packageAssets)
dependsOn(cleanPackageAssets)
into(packageAssets.outputDir)
return
}
// `variant.mergeAssets` will be removed at the end of 2019.
def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
variant.mergeAssetsProvider.get() : variant.mergeAssets
dependsOn(mergeAssets)
dependsOn("clean${mergeAssets.name.capitalize()}")
mergeAssets.mustRunAfter("clean${mergeAssets.name.capitalize()}")
into(mergeAssets.outputDir)
}
Task copyFlutterAssetsTask = copyFlutterAssetsTaskProvider.get()
if (!isUsedAsSubproject) {
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
}
// The following tasks use the output of copyFlutterAssetsTask,
// so it's necessary to declare it as an dependency since Gradle 8.
// See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency.
def tasksToCheck = [
"compress${variant.name.capitalize()}Assets",
"bundle${variant.name.capitalize()}Aar",
"bundle${variant.name.capitalize()}LocalLintAar"
]
tasksToCheck.each { taskTocheck ->
try {
project.tasks.named(taskTocheck).configure { task ->
task.dependsOn(copyFlutterAssetsTask)
}
} catch (UnknownTaskException ignored) {
}
}
return copyFlutterAssetsTask
} // end def addFlutterDeps
if (FlutterPluginUtils.isFlutterAppProject(project)) {
AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android")
android.applicationVariants.configureEach { variant ->
Task assembleTask = variant.assembleProvider.get()
if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask)) {
return
}
Task copyFlutterAssetsTask = addFlutterDeps(variant)
BaseVariantOutput variantOutput = variant.outputs.first()
ProcessAndroidResources processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
// 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|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.each { output ->
assembleTask.doLast {
PackageAndroidArtifact packageApplicationProvider = variant.packageApplicationProvider.get()
Directory outputDirectory = packageApplicationProvider.outputDirectory.get()
String outputDirectoryStr = outputDirectory.toString()
String filename = "app"
String abi = output.getFilter(OutputFile.ABI)
if (abi != null && !abi.isEmpty()) {
filename += "-${abi}"
}
if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
filename += "-${variant.flavorName.toLowerCase()}"
}
filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}"
project.copy {
from new File("$outputDirectoryStr/${output.outputFileName}")
into new File("${project.layout.buildDirectory.dir("outputs/flutter-apk").get()}")
rename {
return "${filename}.apk"
}
}
}
}
}
// Copy the native assets created by build.dart and placed here by flutter assemble.
// This path is not flavor specific and must only be added once.
// If support for flavors is added to native assets, then they must only be added
// once per flavor; see https://github.com/dart-lang/native/issues/1359.
String nativeAssetsDir = "${project.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/"
android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
configurePlugins(project)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
return
}
// Flutter host module project (Add-to-app).
String hostAppProjectName = project.rootProject.hasProperty("flutter.hostAppProjectName") ? project.rootProject.property("flutter.hostAppProjectName") : "app"
Project appProject = project.rootProject.findProject(":${hostAppProjectName}")
assert(appProject != null) : "Project :${hostAppProjectName} doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=<project-name>` in gradle.properties."
// Wait for the host app project configuration.
appProject.afterEvaluate {
assert(appProject.android != null)
project.android.libraryVariants.all { libraryVariant ->
Task copyFlutterAssetsTask
appProject.android.applicationVariants.all { appProjectVariant ->
Task appAssembleTask = appProjectVariant.assembleProvider.get()
if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) {
return
}
// Find a compatible application variant in the host app.
//
// For example, consider a host app that defines the following variants:
// | ----------------- | ----------------------------- |
// | Build Variant | Flutter Equivalent Variant |
// | ----------------- | ----------------------------- |
// | freeRelease | release |
// | freeDebug | debug |
// | freeDevelop | debug |
// | profile | profile |
// | ----------------- | ----------------------------- |
//
// This mapping is based on the following rules:
// 1. If the host app build variant name is `profile` then the equivalent
// Flutter variant is `profile`.
// 2. If the host app build variant is debuggable
// (e.g. `buildType.debuggable = true`), then the equivalent Flutter
// variant is `debug`.
// 3. Otherwise, the equivalent Flutter variant is `release`.
String variantBuildMode = FlutterPluginUtils.buildModeFor(libraryVariant.buildType)
if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
return
}
copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps(libraryVariant)
Task mergeAssets = project
.tasks
.findByPath(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets")
assert(mergeAssets)
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
}
}
configurePlugins(project)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
}
}

View File

@ -0,0 +1,743 @@
// 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.VariantOutput
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.api.ApkVariantOutput
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.PackageAndroidArtifact
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist
import com.flutter.gradle.plugins.PluginHandler
import com.flutter.gradle.tasks.FlutterTask
import org.gradle.api.GradleException
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.api.tasks.bundling.Jar
import org.gradle.internal.os.OperatingSystem
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Paths
import java.util.Properties
class FlutterPlugin : Plugin<Project> {
private var project: Project? = null
private var flutterRoot: File? = null
private var flutterExecutable: File? = null
private var localEngine: String? = null
private var localEngineHost: String? = null
private var localEngineSrcPath: String? = null
private var localProperties: Properties? = null
private var engineVersion: String? = null
private var engineRealm: String? = null
private var pluginHandler: PluginHandler? = null
override fun apply(project: Project) {
this.project = project
val rootProject = project.rootProject
if (FlutterPluginUtils.isFlutterAppProject(project)) {
addTaskForLockfileGeneration(rootProject)
}
val flutterRootSystemVal: String? = System.getenv("FLUTTER_ROOT")
val flutterRootPath: String =
resolveProperty("flutter.sdk", flutterRootSystemVal)
?: throw GradleException(
"Flutter SDK not found. Define location with flutter.sdk in the " +
"local.properties file or with a FLUTTER_ROOT environment variable."
)
flutterRoot = project.file(flutterRootPath)
if (!flutterRoot!!.isDirectory) {
throw GradleException("flutter.sdk must point to the Flutter SDK directory")
}
engineVersion =
if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
"+" // Match any version since there's only one.
} else {
val engineStampPath =
Paths.get(flutterRoot!!.absolutePath, "bin", "cache", "engine.stamp")
val engineStampContent = engineStampPath.toFile().readText().trim()
"1.0.0-$engineStampContent"
}
engineRealm =
Paths
.get(flutterRoot!!.absolutePath, "bin", "cache", "engine.realm")
.toFile()
.readText()
.trim()
if (engineRealm!!.isNotEmpty()) {
engineRealm += "/"
}
// Configure the Maven repository.
val hostedRepository: String =
System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL)
?: FlutterPluginConstants.DEFAULT_MAVEN_HOST
val repository: String? =
if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
project.property(PROP_LOCAL_ENGINE_REPO) as String?
} else {
"$hostedRepository/${engineRealm}download.flutter.io"
}
rootProject.allprojects {
repositories.maven {
url = uri(repository!!)
}
}
project.apply {
from(
Paths.get(
flutterRoot!!.absolutePath,
"packages",
"flutter_tools",
"gradle",
"src",
"main",
"scripts",
"native_plugin_loader.gradle.kts"
)
)
}
val flutterExtension: FlutterExtension =
project.extensions.create("flutter", FlutterExtension::class.java)
// TODO(gmackall): is this actually a different properties file than the previous one?
val rootProjectLocalProperties = Properties()
val rootProjectLocalPropertiesFile = rootProject.file("local.properties")
if (rootProjectLocalPropertiesFile.exists()) {
rootProjectLocalPropertiesFile.reader(StandardCharsets.UTF_8).use { reader ->
rootProjectLocalProperties.load(reader)
}
}
flutterExtension.flutterVersionCode =
rootProjectLocalProperties.getProperty("flutter.versionCode", "1")
flutterExtension.flutterVersionName =
rootProjectLocalProperties.getProperty("flutter.versionName", "1.0")
this.addFlutterTasks(project)
FlutterPluginUtils.forceNdkDownload(project, flutterRootPath)
// By default, assembling APKs generates fat APKs if multiple platforms are passed.
// Configuring split per ABI allows to generate separate APKs for each abi.
// This is a noop when building a bundle.
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
FlutterPluginUtils.getAndroidExtension(project).splits.abi {
isEnable = true
reset()
isUniversalApk = false
}
}
val propDeferredComponentNames: String = "deferred-component-names"
val deferredComponentNamesValue: String? =
project.findProperty(propDeferredComponentNames) as? String
if (deferredComponentNamesValue != null) {
val componentNames: Set<String> =
deferredComponentNamesValue
.split(',')
.map { ":$it" }
.toSet()
// TODO(gmackall): Unify the types we use for the android extension. This is yet
// another type we need unfortunately.
val androidExtensionAsApplicationExtension =
project.extensions.getByType(ApplicationExtension::class.java)
// TODO(gmackall): Should we clear here? I think this is equivalent to what we used to
// do, but unsure. Can't use a closure.
androidExtensionAsApplicationExtension.dynamicFeatures.clear()
androidExtensionAsApplicationExtension.dynamicFeatures.addAll(componentNames)
}
FlutterPluginUtils.getTargetPlatforms(project).forEach { targetArch ->
val abiValue: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
val androidExtension: BaseExtension = FlutterPluginUtils.getAndroidExtension(project)
androidExtension.splits.abi.include(abiValue!!)
}
val flutterExecutableName = getExecutableNameForPlatform("flutter")
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"] as? Boolean
?: false
)
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: String =
Paths
.get(
flutterRoot!!.absolutePath,
"packages",
"flutter_tools",
"gradle",
"flutter_proguard_rules.pro"
).toString()
// TODO(gmackall): reconsider getting the android extension every time
FlutterPluginUtils.getAndroidExtension(project).buildTypes {
// Add profile build type.
create("profile") {
initWith(getByName("debug"))
// TODO(gmackall): do we need to clear?
this.matchingFallbacks.clear()
this.matchingFallbacks.addAll(listOf("debug", "release"))
}
// TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet.
// This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove
// this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see
// increased app size due to this.
if (FlutterPluginUtils.shouldShrinkResources(project)) {
getByName("release") {
isMinifyEnabled = true
// Enables resource shrinking, which is performed by the Android Gradle plugin.
// The resource shrinker can't be used for libraries.
isShrinkResources = FlutterPluginUtils.isBuiltAsApp(project)
// Fallback to `android/app/proguard-rules.pro`.
// This way, custom Proguard rules can be configured as needed.
proguardFiles(
FlutterPluginUtils
.getAndroidExtension(project)
.getDefaultProguardFile("proguard-android-optimize.txt"),
flutterProguardRules,
"proguard-rules.pro"
)
}
}
}
if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) {
// This is required to pass the local engine to flutter build aot.
val engineOutPath: String = project.properties["local-engine-out"] as String
val engineOut: File = project.file(engineOutPath)
if (!engineOut.isDirectory) {
throw GradleException("local-engine-out must point to a local engine build")
}
localEngine = engineOut.name
localEngineSrcPath = engineOut.parentFile.parent
val engineHostOutPath: String = project.properties["local-engine-host-out"] as String
val engineHostOut: File = project.file(engineHostOutPath)
if (!engineHostOut.isDirectory) {
throw GradleException("local-engine-host-out must point to a local engine host build")
}
localEngineHost = engineHostOut.name
}
FlutterPluginUtils.getAndroidExtension(project).buildTypes.all {
addFlutterDependencies(this)
}
}
private fun addFlutterDependencies(buildType: com.android.builder.model.BuildType) {
FlutterPluginUtils.addFlutterDependencies(
project!!,
buildType,
getPluginHandler(project!!),
engineVersion!!
)
}
private fun getExecutableNameForPlatform(baseExecutableName: String): String =
if (OperatingSystem.current().isWindows) "$baseExecutableName.bat" else baseExecutableName
private fun resolveProperty(
propertyName: String,
defaultValue: String?
): String? {
if (localProperties == null) {
localProperties =
readPropertiesIfExist(File(project!!.projectDir.parentFile, "local.properties"))
}
return project?.findProperty(propertyName) as? String ?: localProperties!!.getProperty(
propertyName,
defaultValue
)
}
private fun addTaskForLockfileGeneration(rootProject: Project) {
rootProject.tasks.register("generateLockfiles") {
doLast {
rootProject.subprojects.forEach { subproject ->
val gradlew: String =
getExecutableNameForPlatform("${rootProject.projectDir}/gradlew")
rootProject.exec {
workingDir(rootProject.projectDir)
executable(gradlew)
args(":${subproject.name}:dependencies", "--write-locks")
}
}
}
}
}
private fun addFlutterTasks(projectToAddTasksTo: Project) {
if (projectToAddTasksTo.state.failure != null) {
return
}
FlutterPluginUtils.addTaskForJavaVersion(projectToAddTasksTo)
if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) {
FlutterPluginUtils.addTaskForPrintBuildVariants(projectToAddTasksTo)
FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo)
}
val targetPlatforms: List<String> =
FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo)
val flutterPlugin = this
if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) {
// TODO(gmackall): I think this can be BaseExtension, with findByType.
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
}
val copyFlutterAssetsTask: Task =
addFlutterDeps(variant, flutterPlugin, targetPlatforms)
val variantOutput: BaseVariantOutput = variant.outputs.first()
val processResources: ProcessAndroidResources =
try {
variantOutput.processResourcesProvider.get()
} catch (e: UnknownTaskException) {
variantOutput.processResources
}
processResources.dependsOn(copyFlutterAssetsTask)
// 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|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 {
output as ApkVariantOutput
val packageApplicationProvider: PackageAndroidArtifact =
variant.packageApplicationProvider.get()
val outputDirectory: Directory =
packageApplicationProvider.outputDirectory.get()
val outputDirectoryStr: String = outputDirectory.toString()
var filename = "app"
val abi = output.getFilter(VariantOutput.FilterType.ABI)
if (abi != null && abi.isNotEmpty()) {
filename += "-$abi"
}
if (variant.flavorName != null && variant.flavorName.isNotEmpty()) {
filename += "-${variant.flavorName.toLowerCase()}"
}
filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}"
projectToAddTasksTo.copy {
from(File("$outputDirectoryStr/${output.outputFileName}"))
into(
File(
"${
projectToAddTasksTo.layout.buildDirectory.dir("outputs/flutter-apk")
.get()
}"
)
)
rename { "$filename.apk" }
}
}
}
}
// Copy the native assets created by build.dart and placed here by flutter assemble.
// This path is not flavor specific and must only be added once.
// If support for flavors is added to native assets, then they must only be added
// once per flavor; see https://github.com/dart-lang/native/issues/1359.
val nativeAssetsDir: String =
"${projectToAddTasksTo.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/"
android.sourceSets
.getByName("main")
.jniLibs
.srcDir(nativeAssetsDir)
getPluginHandler(projectToAddTasksTo!!).configurePlugins(engineVersion!!)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
projectToAddTasksTo,
getPluginHandler(projectToAddTasksTo).getPluginList()
)
return
}
// Flutter host module project (Add-to-app).
val hostAppProjectName: String? =
if (projectToAddTasksTo.rootProject.hasProperty("flutter.hostAppProjectName")) {
projectToAddTasksTo.rootProject.property(
"flutter.hostAppProjectName"
) as? String
} else {
"app"
}
val appProject: Project? =
projectToAddTasksTo.rootProject.findProject(":$hostAppProjectName")
check(appProject != null) {
"Project :$hostAppProjectName doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=<project-name>` in gradle.properties."
}
// Wait for the host app project configuration.
appProject.afterEvaluate {
val androidLibraryExtension =
projectToAddTasksTo.extensions.findByType(LibraryExtension::class.java)
check(androidLibraryExtension != null)
androidLibraryExtension.libraryVariants.all libraryVariantAll@{
val libraryVariant = this
var copyFlutterAssetsTask: Task? = null
val androidAppExtension =
appProject.extensions.findByName("android") as? AbstractAppExtension
check(androidAppExtension != null)
androidAppExtension.applicationVariants.all applicationVariantAll@{
val appProjectVariant = this
val appAssembleTask: Task = appProjectVariant.assembleProvider.get()
if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) {
return@applicationVariantAll
}
// Find a compatible application variant in the host app.
//
// For example, consider a host app that defines the following variants:
// | ----------------- | ----------------------------- |
// | Build Variant | Flutter Equivalent Variant |
// | ----------------- | ----------------------------- |
// | freeRelease | release |
// | freeDebug | debug |
// | freeDevelop | debug |
// | profile | profile |
// | ----------------- | ----------------------------- |
//
// This mapping is based on the following rules:
// 1. If the host app build variant name is `profile` then the equivalent
// Flutter variant is `profile`.
// 2. If the host app build variant is debuggable
// (e.g. `buildType.debuggable = true`), then the equivalent Flutter
// variant is `debug`.
// 3. Otherwise, the equivalent Flutter variant is `release`.
val variantBuildMode: String =
FlutterPluginUtils.buildModeFor(libraryVariant.buildType)
if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
return@applicationVariantAll
}
copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps(
libraryVariant,
flutterPlugin,
targetPlatforms
)
val mergeAssets =
projectToAddTasksTo
.tasks
.findByPath(":$hostAppProjectName:merge${appProjectVariant.name.capitalize()}Assets")
check(mergeAssets != null)
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
}
}
getPluginHandler(projectToAddTasksTo).configurePlugins(engineVersion!!)
FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
projectToAddTasksTo,
getPluginHandler(projectToAddTasksTo).getPluginList()
)
}
private fun getPluginHandler(project: Project): PluginHandler {
if (this.pluginHandler == null) {
this.pluginHandler = PluginHandler(project)
}
return this.pluginHandler!!
}
companion object {
const val PROP_LOCAL_ENGINE_REPO: String = "local-engine-repo"
/**
* The name prefix for flutter builds. This is used to identify gradle tasks
* where we expect the flutter tool to provide any error output, and skip the
* standard Gradle error output in the FlutterEventLogger. If you change this,
* be sure to change any instances of this string in symbols in the code below
* to match.
*/
const val FLUTTER_BUILD_PREFIX: String = "flutterBuild"
/**
* Finds a task by name, returning null if the task does not exist.
*/
private fun findTaskOrNull(
project: Project,
taskName: String
): Task? =
try {
project.tasks.named(taskName).get()
} catch (ignored: UnknownTaskException) {
null
}
private fun addFlutterDeps(
variant: BaseVariant,
flutterPlugin: FlutterPlugin,
targetPlatforms: List<String>
): Task {
// Shorthand
val project: Project = flutterPlugin.project!!
val fileSystemRootsValue: Array<String>? =
project
.findProperty("filesystem-roots")
?.toString()
?.split("\\|")
?.toTypedArray()
val fileSystemSchemeValue: String? =
project.findProperty("filesystem-scheme")?.toString()
val trackWidgetCreationValue: Boolean =
project.findProperty("track-widget-creation")?.toString()?.toBoolean() ?: true
val frontendServerStarterPathValue: String? =
project.findProperty("frontend-server-starter-path")?.toString()
val extraFrontEndOptionsValue: String? =
project.findProperty("extra-front-end-options")?.toString()
val extraGenSnapshotOptionsValue: String? =
project.findProperty("extra-gen-snapshot-options")?.toString()
val splitDebugInfoValue: String? = project.findProperty("split-debug-info")?.toString()
val dartObfuscationValue: Boolean =
project.findProperty("dart-obfuscation")?.toString()?.toBoolean() ?: false
val treeShakeIconsOptionsValue: Boolean =
project.findProperty("tree-shake-icons")?.toString()?.toBoolean() ?: false
val dartDefinesValue: String? = project.findProperty("dart-defines")?.toString()
val performanceMeasurementFileValue: String? =
project.findProperty("performance-measurement-file")?.toString()
val codeSizeDirectoryValue: String? =
project.findProperty("code-size-directory")?.toString()
val deferredComponentsValue: Boolean =
project.findProperty("deferred-components")?.toString()?.toBoolean() ?: false
val validateDeferredComponentsValue: Boolean =
project.findProperty("validate-deferred-components")?.toString()?.toBoolean() ?: true
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
variant.outputs.forEach { output ->
// need to force this as the API does not return the right thing for our use.
output as ApkVariantOutput
val filterIdentifier: String =
output.getFilter(VariantOutput.FilterType.ABI)
val abiVersionCode: Int? = FlutterPluginConstants.ABI_VERSION[filterIdentifier]
if (abiVersionCode != null) {
output.versionCodeOverride
}
}
}
// Build an AAR when this property is defined.
val isBuildingAar: Boolean = project.hasProperty("is-plugin")
// In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
// `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR.
// TODO(gmackall): I think this is just always null? Which is great news! Consider removing.
val packageAssets: Task? =
findTaskOrNull(
project,
"package${FlutterPluginUtils.capitalize(variant.name)}Assets"
)
val cleanPackageAssets: Task? =
findTaskOrNull(
project,
"cleanPackage${FlutterPluginUtils.capitalize(variant.name)}Assets"
)
val isUsedAsSubproject: Boolean =
packageAssets != null && cleanPackageAssets != null && !isBuildingAar
val variantBuildMode: String = FlutterPluginUtils.buildModeFor(variant.buildType)
val flavorValue: String = variant.flavorName
val taskName: String =
FlutterPluginUtils.toCamelCase(
listOf(
"compile",
FLUTTER_BUILD_PREFIX,
variant.name
)
)
// The task provider below will shadow a lot of the variable names, so provide this reference
// to access them within that scope.
// Be careful when configuring task below, Groovy has bizarre
// scoping rules: writing `verbose isVerbose()` means calling
// `isVerbose` on the task itself - which would return `verbose`
// original value. You either need to hoist the value
// into a separate variable `verbose verboseValue` or prefix with
// `this` (`verbose this.isVerbose()`).
val compileTaskProvider: TaskProvider<FlutterTask> =
project.tasks.register(taskName, FlutterTask::class.java) {
flutterRoot = flutterPlugin.flutterRoot
flutterExecutable = flutterPlugin.flutterExecutable
buildMode = variantBuildMode
minSdkVersion = variant.mergedFlavor.minSdkVersion!!.apiLevel
localEngine = flutterPlugin.localEngine
localEngineHost = flutterPlugin.localEngineHost
localEngineSrcPath = flutterPlugin.localEngineSrcPath
targetPath = FlutterPluginUtils.getFlutterTarget(project)
verbose = FlutterPluginUtils.isProjectVerbose(project)
fastStart = FlutterPluginUtils.isProjectFastStart(project)
fileSystemRoots = fileSystemRootsValue
fileSystemScheme = fileSystemSchemeValue
trackWidgetCreation = trackWidgetCreationValue
targetPlatformValues = targetPlatforms
sourceDir = FlutterPluginUtils.getFlutterSourceDirectory(project)
intermediateDir =
project.file(
project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/")
)
frontendServerStarterPath = frontendServerStarterPathValue
extraFrontEndOptions = extraFrontEndOptionsValue
extraGenSnapshotOptions = extraGenSnapshotOptionsValue
splitDebugInfo = splitDebugInfoValue
treeShakeIcons = treeShakeIconsOptionsValue
dartObfuscation = dartObfuscationValue
dartDefines = dartDefinesValue
performanceMeasurementFile = performanceMeasurementFileValue
codeSizeDirectory = codeSizeDirectoryValue
deferredComponents = deferredComponentsValue
validateDeferredComponents = validateDeferredComponentsValue
flavor = flavorValue
}
val compileTask: FlutterTask = compileTaskProvider.get()
val libJar: File =
project.file(
project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar")
)
val packJniLibsTaskProvider: TaskProvider<Jar> =
project.tasks.register<Jar>(
"packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}",
Jar::class.java
) {
destinationDirectory.set(libJar.parentFile)
archiveFileName.set(libJar.name)
dependsOn(compileTask)
targetPlatforms.forEach { targetPlatform ->
val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
from("${compileTask.intermediateDir}/$abi") {
include("*.so")
// Move `app.so` to `lib/<abi>/libapp.so`
rename { filename: String -> "lib/$abi/lib$filename" }
}
// Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble.
// The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'.
val buildDir: String =
"${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build"
val nativeAssetsDir: String =
"$buildDir/native_assets/android/jniLibs/lib"
from("$nativeAssetsDir/$abi") {
include("*.so")
rename { filename: String -> "lib/$abi/$filename" }
}
}
}
val packJniLibsTask: Task = packJniLibsTaskProvider.get()
FlutterPluginUtils.addApiDependencies(
project,
variant.name,
project.files({
packJniLibsTask
})
)
val copyFlutterAssetsTaskProvider: TaskProvider<Copy> =
project.tasks.register(
"copyFlutterAssets${variant.name.capitalize()}",
Copy::class.java
) {
dependsOn(compileTask)
with(compileTask.assets)
// TODO(gmackall): Replace with filePermissions.user.read/write = true once
// minimum supported Gradle version is 8.3.
fileMode = 420 // corresponds to unix 0644 in base 8
if (isUsedAsSubproject) {
// TODO(gmackall): above is always false, can delete
dependsOn(packageAssets)
dependsOn(cleanPackageAssets)
into(packageAssets!!.outputs)
}
val mergeAssets =
try {
variant.mergeAssetsProvider.get()
} catch (e: IllegalStateException) {
variant.mergeAssets
}
dependsOn(mergeAssets)
dependsOn("clean${mergeAssets.name.capitalize()}")
mergeAssets.mustRunAfter("clean${mergeAssets.name.capitalize()}")
into(mergeAssets.outputDir)
}
val copyFlutterAssetsTask: Task = copyFlutterAssetsTaskProvider.get()
if (!isUsedAsSubproject) {
val variantOutput: BaseVariantOutput = variant.outputs.first()
val processResources =
try {
variantOutput.processResourcesProvider.get()
} catch (e: IllegalStateException) {
variantOutput.processResources
}
processResources.dependsOn(copyFlutterAssetsTask)
}
// The following tasks use the output of copyFlutterAssetsTask,
// so it's necessary to declare it as an dependency since Gradle 8.
// See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency.
val tasksToCheck =
listOf(
"compress${variant.name.capitalize()}Assets",
"bundle${variant.name.capitalize()}Aar",
"bundle${variant.name.capitalize()}LocalLintAar"
)
tasksToCheck.forEach { taskTocheck ->
try {
project.tasks.named(taskTocheck).configure {
dependsOn(copyFlutterAssetsTask)
}
} catch (ignored: UnknownTaskException) {
// ignored
}
}
return copyFlutterAssetsTask
}
}
}

View File

@ -25,7 +25,6 @@ object FlutterPluginConstants {
const val INTERMEDIATES_DIR = "intermediates" const val INTERMEDIATES_DIR = "intermediates"
const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL" const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL"
const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com" const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config"
/** Maps platforms to ABI architectures. */ /** Maps platforms to ABI architectures. */
@JvmStatic val PLATFORM_ARCH_MAP = @JvmStatic val PLATFORM_ARCH_MAP =
@ -42,7 +41,7 @@ object FlutterPluginConstants {
* Otherwise, the Play Store will complain that the APK variants have the same version. * Otherwise, the Play Store will complain that the APK variants have the same version.
*/ */
@JvmStatic val ABI_VERSION = @JvmStatic val ABI_VERSION =
mapOf<String, Int>( // Explicit type for clarity, though inferred mapOf<String, Int>(
ARCH_ARM32 to 1, ARCH_ARM32 to 1,
ARCH_ARM64 to 2, ARCH_ARM64 to 2,
ARCH_X86 to 3, ARCH_X86 to 3,

View File

@ -10,6 +10,7 @@ import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType import com.android.builder.model.BuildType
import com.flutter.gradle.plugins.PluginHandler
import groovy.lang.Closure import groovy.lang.Closure
import groovy.util.Node import groovy.util.Node
import groovy.util.XmlParser import groovy.util.XmlParser
@ -139,19 +140,6 @@ object FlutterPluginUtils {
// TODO(54566): Can remove this function and its call sites once resolved. // TODO(54566): Can remove this function and its call sites once resolved.
/**
* Returns `true` if the given project is a plugin project having an `android` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
@JvmStatic
@JvmName("pluginSupportsAndroidPlatform")
internal fun pluginSupportsAndroidPlatform(project: Project): Boolean {
val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle")
val buildGradleKts =
File(File(project.projectDir.parentFile, "android"), "build.gradle.kts")
return buildGradle.exists() || buildGradleKts.exists()
}
/** /**
* Returns the Gradle settings script for the build. When both Groovy and * Returns the Gradle settings script for the build. When both Groovy and
* Kotlin variants exist, then Groovy (settings.gradle) is preferred over * Kotlin variants exist, then Groovy (settings.gradle) is preferred over
@ -406,7 +394,7 @@ object FlutterPluginUtils {
return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode
} }
private fun getAndroidExtension(project: Project): BaseExtension { internal fun getAndroidExtension(project: Project): BaseExtension {
// Common supertype of the android extension types. // Common supertype of the android extension types.
// But maybe this should be https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/TestedExtension. // But maybe this should be https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/TestedExtension.
return project.extensions.findByType(BaseExtension::class.java)!! return project.extensions.findByType(BaseExtension::class.java)!!
@ -611,7 +599,7 @@ object FlutterPluginUtils {
// Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings. // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings.
gradleProjectAndroidExtension.externalNativeBuild.cmake.path( gradleProjectAndroidExtension.externalNativeBuild.cmake.path(
"$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt" "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt"
) )
// AGP defaults to outputting build artifacts in `android/app/.cxx`. This directory is a // AGP defaults to outputting build artifacts in `android/app/.cxx`. This directory is a
@ -656,7 +644,7 @@ object FlutterPluginUtils {
internal fun addFlutterDependencies( internal fun addFlutterDependencies(
project: Project, project: Project,
buildType: BuildType, buildType: BuildType,
pluginList: List<Map<String?, Any?>>, pluginHandler: PluginHandler,
engineVersion: String engineVersion: String
) { ) {
val flutterBuildMode: String = buildModeFor(buildType) val flutterBuildMode: String = buildModeFor(buildType)
@ -676,11 +664,9 @@ object FlutterPluginUtils {
// embedding. // embedding.
val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List<Map<String?, Any?>> = val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List<Map<String?, Any?>> =
if (flutterBuildMode == "release") { if (flutterBuildMode == "release") {
getPluginListWithoutDevDependencies( pluginHandler.getPluginListWithoutDevDependencies()
pluginList
)
} else { } else {
pluginList pluginHandler.getPluginList()
} }
if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) { if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) {
@ -702,143 +688,6 @@ object FlutterPluginUtils {
} }
} }
/**
* Gets the list of plugins (as map) that support the Android platform and are dependencies of the
* Android project excluding dev dependencies.
*
* The map value contains either the plugins `name` (String),
* its `path` (String), or its `dependencies` (List<String>).
* See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts
*/
private fun getPluginListWithoutDevDependencies(pluginList: List<Map<String?, Any?>>): List<Map<String?, Any?>> =
pluginList.filter { pluginObject -> pluginObject["dev_dependency"] == false }
/**
* Add the dependencies on other plugin projects to the plugin project.
* A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
* making the Gradle plugin project A depend on the Gradle plugin project B.
*/
@JvmStatic
@JvmName("configurePluginDependencies")
internal fun configurePluginDependencies(
project: Project,
pluginObject: Map<String?, Any?>
) {
val pluginName: String =
requireNotNull(pluginObject["name"] as? String) {
"Missing valid \"name\" property for plugin object: $pluginObject"
}
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
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
// release build, so no need to add its dependencies.
return@forEach
}
val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>)
dependencies.forEach innerForEach@{ pluginDependencyName ->
check(pluginDependencyName is String)
if (pluginDependencyName.isEmpty()) {
return@innerForEach
}
val dependencyProject =
project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach
pluginProject.afterEvaluate {
pluginProject.dependencies.add("implementation", dependencyProject)
}
}
}
}
/**
* Performs configuration related to the plugin's Gradle [Project], including
* 1. Adding the plugin itself as a dependency to the main project.
* 2. Adding the main project's build types to the plugin's build types.
* 3. Adding a dependency on the Flutter embedding to the plugin.
*
* Should only be called on plugins that support the Android platform.
*/
@JvmStatic
@JvmName("configurePluginProject")
internal fun configurePluginProject(
project: Project,
pluginObject: Map<String?, Any?>,
engineVersion: String
) {
// TODO(gmackall): should guard this with a pluginObject.contains().
val pluginName =
requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" }
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
// Apply the "flutter" Gradle extension to plugins so that they can use it's vended
// compile/target/min sdk values.
pluginProject.extensions.create("flutter", FlutterExtension::class.java)
// Add plugin dependency to the app project. We only want to add dependency
// for dev dependencies in non-release builds.
project.afterEvaluate {
getAndroidExtension(project).buildTypes.forEach { buildType ->
if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") {
project.dependencies.add("${buildType.name}Api", pluginProject)
}
}
}
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
val projectCompileSdkVersion: String = getCompileSdkFromProject(project)
val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject)
// TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong
// when comparing preview versions (against non preview, and also in the
// case of alphabet reset which happened with "Baklava".
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
project.logger.quiet("Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher.")
project.logger.quiet(
"For more information about build configuration, see ${FlutterPluginConstants.WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}."
)
}
getAndroidExtension(project).buildTypes.forEach { buildType ->
addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion)
}
}
}
private fun addEmbeddingDependencyToPlugin(
project: Project,
pluginProject: Project,
buildType: BuildType,
engineVersion: String
) {
val flutterBuildMode: String = buildModeFor(buildType)
// TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above
// 3.5. We should try to remove it.
// In AGP 3.5, the embedding must be added as an API implementation,
// so java8 features are desugared against the runtime classpath.
// For more, see https://github.com/flutter/flutter/issues/40126
if (!supportsBuildMode(pluginProject, flutterBuildMode)) {
return
}
if (!pluginProject.hasProperty("android")) {
return
}
// Copy build types from the app to the plugin.
// This allows to build apps with plugins and custom build types or flavors.
getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes)
// The embedding is API dependency of the plugin, so the AGP is able to desugar
// default method implementations when the interface is implemented by a plugin.
//
// See https://issuetracker.google.com/139821726, and
// https://github.com/flutter/flutter/issues/72185 for more details.
addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
}
// ------------------ Task adders (a subset of the above category) // ------------------ 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.

View File

@ -17,24 +17,21 @@ import java.io.File
*/ */
object NativePluginLoaderReflectionBridge { object NativePluginLoaderReflectionBridge {
private var nativePluginLoader: Any? = null
/** /**
* An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts. * An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts.
*/ */
@JvmStatic
fun getPlugins( fun getPlugins(
extraProperties: ExtraPropertiesExtension, extraProperties: ExtraPropertiesExtension,
flutterProjectRoot: File flutterProjectRoot: File
): List<Map<String, Any>> { ): List<Map<String?, Any?>> {
nativePluginLoader = extraProperties.get("nativePluginLoader")!! val nativePluginLoader = extraProperties.get("nativePluginLoader")!!
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val pluginList: List<Map<String, Any>> = val pluginList: List<Map<String?, Any?>> =
nativePluginLoader!!::class nativePluginLoader::class
.members .members
.firstOrNull { it.name == "getPlugins" } .firstOrNull { it.name == "getPlugins" }
?.call(nativePluginLoader, flutterProjectRoot) as List<Map<String, Any>> ?.call(nativePluginLoader, flutterProjectRoot) as List<Map<String?, Any?>>
return pluginList return pluginList
} }
@ -42,16 +39,15 @@ object NativePluginLoaderReflectionBridge {
/** /**
* An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts. * An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts.
*/ */
@JvmStatic
fun getDependenciesMetadata( fun getDependenciesMetadata(
extraProperties: ExtraPropertiesExtension, extraProperties: ExtraPropertiesExtension,
flutterProjectRoot: File flutterProjectRoot: File
): Map<String, Any> { ): Map<String, Any> {
nativePluginLoader = extraProperties.get("nativePluginLoader")!! val nativePluginLoader = extraProperties.get("nativePluginLoader")!!
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val dependenciesMetadata: Map<String, Any> = val dependenciesMetadata: Map<String, Any> =
nativePluginLoader!!::class nativePluginLoader::class
.members .members
.firstOrNull { it.name == "dependenciesMetadata" } .firstOrNull { it.name == "dependenciesMetadata" }
?.call(nativePluginLoader, flutterProjectRoot) as Map<String, Any> ?.call(nativePluginLoader, flutterProjectRoot) as Map<String, Any>

View File

@ -0,0 +1,326 @@
// 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.plugins
import androidx.annotation.VisibleForTesting
import com.android.builder.model.BuildType
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.supportsBuildMode
import com.flutter.gradle.NativePluginLoaderReflectionBridge
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import java.io.File
import java.io.FileNotFoundException
import java.nio.charset.StandardCharsets
/**
* Handles interactions with the flutter plugins (not Gradle plugins) used by the Flutter project,
* such as retrieving them as a list and configuring them as Gradle dependencies of the main Gradle
* project.
*/
class PluginHandler(
val project: Project
) {
private var pluginList: List<Map<String?, Any?>>? = null
private var pluginDependencies: List<Map<String?, Any?>>? = null
/**
* Gets the list of plugins (as map) that support the Android platform.
*
* The map contains the following key - value pairs:
* `name` - the plugins name (String),
* `path` - it's path (String),
* `dependencies` - a list of its dependencies names (List<String>)
* `dev_dependency` - a boolean indicating whether the plugin is a dev dependency (Boolean)
* `native_build` - a boolean indicating whether the plugin has native code (Boolean)
*
* This format is defined in packages/flutter_tools/lib/src/flutter_plugins.dart, in the
* _createPluginMapOfPlatform method.
* See also [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts
*/
internal fun getPluginList(): List<Map<String?, Any?>> {
if (pluginList == null) {
pluginList =
NativePluginLoaderReflectionBridge.getPlugins(
project.extraProperties,
FlutterPluginUtils.getFlutterSourceDirectory(project)
)
}
return pluginList!!
}
// TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
// https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
/** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
private fun getPluginDependencies(): List<Map<String?, Any?>> {
if (pluginDependencies == null) {
val meta: Map<String, Any> =
NativePluginLoaderReflectionBridge.getDependenciesMetadata(
project.extraProperties,
FlutterPluginUtils.getFlutterSourceDirectory(project)
)
check(meta["dependencyGraph"] is List<*>)
@Suppress("UNCHECKED_CAST")
pluginDependencies = meta["dependencyGraph"] as List<Map<String?, Any?>>
}
return pluginDependencies!!
}
// TODO(54566, 48918): Can remove once the issues are resolved.
// This means all references to `.flutter-plugins` are then removed and
// apps only depend exclusively on the `plugins` property in `.flutter-plugins-dependencies`.
/**
* Workaround to load non-native plugins for developers who may still use an
* old `settings.gradle` which includes all the plugins from the
* `.flutter-plugins` file, even if not made for Android.
* The settings.gradle then:
* 1) tries to add the android plugin implementation, which does not
* exist at all, but is also not included successfully
* (which does not throw an error and therefore isn't a problem), or
* 2) includes the plugin successfully as a valid android plugin
* directory exists, even if the surrounding flutter package does not
* support the android platform (see e.g. apple_maps_flutter: 1.0.1).
* So as it's included successfully it expects to be added as API.
* This is only possible by taking all plugins into account, which
* only appear on the `dependencyGraph` and in the `.flutter-plugins` file.
* So in summary the plugins are currently selected from the `dependencyGraph`
* and filtered then with the [doesSupportAndroidPlatform] method instead of
* just using the `plugins.android` list.
*/
private fun configureLegacyPluginEachProjects(engineVersionValue: String) {
try {
// Read the contents of the settings.gradle file.
// Remove block/line comments
var settingsText =
FlutterPluginUtils
.getSettingsGradleFileFromProjectDir(
project.projectDir,
project.logger
).readText(StandardCharsets.UTF_8)
settingsText =
settingsText
.replace(Regex("""(?s)/\*.*?\*/"""), "")
.replace(Regex("""(?m)//.*$"""), "")
if (!settingsText.contains("'.flutter-plugins'")) {
return
}
} catch (ignored: FileNotFoundException) {
throw GradleException(
"settings.gradle/settings.gradle.kts does not exist: " +
FlutterPluginUtils
.getSettingsGradleFileFromProjectDir(
project.projectDir,
project.logger
).absolutePath
)
}
// TODO(matanlurey): https://github.com/flutter/flutter/issues/48918.
project.logger.quiet(
legacyFlutterPluginsWarning
)
val deps: List<Map<String?, Any?>> = getPluginDependencies()
val pluginsNameSet = HashSet<String>()
getPluginList().mapTo(pluginsNameSet) { plugin -> plugin["name"] as String }
deps.filterNot { plugin -> pluginsNameSet.contains(plugin["name"]) }
deps.forEach { plugin: Map<String?, Any?> ->
val pluginProject = project.rootProject.findProject(":${plugin["name"]}")
if (pluginProject == null) {
// Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`.
project.logger.error(
"Plugin project :${plugin["name"]} listed, but not found. Please fix your settings.gradle/settings.gradle.kts."
)
} else if (pluginSupportsAndroidPlatform(project)) {
// Plugin has a functioning `android` folder and is included successfully, although it's not supported.
// It must be configured nonetheless, to not throw an "Unresolved reference" exception.
configurePluginProject(project, plugin, engineVersionValue)
} else {
// Plugin has no or an empty `android` folder. No action required.
}
}
}
internal fun configurePlugins(engineVersionValue: String) {
configureLegacyPluginEachProjects(engineVersionValue)
val pluginList: List<Map<String?, Any?>> = getPluginList()
pluginList.forEach { plugin: Map<String?, Any?> ->
configurePluginProject(
project,
plugin,
engineVersionValue
)
}
pluginList.forEach { plugin: Map<String?, Any?> ->
configurePluginDependencies(project, plugin)
}
}
/**
* Gets the list of plugins (as map) that support the Android platform and are dependencies of the
* Android project excluding dev dependencies.
*
* The map value contains either the plugins `name` (String),
* its `path` (String), or its `dependencies` (List<String>).
* See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts
*/
internal fun getPluginListWithoutDevDependencies(): List<Map<String?, Any?>> =
getPluginList().filter { pluginObject -> pluginObject["dev_dependency"] == false }
companion object {
/**
* Flutter Docs Website URLs for help messages.
*/
private const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config"
@VisibleForTesting internal val legacyFlutterPluginsWarning =
"""
Warning: This project is still reading the deprecated '.flutter-plugins. file.
In an upcoming stable release support for this file will be completely removed and your build will fail.
See https:/flutter.dev/to/flutter-plugins-configuration.
""".trimIndent()
/**
* Performs configuration related to the plugin's Gradle [Project], including
* 1. Adding the plugin itself as a dependency to the main project.
* 2. Adding the main project's build types to the plugin's build types.
* 3. Adding a dependency on the Flutter embedding to the plugin.
*
* Should only be called on plugins that support the Android platform.
*/
private fun configurePluginProject(
project: Project,
pluginObject: Map<String?, Any?>,
engineVersion: String
) {
val pluginName =
requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" }
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
// Apply the "flutter" Gradle extension to plugins so that they can use it's vended
// compile/target/min sdk values.
pluginProject.extensions.create("flutter", FlutterExtension::class.java)
// Add plugin dependency to the app project. We only want to add dependency
// for dev dependencies in non-release builds.
project.afterEvaluate {
getAndroidExtension(project).buildTypes.forEach { buildType ->
if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") {
project.dependencies.add("${buildType.name}Api", pluginProject)
}
}
}
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
val projectCompileSdkVersion: String = getCompileSdkFromProject(project)
val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject)
// TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong
// when comparing preview versions (against non preview, and also in the
// case of alphabet reset which happened with "Baklava".
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
project.logger.quiet(
"Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher."
)
project.logger.quiet(
"For more information about build configuration, see ${WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}."
)
}
getAndroidExtension(project).buildTypes.forEach { buildType ->
addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion)
}
}
}
private fun addEmbeddingDependencyToPlugin(
project: Project,
pluginProject: Project,
buildType: BuildType,
engineVersion: String
) {
val flutterBuildMode: String = buildModeFor(buildType)
// TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above
// 3.5. We should try to remove it.
// In AGP 3.5, the embedding must be added as an API implementation,
// so java8 features are desugared against the runtime classpath.
// For more, see https://github.com/flutter/flutter/issues/40126
if (!supportsBuildMode(pluginProject, flutterBuildMode)) {
return
}
if (!pluginProject.hasProperty("android")) {
return
}
// Copy build types from the app to the plugin.
// This allows to build apps with plugins and custom build types or flavors.
getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes)
// The embedding is API dependency of the plugin, so the AGP is able to desugar
// default method implementations when the interface is implemented by a plugin.
//
// See https://issuetracker.google.com/139821726, and
// https://github.com/flutter/flutter/issues/72185 for more details.
addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
}
/**
* Returns `true` if the given project is a plugin project having an `android` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
internal fun pluginSupportsAndroidPlatform(project: Project): Boolean {
val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle")
val buildGradleKts =
File(File(project.projectDir.parentFile, "android"), "build.gradle.kts")
return buildGradle.exists() || buildGradleKts.exists()
}
/**
* Add the dependencies on other plugin projects to the plugin project.
* A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
* making the Gradle plugin project A depend on the Gradle plugin project B.
*/
private fun configurePluginDependencies(
project: Project,
pluginObject: Map<String?, Any?>
) {
val pluginName: String =
requireNotNull(pluginObject["name"] as? String) {
"Missing valid \"name\" property for plugin object: $pluginObject"
}
val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
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
// release build, so no need to add its dependencies.
return@forEach
}
val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>)
dependencies.forEach innerForEach@{ pluginDependencyName ->
check(pluginDependencyName is String)
if (pluginDependencyName.isEmpty()) {
return@innerForEach
}
val dependencyProject =
project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach
pluginProject.afterEvaluate {
// this.dependencies.add("implementation", dependencyProject)
pluginProject.dependencies.add("implementation", dependencyProject)
}
}
}
}
}
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package com.flutter.gradle package com.flutter.gradle.tasks
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package com.flutter.gradle package com.flutter.gradle.tasks
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import org.gradle.api.Action import org.gradle.api.Action

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package com.flutter.gradle package com.flutter.gradle.tasks
import org.gradle.api.file.CopySpec import org.gradle.api.file.CopySpec
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection

View File

@ -2,8 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
package com.flutter.gradle package com.flutter.gradle.tasks
import com.flutter.gradle.FlutterPluginConstants
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.CopySpec import org.gradle.api.file.CopySpec
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection

View File

@ -72,9 +72,63 @@ class NativePluginLoader {
*/ */
fun getDependenciesMetadata(flutterSourceDirectory: File): Map<String, Any>? { fun getDependenciesMetadata(flutterSourceDirectory: File): Map<String, Any>? {
// Consider a `.flutter-plugins-dependencies` file with the following content: // Consider a `.flutter-plugins-dependencies` file with the following content:
// { ... (example content as in the original Groovy code) ... } // {
// "plugins": {
// "android": [
// {
// "name": "plugin-a",
// "path": "/path/to/plugin-a",
// "dependencies": ["plugin-b", "plugin-c"],
// "native_build": true
// "dev_dependency": false
// },
// {
// "name": "plugin-b",
// "path": "/path/to/plugin-b",
// "dependencies": ["plugin-c"],
// "native_build": true
// "dev_dependency": false
// },
// {
// "name": "plugin-c",
// "path": "/path/to/plugin-c",
// "dependencies": [],
// "native_build": true
// "dev_dependency": false
// },
// {
// "name": "plugin-d",
// "path": "/path/to/plugin-d",
// "dependencies": [],
// "native_build": true
// "dev_dependency": true
// },
// ],
// },
// "dependencyGraph": [
// {
// "name": "plugin-a",
// "dependencies": ["plugin-b","plugin-c"]
// },
// {
// "name": "plugin-b",
// "dependencies": ["plugin-c"]
// },
// {
// "name": "plugin-c",
// "dependencies": []
// },
// {
// "name": "plugin-d",
// "dependencies": []
// }
// ]
// }
// This means, `plugin-a` depends on `plugin-b` and `plugin-c`. // This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
// ... (rest of the comment as in the original Groovy code) ... // `plugin-b` depends on `plugin-c`.
// `plugin-c` doesn't depend on anything.
// `plugin-d` also doesn't depend on anything, but it is a dev
// dependency to the Flutter project, so it is marked as such.
if (parsedFlutterPluginsDependencies != null) { if (parsedFlutterPluginsDependencies != null) {
return parsedFlutterPluginsDependencies return parsedFlutterPluginsDependencies
} }

View File

@ -1,4 +1,9 @@
// 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 package com.flutter.gradle
import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationDefaultConfig
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import com.flutter.gradle.BaseApplicationNameHandler.GRADLE_BASE_APPLICATION_NAME_PROPERTY import com.flutter.gradle.BaseApplicationNameHandler.GRADLE_BASE_APPLICATION_NAME_PROPERTY

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import org.gradle.internal.impldep.org.junit.Assert.assertThrows import org.gradle.internal.impldep.org.junit.Assert.assertThrows

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import com.android.build.api.AndroidPluginVersion import com.android.build.api.AndroidPluginVersion

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import org.gradle.api.GradleException import org.gradle.api.GradleException

View File

@ -0,0 +1,78 @@
package com.flutter.gradle
import com.android.build.api.dsl.ApplicationDefaultConfig
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path
import kotlin.io.path.writeText
import kotlin.test.Test
class FlutterPluginTest {
@Test
fun `FlutterPlugin apply() adds expected tasks`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("project-dir").resolve("android").resolve("app")
projectDir.toFile().mkdirs()
val settingsFile = projectDir.parent.resolve("settings.gradle")
settingsFile.writeText("empty for now")
val fakeFlutterSdkDir = tempDir.resolve("fake-flutter-sdk")
fakeFlutterSdkDir.toFile().mkdirs()
val fakeCacheDir = fakeFlutterSdkDir.resolve("bin").resolve("cache")
fakeCacheDir.toFile().mkdirs()
val fakeEngineStampFile = fakeCacheDir.resolve("engine.stamp")
fakeEngineStampFile.writeText(FAKE_ENGINE_STAMP)
val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM)
val project = mockk<Project>(relaxed = true)
val mockAbstractAppExtension = mockk<AbstractAppExtension>(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
every { project.projectDir } returns projectDir.toFile()
every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString()
every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile()
val flutterExtension = FlutterExtension()
every { project.extensions.create("flutter", any<Class<*>>()) } returns flutterExtension
every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
val mockBaseExtension = mockk<BaseExtension>(relaxed = true)
every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension
val mockApplicationExtension = mockk<ApplicationExtension>(relaxed = true)
every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
val mockApplicationDefaultConfig = mockk<ApplicationDefaultConfig>(relaxed = true)
every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
every { project.rootProject } returns project
every { project.state.failure } 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)
every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet
// mock return of NativePluginLoaderReflectionBridge.getPlugins
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns
listOf()
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.file(flutterExtension.source!!) } returns mockk()
val flutterPlugin = FlutterPlugin()
flutterPlugin.apply(project)
verify { project.tasks.register("generateLockfiles", any()) }
verify { project.tasks.register("javaVersion", any()) }
verify { project.tasks.register("printBuildVariants", any()) }
}
companion object {
const val FAKE_ENGINE_STAMP = "901b0f1afe77c3555abee7b86a26aaa37f131379"
const val FAKE_ENGINE_REALM = "made_up_realm"
}
}

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.AbstractAppExtension
@ -8,25 +12,26 @@ import com.android.build.gradle.internal.dsl.CmakeOptions
import com.android.build.gradle.internal.dsl.DefaultConfig import com.android.build.gradle.internal.dsl.DefaultConfig
import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType import com.android.builder.model.BuildType
import com.flutter.gradle.plugins.PluginHandler
import io.mockk.called import io.mockk.called
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.DomainObjectCollection import org.gradle.api.DomainObjectCollection
import org.gradle.api.DomainObjectSet import org.gradle.api.DomainObjectSet
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.UnknownTaskException import org.gradle.api.UnknownTaskException
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.logging.Logger import org.gradle.api.logging.Logger
import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import java.io.File import java.io.File
@ -36,12 +41,10 @@ import kotlin.io.path.createDirectory
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContains import kotlin.test.assertContains
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FlutterPluginUtilsTest { class FlutterPluginUtilsTest {
companion object { companion object {
val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
val devDependency: Map<String?, Any?> = val devDependency: Map<String?, Any?> =
mapOf( mapOf(
@ -219,45 +222,6 @@ class FlutterPluginUtilsTest {
assertEquals(true, result) assertEquals(true, result)
} }
// pluginSupportsAndroidPlatform
@Test
fun `pluginSupportsAndroidPlatform returns true when android directory exists with gradle build file`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
val androidDir = tempDir.resolve("android")
androidDir.toFile().mkdirs()
File(androidDir.toFile(), "build.gradle").createNewFile()
val mockProject =
mockk<Project> {
every { this@mockk.projectDir } returns projectDir.toFile()
}
assertTrue {
FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject)
} // Replace YourClass with the actual class containing the method
}
@Test
fun `pluginSupportsAndroidPlatform returns false when gradle build file does not exist`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
val mockProject =
mockk<Project> {
every { this@mockk.projectDir } returns projectDir.toFile()
}
assertFalse {
FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject)
} // Replace YourClass with the actual class containing the method
}
// settingsGradleFile // settingsGradleFile
@Test @Test
fun `settingsGradleFile returns groovy settings gradle file when it exists`( fun `settingsGradleFile returns groovy settings gradle file when it exists`(
@ -872,7 +836,7 @@ class FlutterPluginUtilsTest {
verify(exactly = 1) { verify(exactly = 1) {
mockCmakeOptions.path mockCmakeOptions.path
} }
verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt") } verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt") }
verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) } verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) }
verify(exactly = 1) { verify(exactly = 1) {
mockDefaultConfig.externalNativeBuild.cmake.arguments( mockDefaultConfig.externalNativeBuild.cmake.arguments(
@ -888,6 +852,12 @@ class FlutterPluginUtilsTest {
@Test @Test
fun `addFlutterDependencies returns early if buildMode is not supported`() { fun `addFlutterDependencies returns early if buildMode is not supported`() {
val project = mockk<Project>() val project = mockk<Project>()
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>() val buildType: BuildType = mockk<BuildType>()
every { buildType.name } returns "debug" every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true every { buildType.isDebuggable } returns true
@ -899,7 +869,7 @@ class FlutterPluginUtilsTest {
FlutterPluginUtils.addFlutterDependencies( FlutterPluginUtils.addFlutterDependencies(
project = project, project = project,
buildType = buildType, buildType = buildType,
pluginList = pluginListWithoutDevDependency, pluginHandler = pluginHandler,
engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
) )
@ -914,8 +884,14 @@ class FlutterPluginUtilsTest {
@Test @Test
fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() { fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() {
val project = mockk<Project>() val project = mockk<Project>()
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency
val buildType: BuildType = mockk<BuildType>() val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug" every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false every { project.hasProperty("local-engine-repo") } returns false
@ -927,7 +903,7 @@ class FlutterPluginUtilsTest {
FlutterPluginUtils.addFlutterDependencies( FlutterPluginUtils.addFlutterDependencies(
project = project, project = project,
buildType = buildType, buildType = buildType,
pluginList = pluginListWithoutDevDependency, pluginHandler = pluginHandler,
engineVersion = engineVersion engineVersion = engineVersion
) )
@ -945,8 +921,15 @@ class FlutterPluginUtilsTest {
@Test @Test
fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() { fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() {
val project = mockk<Project>() val project = mockk<Project>()
val pluginListWithSingleDevDependency = listOf(devDependency)
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>() val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "release" every { buildType.name } returns "release"
every { buildType.isDebuggable } returns false every { buildType.isDebuggable } returns false
every { project.hasProperty("local-engine-repo") } returns false every { project.hasProperty("local-engine-repo") } returns false
@ -955,12 +938,10 @@ class FlutterPluginUtilsTest {
every { project.configurations.named("api") } returns mockk() every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk() every { project.dependencies.add(any(), any()) } returns mockk()
val pluginListWithSingleDevDependency = listOf(devDependency)
FlutterPluginUtils.addFlutterDependencies( FlutterPluginUtils.addFlutterDependencies(
project = project, project = project,
buildType = buildType, buildType = buildType,
pluginList = pluginListWithSingleDevDependency, pluginHandler = pluginHandler,
engineVersion = engineVersion engineVersion = engineVersion
) )
@ -994,8 +975,15 @@ class FlutterPluginUtilsTest {
@Test @Test
fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() { fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() {
val project = mockk<Project>() val project = mockk<Project>()
val pluginListWithSingleDevDependency = listOf(devDependency)
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency
val buildType: BuildType = mockk<BuildType>() val buildType: BuildType = mockk<BuildType>()
val engineVersion = exampleEngineVersion val engineVersion = EXAMPLE_ENGINE_VERSION
every { buildType.name } returns "debug" every { buildType.name } returns "debug"
every { buildType.isDebuggable } returns true every { buildType.isDebuggable } returns true
every { project.hasProperty("local-engine-repo") } returns false every { project.hasProperty("local-engine-repo") } returns false
@ -1004,12 +992,10 @@ class FlutterPluginUtilsTest {
every { project.configurations.named("api") } returns mockk() every { project.configurations.named("api") } returns mockk()
every { project.dependencies.add(any(), any()) } returns mockk() every { project.dependencies.add(any(), any()) } returns mockk()
val pluginListWithSingleDevDependency = listOf(devDependency)
FlutterPluginUtils.addFlutterDependencies( FlutterPluginUtils.addFlutterDependencies(
project = project, project = project,
buildType = buildType, buildType = buildType,
pluginList = pluginListWithSingleDevDependency, pluginHandler = pluginHandler,
engineVersion = engineVersion engineVersion = engineVersion
) )
@ -1034,168 +1020,6 @@ class FlutterPluginUtilsTest {
} }
} }
// configurePluginDependencies TODO
@Test
fun `configurePluginDependencies throws IllegalArgumentException when plugin has no name`() {
val project = mockk<Project>()
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithoutName
)
}
}
@Test
fun `configurePluginDependencies throws IllegalArgumentException when plugin has null dependencies`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val pluginWithNullDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithNullDependencies["dependencies"] = null
every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator()
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithNullDependencies
)
}
}
@Test
fun `configurePluginDependencies adds plugin dependencies`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val pluginDependencyProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val pluginWithDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithDependencies["dependencies"] =
listOf(flutterPluginAndroidLifecycleDependency["name"])
every { project.rootProject.findProject(":${pluginWithDependencies["name"]}") } returns pluginProject
every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator()
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
val captureActionSlot = slot<Action<Project>>()
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockDependencyHandler = mockk<DependencyHandler>()
every { pluginProject.dependencies } returns mockDependencyHandler
every { mockDependencyHandler.add(any(), any()) } returns mockk()
FlutterPluginUtils.configurePluginDependencies(
project = project,
pluginObject = pluginWithDependencies
)
verify { pluginProject.afterEvaluate(capture(captureActionSlot)) }
captureActionSlot.captured.execute(pluginDependencyProject)
verify { mockDependencyHandler.add("implementation", pluginDependencyProject) }
}
// configurePluginProject
@Test
fun `configurePluginProject throws IllegalArgumentException when plugin has no name`() {
val project = mockk<Project>()
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
assertThrows<IllegalArgumentException> {
FlutterPluginUtils.configurePluginProject(
project = project,
pluginObject = pluginWithoutName,
engineVersion = exampleEngineVersion
)
}
}
@Test
fun `configurePluginProject adds plugin project`() {
val project = mockk<Project>()
val pluginProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
every { pluginProject.hasProperty("local-engine-repo") } returns false
every { pluginProject.hasProperty("android") } returns true
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject
every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
val captureActionSlot = slot<Action<Project>>()
val capturePluginActionSlot = slot<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
val mockPluginProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
every { mockPluginProjectBuildTypes.addAll(any()) } returns true
every { pluginProject.configurations.named(any<String>()) } returns mockk()
every { pluginProject.dependencies.add(any(), any()) } returns mockk()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator() andThen
mutableListOf( // can't return the same iterator as it is stateful
mockBuildType
).iterator()
every { project.dependencies.add(any(), any()) } returns mockk()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
FlutterPluginUtils.configurePluginProject(
project = project,
pluginObject = cameraDependency,
engineVersion = exampleEngineVersion
)
verify { project.afterEvaluate(capture(captureActionSlot)) }
verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) }
captureActionSlot.captured.execute(project)
capturePluginActionSlot.captured.execute(pluginProject)
verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) }
verify {
pluginProject.dependencies.add(
"debugApi",
"io.flutter:flutter_embedding_debug:$exampleEngineVersion"
)
}
verify { project.dependencies.add("debugApi", pluginProject) }
verify { mockLogger wasNot called }
verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) }
}
// addTaskForJavaVersion // addTaskForJavaVersion
@Test @Test
fun `addTaskForJavaVersion adds task for Java version`() { fun `addTaskForJavaVersion adds task for Java version`() {

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import kotlin.test.Test import kotlin.test.Test

View File

@ -1,3 +1,7 @@
// 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 package com.flutter.gradle
import kotlin.test.Test import kotlin.test.Test

View File

@ -0,0 +1,443 @@
// 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.plugins
import com.android.build.gradle.BaseExtension
import com.flutter.gradle.FlutterExtension
import com.flutter.gradle.FlutterPluginUtilsTest.Companion.EXAMPLE_ENGINE_VERSION
import com.flutter.gradle.FlutterPluginUtilsTest.Companion.cameraDependency
import com.flutter.gradle.FlutterPluginUtilsTest.Companion.flutterPluginAndroidLifecycleDependency
import com.flutter.gradle.FlutterPluginUtilsTest.Companion.pluginListWithDevDependency
import com.flutter.gradle.FlutterPluginUtilsTest.Companion.pluginListWithoutDevDependency
import com.flutter.gradle.NativePluginLoaderReflectionBridge
import io.mockk.called
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.jetbrains.kotlin.gradle.plugin.extraProperties
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class PluginHandlerTest {
// getPluginListWithoutDevDependencies
@Test
fun `getPluginListWithoutDevDependencies removes dev dependencies from list`() {
val project = mockk<Project>()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
every {
NativePluginLoaderReflectionBridge.getPlugins(
any(),
any()
)
} returns pluginListWithDevDependency
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val result = pluginHandler.getPluginListWithoutDevDependencies()
assertEquals(pluginListWithoutDevDependency, result)
}
@Test
fun `getPluginListWithoutDevDependencies does not modify list without dev dependencies`() {
val project = mockk<Project>()
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
every {
NativePluginLoaderReflectionBridge.getPlugins(
any(),
any()
)
} returns pluginListWithoutDevDependency
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val result = pluginHandler.getPluginListWithoutDevDependencies()
assertEquals(pluginListWithoutDevDependency, result)
}
// getPluginList skipped as it is a wrapper around a single reflection call
// pluginSupportsAndroidPlatform
@Test
fun `pluginSupportsAndroidPlatform returns true when android directory exists with gradle build file`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
val androidDir = tempDir.resolve("android")
androidDir.toFile().mkdirs()
File(androidDir.toFile(), "build.gradle").createNewFile()
val mockProject =
mockk<Project> {
every { this@mockk.projectDir } returns projectDir.toFile()
}
assertTrue {
PluginHandler.pluginSupportsAndroidPlatform(mockProject)
} // Replace YourClass with the actual class containing the method
}
@Test
fun `pluginSupportsAndroidPlatform returns false when gradle build file does not exist`(
@TempDir tempDir: Path
) {
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
val mockProject =
mockk<Project> {
every { this@mockk.projectDir } returns projectDir.toFile()
}
assertFalse {
PluginHandler.pluginSupportsAndroidPlatform(mockProject)
}
}
@Test
fun `configurePlugins throws IllegalArgumentException when plugin has no name`(
@TempDir tempDir: Path
) {
val project = mockk<Project>()
// configuration for configureLegacyPluginEachProjects
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
every { project.projectDir } returns projectDir.toFile()
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithoutName.remove("name")
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns
listOf(
pluginWithoutName
)
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val pluginHandler = PluginHandler(project)
assertThrows<IllegalArgumentException> {
pluginHandler.configurePlugins(
engineVersionValue = EXAMPLE_ENGINE_VERSION
)
}
}
@Test
fun `configurePlugins adds plugin project and configures its dependencies`(
@TempDir tempDir: Path
) {
val project = mockk<Project>()
// configuration for configureLegacyPluginEachProjects
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
every { project.projectDir } returns projectDir.toFile()
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
val pluginProject = mockk<Project>()
val pluginDependencyProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
every { pluginProject.hasProperty("local-engine-repo") } returns false
every { pluginProject.hasProperty("android") } returns true
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject
every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject
every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
val captureActionSlot = slot<Action<Project>>()
val capturePluginActionSlot = mutableListOf<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
val mockPluginProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
every { mockPluginProjectBuildTypes.addAll(any()) } returns true
every { pluginProject.configurations.named(any<String>()) } returns mockk()
every { pluginProject.dependencies.add(any(), any()) } returns mockk()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator() andThen
mutableListOf( // can't return the same iterator as it is stateful
mockBuildType
).iterator() andThen
mutableListOf( // and again
mockBuildType
).iterator()
every { project.dependencies.add(any(), any()) } returns mockk()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
val pluginWithDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithDependencies["dependencies"] =
listOf(flutterPluginAndroidLifecycleDependency["name"])
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns
listOf(
pluginWithDependencies
)
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
pluginHandler.configurePlugins(
engineVersionValue = EXAMPLE_ENGINE_VERSION
)
verify { project.afterEvaluate(capture(captureActionSlot)) }
verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) }
captureActionSlot.captured.execute(project)
capturePluginActionSlot[0].execute(pluginProject)
capturePluginActionSlot[1].execute(pluginProject)
verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) }
verify {
pluginProject.dependencies.add(
"debugApi",
"io.flutter:flutter_embedding_debug:$EXAMPLE_ENGINE_VERSION"
)
}
verify { project.dependencies.add("debugApi", pluginProject) }
verify { mockLogger wasNot called }
verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) }
verify { pluginProject.dependencies.add("implementation", pluginDependencyProject) }
}
@Test
fun `configurePlugins throws IllegalArgumentException when plugin has null dependencies`(
@TempDir tempDir: Path
) {
val project = mockk<Project>()
// configuration for configureLegacyPluginEachProjects
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
every { project.projectDir } returns projectDir.toFile()
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
val pluginProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
every { pluginProject.hasProperty("local-engine-repo") } returns false
every { pluginProject.hasProperty("android") } returns true
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
val pluginWithNullDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithNullDependencies["dependencies"] = null
every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject
every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
val mockPluginProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
every { mockPluginProjectBuildTypes.addAll(any()) } returns true
every { pluginProject.configurations.named(any<String>()) } returns mockk()
every { pluginProject.dependencies.add(any(), any()) } returns mockk()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator() andThen
mutableListOf( // can't return the same iterator as it is stateful
mockBuildType
).iterator() andThen
mutableListOf( // and again
mockBuildType
).iterator()
every { project.dependencies.add(any(), any()) } returns mockk()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns
listOf(
pluginWithNullDependencies
)
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
assertThrows<IllegalArgumentException> {
pluginHandler.configurePlugins(
engineVersionValue = EXAMPLE_ENGINE_VERSION
)
}
}
@Test
fun `configurePlugins works for old flutter-plugins file`(
@TempDir tempDir: Path
) {
val project = mockk<Project>()
// configuration for configureLegacyPluginEachProjects
val projectDir = tempDir.resolve("my-plugin")
projectDir.toFile().mkdirs()
every { project.projectDir } returns projectDir.toFile()
val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
settingsGradle.createNewFile()
settingsGradle.writeText("def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')")
val mockLogger = mockk<Logger>()
every { project.logger } returns mockLogger
every { mockLogger.quiet(any()) } returns Unit
val pluginProject = mockk<Project>()
val pluginDependencyProject = mockk<Project>()
val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
every { pluginProject.hasProperty("local-engine-repo") } returns false
every { pluginProject.hasProperty("android") } returns true
every { mockBuildType.name } returns "debug"
every { mockBuildType.isDebuggable } returns true
every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject
every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject
every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
val captureActionSlot = slot<Action<Project>>()
val capturePluginActionSlot = mutableListOf<Action<Project>>()
every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
val mockProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
val mockPluginProjectBuildTypes =
mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
every { mockPluginProjectBuildTypes.addAll(any()) } returns true
every { pluginProject.configurations.named(any<String>()) } returns mockk()
every { pluginProject.dependencies.add(any(), any()) } returns mockk()
every {
project.extensions
.findByType(BaseExtension::class.java)!!
.buildTypes
.iterator()
} returns
mutableListOf(
mockBuildType
).iterator() andThen
mutableListOf( // can't return the same iterator as it is stateful
mockBuildType
).iterator() andThen
mutableListOf( // and again
mockBuildType
).iterator()
every { project.dependencies.add(any(), any()) } returns mockk()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val pluginHandler = PluginHandler(project)
mockkObject(NativePluginLoaderReflectionBridge)
// mock return of NativePluginLoaderReflectionBridge.getPlugins
val pluginWithDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
pluginWithDependencies["dependencies"] =
listOf(flutterPluginAndroidLifecycleDependency["name"])
every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns
listOf(
pluginWithDependencies
)
// mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge.getPlugins
every { project.extraProperties } returns mockk()
every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension()
every { project.file(any()) } returns mockk()
val dependencyGraph =
listOf<Map<String?, Any?>>(
mapOf(
"name" to cameraDependency["name"],
"dependencies" to listOf(flutterPluginAndroidLifecycleDependency["name"])
),
mapOf(
"name" to flutterPluginAndroidLifecycleDependency["name"],
"dependencies" to listOf<String>()
)
)
every { NativePluginLoaderReflectionBridge.getDependenciesMetadata(any(), any()) } returns
mapOf("dependencyGraph" to dependencyGraph)
pluginHandler.configurePlugins(
engineVersionValue = EXAMPLE_ENGINE_VERSION
)
verify { project.afterEvaluate(capture(captureActionSlot)) }
verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) }
captureActionSlot.captured.execute(project)
capturePluginActionSlot[0].execute(pluginProject)
capturePluginActionSlot[1].execute(pluginProject)
verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) }
verify {
pluginProject.dependencies.add(
"debugApi",
"io.flutter:flutter_embedding_debug:$EXAMPLE_ENGINE_VERSION"
)
}
verify { project.dependencies.add("debugApi", pluginProject) }
verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) }
verify { pluginProject.dependencies.add("implementation", pluginDependencyProject) }
verify { mockLogger.quiet(PluginHandler.legacyFlutterPluginsWarning) }
}
}

View File

@ -1,5 +1,10 @@
package com.flutter.gradle // 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.tasks
import com.flutter.gradle.DependencyVersionChecker
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify

View File

@ -1,5 +1,10 @@
package com.flutter.gradle // 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.tasks
import com.flutter.gradle.FlutterPluginConstants
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot

View File

@ -42,7 +42,7 @@ import 'migrations/top_level_gradle_build_file_migration.dart';
/// The regex to grab variant names from printBuildVariants gradle task /// The regex to grab variant names from printBuildVariants gradle task
/// ///
/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy /// The task is defined in flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
/// ///
/// The expected output from the task should be similar to: /// The expected output from the task should be similar to:
/// ///

View File

@ -917,7 +917,7 @@ const String kAndroidArchs = 'AndroidArchs';
/// ///
/// If not provided, defaults to `minSdkVersion` from gradle_utils.dart. /// If not provided, defaults to `minSdkVersion` from gradle_utils.dart.
/// ///
/// This is passed in by flutter.groovy's invocation of `flutter assemble`. /// This is passed in by the Flutter Gradle plugin's invocation of `flutter assemble`.
/// ///
/// For more info, see: /// For more info, see:
/// https://developer.android.com/ndk/guides/sdk-versions#minsdkversion /// https://developer.android.com/ndk/guides/sdk-versions#minsdkversion