mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
Reland: "Fix how Gradle resolves Android plugin" (#137115)
Relands #97823 When the tool migrated to `.flutter-plugins-dependencies`, the Gradle plugin was never changed. Until now, the plugin had the heuristic that a plugin with a `android/build.gradle` file supported the Android platform. Also applies schema of `getPluginDependencies` to `getPluginList` which uses a `List` of Object instead of `Properties`. Fixes #97729 Cause of the error:5f105a6ca7/packages/flutter_tools/gradle/flutter.gradle (L421C25-L421C25)
Fixes #98048 The deprecated line `include ":$name"` in `settings.gradle` (pluginEach) in old projects causes the `project.rootProject.findProject` to also find the plugin "project", so it is not failing on the `afterEvaluate` method. But the plugin shouldn't be included in the first place as it fails with `Could not find method implementation() for arguments` error in special cases. Related to #48918, see [_writeFlutterPluginsListLegacy](27bc1cf61a/packages/flutter_tools/lib/src/flutter_plugins.dart (L248)
). Co-authored-by: Emmanuel Garcia <egarciad@google.com>
This commit is contained in:
parent
430254e13b
commit
f5ac225c8d
|
@ -5,41 +5,26 @@
|
|||
// This file is included from `<module>/.android/include_flutter.groovy`,
|
||||
// so it can be versioned with the Flutter SDK.
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import java.nio.file.Paths
|
||||
|
||||
File pathToThisDirectory = buildscript.sourceFile.parentFile
|
||||
apply from: Paths.get(pathToThisDirectory.absolutePath, "src", "main", "groovy", "native_plugin_loader.groovy")
|
||||
|
||||
def moduleProjectRoot = project(':flutter').projectDir.parentFile.parentFile
|
||||
|
||||
def object = null;
|
||||
String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
|
||||
// If this logic is changed, also change the logic in app_plugin_loader.gradle.
|
||||
def pluginsFile = new File(moduleProjectRoot, '.flutter-plugins-dependencies')
|
||||
if (pluginsFile.exists()) {
|
||||
object = new JsonSlurper().parseText(pluginsFile.text)
|
||||
assert object instanceof Map
|
||||
assert object.plugins instanceof Map
|
||||
assert object.plugins.android instanceof List
|
||||
// Includes the Flutter plugins that support the Android platform.
|
||||
object.plugins.android.each { androidPlugin ->
|
||||
assert androidPlugin.name instanceof String
|
||||
assert androidPlugin.path instanceof String
|
||||
// Skip plugins that have no native build (such as a Dart-only
|
||||
// implementation of a federated plugin).
|
||||
def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true
|
||||
if (!needsBuild) {
|
||||
return
|
||||
}
|
||||
def pluginDirectory = new File(androidPlugin.path, 'android')
|
||||
assert pluginDirectory.exists()
|
||||
include ":${androidPlugin.name}"
|
||||
project(":${androidPlugin.name}").projectDir = pluginDirectory
|
||||
}
|
||||
List<Map<String, Object>> nativePlugins = nativePluginLoader.getPlugins(moduleProjectRoot)
|
||||
nativePlugins.each { androidPlugin ->
|
||||
def pluginDirectory = new File(androidPlugin.path as String, 'android')
|
||||
assert pluginDirectory.exists()
|
||||
include ":${androidPlugin.name}"
|
||||
project(":${androidPlugin.name}").projectDir = pluginDirectory
|
||||
}
|
||||
|
||||
String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
|
||||
gradle.getGradle().projectsLoaded { g ->
|
||||
g.rootProject.beforeEvaluate { p ->
|
||||
p.subprojects { subproject ->
|
||||
if (object != null && object.plugins != null && object.plugins.android != null
|
||||
&& object.plugins.android.name.contains(subproject.name)) {
|
||||
if (nativePlugins.name.contains(subproject.name)) {
|
||||
File androidPluginBuildOutputDir = new File(flutterModulePath + File.separator
|
||||
+ "plugins_build_output" + File.separator + subproject.name);
|
||||
if (!androidPluginBuildOutputDir.exists()) {
|
||||
|
|
|
@ -1,39 +1,29 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.initialization.Settings
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
apply plugin: FlutterAppPluginLoaderPlugin
|
||||
|
||||
class FlutterAppPluginLoaderPlugin implements Plugin<Settings> {
|
||||
// This string must match _kFlutterPluginsHasNativeBuildKey defined in
|
||||
// packages/flutter_tools/lib/src/flutter_plugins.dart.
|
||||
private final String nativeBuildKey = 'native_build'
|
||||
|
||||
@Override
|
||||
void apply(Settings settings) {
|
||||
def flutterProjectRoot = settings.settingsDir.parentFile
|
||||
|
||||
// If this logic is changed, also change the logic in module_plugin_loader.gradle.
|
||||
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
|
||||
if (!pluginsFile.exists()) {
|
||||
return
|
||||
if(!settings.ext.hasProperty('flutterSdkPath')) {
|
||||
def properties = new Properties()
|
||||
def localPropertiesFile = new File(settings.rootProject.projectDir, "local.properties")
|
||||
localPropertiesFile.withInputStream { properties.load(it) }
|
||||
settings.ext.flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert settings.ext.flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
}
|
||||
|
||||
// Load shared gradle functions
|
||||
settings.apply from: Paths.get(settings.ext.flutterSdkPath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
|
||||
|
||||
def object = new JsonSlurper().parseText(pluginsFile.text)
|
||||
assert object instanceof Map
|
||||
assert object.plugins instanceof Map
|
||||
assert object.plugins.android instanceof List
|
||||
// Includes the Flutter plugins that support the Android platform.
|
||||
object.plugins.android.each { androidPlugin ->
|
||||
assert androidPlugin.name instanceof String
|
||||
assert androidPlugin.path instanceof String
|
||||
// Skip plugins that have no native build (such as a Dart-only implementation
|
||||
// of a federated plugin).
|
||||
def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true
|
||||
if (!needsBuild) {
|
||||
return
|
||||
}
|
||||
def pluginDirectory = new File(androidPlugin.path, 'android')
|
||||
List<Map<String, Object>> nativePlugins = settings.ext.nativePluginLoader.getPlugins(flutterProjectRoot)
|
||||
nativePlugins.each { androidPlugin ->
|
||||
def pluginDirectory = new File(androidPlugin.path as String, 'android')
|
||||
assert pluginDirectory.exists()
|
||||
settings.include(":${androidPlugin.name}")
|
||||
settings.project(":${androidPlugin.name}").projectDir = pluginDirectory
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.json.JsonGenerator
|
||||
import groovy.xml.QName
|
||||
import java.nio.file.Paths
|
||||
|
@ -65,7 +64,7 @@ class FlutterExtension {
|
|||
* Specifies the relative directory to the Flutter project directory.
|
||||
* In an app project, this is ../.. since the app's build.gradle is under android/app.
|
||||
*/
|
||||
String source
|
||||
String source = '../..'
|
||||
|
||||
/** Allows to override the target file. Otherwise, the target is lib/main.dart. */
|
||||
String target
|
||||
|
@ -222,6 +221,9 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
}
|
||||
}
|
||||
|
||||
// Load shared gradle functions
|
||||
project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
|
||||
|
||||
project.extensions.create("flutter", FlutterExtension)
|
||||
this.addFlutterTasks(project)
|
||||
|
||||
|
@ -357,7 +359,7 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
// This prevents duplicated classes when using custom build types. That is, a custom build
|
||||
// type like profile is used, and the plugin and app projects have API dependencies on the
|
||||
// embedding.
|
||||
if (!isFlutterAppProject() || getPluginList().size() == 0) {
|
||||
if (!isFlutterAppProject() || getPluginList(project).size() == 0) {
|
||||
addApiDependencies(project, buildType.name,
|
||||
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
|
||||
}
|
||||
|
@ -390,19 +392,77 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
* Configures the Flutter plugin dependencies.
|
||||
*
|
||||
* The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
|
||||
* the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location.
|
||||
* 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() {
|
||||
getPluginList().each(this.&configurePluginProject)
|
||||
getPluginDependencies().each(this.&configurePluginDependencies)
|
||||
private void configurePlugins(Project project) {
|
||||
configureLegacyPluginEachProjects(project)
|
||||
getPluginList(project).each(this.&configurePluginProject)
|
||||
getPluginList(project).each(this.&configurePluginDependencies)
|
||||
}
|
||||
|
||||
// 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 configureLegacyPluginEachProjects(Project project) {
|
||||
File settingsGradle = new File(project.projectDir.parentFile, 'settings.gradle')
|
||||
try {
|
||||
if (!settingsGradle.text.contains("'.flutter-plugins'")) {
|
||||
return
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {
|
||||
throw new GradleException("settings.gradle does not exist: ${settingsGradle.absolutePath}")
|
||||
}
|
||||
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.")
|
||||
} else if (doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path as String)) {
|
||||
// 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(it)
|
||||
} else {
|
||||
// Plugin has no or an empty `android` folder. No action required.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(54566): Can remove this function and its call sites once resolved.
|
||||
/**
|
||||
* Returns `true` if the given path contains an `android/build.gradle` file.
|
||||
*/
|
||||
private static Boolean doesSupportAndroidPlatform(String path) {
|
||||
File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
|
||||
return editableAndroidProject.exists()
|
||||
}
|
||||
|
||||
/** Adds the plugin project dependency to the app project. */
|
||||
private void configurePluginProject(String pluginName, String _) {
|
||||
Project pluginProject = project.rootProject.findProject(":$pluginName")
|
||||
private void configurePluginProject(Map<String, Object> pluginObject) {
|
||||
assert(pluginObject.name instanceof String)
|
||||
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
|
||||
if (pluginProject == null) {
|
||||
project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
|
||||
return
|
||||
}
|
||||
// Add plugin dependency to the app project.
|
||||
|
@ -441,7 +501,7 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
pluginProject.afterEvaluate {
|
||||
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
|
||||
if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
|
||||
project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
|
||||
project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
|
||||
project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
|
||||
}
|
||||
|
||||
|
@ -493,10 +553,14 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
|
||||
String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
|
||||
String maxPluginNdkVersion = projectNdkVersion
|
||||
int numProcessedPlugins = getPluginList().size()
|
||||
int numProcessedPlugins = getPluginList(project).size()
|
||||
|
||||
getPluginList().each { plugin ->
|
||||
Project pluginProject = project.rootProject.findProject(plugin.key)
|
||||
getPluginList(project).each { pluginObject ->
|
||||
assert(pluginObject.name instanceof String)
|
||||
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
|
||||
if (pluginProject == null) {
|
||||
return
|
||||
}
|
||||
pluginProject.afterEvaluate {
|
||||
// Default to int min if using a preview version to skip the sdk check.
|
||||
int pluginCompileSdkVersion = Integer.MIN_VALUE;
|
||||
|
@ -530,35 +594,25 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
return gradleProject.android.compileSdkVersion.substring(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given path contains an `android/build.gradle` file.
|
||||
*/
|
||||
private Boolean doesSupportAndroidPlatform(String path) {
|
||||
File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
|
||||
return editableAndroidProject.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 void configurePluginDependencies(Object dependencyObject) {
|
||||
assert(dependencyObject.name instanceof String)
|
||||
Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
|
||||
if (pluginProject == null ||
|
||||
!doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
|
||||
private void configurePluginDependencies(Map<String, Object> pluginObject) {
|
||||
assert(pluginObject.name instanceof String)
|
||||
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
|
||||
if (pluginProject == null) {
|
||||
return
|
||||
}
|
||||
assert(dependencyObject.dependencies instanceof List)
|
||||
dependencyObject.dependencies.each { pluginDependencyName ->
|
||||
assert(pluginDependencyName instanceof String)
|
||||
def dependencies = pluginObject.dependencies
|
||||
assert(dependencies instanceof List<String>)
|
||||
dependencies.each { pluginDependencyName ->
|
||||
if (pluginDependencyName.empty) {
|
||||
return
|
||||
}
|
||||
Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
|
||||
if (dependencyProject == null ||
|
||||
!doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
|
||||
if (dependencyProject == null) {
|
||||
return
|
||||
}
|
||||
// Wait for the Android plugin to load and add the dependency to the plugin project.
|
||||
|
@ -570,52 +624,27 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
}
|
||||
}
|
||||
|
||||
private Properties getPluginList() {
|
||||
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
|
||||
Properties allPlugins = readPropertiesIfExist(pluginsFile)
|
||||
Properties androidPlugins = new Properties()
|
||||
allPlugins.each { name, path ->
|
||||
if (doesSupportAndroidPlatform(path)) {
|
||||
androidPlugins.setProperty(name, path)
|
||||
}
|
||||
// TODO(amirh): log an error if this plugin was specified to be an Android
|
||||
// plugin according to the new schema, and was missing a build.gradle file.
|
||||
// https://github.com/flutter/flutter/issues/40784
|
||||
}
|
||||
return androidPlugins
|
||||
/**
|
||||
* 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/groovy/native_plugin_loader.groovy
|
||||
*/
|
||||
private List<Map<String, Object>> getPluginList(Project project) {
|
||||
return project.ext.nativePluginLoader.getPlugins(getFlutterSourceDirectory())
|
||||
}
|
||||
|
||||
// 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 getPluginDependencies() {
|
||||
// Consider a `.flutter-plugins-dependencies` file with the following content:
|
||||
// {
|
||||
// "dependencyGraph": [
|
||||
// {
|
||||
// "name": "plugin-a",
|
||||
// "dependencies": ["plugin-b","plugin-c"]
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-b",
|
||||
// "dependencies": ["plugin-c"]
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-c",
|
||||
// "dependencies": []'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
|
||||
// `plugin-b` depends on `plugin-c`.
|
||||
// `plugin-c` doesn't depend on anything.
|
||||
File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
|
||||
if (pluginsDependencyFile.exists()) {
|
||||
def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
|
||||
assert(object instanceof Map)
|
||||
assert(object.dependencyGraph instanceof List)
|
||||
return object.dependencyGraph
|
||||
private List<Map<String, Object>> getPluginDependencies(Project project) {
|
||||
Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory())
|
||||
if (meta == null) {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
assert(meta.dependencyGraph instanceof List<Map>)
|
||||
return meta.dependencyGraph as List<Map<String, Object>>
|
||||
}
|
||||
|
||||
private static String toCamelCase(List<String> parts) {
|
||||
|
@ -1226,7 +1255,7 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
def nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
|
||||
project.android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
|
||||
}
|
||||
configurePlugins()
|
||||
configurePlugins(project)
|
||||
detectLowCompileSdkVersionOrNdkVersion()
|
||||
return
|
||||
}
|
||||
|
@ -1278,7 +1307,7 @@ class FlutterPlugin implements Plugin<Project> {
|
|||
}
|
||||
}
|
||||
}
|
||||
configurePlugins()
|
||||
configurePlugins(project)
|
||||
detectLowCompileSdkVersionOrNdkVersion()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
// 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 groovy.json.JsonSlurper
|
||||
|
||||
@Singleton
|
||||
class NativePluginLoader {
|
||||
|
||||
// This string must match _kFlutterPluginsHasNativeBuildKey defined in
|
||||
// packages/flutter_tools/lib/src/flutter_plugins.dart.
|
||||
static final String nativeBuildKey = "native_build"
|
||||
static final String flutterPluginsDependenciesFile = ".flutter-plugins-dependencies"
|
||||
|
||||
/**
|
||||
* Gets the list of plugins that support the Android platform.
|
||||
* The list contains map elements with the following content:
|
||||
* {
|
||||
* "name": "plugin-a",
|
||||
* "path": "/path/to/plugin-a",
|
||||
* "dependencies": ["plugin-b", "plugin-c"],
|
||||
* "native_build": true
|
||||
* }
|
||||
*
|
||||
* Therefore the map value can either be a `String`, a `List<String>` or a `boolean`.
|
||||
*/
|
||||
List<Map<String, Object>> getPlugins(File flutterSourceDirectory) {
|
||||
List<Map<String, Object>> nativePlugins = []
|
||||
def meta = getDependenciesMetadata(flutterSourceDirectory)
|
||||
if (meta == null) {
|
||||
return nativePlugins
|
||||
}
|
||||
|
||||
assert(meta.plugins instanceof Map<String, Object>)
|
||||
def androidPlugins = meta.plugins.android
|
||||
assert(androidPlugins instanceof List<Map>)
|
||||
// Includes the Flutter plugins that support the Android platform.
|
||||
androidPlugins.each { Map<String, Object> androidPlugin ->
|
||||
// The property types can be found in _filterPluginsByPlatform defined in
|
||||
// packages/flutter_tools/lib/src/flutter_plugins.dart.
|
||||
assert(androidPlugin.name instanceof String)
|
||||
assert(androidPlugin.path instanceof String)
|
||||
assert(androidPlugin.dependencies instanceof List<String>)
|
||||
// Skip plugins that have no native build (such as a Dart-only implementation
|
||||
// of a federated plugin).
|
||||
def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true
|
||||
if (needsBuild) {
|
||||
nativePlugins.add(androidPlugin)
|
||||
}
|
||||
}
|
||||
return nativePlugins
|
||||
}
|
||||
|
||||
|
||||
private Map<String, Object> parsedFlutterPluginsDependencies
|
||||
|
||||
/**
|
||||
* Parses <project-src>/.flutter-plugins-dependencies
|
||||
*/
|
||||
Map<String, Object> getDependenciesMetadata(File flutterSourceDirectory) {
|
||||
// Consider a `.flutter-plugins-dependencies` file with the following content:
|
||||
// {
|
||||
// "plugins": {
|
||||
// "android": [
|
||||
// {
|
||||
// "name": "plugin-a",
|
||||
// "path": "/path/to/plugin-a",
|
||||
// "dependencies": ["plugin-b", "plugin-c"],
|
||||
// "native_build": true
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-b",
|
||||
// "path": "/path/to/plugin-b",
|
||||
// "dependencies": ["plugin-c"],
|
||||
// "native_build": true
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-c",
|
||||
// "path": "/path/to/plugin-c",
|
||||
// "dependencies": [],
|
||||
// "native_build": true
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// "dependencyGraph": [
|
||||
// {
|
||||
// "name": "plugin-a",
|
||||
// "dependencies": ["plugin-b","plugin-c"]
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-b",
|
||||
// "dependencies": ["plugin-c"]
|
||||
// },
|
||||
// {
|
||||
// "name": "plugin-c",
|
||||
// "dependencies": []
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
|
||||
// `plugin-b` depends on `plugin-c`.
|
||||
// `plugin-c` doesn't depend on anything.
|
||||
if (parsedFlutterPluginsDependencies) {
|
||||
return parsedFlutterPluginsDependencies
|
||||
}
|
||||
File pluginsDependencyFile = new File(flutterSourceDirectory, flutterPluginsDependenciesFile)
|
||||
if (pluginsDependencyFile.exists()) {
|
||||
def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
|
||||
assert(object instanceof Map<String, Object>)
|
||||
parsedFlutterPluginsDependencies = object
|
||||
return object
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(135392): Remove and use declarative form when migrated
|
||||
ext {
|
||||
nativePluginLoader = NativePluginLoader.instance
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
// 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 'package:file_testing/file_testing.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import 'test_data/plugin_each_settings_gradle_project.dart';
|
||||
import 'test_data/plugin_project.dart';
|
||||
import 'test_data/project.dart';
|
||||
import 'test_utils.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() {
|
||||
Cache.flutterRoot = getFlutterRoot();
|
||||
tempDir = createResolvedTempDirectorySync('flutter_plugin_test.');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/97729 (#137115).
|
||||
/// Creates a project which uses a plugin, which is not supported on Android.
|
||||
/// This means it has no entry in pubspec.yaml for:
|
||||
/// flutter -> plugin -> platforms -> android
|
||||
///
|
||||
/// [createAndroidPluginFolder] indicates that the plugin can additionally
|
||||
/// have a functioning `android` folder.
|
||||
Future<ProcessResult> testUnsupportedPlugin({
|
||||
required Project project,
|
||||
required bool createAndroidPluginFolder,
|
||||
}) async {
|
||||
final String flutterBin = fileSystem.path.join(
|
||||
getFlutterRoot(),
|
||||
'bin',
|
||||
'flutter',
|
||||
);
|
||||
|
||||
// Create dummy plugin that supports iOS and optionally Android.
|
||||
processManager.runSync(<String>[
|
||||
flutterBin,
|
||||
...getLocalEngineArguments(),
|
||||
'create',
|
||||
'--template=plugin',
|
||||
'--platforms=ios${createAndroidPluginFolder ? ',android' : ''}',
|
||||
'test_plugin',
|
||||
], workingDirectory: tempDir.path);
|
||||
|
||||
final Directory pluginAppDir = tempDir.childDirectory('test_plugin');
|
||||
|
||||
final File pubspecFile = pluginAppDir.childFile('pubspec.yaml');
|
||||
String pubspecYamlSrc =
|
||||
pubspecFile.readAsStringSync().replaceAll('\r\n', '\n');
|
||||
if (createAndroidPluginFolder) {
|
||||
// Override pubspec to drop support for the Android implementation.
|
||||
pubspecYamlSrc = pubspecYamlSrc
|
||||
.replaceFirst(
|
||||
RegExp(r'name:.*\n'),
|
||||
'name: test_plugin\n',
|
||||
)
|
||||
.replaceFirst('''
|
||||
android:
|
||||
package: com.example.test_plugin
|
||||
pluginClass: TestPlugin
|
||||
''', '''
|
||||
# android:
|
||||
# package: com.example.test_plugin
|
||||
# pluginClass: TestPlugin
|
||||
''');
|
||||
|
||||
pubspecFile.writeAsStringSync(pubspecYamlSrc);
|
||||
|
||||
// Check the android directory and the build.gradle file within.
|
||||
final File pluginGradleFile =
|
||||
pluginAppDir.childDirectory('android').childFile('build.gradle');
|
||||
expect(pluginGradleFile, exists);
|
||||
} else {
|
||||
expect(pubspecYamlSrc, isNot(contains('android:')));
|
||||
}
|
||||
|
||||
// Create a project which includes the plugin to test against
|
||||
final Directory pluginExampleAppDir =
|
||||
pluginAppDir.childDirectory('example');
|
||||
|
||||
await project.setUpIn(pluginExampleAppDir);
|
||||
|
||||
// Run flutter build apk to build plugin example project.
|
||||
return processManager.runSync(<String>[
|
||||
flutterBin,
|
||||
...getLocalEngineArguments(),
|
||||
'build',
|
||||
'apk',
|
||||
'--debug',
|
||||
], workingDirectory: pluginExampleAppDir.path);
|
||||
}
|
||||
|
||||
test('skip plugin if it does not support the Android platform', () async {
|
||||
final Project project = PluginWithPathAndroidProject();
|
||||
final ProcessResult buildApkResult = await testUnsupportedPlugin(
|
||||
project: project, createAndroidPluginFolder: false);
|
||||
expect(buildApkResult.stderr.toString(),
|
||||
isNot(contains('Please fix your settings.gradle.')));
|
||||
expect(buildApkResult, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
test(
|
||||
'skip plugin with android folder if it does not support the Android platform',
|
||||
() async {
|
||||
final Project project = PluginWithPathAndroidProject();
|
||||
final ProcessResult buildApkResult = await testUnsupportedPlugin(
|
||||
project: project, createAndroidPluginFolder: true);
|
||||
expect(buildApkResult.stderr.toString(),
|
||||
isNot(contains('Please fix your settings.gradle.')));
|
||||
expect(buildApkResult, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
// TODO(54566): Remove test when issue is resolved.
|
||||
/// Test project with a `settings.gradle` (PluginEach) that apps were created
|
||||
/// with until Flutter v1.22.0.
|
||||
/// It uses the `.flutter-plugins` file to load EACH plugin.
|
||||
test(
|
||||
'skip plugin if it does not support the Android platform with a _plugin.each_ settings.gradle',
|
||||
() async {
|
||||
final Project project = PluginEachWithPathAndroidProject();
|
||||
final ProcessResult buildApkResult = await testUnsupportedPlugin(
|
||||
project: project, createAndroidPluginFolder: false);
|
||||
expect(buildApkResult.stderr.toString(),
|
||||
isNot(contains('Please fix your settings.gradle.')));
|
||||
expect(buildApkResult, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
// TODO(54566): Remove test when issue is resolved.
|
||||
/// Test project with a `settings.gradle` (PluginEach) that apps were created
|
||||
/// with until Flutter v1.22.0.
|
||||
/// It uses the `.flutter-plugins` file to load EACH plugin.
|
||||
/// The plugin includes a functional 'android' folder.
|
||||
test(
|
||||
'skip plugin with android folder if it does not support the Android platform with a _plugin.each_ settings.gradle',
|
||||
() async {
|
||||
final Project project = PluginEachWithPathAndroidProject();
|
||||
final ProcessResult buildApkResult = await testUnsupportedPlugin(
|
||||
project: project, createAndroidPluginFolder: true);
|
||||
expect(buildApkResult.stderr.toString(),
|
||||
isNot(contains('Please fix your settings.gradle.')));
|
||||
expect(buildApkResult, const ProcessResultMatcher());
|
||||
});
|
||||
|
||||
// TODO(54566): Remove test when issue is resolved.
|
||||
/// Test project with a `settings.gradle` (PluginEach) that apps were created
|
||||
/// with until Flutter v1.22.0.
|
||||
/// It is compromised by removing the 'include' statement of the plugins.
|
||||
/// As the "'.flutter-plugins'" keyword is still present, the framework
|
||||
/// assumes that all plugins are included, which is not the case.
|
||||
/// Therefore it should throw an error.
|
||||
test(
|
||||
'skip plugin if it does not support the Android platform with a compromised _plugin.each_ settings.gradle',
|
||||
() async {
|
||||
final Project project = PluginCompromisedEachWithPathAndroidProject();
|
||||
final ProcessResult buildApkResult = await testUnsupportedPlugin(
|
||||
project: project, createAndroidPluginFolder: true);
|
||||
expect(
|
||||
buildApkResult,
|
||||
const ProcessResultMatcher(
|
||||
stderrPattern: 'Please fix your settings.gradle.'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const String pubspecWithPluginPath = r'''
|
||||
name: test
|
||||
environment:
|
||||
sdk: '>=3.2.0-0 <4.0.0'
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
test_plugin:
|
||||
path: ../
|
||||
''';
|
||||
|
||||
/// Project that load's a plugin from the specified path.
|
||||
class PluginWithPathAndroidProject extends PluginProject {
|
||||
@override
|
||||
String get pubspec => pubspecWithPluginPath;
|
||||
}
|
||||
|
||||
// TODO(54566): Remove class when issue is resolved.
|
||||
/// [PluginEachSettingsGradleProject] that load's a plugin from the specified
|
||||
/// path.
|
||||
class PluginEachWithPathAndroidProject extends PluginEachSettingsGradleProject {
|
||||
@override
|
||||
String get pubspec => pubspecWithPluginPath;
|
||||
}
|
||||
|
||||
// TODO(54566): Remove class when issue is resolved.
|
||||
/// [PluginCompromisedEachSettingsGradleProject] that load's a plugin from the
|
||||
/// specified path.
|
||||
class PluginCompromisedEachWithPathAndroidProject
|
||||
extends PluginCompromisedEachSettingsGradleProject {
|
||||
@override
|
||||
String get pubspec => pubspecWithPluginPath;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// 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.
|
||||
|
||||
// TODO(54566): Remove this file when issue is resolved.
|
||||
|
||||
import 'deferred_components_config.dart';
|
||||
import 'plugin_project.dart';
|
||||
|
||||
/// Project to test the deprecated `settings.gradle` (PluginEach) that apps were
|
||||
/// created with until Flutter v1.22.0.
|
||||
/// It uses the `.flutter-plugins` file to load EACH plugin.
|
||||
class PluginEachSettingsGradleProject extends PluginProject {
|
||||
@override
|
||||
DeferredComponentsConfig get deferredComponents =>
|
||||
PluginEachSettingsGradleDeferredComponentsConfig();
|
||||
}
|
||||
|
||||
class PluginEachSettingsGradleDeferredComponentsConfig
|
||||
extends PluginDeferredComponentsConfig {
|
||||
@override
|
||||
String get androidSettings => r'''
|
||||
include ':app'
|
||||
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||
def plugins = new Properties()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
}
|
||||
plugins.each { name, path ->
|
||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||
include ":$name"
|
||||
project(":$name").projectDir = pluginDirectory
|
||||
}
|
||||
''';
|
||||
}
|
||||
|
||||
/// Project to test the deprecated `settings.gradle` (PluginEach) that apps were
|
||||
/// created with until Flutter v1.22.0.
|
||||
/// It uses the `.flutter-plugins` file to get EACH plugin.
|
||||
/// It is compromised by removing the 'include' statement of the plugins.
|
||||
class PluginCompromisedEachSettingsGradleProject extends PluginProject {
|
||||
@override
|
||||
DeferredComponentsConfig get deferredComponents =>
|
||||
PluginCompromisedEachSettingsGradleDeferredComponentsConfig();
|
||||
}
|
||||
|
||||
class PluginCompromisedEachSettingsGradleDeferredComponentsConfig
|
||||
extends PluginDeferredComponentsConfig {
|
||||
@override
|
||||
String get androidSettings => r'''
|
||||
include ':app'
|
||||
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||
def plugins = new Properties()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
}
|
||||
''';
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// 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 'basic_project.dart';
|
||||
import 'deferred_components_config.dart';
|
||||
import 'deferred_components_project.dart';
|
||||
|
||||
/// Project which can load native plugins
|
||||
class PluginProject extends BasicProject {
|
||||
@override
|
||||
final DeferredComponentsConfig? deferredComponents =
|
||||
PluginDeferredComponentsConfig();
|
||||
}
|
||||
|
||||
class PluginDeferredComponentsConfig extends BasicDeferredComponentsConfig {
|
||||
@override
|
||||
String get androidBuild => r'''
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
configurations.classpath {
|
||||
resolutionStrategy.activateDependencyLocking()
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
dependencyLocking {
|
||||
ignoredDependencies.add('io.flutter:*')
|
||||
lockFile = file("${rootProject.projectDir}/project-${project.name}.lockfile")
|
||||
if (!project.hasProperty('local-engine-repo')) {
|
||||
lockAllConfigurations()
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
''';
|
||||
|
||||
@override
|
||||
String get androidSettings => r'''
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
''';
|
||||
|
||||
@override
|
||||
String get appManifest => r'''
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.yourcompany.flavors">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:label="flavors">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
''';
|
||||
}
|
Loading…
Reference in a new issue