Reland (2): "Fix how Gradle resolves Android plugin" (#142498)

Previous PR: #137115, 
Revert: #142464
Fixes #141940
Closes #142487
This commit is contained in:
Gustl22 2024-02-19 19:07:33 +01:00 committed by GitHub
parent 05daee3d91
commit 9620e3f69c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 766 additions and 134 deletions

View file

@ -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()) {

View file

@ -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

View file

@ -4,7 +4,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
@ -202,6 +201,8 @@ class FlutterPlugin implements Plugin<Project> {
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.
@ -258,6 +259,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")
FlutterExtension extension = project.extensions.create("flutter", FlutterExtension)
Properties localProperties = new Properties()
File localPropertiesFile = rootProject.file("local.properties")
@ -620,7 +624,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")
}
@ -642,19 +646,104 @@ 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 void configureLegacyPluginEachProjects(Project project) {
try {
if (!settingsGradleFile(project).text.contains("'.flutter-plugins'")) {
return
}
} catch (FileNotFoundException ignored) {
throw new GradleException("settings.gradle/settings.gradle.kts does not exist: ${settingsGradleFile(project).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/settings.gradle.kts.")
} 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)
/* groovylint-disable-next-line EmptyElseBlock */
} 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` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
private Boolean doesSupportAndroidPlatform(String path) {
File buildGradle = new File(path, 'android' + File.separator + 'build.gradle')
File buildGradleKts = new File(path, 'android' + File.separator + 'build.gradle.kts')
if (buildGradle.exists() && buildGradleKts.exists()) {
project.logger.error(
"Both build.gradle and build.gradle.kts exist, so " +
"build.gradle.kts is ignored. This is likely a mistake."
)
}
return buildGradle.exists() || buildGradleKts.exists()
}
/**
* Returns the Gradle settings script for the build. When both Groovy and
* Kotlin variants exist, then Groovy (settings.gradle) is preferred over
* Kotlin (settings.gradle.kts). This is the same behavior as Gradle 8.5.
*/
private File settingsGradleFile(Project project) {
File settingsGradle = new File(project.projectDir.parentFile, "settings.gradle")
File settingsGradleKts = new File(project.projectDir.parentFile, "settings.gradle.kts")
if (settingsGradle.exists() && settingsGradleKts.exists()) {
project.logger.error(
"Both settings.gradle and settings.gradle.kts exist, so " +
"settings.gradle.kts is ignored. This is likely a mistake."
)
}
return settingsGradle.exists() ? settingsGradle : settingsGradleKts
}
/** 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.
@ -693,7 +782,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.")
}
@ -746,10 +835,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
@ -783,44 +876,25 @@ class FlutterPlugin implements Plugin<Project> {
return gradleProject.android.compileSdkVersion.substring(8)
}
/**
* Returns `true` if the given path contains an `android` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
private Boolean doesSupportAndroidPlatform(String path) {
File buildGradle = new File(path, 'android' + File.separator + 'build.gradle')
File buildGradleKts = new File(path, 'android' + File.separator + 'build.gradle.kts')
if (buildGradle.exists() && buildGradleKts.exists()) {
logger.error(
"Both build.gradle and build.gradle.kts exist, so " +
"build.gradle.kts is ignored. This is likely a mistake."
)
}
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 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.
@ -832,52 +906,34 @@ class FlutterPlugin implements Plugin<Project> {
}
}
private Properties getPluginList() {
File pluginsFile = new File(getFlutterSourceDirectory(), '.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
/**
* 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) {
if (pluginList == null) {
pluginList = project.ext.nativePluginLoader.getPlugins(getFlutterSourceDirectory())
}
return androidPlugins
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 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(getFlutterSourceDirectory(), '.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) {
if (pluginDependencies == null) {
Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory())
if (meta == null) {
pluginDependencies = []
} else {
assert(meta.dependencyGraph instanceof List<Map>)
pluginDependencies = meta.dependencyGraph as List<Map<String, Object>>
}
}
return []
return pluginDependencies
}
private String resolveProperty(String name, String defaultValue) {
@ -1317,7 +1373,7 @@ class FlutterPlugin implements Plugin<Project> {
String nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
project.android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
}
configurePlugins()
configurePlugins(project)
detectLowCompileSdkVersionOrNdkVersion()
return
}
@ -1369,7 +1425,7 @@ class FlutterPlugin implements Plugin<Project> {
}
}
}
configurePlugins()
configurePlugins(project)
detectLowCompileSdkVersionOrNdkVersion()
}

View file

@ -0,0 +1,119 @@
// 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
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 = new NativePluginLoader()
}

View file

@ -0,0 +1,115 @@
// 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/file.dart';
import 'package:flutter_tools/src/base/io.dart';
import '../src/common.dart';
import 'test_utils.dart';
void main() {
late Directory tempDir;
setUp(() async {
tempDir = createResolvedTempDirectorySync('gradle_daemon_test.');
});
tearDown(() async {
tryToDelete(tempDir);
});
testWithoutContext(
'gradle task succeeds when adding plugins with gradle daemon enabled',
() async {
final String flutterBin =
fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
final Directory appDir = tempDir.childDirectory('testapp');
final Directory androidDir = appDir.childDirectory('android');
// Create dummy plugins
processManager.runSync(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--template=plugin',
'--platforms=android',
'test_plugin_one',
], workingDirectory: tempDir.path);
processManager.runSync(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--template=plugin',
'--platforms=android',
'test_plugin_two',
], workingDirectory: tempDir.path);
// Create a new flutter project.
ProcessResult result = await processManager.run(<String>[
flutterBin,
'create',
appDir.path,
'--project-name=testapp',
], workingDirectory: tempDir.path);
expect(result, const ProcessResultMatcher());
// Enable gradle daemon for this project
final File gradleProperties = androidDir.childFile('gradle.properties');
gradleProperties.writeAsStringSync(r'''
org.gradle.daemon=true
''', mode: FileMode.append);
// TODO(gustl22): Override with in 'gradle.properties' has no effect, set GRADLE_OPTS instead,
// see https://github.com/gradle/gradle/issues/19501
final Map<String, String> envVars = <String, String>{
'GRADLE_OPTS': '-Dorg.gradle.daemon=true'
};
// Stop gradle daemon
result = await processManager.run(<String>[
androidDir.childFile('gradlew').path,
'--stop',
], workingDirectory: androidDir.path);
expect(result, const ProcessResultMatcher());
result = await processManager.run(<String>[
flutterBin,
'pub',
'add',
'test_plugin_one',
'--path',
'../test_plugin_one',
], workingDirectory: appDir.path, environment: envVars);
expect(result, const ProcessResultMatcher());
// Build with gradle daemon
result = await processManager.run(<String>[
flutterBin,
'build',
'apk',
'--debug',
], workingDirectory: appDir.path, environment: envVars);
expect(result, const ProcessResultMatcher());
// Add second plugin
result = await processManager.run(<String>[
flutterBin,
'pub',
'add',
'test_plugin_two',
'--path',
'../test_plugin_two',
], workingDirectory: appDir.path, environment: envVars);
expect(result, const ProcessResultMatcher());
// Build again with cached plugin through daemon
result = await processManager.run(<String>[
flutterBin,
'build',
'apk',
'--debug',
], workingDirectory: appDir.path, environment: envVars);
expect(result, const ProcessResultMatcher());
});
}

View file

@ -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;
}

View file

@ -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) }
}
''';
}

View file

@ -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>
''';
}