Skip to content

Commit 164cf74

Browse files
Gustl22Emmanuel Garcia
authored andcommitted
Reland: "Fix how Gradle resolves Android plugin" (flutter#137115)
Relands flutter#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 flutter#97729 Cause of the error: https://github.com/flutter/flutter/blob/5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c/packages/flutter_tools/gradle/flutter.gradle#L421C25-L421C25 Fixes flutter#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 flutter#48918, see [_writeFlutterPluginsListLegacy](https://github.com/flutter/flutter/blob/27bc1cf61a5b54bf655062be63050123abb617e4/packages/flutter_tools/lib/src/flutter_plugins.dart#L248). Co-authored-by: Emmanuel Garcia <egarciad@google.com>
1 parent aba3631 commit 164cf74

File tree

7 files changed

+619
-128
lines changed

7 files changed

+619
-128
lines changed

packages/flutter_tools/gradle/module_plugin_loader.gradle

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,26 @@
55
// This file is included from `<module>/.android/include_flutter.groovy`,
66
// so it can be versioned with the Flutter SDK.
77

8-
import groovy.json.JsonSlurper
8+
import java.nio.file.Paths
9+
10+
File pathToThisDirectory = buildscript.sourceFile.parentFile
11+
apply from: Paths.get(pathToThisDirectory.absolutePath, "src", "main", "groovy", "native_plugin_loader.groovy")
912

1013
def moduleProjectRoot = project(':flutter').projectDir.parentFile.parentFile
1114

12-
def object = null;
13-
String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
14-
// If this logic is changed, also change the logic in app_plugin_loader.gradle.
15-
def pluginsFile = new File(moduleProjectRoot, '.flutter-plugins-dependencies')
16-
if (pluginsFile.exists()) {
17-
object = new JsonSlurper().parseText(pluginsFile.text)
18-
assert object instanceof Map
19-
assert object.plugins instanceof Map
20-
assert object.plugins.android instanceof List
21-
// Includes the Flutter plugins that support the Android platform.
22-
object.plugins.android.each { androidPlugin ->
23-
assert androidPlugin.name instanceof String
24-
assert androidPlugin.path instanceof String
25-
// Skip plugins that have no native build (such as a Dart-only
26-
// implementation of a federated plugin).
27-
def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true
28-
if (!needsBuild) {
29-
return
30-
}
31-
def pluginDirectory = new File(androidPlugin.path, 'android')
32-
assert pluginDirectory.exists()
33-
include ":${androidPlugin.name}"
34-
project(":${androidPlugin.name}").projectDir = pluginDirectory
35-
}
15+
List<Map<String, Object>> nativePlugins = nativePluginLoader.getPlugins(moduleProjectRoot)
16+
nativePlugins.each { androidPlugin ->
17+
def pluginDirectory = new File(androidPlugin.path as String, 'android')
18+
assert pluginDirectory.exists()
19+
include ":${androidPlugin.name}"
20+
project(":${androidPlugin.name}").projectDir = pluginDirectory
3621
}
3722

23+
String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
3824
gradle.getGradle().projectsLoaded { g ->
3925
g.rootProject.beforeEvaluate { p ->
4026
p.subprojects { subproject ->
41-
if (object != null && object.plugins != null && object.plugins.android != null
42-
&& object.plugins.android.name.contains(subproject.name)) {
27+
if (nativePlugins.name.contains(subproject.name)) {
4328
File androidPluginBuildOutputDir = new File(flutterModulePath + File.separator
4429
+ "plugins_build_output" + File.separator + subproject.name);
4530
if (!androidPluginBuildOutputDir.exists()) {

packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
1-
import groovy.json.JsonSlurper
21
import org.gradle.api.Plugin
32
import org.gradle.api.initialization.Settings
43

4+
import java.nio.file.Paths
5+
56
apply plugin: FlutterAppPluginLoaderPlugin
67

78
class FlutterAppPluginLoaderPlugin implements Plugin<Settings> {
8-
// This string must match _kFlutterPluginsHasNativeBuildKey defined in
9-
// packages/flutter_tools/lib/src/flutter_plugins.dart.
10-
private final String nativeBuildKey = 'native_build'
11-
129
@Override
1310
void apply(Settings settings) {
1411
def flutterProjectRoot = settings.settingsDir.parentFile
1512

16-
// If this logic is changed, also change the logic in module_plugin_loader.gradle.
17-
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
18-
if (!pluginsFile.exists()) {
19-
return
13+
if(!settings.ext.hasProperty('flutterSdkPath')) {
14+
def properties = new Properties()
15+
def localPropertiesFile = new File(settings.rootProject.projectDir, "local.properties")
16+
localPropertiesFile.withInputStream { properties.load(it) }
17+
settings.ext.flutterSdkPath = properties.getProperty("flutter.sdk")
18+
assert settings.ext.flutterSdkPath != null, "flutter.sdk not set in local.properties"
2019
}
20+
21+
// Load shared gradle functions
22+
settings.apply from: Paths.get(settings.ext.flutterSdkPath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
2123

22-
def object = new JsonSlurper().parseText(pluginsFile.text)
23-
assert object instanceof Map
24-
assert object.plugins instanceof Map
25-
assert object.plugins.android instanceof List
26-
// Includes the Flutter plugins that support the Android platform.
27-
object.plugins.android.each { androidPlugin ->
28-
assert androidPlugin.name instanceof String
29-
assert androidPlugin.path instanceof String
30-
// Skip plugins that have no native build (such as a Dart-only implementation
31-
// of a federated plugin).
32-
def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true
33-
if (!needsBuild) {
34-
return
35-
}
36-
def pluginDirectory = new File(androidPlugin.path, 'android')
24+
List<Map<String, Object>> nativePlugins = settings.ext.nativePluginLoader.getPlugins(flutterProjectRoot)
25+
nativePlugins.each { androidPlugin ->
26+
def pluginDirectory = new File(androidPlugin.path as String, 'android')
3727
assert pluginDirectory.exists()
3828
settings.include(":${androidPlugin.name}")
3929
settings.project(":${androidPlugin.name}").projectDir = pluginDirectory

packages/flutter_tools/gradle/src/main/groovy/flutter.groovy

Lines changed: 106 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// found in the LICENSE file.
44

55
import com.android.build.OutputFile
6-
import groovy.json.JsonSlurper
76
import groovy.json.JsonGenerator
87
import groovy.xml.QName
98
import java.nio.file.Paths
@@ -65,7 +64,7 @@ class FlutterExtension {
6564
* Specifies the relative directory to the Flutter project directory.
6665
* In an app project, this is ../.. since the app's build.gradle is under android/app.
6766
*/
68-
String source
67+
String source = '../..'
6968

7069
/** Allows to override the target file. Otherwise, the target is lib/main.dart. */
7170
String target
@@ -222,6 +221,9 @@ class FlutterPlugin implements Plugin<Project> {
222221
}
223222
}
224223

224+
// Load shared gradle functions
225+
project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
226+
225227
project.extensions.create("flutter", FlutterExtension)
226228
this.addFlutterTasks(project)
227229

@@ -357,7 +359,7 @@ class FlutterPlugin implements Plugin<Project> {
357359
// This prevents duplicated classes when using custom build types. That is, a custom build
358360
// type like profile is used, and the plugin and app projects have API dependencies on the
359361
// embedding.
360-
if (!isFlutterAppProject() || getPluginList().size() == 0) {
362+
if (!isFlutterAppProject() || getPluginList(project).size() == 0) {
361363
addApiDependencies(project, buildType.name,
362364
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
363365
}
@@ -390,19 +392,77 @@ class FlutterPlugin implements Plugin<Project> {
390392
* Configures the Flutter plugin dependencies.
391393
*
392394
* The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
393-
* the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location.
395+
* the tool generates a `.flutter-plugins-dependencies` file, which contains a map to each plugin location.
394396
* Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject.
395397
*/
396-
private void configurePlugins() {
397-
getPluginList().each(this.&configurePluginProject)
398-
getPluginDependencies().each(this.&configurePluginDependencies)
398+
private void configurePlugins(Project project) {
399+
configureLegacyPluginEachProjects(project)
400+
getPluginList(project).each(this.&configurePluginProject)
401+
getPluginList(project).each(this.&configurePluginDependencies)
402+
}
403+
404+
// TODO(54566, 48918): Can remove once the issues are resolved.
405+
// This means all references to `.flutter-plugins` are then removed and
406+
// apps only depend exclusively on the `plugins` property in `.flutter-plugins-dependencies`.
407+
/**
408+
* Workaround to load non-native plugins for developers who may still use an
409+
* old `settings.gradle` which includes all the plugins from the
410+
* `.flutter-plugins` file, even if not made for Android.
411+
* The settings.gradle then:
412+
* 1) tries to add the android plugin implementation, which does not
413+
* exist at all, but is also not included successfully
414+
* (which does not throw an error and therefore isn't a problem), or
415+
* 2) includes the plugin successfully as a valid android plugin
416+
* directory exists, even if the surrounding flutter package does not
417+
* support the android platform (see e.g. apple_maps_flutter: 1.0.1).
418+
* So as it's included successfully it expects to be added as API.
419+
* This is only possible by taking all plugins into account, which
420+
* only appear on the `dependencyGraph` and in the `.flutter-plugins` file.
421+
* So in summary the plugins are currently selected from the `dependencyGraph`
422+
* and filtered then with the [doesSupportAndroidPlatform] method instead of
423+
* just using the `plugins.android` list.
424+
*/
425+
private configureLegacyPluginEachProjects(Project project) {
426+
File settingsGradle = new File(project.projectDir.parentFile, 'settings.gradle')
427+
try {
428+
if (!settingsGradle.text.contains("'.flutter-plugins'")) {
429+
return
430+
}
431+
} catch (FileNotFoundException ignored) {
432+
throw new GradleException("settings.gradle does not exist: ${settingsGradle.absolutePath}")
433+
}
434+
List<Map<String, Object>> deps = getPluginDependencies(project)
435+
List<String> plugins = getPluginList(project).collect { it.name as String }
436+
deps.removeIf { plugins.contains(it.name) }
437+
deps.each {
438+
Project pluginProject = project.rootProject.findProject(":${it.name}")
439+
if (pluginProject == null) {
440+
// Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`.
441+
project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle.")
442+
} else if (doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path as String)) {
443+
// Plugin has a functioning `android` folder and is included successfully, although it's not supported.
444+
// It must be configured nonetheless, to not throw an "Unresolved reference" exception.
445+
configurePluginProject(it)
446+
} else {
447+
// Plugin has no or an empty `android` folder. No action required.
448+
}
449+
}
450+
}
451+
452+
// TODO(54566): Can remove this function and its call sites once resolved.
453+
/**
454+
* Returns `true` if the given path contains an `android/build.gradle` file.
455+
*/
456+
private static Boolean doesSupportAndroidPlatform(String path) {
457+
File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
458+
return editableAndroidProject.exists()
399459
}
400460

401461
/** Adds the plugin project dependency to the app project. */
402-
private void configurePluginProject(String pluginName, String _) {
403-
Project pluginProject = project.rootProject.findProject(":$pluginName")
462+
private void configurePluginProject(Map<String, Object> pluginObject) {
463+
assert(pluginObject.name instanceof String)
464+
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
404465
if (pluginProject == null) {
405-
project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
406466
return
407467
}
408468
// Add plugin dependency to the app project.
@@ -441,7 +501,7 @@ class FlutterPlugin implements Plugin<Project> {
441501
pluginProject.afterEvaluate {
442502
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
443503
if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
444-
project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
504+
project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
445505
project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
446506
}
447507

@@ -493,10 +553,14 @@ class FlutterPlugin implements Plugin<Project> {
493553
String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
494554
String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
495555
String maxPluginNdkVersion = projectNdkVersion
496-
int numProcessedPlugins = getPluginList().size()
556+
int numProcessedPlugins = getPluginList(project).size()
497557

498-
getPluginList().each { plugin ->
499-
Project pluginProject = project.rootProject.findProject(plugin.key)
558+
getPluginList(project).each { pluginObject ->
559+
assert(pluginObject.name instanceof String)
560+
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
561+
if (pluginProject == null) {
562+
return
563+
}
500564
pluginProject.afterEvaluate {
501565
// Default to int min if using a preview version to skip the sdk check.
502566
int pluginCompileSdkVersion = Integer.MIN_VALUE;
@@ -530,35 +594,25 @@ class FlutterPlugin implements Plugin<Project> {
530594
return gradleProject.android.compileSdkVersion.substring(8);
531595
}
532596

533-
/**
534-
* Returns `true` if the given path contains an `android/build.gradle` file.
535-
*/
536-
private Boolean doesSupportAndroidPlatform(String path) {
537-
File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
538-
return editableAndroidProject.exists()
539-
}
540-
541597
/**
542598
* Add the dependencies on other plugin projects to the plugin project.
543599
* A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
544600
* making the Gradle plugin project A depend on the Gradle plugin project B.
545601
*/
546-
private void configurePluginDependencies(Object dependencyObject) {
547-
assert(dependencyObject.name instanceof String)
548-
Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
549-
if (pluginProject == null ||
550-
!doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
602+
private void configurePluginDependencies(Map<String, Object> pluginObject) {
603+
assert(pluginObject.name instanceof String)
604+
Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
605+
if (pluginProject == null) {
551606
return
552607
}
553-
assert(dependencyObject.dependencies instanceof List)
554-
dependencyObject.dependencies.each { pluginDependencyName ->
555-
assert(pluginDependencyName instanceof String)
608+
def dependencies = pluginObject.dependencies
609+
assert(dependencies instanceof List<String>)
610+
dependencies.each { pluginDependencyName ->
556611
if (pluginDependencyName.empty) {
557612
return
558613
}
559614
Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
560-
if (dependencyProject == null ||
561-
!doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
615+
if (dependencyProject == null) {
562616
return
563617
}
564618
// Wait for the Android plugin to load and add the dependency to the plugin project.
@@ -570,52 +624,27 @@ class FlutterPlugin implements Plugin<Project> {
570624
}
571625
}
572626

573-
private Properties getPluginList() {
574-
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
575-
Properties allPlugins = readPropertiesIfExist(pluginsFile)
576-
Properties androidPlugins = new Properties()
577-
allPlugins.each { name, path ->
578-
if (doesSupportAndroidPlatform(path)) {
579-
androidPlugins.setProperty(name, path)
580-
}
581-
// TODO(amirh): log an error if this plugin was specified to be an Android
582-
// plugin according to the new schema, and was missing a build.gradle file.
583-
// https://github.com/flutter/flutter/issues/40784
584-
}
585-
return androidPlugins
627+
/**
628+
* Gets the list of plugins (as map) that support the Android platform.
629+
*
630+
* The map value contains either the plugins `name` (String),
631+
* its `path` (String), or its `dependencies` (List<String>).
632+
* See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
633+
*/
634+
private List<Map<String, Object>> getPluginList(Project project) {
635+
return project.ext.nativePluginLoader.getPlugins(getFlutterSourceDirectory())
586636
}
587637

638+
// TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
639+
// https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
588640
/** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
589-
private List getPluginDependencies() {
590-
// Consider a `.flutter-plugins-dependencies` file with the following content:
591-
// {
592-
// "dependencyGraph": [
593-
// {
594-
// "name": "plugin-a",
595-
// "dependencies": ["plugin-b","plugin-c"]
596-
// },
597-
// {
598-
// "name": "plugin-b",
599-
// "dependencies": ["plugin-c"]
600-
// },
601-
// {
602-
// "name": "plugin-c",
603-
// "dependencies": []'
604-
// }
605-
// ]
606-
// }
607-
//
608-
// This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
609-
// `plugin-b` depends on `plugin-c`.
610-
// `plugin-c` doesn't depend on anything.
611-
File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
612-
if (pluginsDependencyFile.exists()) {
613-
def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
614-
assert(object instanceof Map)
615-
assert(object.dependencyGraph instanceof List)
616-
return object.dependencyGraph
617-
}
618-
return []
641+
private List<Map<String, Object>> getPluginDependencies(Project project) {
642+
Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory())
643+
if (meta == null) {
644+
return []
645+
}
646+
assert(meta.dependencyGraph instanceof List<Map>)
647+
return meta.dependencyGraph as List<Map<String, Object>>
619648
}
620649

621650
private static String toCamelCase(List<String> parts) {
@@ -1226,7 +1255,7 @@ class FlutterPlugin implements Plugin<Project> {
12261255
def nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
12271256
project.android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
12281257
}
1229-
configurePlugins()
1258+
configurePlugins(project)
12301259
detectLowCompileSdkVersionOrNdkVersion()
12311260
return
12321261
}
@@ -1278,7 +1307,7 @@ class FlutterPlugin implements Plugin<Project> {
12781307
}
12791308
}
12801309
}
1281-
configurePlugins()
1310+
configurePlugins(project)
12821311
detectLowCompileSdkVersionOrNdkVersion()
12831312
}
12841313

0 commit comments

Comments
 (0)