diff --git a/packages/flutter_tools/gradle/module_plugin_loader.gradle b/packages/flutter_tools/gradle/module_plugin_loader.gradle index 2e3a800914..6e52b5ae9b 100644 --- a/packages/flutter_tools/gradle/module_plugin_loader.gradle +++ b/packages/flutter_tools/gradle/module_plugin_loader.gradle @@ -5,41 +5,26 @@ // This file is included from `/.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> 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()) { diff --git a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy index 402ab64e62..16965213df 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy @@ -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 { - // 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> 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 diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index ebf43196a5..de00c72912 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -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 { private Properties localProperties private String engineVersion private String engineRealm + private List> pluginList + private List> pluginDependencies /** * Flutter Docs Website URLs for help messages. @@ -258,6 +259,9 @@ class FlutterPlugin implements Plugin { } } + // 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 { // 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 { * 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> deps = getPluginDependencies(project) + List 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 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 { 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 { 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 { 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 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) + 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 { } } - 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). + * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy + */ + private List> 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> getPluginDependencies(Project project) { + if (pluginDependencies == null) { + Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory()) + if (meta == null) { + pluginDependencies = [] + } else { + assert(meta.dependencyGraph instanceof List) + pluginDependencies = meta.dependencyGraph as List> + } } - return [] + return pluginDependencies } private String resolveProperty(String name, String defaultValue) { @@ -1317,7 +1373,7 @@ class FlutterPlugin implements Plugin { 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 { } } } - configurePlugins() + configurePlugins(project) detectLowCompileSdkVersionOrNdkVersion() } diff --git a/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy b/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy new file mode 100644 index 0000000000..1ca726ac60 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy @@ -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` or a `boolean`. + */ + List> getPlugins(File flutterSourceDirectory) { + List> nativePlugins = [] + def meta = getDependenciesMetadata(flutterSourceDirectory) + if (meta == null) { + return nativePlugins + } + + assert(meta.plugins instanceof Map) + def androidPlugins = meta.plugins.android + assert(androidPlugins instanceof List) + // Includes the Flutter plugins that support the Android platform. + androidPlugins.each { Map 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) + // 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 parsedFlutterPluginsDependencies + + /** + * Parses /.flutter-plugins-dependencies + */ + Map 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) + parsedFlutterPluginsDependencies = object + return object + } + return null + } +} + +// TODO(135392): Remove and use declarative form when migrated +ext { + nativePluginLoader = new NativePluginLoader() +} diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_daemon_cache_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_daemon_cache_test.dart new file mode 100644 index 0000000000..b9b3af1db5 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_gradle_daemon_cache_test.dart @@ -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([ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--template=plugin', + '--platforms=android', + 'test_plugin_one', + ], workingDirectory: tempDir.path); + processManager.runSync([ + flutterBin, + ...getLocalEngineArguments(), + 'create', + '--template=plugin', + '--platforms=android', + 'test_plugin_two', + ], workingDirectory: tempDir.path); + + // Create a new flutter project. + ProcessResult result = await processManager.run([ + 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 envVars = { + 'GRADLE_OPTS': '-Dorg.gradle.daemon=true' + }; + + // Stop gradle daemon + result = await processManager.run([ + androidDir.childFile('gradlew').path, + '--stop', + ], workingDirectory: androidDir.path); + expect(result, const ProcessResultMatcher()); + + result = await processManager.run([ + 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([ + flutterBin, + 'build', + 'apk', + '--debug', + ], workingDirectory: appDir.path, environment: envVars); + expect(result, const ProcessResultMatcher()); + + // Add second plugin + result = await processManager.run([ + 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([ + flutterBin, + 'build', + 'apk', + '--debug', + ], workingDirectory: appDir.path, environment: envVars); + expect(result, const ProcessResultMatcher()); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart new file mode 100644 index 0000000000..c52c0b6ee3 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart @@ -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 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([ + 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([ + 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; +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart b/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart new file mode 100644 index 0000000000..7747bed53d --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart @@ -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) } +} + '''; +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart b/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart new file mode 100644 index 0000000000..2714ca627e --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart @@ -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''' + + + + + + + + + + + + +'''; +}