From e971379436d426324f4a02fe2b1bfdb24a261764 Mon Sep 17 00:00:00 2001 From: Gray Mackall <34871572+gmackall@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:29:13 -0700 Subject: [PATCH] Convert `AppPluginLoaderPlugin` to Kotlin, and add `NativePluginLoaderReflectionBridge` to expose it in Kotlin (#166027) Graph [stolen from Barteks comment](https://github.com/flutter/flutter/pull/161352#issuecomment-2611252732) documenting the existing (pre pr) state: ```mermaid graph TD; flutter.groovy -- import --> native_plugin_loader.groovy; flutter.groovy -- import --> BaseApplicationNameHandler.kt; module_plugin_loader.groovy -- "ext" --> native_plugin_loader.groovy; app_plugin_loader.groovy -- import --> native_plugin_loader.groovy; include_flutter.groovy -- "apply from: " --> module_plugin_loader.groovy; ``` 1. Converts the `app_plugin_loader.groovy` to kotlin source. 2. Converts the `module_plugin_loader.groovy` to kotlin script. This can't be changed to kotlin source yet, as we will need to instruct users to make a change to their host app-level gradle files before we can turn down script application of this separate gradle plugin. This is a breaking change, and will need a quarter at least of notice. 3. Unfortunately, the main Flutter Gradle plugin depends on being able to call methods of the `native_plugin_loader`, which we could do in groovy via wacky dynamic behavior, calling across the compiled plugin->script plugin barrier. We can't do this in Kotlin source, and we also can't fully convert `native_plugin_loader` to kotlin source yet because of (2), so I've added a `NativePluginLoaderReflectionBridge` that allows us to access the methods in the `native_plugin_loader.gradle.kts` from the Kotlin source files, calling across the compiled plugin->script plugin barrier as we were before. The plan here is 1. to follow up by adding a converted `native_plugin_loader.gradle.kts` in Kotlin source, and migrating all paths but the host-app using a module-as-source to use the converted approach (but not deleting the old way) 2. maintaining both ways for a release or two, with the script application printing a message notifying users to update to the non-script based application of the `module_plugin_loader`. 3. Then we can delete the script based apply, and also the reflection bridge. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Gray Mackall --- .../flutter_tools/gradle/build.gradle.kts | 2 +- .../gradle/module_plugin_loader.gradle | 2 +- .../src/main/groovy/app_plugin_loader.groovy | 32 ---- .../gradle/src/main/groovy/flutter.groovy | 11 +- .../main/groovy/native_plugin_loader.groovy | 137 ------------------ .../main/kotlin/BaseApplicationNameHandler.kt | 4 + .../src/main/kotlin/BaseFlutterTaskHelper.kt | 4 + .../main/kotlin/DependencyVersionChecker.kt | 4 + .../kotlin/FlutterAppPluginLoaderPlugin.kt | 66 +++++++++ .../src/main/kotlin/FlutterExtension.kt | 4 + .../src/main/kotlin/FlutterPluginConstants.kt | 4 + .../src/main/kotlin/FlutterPluginUtils.kt | 6 +- .../gradle/src/main/kotlin/FlutterTask.kt | 4 + .../src/main/kotlin/FlutterTaskHelper.kt | 4 + .../NativePluginLoaderReflectionBridge.kt | 61 ++++++++ .../scripts/native_plugin_loader.gradle.kts | 95 ++++++++++++ 16 files changed, 263 insertions(+), 177 deletions(-) delete mode 100644 packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy delete mode 100644 packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt create mode 100644 packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index b0991a9ae0..b3863fd450 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -39,7 +39,7 @@ gradlePlugin { // The "flutterAppPluginLoaderPlugin" name isn't used anywhere. create("flutterAppPluginLoaderPlugin") { id = "dev.flutter.flutter-plugin-loader" - implementationClass = "FlutterAppPluginLoaderPlugin" + implementationClass = "com.flutter.gradle.FlutterAppPluginLoaderPlugin" } } } diff --git a/packages/flutter_tools/gradle/module_plugin_loader.gradle b/packages/flutter_tools/gradle/module_plugin_loader.gradle index 98ffc28ba7..3f61d44926 100644 --- a/packages/flutter_tools/gradle/module_plugin_loader.gradle +++ b/packages/flutter_tools/gradle/module_plugin_loader.gradle @@ -8,7 +8,7 @@ import java.nio.file.Paths File pathToThisDirectory = buildscript.sourceFile.parentFile -apply from: Paths.get(pathToThisDirectory.absolutePath, "src", "main", "groovy", "native_plugin_loader.groovy") +apply from: Paths.get(pathToThisDirectory.absolutePath, "src", "main", "scripts", "native_plugin_loader.gradle.kts") def moduleProjectRoot = project(':flutter').projectDir.parentFile.parentFile 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 deleted file mode 100644 index 0a03e26999..0000000000 --- a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy +++ /dev/null @@ -1,32 +0,0 @@ -import org.gradle.api.Plugin -import org.gradle.api.initialization.Settings - -import java.nio.file.Paths - -apply plugin: FlutterAppPluginLoaderPlugin - -class FlutterAppPluginLoaderPlugin implements Plugin { - @Override - void apply(Settings settings) { - def flutterProjectRoot = settings.settingsDir.parentFile - - 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") - - 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 b988b58e3e..24fe304665 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -15,6 +15,7 @@ import com.flutter.gradle.FlutterExtension import com.flutter.gradle.FlutterPluginConstants import com.flutter.gradle.FlutterTask import com.flutter.gradle.FlutterPluginUtils +import com.flutter.gradle.NativePluginLoaderReflectionBridge import org.gradle.api.file.Directory import java.nio.file.Paths @@ -114,7 +115,7 @@ 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") + project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "scripts", "native_plugin_loader.gradle.kts") FlutterExtension extension = project.extensions.create("flutter", FlutterExtension) Properties localProperties = new Properties() @@ -298,7 +299,7 @@ class FlutterPlugin implements Plugin { * and filtered then with the [doesSupportAndroidPlatform] method instead of * just using the `plugins.android` list. */ - private void configureLegacyPluginEachProjects(Project project) { + static private void configureLegacyPluginEachProjects(Project project) { try { // Read the contents of the settings.gradle file. // Remove block/line comments @@ -340,11 +341,11 @@ class FlutterPlugin implements Plugin { * * 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 + * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts */ private List> getPluginList(Project project) { if (pluginList == null) { - pluginList = project.ext.nativePluginLoader.getPlugins(FlutterPluginUtils.getFlutterSourceDirectory(project)) + pluginList = NativePluginLoaderReflectionBridge.getPlugins(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project)) } return pluginList } @@ -354,7 +355,7 @@ class FlutterPlugin implements Plugin { /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */ private List> getPluginDependencies(Project project) { if (pluginDependencies == null) { - Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(FlutterPluginUtils.getFlutterSourceDirectory(project)) + Map meta = NativePluginLoaderReflectionBridge.getDependenciesMetadata(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project)) if (meta == null) { pluginDependencies = [] } else { 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 deleted file mode 100644 index 177fb87bbf..0000000000 --- a/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy +++ /dev/null @@ -1,137 +0,0 @@ -// 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 - * "dev_dependency": false - * } - * - * 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) - assert(androidPlugin.dev_dependency instanceof Boolean) - // 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 - // "dev_dependency": false - // }, - // { - // "name": "plugin-b", - // "path": "/path/to/plugin-b", - // "dependencies": ["plugin-c"], - // "native_build": true - // "dev_dependency": false - // }, - // { - // "name": "plugin-c", - // "path": "/path/to/plugin-c", - // "dependencies": [], - // "native_build": true - // "dev_dependency": false - // }, - // { - // "name": "plugin-d", - // "path": "/path/to/plugin-d", - // "dependencies": [], - // "native_build": true - // "dev_dependency": true - // }, - // ], - // }, - // "dependencyGraph": [ - // { - // "name": "plugin-a", - // "dependencies": ["plugin-b","plugin-c"] - // }, - // { - // "name": "plugin-b", - // "dependencies": ["plugin-c"] - // }, - // { - // "name": "plugin-c", - // "dependencies": [] - // }, - // { - // "name": "plugin-d", - // "dependencies": [] - // } - // ] - // } - // This means, `plugin-a` depends on `plugin-b` and `plugin-c`. - // `plugin-b` depends on `plugin-c`. - // `plugin-c` doesn't depend on anything. - // `plugin-d` also doesn't depend on anything, but it is a dev - // dependency to the Flutter project, so it is marked as such. - if (parsedFlutterPluginsDependencies) { - 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/gradle/src/main/kotlin/BaseApplicationNameHandler.kt b/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt index 60c94aa72f..ae70f5917a 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import com.android.build.api.dsl.ApplicationExtension diff --git a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt index bc90be4a6d..fa47257a0e 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import androidx.annotation.VisibleForTesting diff --git a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt index 79821f1d74..92b516ba75 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import androidx.annotation.VisibleForTesting diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt new file mode 100644 index 0000000000..2ebfb9ee89 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt @@ -0,0 +1,66 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package com.flutter.gradle + +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import java.io.File +import java.nio.file.Paths +import java.util.Properties + +private const val FLUTTER_SDK_PATH = "flutterSdkPath" + +// Integration tests that cover this class include +// - packages/flutter_tools/test/integration.shard/android_gradle_daemon_cache_test.dart +// - packages/flutter_tools/test/integration.shard/android_plugin_compilesdkversion_mismatch_test.dart +// And can be run by following the README in packages/flutter_tools/. + +/** + * This plugin applies the native plugin loader plugin (../scripts/native_plugin_loader.gradle.kts) + * and then configures the main project to `include` each of the loaded flutter plugins. + */ +class FlutterAppPluginLoaderPlugin : Plugin { + override fun apply(settings: Settings) { + val flutterProjectRoot: File = settings.settingsDir.parentFile + + if (!settings.extraProperties.has(FLUTTER_SDK_PATH)) { + val properties = Properties() + val localPropertiesFile = File(settings.rootProject.projectDir, "local.properties") + localPropertiesFile.inputStream().use { properties.load(it) } + settings.extraProperties.set(FLUTTER_SDK_PATH, properties.getProperty("flutter.sdk")) + assert( + settings.extraProperties.get(FLUTTER_SDK_PATH) != null + ) { "flutter.sdk not set in local.properties" } + } + + settings.apply { + from( + Paths.get( + settings.extraProperties.get(FLUTTER_SDK_PATH) as String, + "packages", + "flutter_tools", + "gradle", + "src", + "main", + "scripts", + "native_plugin_loader.gradle.kts" + ) + ) + } + + NativePluginLoaderReflectionBridge + .getPlugins(settings.extraProperties, flutterProjectRoot) + .forEach { androidPlugin -> + val pluginDirectory = File(androidPlugin["path"] as String, "android") + check( + pluginDirectory.exists() + ) { "Plugin directory does not exist: ${pluginDirectory.absolutePath}" } + val pluginName = androidPlugin["name"] as String + settings.include(":$pluginName") + settings.project(":$pluginName").projectDir = pluginDirectory + } + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt index ab41e607d1..7f41f08ad2 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import org.gradle.api.GradleException diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt index 2c21aada82..d4fe426e26 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle // TODO(gmackall): this should be collapsed back into the core FlutterPlugin once the Groovy to diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index bf59732f68..1dcfb3b4fc 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import com.android.build.gradle.AbstractAppExtension @@ -704,7 +708,7 @@ object FlutterPluginUtils { * * 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 + * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts */ private fun getPluginListWithoutDevDependencies(pluginList: List>): List> = pluginList.filter { pluginObject -> pluginObject["dev_dependency"] == false } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt index e8f8a899dd..3ff14a5247 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import org.gradle.api.file.CopySpec diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt index 2a449ed79f..a99a9ea811 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package com.flutter.gradle import org.gradle.api.Project diff --git a/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt new file mode 100644 index 0000000000..df90089516 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt @@ -0,0 +1,61 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package com.flutter.gradle + +import org.gradle.api.plugins.ExtraPropertiesExtension +import java.io.File + +// TODO(gmackall): Remove reflection after migrating to plugin style application in +// https://github.com/flutter/flutter/issues/166461. +// New methods should not be added. + +/** + * Class to hide from Kotlin source the dangerous reflection being used to call methods defined + * in script gradle plugins. + */ + +object NativePluginLoaderReflectionBridge { + private var nativePluginLoader: Any? = null + + /** + * An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts. + */ + @JvmStatic + fun getPlugins( + extraProperties: ExtraPropertiesExtension, + flutterProjectRoot: File + ): List> { + nativePluginLoader = extraProperties.get("nativePluginLoader")!! + + @Suppress("UNCHECKED_CAST") + val pluginList: List> = + nativePluginLoader!!::class + .members + .firstOrNull { it.name == "getPlugins" } + ?.call(nativePluginLoader, flutterProjectRoot) as List> + + return pluginList + } + + /** + * An abstraction to hide reflection from calling sites. See ../scripts/native_plugin_loader.gradle.kts. + */ + @JvmStatic + fun getDependenciesMetadata( + extraProperties: ExtraPropertiesExtension, + flutterProjectRoot: File + ): Map { + nativePluginLoader = extraProperties.get("nativePluginLoader")!! + + @Suppress("UNCHECKED_CAST") + val dependenciesMetadata: Map = + nativePluginLoader!!::class + .members + .firstOrNull { it.name == "dependenciesMetadata" } + ?.call(nativePluginLoader, flutterProjectRoot) as Map + + return dependenciesMetadata + } +} diff --git a/packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts b/packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts new file mode 100644 index 0000000000..38fc5889bc --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts @@ -0,0 +1,95 @@ +// 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 +import java.io.File + +// When changing the names of either +// 1. this file or +// 2. the names of the methods on this class +// be sure to also modify the corresponding values in ../kotlin/NativePluginLoaderReflectionBridge.kt +class NativePluginLoader { + companion object { + // This string must match _kFlutterPluginsHasNativeBuildKey defined in + // packages/flutter_tools/lib/src/flutter_plugins.dart. + const val NATIVE_BUILD_KEY = "native_build" + const val FLUTTER_PLUGINS_DEPENDENCIES_FILE = ".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 + * "dev_dependency": false + * } + * + * Therefore the map value can either be a `String`, a `List` or a `Boolean`. + */ + fun getPlugins(flutterSourceDirectory: File): List> { + val nativePlugins = mutableListOf>() + val meta = getDependenciesMetadata(flutterSourceDirectory) + if (meta == null) { + return nativePlugins + } + + val pluginsMap: Map<*, *> = (meta["plugins"] as? Map<*, *>) ?: error("Metadata 'plugins' is not a Map: $meta") + val androidPluginsUntyped = pluginsMap["android"] + if (androidPluginsUntyped == null) { + return nativePlugins // Return empty list if android plugins are not found + } + val androidPlugins = androidPluginsUntyped as? List<*> ?: error("Metadata 'plugins.android' is not a List: $meta") + + // Includes the Flutter plugins that support the Android platform. + androidPlugins.forEach { androidPluginUntyped -> + val androidPlugin = androidPluginUntyped as? Map<*, *> ?: error("androidPlugin is not a Map: $androidPluginUntyped") + + // The property types can be found in _filterPluginsByPlatform defined in + // packages/flutter_tools/lib/src/flutter_plugins.dart. + check(androidPlugin["name"] is String) { "androidPlugin 'name' is not a String: $androidPlugin" } + check(androidPlugin["path"] is String) { "androidPlugin 'path' is not a String: $androidPlugin" } + check(androidPlugin["dependencies"] is List<*>) { "androidPlugin 'dependencies' is not a List: $androidPlugin" } + check(androidPlugin["dev_dependency"] is Boolean) { "androidPlugin 'dev_dependency' is not a Boolean: $androidPlugin" } + + // Skip plugins that have no native build (such as a Dart-only implementation + // of a federated plugin). + val needsBuild = androidPlugin[NATIVE_BUILD_KEY] as? Boolean ?: true + if (needsBuild) { + nativePlugins.add(androidPlugin as Map) // Safe cast when adding, assuming type is now validated + } + } + return nativePlugins.toList() // Return immutable list + } + + private var parsedFlutterPluginsDependencies: Map? = null + + /** + * Parses /.flutter-plugins-dependencies + */ + fun getDependenciesMetadata(flutterSourceDirectory: File): Map? { + // Consider a `.flutter-plugins-dependencies` file with the following content: + // { ... (example content as in the original Groovy code) ... } + // This means, `plugin-a` depends on `plugin-b` and `plugin-c`. + // ... (rest of the comment as in the original Groovy code) ... + if (parsedFlutterPluginsDependencies != null) { + return parsedFlutterPluginsDependencies + } + val pluginsDependencyFile = File(flutterSourceDirectory, FLUTTER_PLUGINS_DEPENDENCIES_FILE) + if (pluginsDependencyFile.exists()) { + val slurper = JsonSlurper() + val readText = slurper.parseText(pluginsDependencyFile.readText()) + val parsedText = + readText as? Map + ?: error("Parsed JSON is not a Map: $readText") + parsedFlutterPluginsDependencies = parsedText + return parsedText + } + return null + } +} + +extra["nativePluginLoader"] = NativePluginLoader()