From 56e11aed713984b94b277d0bccd4711ee027ee63 Mon Sep 17 00:00:00 2001 From: Gray Mackall <34871572+gmackall@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:36:26 -0700 Subject: [PATCH] [reland] Convert the Flutter Gradle Plugin entirely to Kotlin source (#166676) Relands https://github.com/flutter/flutter/pull/166114. The original PR failed this postsubmit https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8718287794116896097/+/u/run_engine_dependency_proxy_test/stdout Because ``` Task result: { "success": false, "reason": "Task failed: Expected Android engine maven dependency URL to resolve to https://storage.googleapis.com/download.flutter.io. Got https://storage.googleapis.com//download.flutter.io instead" } ``` which was because apparently in Groovy ```groovy String foo = "" if (foo) { // branch } ``` Evaluates to false (i.e. does not take the branch). So we need to check if the `engineRealm` string is empty, which is what the additional commit does. ## 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 `///`). - [x] 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/src/main/groovy/flutter.groovy | 722 ----------------- .../gradle/src/main/kotlin/FlutterPlugin.kt | 743 ++++++++++++++++++ .../src/main/kotlin/FlutterPluginConstants.kt | 3 +- .../src/main/kotlin/FlutterPluginUtils.kt | 163 +--- .../NativePluginLoaderReflectionBridge.kt | 18 +- .../src/main/kotlin/plugins/PluginHandler.kt | 326 ++++++++ .../kotlin/{ => tasks}/BaseFlutterTask.kt | 2 +- .../{ => tasks}/BaseFlutterTaskHelper.kt | 2 +- .../main/kotlin/{ => tasks}/FlutterTask.kt | 2 +- .../kotlin/{ => tasks}/FlutterTaskHelper.kt | 3 +- .../main/{groovy => scripts}/CMakeLists.txt | 0 .../scripts/native_plugin_loader.gradle.kts | 58 +- .../kotlin/BaseApplicationNameHandlerTest.kt | 5 + .../gradle/src/test/kotlin/DeeplinkTest.kt | 4 + .../kotlin/DependencyVersionCheckerTest.kt | 4 + .../src/test/kotlin/FlutterExtensionTest.kt | 4 + .../src/test/kotlin/FlutterPluginTest.kt | 78 ++ .../src/test/kotlin/FlutterPluginUtilsTest.kt | 260 +----- .../src/test/kotlin/IntentFilterCheckTest.kt | 4 + .../src/test/kotlin/VersionUtilsTest.kt | 4 + .../test/kotlin/plugins/PluginHandlerTest.kt | 443 +++++++++++ .../{ => tasks}/BaseFlutterTaskHelperTest.kt | 7 +- .../{ => tasks}/FlutterTaskHelperTest.kt | 7 +- .../flutter_tools/lib/src/android/gradle.dart | 2 +- .../flutter_tools/lib/src/build_info.dart | 2 +- 26 files changed, 1747 insertions(+), 1121 deletions(-) delete mode 100644 packages/flutter_tools/gradle/src/main/groovy/flutter.groovy create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt rename packages/flutter_tools/gradle/src/main/kotlin/{ => tasks}/BaseFlutterTask.kt (99%) rename packages/flutter_tools/gradle/src/main/kotlin/{ => tasks}/BaseFlutterTaskHelper.kt (99%) rename packages/flutter_tools/gradle/src/main/kotlin/{ => tasks}/FlutterTask.kt (98%) rename packages/flutter_tools/gradle/src/main/kotlin/{ => tasks}/FlutterTaskHelper.kt (97%) rename packages/flutter_tools/gradle/src/main/{groovy => scripts}/CMakeLists.txt (100%) create mode 100644 packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt create mode 100644 packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt rename packages/flutter_tools/gradle/src/test/kotlin/{ => tasks}/BaseFlutterTaskHelperTest.kt (99%) rename packages/flutter_tools/gradle/src/test/kotlin/{ => tasks}/FlutterTaskHelperTest.kt (96%) diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index b3863fd450..0797f58275 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -34,7 +34,7 @@ gradlePlugin { // The "flutterPlugin" name isn't used anywhere. create("flutterPlugin") { id = "dev.flutter.flutter-gradle-plugin" - implementationClass = "FlutterPlugin" + implementationClass = "com.flutter.gradle.FlutterPlugin" } // The "flutterAppPluginLoaderPlugin" name isn't used anywhere. create("flutterAppPluginLoaderPlugin") { diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy deleted file mode 100644 index 24fe304665..0000000000 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ /dev/null @@ -1,722 +0,0 @@ -/* groovylint-disable LineLength, UnnecessaryGString, UnnecessaryGetter */ -// 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 com.android.build.OutputFile -import com.android.build.gradle.AbstractAppExtension -import com.android.build.gradle.api.BaseVariantOutput -import com.android.build.gradle.tasks.PackageAndroidArtifact -import com.android.build.gradle.tasks.ProcessAndroidResources -import com.android.builder.model.BuildType -import com.flutter.gradle.BaseApplicationNameHandler -import com.flutter.gradle.DependencyVersionChecker -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 -import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.api.GradleException -import org.gradle.api.JavaVersion -import org.gradle.api.Project -import org.gradle.api.Plugin -import org.gradle.api.Task -import org.gradle.api.UnknownTaskException -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.bundling.Jar -import org.gradle.internal.os.OperatingSystem - - -class FlutterPlugin implements Plugin { - - private final static String propLocalEngineRepo = "local-engine-repo" - - /** - * The name prefix for flutter builds. This is used to identify gradle tasks - * where we expect the flutter tool to provide any error output, and skip the - * standard Gradle error output in the FlutterEventLogger. If you change this, - * be sure to change any instances of this string in symbols in the code below - * to match. - */ - static final String FLUTTER_BUILD_PREFIX = "flutterBuild" - - private Project project - private File flutterRoot - private File flutterExecutable - private String localEngine - private String localEngineHost - private String localEngineSrcPath - private Properties localProperties - private String engineVersion - private String engineRealm - private List> pluginList - private List> pluginDependencies - - /** - * Flutter Docs Website URLs for help messages. - */ - private final String kWebsiteDeploymentAndroidBuildConfig = "https://flutter.dev/to/review-gradle-config" - - @Override - void apply(Project project) { - this.project = project - - Project rootProject = project.rootProject - if (FlutterPluginUtils.isFlutterAppProject(project)) { - rootProject.tasks.register("generateLockfiles") { - doLast { - rootProject.subprojects.each { subproject -> - String gradlew = (OperatingSystem.current().isWindows()) ? - "${rootProject.projectDir}/gradlew.bat" : "${rootProject.projectDir}/gradlew" - rootProject.exec { - workingDir(rootProject.projectDir) - executable(gradlew) - args(":${subproject.name}:dependencies", "--write-locks") - } - } - } - } - } - - String flutterRootPath = resolveProperty("flutter.sdk", System.getenv("FLUTTER_ROOT")) - if (flutterRootPath == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.") - } - flutterRoot = project.file(flutterRootPath) - if (!flutterRoot.isDirectory()) { - throw new GradleException("flutter.sdk must point to the Flutter SDK directory") - } - - engineVersion = FlutterPluginUtils.shouldProjectUseLocalEngine(project) - ? "+" // Match any version since there's only one. - : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "cache", "engine.stamp").toFile().text.trim() - - engineRealm = Paths.get(flutterRoot.absolutePath, "bin", "cache", "engine.realm").toFile().text.trim() - if (engineRealm) { - engineRealm += "/" - } - - // Configure the Maven repository. - String hostedRepository = System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL) ?: FlutterPluginConstants.DEFAULT_MAVEN_HOST - String repository = FlutterPluginUtils.shouldProjectUseLocalEngine(project) - ? project.property(propLocalEngineRepo) - : "$hostedRepository/${engineRealm}download.flutter.io" - rootProject.allprojects { - repositories { - maven { - url(repository) - } - } - } - - // Load shared gradle functions - 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() - File localPropertiesFile = rootProject.file("local.properties") - if (localPropertiesFile.exists()) { - localPropertiesFile.withReader("UTF-8") { reader -> - localProperties.load(reader) - } - } - - extension.flutterVersionCode = localProperties.getProperty("flutter.versionCode", "1") - extension.flutterVersionName = localProperties.getProperty("flutter.versionName", "1.0") - - this.addFlutterTasks(project) - FlutterPluginUtils.forceNdkDownload(project, flutterRootPath) - - // By default, assembling APKs generates fat APKs if multiple platforms are passed. - // Configuring split per ABI allows to generate separate APKs for each abi. - // This is a noop when building a bundle. - if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { - project.android { - splits { - abi { - // Enables building multiple APKs per ABI. - enable(true) - // Resets the list of ABIs that Gradle should create APKs for to none. - reset() - // Specifies that we do not want to also generate a universal APK that includes all ABIs. - universalApk(false) - } - } - } - } - final String propDeferredComponentNames = "deferred-component-names" - if (project.hasProperty(propDeferredComponentNames)) { - String[] componentNames = project.property(propDeferredComponentNames).split(",").collect {":${it}"} - project.android { - dynamicFeatures = componentNames - } - } - - FlutterPluginUtils.getTargetPlatforms(project).each { targetArch -> - String abiValue = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch] - project.android { - if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { - splits { - abi { - include(abiValue) - } - } - } - } - } - - String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter" - flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile() - - // Validate that the provided Gradle, Java, AGP, and KGP versions are all within our - // supported range. - // TODO(gmackall) Dependency version checking is currently implemented as an additional - // Gradle plugin because we can't import it from Groovy code. As part of the Groovy - // -> Kotlin migration, we should remove this complexity and perform the checks inside - // of the main Flutter Gradle Plugin. - // See https://github.com/flutter/flutter/issues/121541#issuecomment-1920363687. - final Boolean shouldSkipDependencyChecks = project.hasProperty("skipDependencyChecks") && project.getProperty("skipDependencyChecks") - if (!shouldSkipDependencyChecks) { - try { - DependencyVersionChecker.checkDependencyVersions(project) - } catch (Exception e) { - if (!project.hasProperty("usesUnsupportedDependencyVersions") || !project.usesUnsupportedDependencyVersions) { - // Possible bug in dependency checking code - warn and do not block build. - project.logger.error("Warning: Flutter was unable to detect project Gradle, Java, " + - "AGP, and KGP versions. Skipping dependency version checking. Error was: " - + e) - } - else { - // If usesUnsupportedDependencyVersions is set, the exception was thrown by us - // in the dependency version checker plugin so re-throw it here. - throw e - } - } - } - - // Use Kotlin source to handle baseApplicationName logic due to Groovy dynamic dispatch bug. - BaseApplicationNameHandler.setBaseName(project) - - String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", - "gradle", "flutter_proguard_rules.pro") - project.android.buildTypes { - // Add profile build type. - profile { - initWith(debug) - if (it.hasProperty("matchingFallbacks")) { - matchingFallbacks = ["debug", "release"] - } - } - // TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet. - // This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove - // this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see - // increased app size due to this. - if (FlutterPluginUtils.shouldShrinkResources(project)) { - release { - // Enables code shrinking, obfuscation, and optimization for only - // your project's release build type. - minifyEnabled(true) - // Enables resource shrinking, which is performed by the Android Gradle plugin. - // The resource shrinker can't be used for libraries. - shrinkResources(FlutterPluginUtils.isBuiltAsApp(project)) - // Fallback to `android/app/proguard-rules.pro`. - // This way, custom Proguard rules can be configured as needed. - proguardFiles(project.android.getDefaultProguardFile("proguard-android-optimize.txt"), flutterProguardRules, "proguard-rules.pro") - } - } - } - - if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) { - // This is required to pass the local engine to flutter build aot. - String engineOutPath = project.property("local-engine-out") - File engineOut = project.file(engineOutPath) - if (!engineOut.isDirectory()) { - throw new GradleException("local-engine-out must point to a local engine build") - } - localEngine = engineOut.name - localEngineSrcPath = engineOut.parentFile.parent - - String engineHostOutPath = project.property("local-engine-host-out") - File engineHostOut = project.file(engineHostOutPath) - if (!engineHostOut.isDirectory()) { - throw new GradleException("local-engine-host-out must point to a local engine host build") - } - localEngineHost = engineHostOut.name - } - project.android.buildTypes.all(this.&addFlutterDependencies) - } - - /** - * Adds the dependencies required by the Flutter project. - * This includes: - * 1. The embedding - * 2. libflutter.so - */ - void addFlutterDependencies(BuildType buildType) { - FlutterPluginUtils.addFlutterDependencies(project, buildType, getPluginList(project), engineVersion) - } - - /** - * Configures the Flutter plugin dependencies. - * - * The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`, - * 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(Project project) { - configureLegacyPluginEachProjects(project) - getPluginList(project).each { Map plugin -> - FlutterPluginUtils.configurePluginProject(project, plugin, engineVersion) - } - getPluginList(project).each {Map plugin -> - FlutterPluginUtils.configurePluginDependencies(project, plugin) - } - } - - // 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. - */ - static private void configureLegacyPluginEachProjects(Project project) { - try { - // Read the contents of the settings.gradle file. - // Remove block/line comments - String settingsText = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).text - settingsText = settingsText.replaceAll(/(?s)\/\*.*?\*\//, '').replaceAll(/(?m)\/\/.*$/, '') - - if (!settingsText.contains("'.flutter-plugins'")) { - return - } - } catch (FileNotFoundException ignored) { - throw new GradleException("settings.gradle/settings.gradle.kts does not exist: " + - "${FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).absolutePath}") - } - // TODO(matanlurey): https://github.com/flutter/flutter/issues/48918. - project.logger.quiet("Warning: This project is still reading the deprecated '.flutter-plugins. file.") - project.logger.quiet("In an upcoming stable release support for this file will be completely removed and your build will fail.") - project.logger.quiet("See https:/flutter.dev/to/flutter-plugins-configuration.") - 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 (FlutterPluginUtils.pluginSupportsAndroidPlatform(pluginProject)) { - // 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. - FlutterPluginUtils.configurePluginProject(project, it, engineVersion) - /* groovylint-disable-next-line EmptyElseBlock */ - } else { - // Plugin has no or an empty `android` folder. No action required. - } - } - } - - /** - * 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/scripts/native_plugin_loader.gradle.kts - */ - private List> getPluginList(Project project) { - if (pluginList == null) { - pluginList = NativePluginLoaderReflectionBridge.getPlugins(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project)) - } - 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(Project project) { - if (pluginDependencies == null) { - Map meta = NativePluginLoaderReflectionBridge.getDependenciesMetadata(project.ext, FlutterPluginUtils.getFlutterSourceDirectory(project)) - if (meta == null) { - pluginDependencies = [] - } else { - assert(meta.dependencyGraph instanceof List) - pluginDependencies = meta.dependencyGraph as List> - } - } - return pluginDependencies - } - - private String resolveProperty(String name, String defaultValue) { - if (localProperties == null) { - localProperties = FlutterPluginUtils.readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties")) - } - return project.findProperty(name) ?: localProperties?.getProperty(name, defaultValue) - } - - private void addFlutterTasks(Project project) { - if (project.state.failure) { - return - } - String[] fileSystemRootsValue = null - final String propFileSystemRoots = "filesystem-roots" - if (project.hasProperty(propFileSystemRoots)) { - fileSystemRootsValue = project.property(propFileSystemRoots).split("\\|") - } - String fileSystemSchemeValue = null - final String propFileSystemScheme = "filesystem-scheme" - if (project.hasProperty(propFileSystemScheme)) { - fileSystemSchemeValue = project.property(propFileSystemScheme) - } - Boolean trackWidgetCreationValue = true - final String propTrackWidgetCreation = "track-widget-creation" - if (project.hasProperty(propTrackWidgetCreation)) { - trackWidgetCreationValue = project.property(propTrackWidgetCreation).toBoolean() - } - String frontendServerStarterPathValue = null - final String propFrontendServerStarterPath = "frontend-server-starter-path" - if (project.hasProperty(propFrontendServerStarterPath)) { - frontendServerStarterPathValue = project.property(propFrontendServerStarterPath) - } - String extraFrontEndOptionsValue = null - final String propExtraFrontEndOptions = "extra-front-end-options" - if (project.hasProperty(propExtraFrontEndOptions)) { - extraFrontEndOptionsValue = project.property(propExtraFrontEndOptions) - } - String extraGenSnapshotOptionsValue = null - final String propExtraGenSnapshotOptions = "extra-gen-snapshot-options" - if (project.hasProperty(propExtraGenSnapshotOptions)) { - extraGenSnapshotOptionsValue = project.property(propExtraGenSnapshotOptions) - } - String splitDebugInfoValue = null - final String propSplitDebugInfo = "split-debug-info" - if (project.hasProperty(propSplitDebugInfo)) { - splitDebugInfoValue = project.property(propSplitDebugInfo) - } - Boolean dartObfuscationValue = false - final String propDartObfuscation = "dart-obfuscation" - if (project.hasProperty(propDartObfuscation)) { - dartObfuscationValue = project.property(propDartObfuscation).toBoolean() - } - Boolean treeShakeIconsOptionsValue = false - final String propTreeShakeIcons = "tree-shake-icons" - if (project.hasProperty(propTreeShakeIcons)) { - treeShakeIconsOptionsValue = project.property(propTreeShakeIcons).toBoolean() - } - String dartDefinesValue = null - final String propDartDefines = "dart-defines" - if (project.hasProperty(propDartDefines)) { - dartDefinesValue = project.property(propDartDefines) - } - String performanceMeasurementFileValue - final String propPerformanceMeasurementFile = "performance-measurement-file" - if (project.hasProperty(propPerformanceMeasurementFile)) { - performanceMeasurementFileValue = project.property(propPerformanceMeasurementFile) - } - String codeSizeDirectoryValue - final String propCodeSizeDirectory = "code-size-directory" - if (project.hasProperty(propCodeSizeDirectory)) { - codeSizeDirectoryValue = project.property(propCodeSizeDirectory) - } - Boolean deferredComponentsValue = false - final String propDeferredComponents = "deferred-components" - if (project.hasProperty(propDeferredComponents)) { - deferredComponentsValue = project.property(propDeferredComponents).toBoolean() - } - Boolean validateDeferredComponentsValue = true - final String propValidateDeferredComponents = "validate-deferred-components" - if (project.hasProperty(propValidateDeferredComponents)) { - validateDeferredComponentsValue = project.property(propValidateDeferredComponents).toBoolean() - } - FlutterPluginUtils.addTaskForJavaVersion(project) - if (FlutterPluginUtils.isFlutterAppProject(project)) { - FlutterPluginUtils.addTaskForPrintBuildVariants(project) - FlutterPluginUtils.addTasksForOutputsAppLinkSettings(project) - } - List targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project) - def addFlutterDeps = { variant -> - if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { - variant.outputs.each { output -> - // Assigns the new version code to versionCodeOverride, which changes the version code - // for only the output APK, not for the variant itself. Skipping this step simply - // causes Gradle to use the value of variant.versionCode for the APK. - // For more, see https://developer.android.com/studio/build/configure-apk-splits - Integer abiVersionCode = FlutterPluginConstants.ABI_VERSION[output.getFilter(OutputFile.ABI)] - if (abiVersionCode != null) { - output.versionCodeOverride = - abiVersionCode * 1000 + variant.versionCode - } - } - } - // Build an AAR when this property is defined. - boolean isBuildingAar = project.hasProperty("is-plugin") - // In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project. - // `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR. - Task packageAssets - Task cleanPackageAssets - try { - packageAssets = project.tasks.named("package${variant.name.capitalize()}Assets").get() - } catch (UnknownTaskException ignored) { - packageAssets = null - } - try { - cleanPackageAssets = project.tasks.named("cleanPackage${variant.name.capitalize()}Assets").get() - } catch (UnknownTaskException ignored) { - cleanPackageAssets = null - } - boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar - - String variantBuildMode = FlutterPluginUtils.buildModeFor(variant.buildType) - String flavorValue = variant.getFlavorName() - String taskName = FlutterPluginUtils.toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) - // Be careful when configuring task below, Groovy has bizarre - // scoping rules: writing `verbose isVerbose()` means calling - // `isVerbose` on the task itself - which would return `verbose` - // original value. You either need to hoist the value - // into a separate variable `verbose verboseValue` or prefix with - // `this` (`verbose this.isVerbose()`). - TaskProvider compileTaskProvider = project.tasks.register(taskName , FlutterTask) { - flutterRoot(this.flutterRoot) - flutterExecutable(this.flutterExecutable) - buildMode(variantBuildMode) - minSdkVersion(variant.mergedFlavor.minSdkVersion.apiLevel) - localEngine(this.localEngine) - localEngineHost(this.localEngineHost) - localEngineSrcPath(this.localEngineSrcPath) - targetPath(FlutterPluginUtils.getFlutterTarget(project)) - verbose(FlutterPluginUtils.isProjectVerbose(project)) - fastStart(FlutterPluginUtils.isProjectFastStart(project)) - fileSystemRoots(fileSystemRootsValue) - fileSystemScheme(fileSystemSchemeValue) - trackWidgetCreation(trackWidgetCreationValue) - targetPlatformValues = targetPlatforms - sourceDir(FlutterPluginUtils.getFlutterSourceDirectory(project)) - intermediateDir(project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/"))) - frontendServerStarterPath(frontendServerStarterPathValue) - extraFrontEndOptions(extraFrontEndOptionsValue) - extraGenSnapshotOptions(extraGenSnapshotOptionsValue) - splitDebugInfo(splitDebugInfoValue) - treeShakeIcons(treeShakeIconsOptionsValue) - dartObfuscation(dartObfuscationValue) - dartDefines(dartDefinesValue) - performanceMeasurementFile(performanceMeasurementFileValue) - codeSizeDirectory(codeSizeDirectoryValue) - deferredComponents(deferredComponentsValue) - validateDeferredComponents(validateDeferredComponentsValue) - flavor(flavorValue) - } - Task compileTask = compileTaskProvider.get() - File libJar = project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar")) - TaskProvider packJniLibsTaskProvider = project.tasks.register("packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", Jar) { - destinationDirectory = libJar.parentFile - archiveFileName = libJar.name - dependsOn(compileTask) - targetPlatforms.each { targetPlatform -> - String abi = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform] - from("${compileTask.intermediateDir}/${abi}") { - include("*.so") - // Move `app.so` to `lib//libapp.so` - rename { String filename -> - return "lib/${abi}/lib${filename}" - } - } - // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble. - // The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'. - String buildDir = "${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build" - String nativeAssetsDir = "${buildDir}/native_assets/android/jniLibs/lib" - from("${nativeAssetsDir}/${abi}") { - include("*.so") - rename { String filename -> - return "lib/${abi}/${filename}" - } - } - } - } - Task packJniLibsTask = packJniLibsTaskProvider.get() - FlutterPluginUtils.addApiDependencies(project, variant.name, project.files { - packJniLibsTask - }) - TaskProvider copyFlutterAssetsTaskProvider = project.tasks.register( - "copyFlutterAssets${variant.name.capitalize()}" , Copy - ) { - dependsOn(compileTask) - with(compileTask.assets) - String currentGradleVersion = project.getGradle().getGradleVersion() - - // See https://docs.gradle.org/current/javadoc/org/gradle/api/file/ConfigurableFilePermissions.html - // See https://github.com/flutter/flutter/pull/50047 - if (FlutterPluginUtils.compareVersionStrings(currentGradleVersion, "8.3") >= 0) { - filePermissions { - user { - read = true - write = true - } - } - } else { - // See https://docs.gradle.org/8.2/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:fileMode - // See https://github.com/flutter/flutter/pull/50047 - fileMode(0644) - } - if (isUsedAsSubproject) { - dependsOn(packageAssets) - dependsOn(cleanPackageAssets) - into(packageAssets.outputDir) - return - } - // `variant.mergeAssets` will be removed at the end of 2019. - def mergeAssets = variant.hasProperty("mergeAssetsProvider") ? - variant.mergeAssetsProvider.get() : variant.mergeAssets - dependsOn(mergeAssets) - dependsOn("clean${mergeAssets.name.capitalize()}") - mergeAssets.mustRunAfter("clean${mergeAssets.name.capitalize()}") - into(mergeAssets.outputDir) - } - Task copyFlutterAssetsTask = copyFlutterAssetsTaskProvider.get() - if (!isUsedAsSubproject) { - def variantOutput = variant.outputs.first() - def processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ? - variantOutput.processResourcesProvider.get() : variantOutput.processResources - processResources.dependsOn(copyFlutterAssetsTask) - } - // The following tasks use the output of copyFlutterAssetsTask, - // so it's necessary to declare it as an dependency since Gradle 8. - // See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency. - def tasksToCheck = [ - "compress${variant.name.capitalize()}Assets", - "bundle${variant.name.capitalize()}Aar", - "bundle${variant.name.capitalize()}LocalLintAar" - ] - tasksToCheck.each { taskTocheck -> - try { - project.tasks.named(taskTocheck).configure { task -> - task.dependsOn(copyFlutterAssetsTask) - } - } catch (UnknownTaskException ignored) { - } - } - return copyFlutterAssetsTask - } // end def addFlutterDeps - if (FlutterPluginUtils.isFlutterAppProject(project)) { - AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android") - android.applicationVariants.configureEach { variant -> - Task assembleTask = variant.assembleProvider.get() - if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask)) { - return - } - Task copyFlutterAssetsTask = addFlutterDeps(variant) - BaseVariantOutput variantOutput = variant.outputs.first() - ProcessAndroidResources processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ? - variantOutput.processResourcesProvider.get() : variantOutput.processResources - processResources.dependsOn(copyFlutterAssetsTask) - - // Copy the output APKs into a known location, so `flutter run` or `flutter build apk` - // can discover them. By default, this is `/build/app/outputs/flutter-apk/.apk`. - // - // The filename consists of `app<-abi>?<-flavor-name>?-.apk`. - // Where: - // * `abi` can be `armeabi-v7a|arm64-v8a|x86|x86_64` only if the flag `split-per-abi` is set. - // * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called. - // * `build-mode` can be `release|debug|profile`. - variant.outputs.each { output -> - assembleTask.doLast { - PackageAndroidArtifact packageApplicationProvider = variant.packageApplicationProvider.get() - Directory outputDirectory = packageApplicationProvider.outputDirectory.get() - String outputDirectoryStr = outputDirectory.toString() - String filename = "app" - String abi = output.getFilter(OutputFile.ABI) - if (abi != null && !abi.isEmpty()) { - filename += "-${abi}" - } - if (variant.flavorName != null && !variant.flavorName.isEmpty()) { - filename += "-${variant.flavorName.toLowerCase()}" - } - filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}" - project.copy { - from new File("$outputDirectoryStr/${output.outputFileName}") - into new File("${project.layout.buildDirectory.dir("outputs/flutter-apk").get()}") - rename { - return "${filename}.apk" - } - } - } - } - } - // Copy the native assets created by build.dart and placed here by flutter assemble. - // This path is not flavor specific and must only be added once. - // If support for flavors is added to native assets, then they must only be added - // once per flavor; see https://github.com/dart-lang/native/issues/1359. - String nativeAssetsDir = "${project.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/" - android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir) - configurePlugins(project) - FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project)) - return - } - // Flutter host module project (Add-to-app). - String hostAppProjectName = project.rootProject.hasProperty("flutter.hostAppProjectName") ? project.rootProject.property("flutter.hostAppProjectName") : "app" - Project appProject = project.rootProject.findProject(":${hostAppProjectName}") - assert(appProject != null) : "Project :${hostAppProjectName} doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=` in gradle.properties." - // Wait for the host app project configuration. - appProject.afterEvaluate { - assert(appProject.android != null) - project.android.libraryVariants.all { libraryVariant -> - Task copyFlutterAssetsTask - appProject.android.applicationVariants.all { appProjectVariant -> - Task appAssembleTask = appProjectVariant.assembleProvider.get() - if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) { - return - } - // Find a compatible application variant in the host app. - // - // For example, consider a host app that defines the following variants: - // | ----------------- | ----------------------------- | - // | Build Variant | Flutter Equivalent Variant | - // | ----------------- | ----------------------------- | - // | freeRelease | release | - // | freeDebug | debug | - // | freeDevelop | debug | - // | profile | profile | - // | ----------------- | ----------------------------- | - // - // This mapping is based on the following rules: - // 1. If the host app build variant name is `profile` then the equivalent - // Flutter variant is `profile`. - // 2. If the host app build variant is debuggable - // (e.g. `buildType.debuggable = true`), then the equivalent Flutter - // variant is `debug`. - // 3. Otherwise, the equivalent Flutter variant is `release`. - String variantBuildMode = FlutterPluginUtils.buildModeFor(libraryVariant.buildType) - if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) { - return - } - copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps(libraryVariant) - Task mergeAssets = project - .tasks - .findByPath(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets") - assert(mergeAssets) - mergeAssets.dependsOn(copyFlutterAssetsTask) - } - } - } - configurePlugins(project) - FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project)) - } -} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt new file mode 100644 index 0000000000..d7b81902d2 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -0,0 +1,743 @@ +// 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.VariantOutput +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.AbstractAppExtension +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.api.ApkVariantOutput +import com.android.build.gradle.api.BaseVariant +import com.android.build.gradle.api.BaseVariantOutput +import com.android.build.gradle.tasks.PackageAndroidArtifact +import com.android.build.gradle.tasks.ProcessAndroidResources +import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist +import com.flutter.gradle.plugins.PluginHandler +import com.flutter.gradle.tasks.FlutterTask +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownTaskException +import org.gradle.api.file.Directory +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Jar +import org.gradle.internal.os.OperatingSystem +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Paths +import java.util.Properties + +class FlutterPlugin : Plugin { + private var project: Project? = null + private var flutterRoot: File? = null + private var flutterExecutable: File? = null + private var localEngine: String? = null + private var localEngineHost: String? = null + private var localEngineSrcPath: String? = null + private var localProperties: Properties? = null + private var engineVersion: String? = null + private var engineRealm: String? = null + private var pluginHandler: PluginHandler? = null + + override fun apply(project: Project) { + this.project = project + + val rootProject = project.rootProject + if (FlutterPluginUtils.isFlutterAppProject(project)) { + addTaskForLockfileGeneration(rootProject) + } + + val flutterRootSystemVal: String? = System.getenv("FLUTTER_ROOT") + val flutterRootPath: String = + resolveProperty("flutter.sdk", flutterRootSystemVal) + ?: throw GradleException( + "Flutter SDK not found. Define location with flutter.sdk in the " + + "local.properties file or with a FLUTTER_ROOT environment variable." + ) + + flutterRoot = project.file(flutterRootPath) + if (!flutterRoot!!.isDirectory) { + throw GradleException("flutter.sdk must point to the Flutter SDK directory") + } + + engineVersion = + if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) { + "+" // Match any version since there's only one. + } else { + val engineStampPath = + Paths.get(flutterRoot!!.absolutePath, "bin", "cache", "engine.stamp") + val engineStampContent = engineStampPath.toFile().readText().trim() + "1.0.0-$engineStampContent" + } + + engineRealm = + Paths + .get(flutterRoot!!.absolutePath, "bin", "cache", "engine.realm") + .toFile() + .readText() + .trim() + if (engineRealm!!.isNotEmpty()) { + engineRealm += "/" + } + + // Configure the Maven repository. + val hostedRepository: String = + System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL) + ?: FlutterPluginConstants.DEFAULT_MAVEN_HOST + val repository: String? = + if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) { + project.property(PROP_LOCAL_ENGINE_REPO) as String? + } else { + "$hostedRepository/${engineRealm}download.flutter.io" + } + rootProject.allprojects { + repositories.maven { + url = uri(repository!!) + } + } + + project.apply { + from( + Paths.get( + flutterRoot!!.absolutePath, + "packages", + "flutter_tools", + "gradle", + "src", + "main", + "scripts", + "native_plugin_loader.gradle.kts" + ) + ) + } + + val flutterExtension: FlutterExtension = + project.extensions.create("flutter", FlutterExtension::class.java) + + // TODO(gmackall): is this actually a different properties file than the previous one? + val rootProjectLocalProperties = Properties() + val rootProjectLocalPropertiesFile = rootProject.file("local.properties") + if (rootProjectLocalPropertiesFile.exists()) { + rootProjectLocalPropertiesFile.reader(StandardCharsets.UTF_8).use { reader -> + rootProjectLocalProperties.load(reader) + } + } + flutterExtension.flutterVersionCode = + rootProjectLocalProperties.getProperty("flutter.versionCode", "1") + flutterExtension.flutterVersionName = + rootProjectLocalProperties.getProperty("flutter.versionName", "1.0") + + this.addFlutterTasks(project) + FlutterPluginUtils.forceNdkDownload(project, flutterRootPath) + + // By default, assembling APKs generates fat APKs if multiple platforms are passed. + // Configuring split per ABI allows to generate separate APKs for each abi. + // This is a noop when building a bundle. + if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { + FlutterPluginUtils.getAndroidExtension(project).splits.abi { + isEnable = true + reset() + isUniversalApk = false + } + } + val propDeferredComponentNames: String = "deferred-component-names" + val deferredComponentNamesValue: String? = + project.findProperty(propDeferredComponentNames) as? String + if (deferredComponentNamesValue != null) { + val componentNames: Set = + deferredComponentNamesValue + .split(',') + .map { ":$it" } + .toSet() + // TODO(gmackall): Unify the types we use for the android extension. This is yet + // another type we need unfortunately. + val androidExtensionAsApplicationExtension = + project.extensions.getByType(ApplicationExtension::class.java) + // TODO(gmackall): Should we clear here? I think this is equivalent to what we used to + // do, but unsure. Can't use a closure. + androidExtensionAsApplicationExtension.dynamicFeatures.clear() + androidExtensionAsApplicationExtension.dynamicFeatures.addAll(componentNames) + } + + FlutterPluginUtils.getTargetPlatforms(project).forEach { targetArch -> + val abiValue: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch] + val androidExtension: BaseExtension = FlutterPluginUtils.getAndroidExtension(project) + androidExtension.splits.abi.include(abiValue!!) + } + + val flutterExecutableName = getExecutableNameForPlatform("flutter") + flutterExecutable = + Paths.get(flutterRoot!!.absolutePath, "bin", flutterExecutableName).toFile() + + // Validate that the provided Gradle, Java, AGP, and KGP versions are all within our + // supported range. + val shouldSkipDependencyChecks: Boolean = + project.hasProperty("skipDependencyChecks") && + ( + project.properties["skipDependencyChecks"] as? Boolean + ?: false + ) + if (!shouldSkipDependencyChecks) { + try { + DependencyVersionChecker.checkDependencyVersions(project) + } catch (e: Exception) { + if (!project.hasProperty("usesUnsupportedDependencyVersions") || + !(project.properties["usesUnsupportedDependencyVersions"] as Boolean) + ) { + // Possible bug in dependency checking code - warn and do not block build. + project.logger.error( + "Warning: Flutter was unable to detect project Gradle, Java, " + + "AGP, and KGP versions. Skipping dependency version checking. Error was: " + + e + ) + } else { + // If usesUnsupportedDependencyVersions is set, the exception was thrown by us + // in the dependency version checker plugin so re-throw it here. + throw e + } + } + } + + BaseApplicationNameHandler.setBaseName(project) + val flutterProguardRules: String = + Paths + .get( + flutterRoot!!.absolutePath, + "packages", + "flutter_tools", + "gradle", + "flutter_proguard_rules.pro" + ).toString() + // TODO(gmackall): reconsider getting the android extension every time + FlutterPluginUtils.getAndroidExtension(project).buildTypes { + // Add profile build type. + create("profile") { + initWith(getByName("debug")) + // TODO(gmackall): do we need to clear? + this.matchingFallbacks.clear() + this.matchingFallbacks.addAll(listOf("debug", "release")) + } + + // TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet. + // This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove + // this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see + // increased app size due to this. + if (FlutterPluginUtils.shouldShrinkResources(project)) { + getByName("release") { + isMinifyEnabled = true + // Enables resource shrinking, which is performed by the Android Gradle plugin. + // The resource shrinker can't be used for libraries. + isShrinkResources = FlutterPluginUtils.isBuiltAsApp(project) + // Fallback to `android/app/proguard-rules.pro`. + // This way, custom Proguard rules can be configured as needed. + proguardFiles( + FlutterPluginUtils + .getAndroidExtension(project) + .getDefaultProguardFile("proguard-android-optimize.txt"), + flutterProguardRules, + "proguard-rules.pro" + ) + } + } + } + + if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) { + // This is required to pass the local engine to flutter build aot. + val engineOutPath: String = project.properties["local-engine-out"] as String + val engineOut: File = project.file(engineOutPath) + if (!engineOut.isDirectory) { + throw GradleException("local-engine-out must point to a local engine build") + } + localEngine = engineOut.name + localEngineSrcPath = engineOut.parentFile.parent + + val engineHostOutPath: String = project.properties["local-engine-host-out"] as String + val engineHostOut: File = project.file(engineHostOutPath) + if (!engineHostOut.isDirectory) { + throw GradleException("local-engine-host-out must point to a local engine host build") + } + localEngineHost = engineHostOut.name + } + FlutterPluginUtils.getAndroidExtension(project).buildTypes.all { + addFlutterDependencies(this) + } + } + + private fun addFlutterDependencies(buildType: com.android.builder.model.BuildType) { + FlutterPluginUtils.addFlutterDependencies( + project!!, + buildType, + getPluginHandler(project!!), + engineVersion!! + ) + } + + private fun getExecutableNameForPlatform(baseExecutableName: String): String = + if (OperatingSystem.current().isWindows) "$baseExecutableName.bat" else baseExecutableName + + private fun resolveProperty( + propertyName: String, + defaultValue: String? + ): String? { + if (localProperties == null) { + localProperties = + readPropertiesIfExist(File(project!!.projectDir.parentFile, "local.properties")) + } + return project?.findProperty(propertyName) as? String ?: localProperties!!.getProperty( + propertyName, + defaultValue + ) + } + + private fun addTaskForLockfileGeneration(rootProject: Project) { + rootProject.tasks.register("generateLockfiles") { + doLast { + rootProject.subprojects.forEach { subproject -> + val gradlew: String = + getExecutableNameForPlatform("${rootProject.projectDir}/gradlew") + rootProject.exec { + workingDir(rootProject.projectDir) + executable(gradlew) + args(":${subproject.name}:dependencies", "--write-locks") + } + } + } + } + } + + private fun addFlutterTasks(projectToAddTasksTo: Project) { + if (projectToAddTasksTo.state.failure != null) { + return + } + + FlutterPluginUtils.addTaskForJavaVersion(projectToAddTasksTo) + if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) { + FlutterPluginUtils.addTaskForPrintBuildVariants(projectToAddTasksTo) + FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo) + } + + val targetPlatforms: List = + FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo) + + val flutterPlugin = this + + if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) { + // TODO(gmackall): I think this can be BaseExtension, with findByType. + val android: AbstractAppExtension = + projectToAddTasksTo.extensions.findByName("android") as AbstractAppExtension + android.applicationVariants.configureEach { + val variant = this + val assembleTask = variant.assembleProvider.get() + if (!FlutterPluginUtils.shouldConfigureFlutterTask( + projectToAddTasksTo, + assembleTask + ) + ) { + return@configureEach + } + val copyFlutterAssetsTask: Task = + addFlutterDeps(variant, flutterPlugin, targetPlatforms) + val variantOutput: BaseVariantOutput = variant.outputs.first() + val processResources: ProcessAndroidResources = + try { + variantOutput.processResourcesProvider.get() + } catch (e: UnknownTaskException) { + variantOutput.processResources + } + processResources.dependsOn(copyFlutterAssetsTask) + + // Copy the output APKs into a known location, so `flutter run` or `flutter build apk` + // can discover them. By default, this is `/build/app/outputs/flutter-apk/.apk`. + // + // The filename consists of `app<-abi>?<-flavor-name>?-.apk`. + // Where: + // * `abi` can be `armeabi-v7a|arm64-v8a|x86|x86_64` only if the flag `split-per-abi` is set. + // * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called. + // * `build-mode` can be `release|debug|profile`. + variant.outputs.forEach { output -> + assembleTask.doLast { + output as ApkVariantOutput + val packageApplicationProvider: PackageAndroidArtifact = + variant.packageApplicationProvider.get() + val outputDirectory: Directory = + packageApplicationProvider.outputDirectory.get() + val outputDirectoryStr: String = outputDirectory.toString() + var filename = "app" + val abi = output.getFilter(VariantOutput.FilterType.ABI) + if (abi != null && abi.isNotEmpty()) { + filename += "-$abi" + } + if (variant.flavorName != null && variant.flavorName.isNotEmpty()) { + filename += "-${variant.flavorName.toLowerCase()}" + } + filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}" + projectToAddTasksTo.copy { + from(File("$outputDirectoryStr/${output.outputFileName}")) + into( + File( + "${ + projectToAddTasksTo.layout.buildDirectory.dir("outputs/flutter-apk") + .get() + }" + ) + ) + rename { "$filename.apk" } + } + } + } + } + // Copy the native assets created by build.dart and placed here by flutter assemble. + // This path is not flavor specific and must only be added once. + // If support for flavors is added to native assets, then they must only be added + // once per flavor; see https://github.com/dart-lang/native/issues/1359. + val nativeAssetsDir: String = + "${projectToAddTasksTo.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/" + android.sourceSets + .getByName("main") + .jniLibs + .srcDir(nativeAssetsDir) + getPluginHandler(projectToAddTasksTo!!).configurePlugins(engineVersion!!) + FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion( + projectToAddTasksTo, + getPluginHandler(projectToAddTasksTo).getPluginList() + ) + return + } + // Flutter host module project (Add-to-app). + val hostAppProjectName: String? = + if (projectToAddTasksTo.rootProject.hasProperty("flutter.hostAppProjectName")) { + projectToAddTasksTo.rootProject.property( + "flutter.hostAppProjectName" + ) as? String + } else { + "app" + } + val appProject: Project? = + projectToAddTasksTo.rootProject.findProject(":$hostAppProjectName") + check(appProject != null) { + "Project :$hostAppProjectName doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=` in gradle.properties." + } + // Wait for the host app project configuration. + appProject.afterEvaluate { + val androidLibraryExtension = + projectToAddTasksTo.extensions.findByType(LibraryExtension::class.java) + check(androidLibraryExtension != null) + androidLibraryExtension.libraryVariants.all libraryVariantAll@{ + val libraryVariant = this + var copyFlutterAssetsTask: Task? = null + val androidAppExtension = + appProject.extensions.findByName("android") as? AbstractAppExtension + check(androidAppExtension != null) + androidAppExtension.applicationVariants.all applicationVariantAll@{ + val appProjectVariant = this + val appAssembleTask: Task = appProjectVariant.assembleProvider.get() + if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) { + return@applicationVariantAll + } + + // Find a compatible application variant in the host app. + // + // For example, consider a host app that defines the following variants: + // | ----------------- | ----------------------------- | + // | Build Variant | Flutter Equivalent Variant | + // | ----------------- | ----------------------------- | + // | freeRelease | release | + // | freeDebug | debug | + // | freeDevelop | debug | + // | profile | profile | + // | ----------------- | ----------------------------- | + // + // This mapping is based on the following rules: + // 1. If the host app build variant name is `profile` then the equivalent + // Flutter variant is `profile`. + // 2. If the host app build variant is debuggable + // (e.g. `buildType.debuggable = true`), then the equivalent Flutter + // variant is `debug`. + // 3. Otherwise, the equivalent Flutter variant is `release`. + val variantBuildMode: String = + FlutterPluginUtils.buildModeFor(libraryVariant.buildType) + if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) { + return@applicationVariantAll + } + copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps( + libraryVariant, + flutterPlugin, + targetPlatforms + ) + val mergeAssets = + projectToAddTasksTo + .tasks + .findByPath(":$hostAppProjectName:merge${appProjectVariant.name.capitalize()}Assets") + check(mergeAssets != null) + mergeAssets.dependsOn(copyFlutterAssetsTask) + } + } + } + getPluginHandler(projectToAddTasksTo).configurePlugins(engineVersion!!) + FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion( + projectToAddTasksTo, + getPluginHandler(projectToAddTasksTo).getPluginList() + ) + } + + private fun getPluginHandler(project: Project): PluginHandler { + if (this.pluginHandler == null) { + this.pluginHandler = PluginHandler(project) + } + return this.pluginHandler!! + } + + companion object { + const val PROP_LOCAL_ENGINE_REPO: String = "local-engine-repo" + + /** + * The name prefix for flutter builds. This is used to identify gradle tasks + * where we expect the flutter tool to provide any error output, and skip the + * standard Gradle error output in the FlutterEventLogger. If you change this, + * be sure to change any instances of this string in symbols in the code below + * to match. + */ + const val FLUTTER_BUILD_PREFIX: String = "flutterBuild" + + /** + * Finds a task by name, returning null if the task does not exist. + */ + private fun findTaskOrNull( + project: Project, + taskName: String + ): Task? = + try { + project.tasks.named(taskName).get() + } catch (ignored: UnknownTaskException) { + null + } + + private fun addFlutterDeps( + variant: BaseVariant, + flutterPlugin: FlutterPlugin, + targetPlatforms: List + ): Task { + // Shorthand + val project: Project = flutterPlugin.project!! + + val fileSystemRootsValue: Array? = + project + .findProperty("filesystem-roots") + ?.toString() + ?.split("\\|") + ?.toTypedArray() + val fileSystemSchemeValue: String? = + project.findProperty("filesystem-scheme")?.toString() + val trackWidgetCreationValue: Boolean = + project.findProperty("track-widget-creation")?.toString()?.toBoolean() ?: true + val frontendServerStarterPathValue: String? = + project.findProperty("frontend-server-starter-path")?.toString() + val extraFrontEndOptionsValue: String? = + project.findProperty("extra-front-end-options")?.toString() + val extraGenSnapshotOptionsValue: String? = + project.findProperty("extra-gen-snapshot-options")?.toString() + val splitDebugInfoValue: String? = project.findProperty("split-debug-info")?.toString() + val dartObfuscationValue: Boolean = + project.findProperty("dart-obfuscation")?.toString()?.toBoolean() ?: false + val treeShakeIconsOptionsValue: Boolean = + project.findProperty("tree-shake-icons")?.toString()?.toBoolean() ?: false + val dartDefinesValue: String? = project.findProperty("dart-defines")?.toString() + val performanceMeasurementFileValue: String? = + project.findProperty("performance-measurement-file")?.toString() + val codeSizeDirectoryValue: String? = + project.findProperty("code-size-directory")?.toString() + val deferredComponentsValue: Boolean = + project.findProperty("deferred-components")?.toString()?.toBoolean() ?: false + val validateDeferredComponentsValue: Boolean = + project.findProperty("validate-deferred-components")?.toString()?.toBoolean() ?: true + + if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { + variant.outputs.forEach { output -> + // need to force this as the API does not return the right thing for our use. + output as ApkVariantOutput + val filterIdentifier: String = + output.getFilter(VariantOutput.FilterType.ABI) + val abiVersionCode: Int? = FlutterPluginConstants.ABI_VERSION[filterIdentifier] + if (abiVersionCode != null) { + output.versionCodeOverride + } + } + } + + // Build an AAR when this property is defined. + val isBuildingAar: Boolean = project.hasProperty("is-plugin") + // In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project. + // `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR. + // TODO(gmackall): I think this is just always null? Which is great news! Consider removing. + val packageAssets: Task? = + findTaskOrNull( + project, + "package${FlutterPluginUtils.capitalize(variant.name)}Assets" + ) + val cleanPackageAssets: Task? = + findTaskOrNull( + project, + "cleanPackage${FlutterPluginUtils.capitalize(variant.name)}Assets" + ) + + val isUsedAsSubproject: Boolean = + packageAssets != null && cleanPackageAssets != null && !isBuildingAar + + val variantBuildMode: String = FlutterPluginUtils.buildModeFor(variant.buildType) + val flavorValue: String = variant.flavorName + val taskName: String = + FlutterPluginUtils.toCamelCase( + listOf( + "compile", + FLUTTER_BUILD_PREFIX, + variant.name + ) + ) + // The task provider below will shadow a lot of the variable names, so provide this reference + // to access them within that scope. + + // Be careful when configuring task below, Groovy has bizarre + // scoping rules: writing `verbose isVerbose()` means calling + // `isVerbose` on the task itself - which would return `verbose` + // original value. You either need to hoist the value + // into a separate variable `verbose verboseValue` or prefix with + // `this` (`verbose this.isVerbose()`). + val compileTaskProvider: TaskProvider = + project.tasks.register(taskName, FlutterTask::class.java) { + flutterRoot = flutterPlugin.flutterRoot + flutterExecutable = flutterPlugin.flutterExecutable + buildMode = variantBuildMode + minSdkVersion = variant.mergedFlavor.minSdkVersion!!.apiLevel + localEngine = flutterPlugin.localEngine + localEngineHost = flutterPlugin.localEngineHost + localEngineSrcPath = flutterPlugin.localEngineSrcPath + targetPath = FlutterPluginUtils.getFlutterTarget(project) + verbose = FlutterPluginUtils.isProjectVerbose(project) + fastStart = FlutterPluginUtils.isProjectFastStart(project) + fileSystemRoots = fileSystemRootsValue + fileSystemScheme = fileSystemSchemeValue + trackWidgetCreation = trackWidgetCreationValue + targetPlatformValues = targetPlatforms + sourceDir = FlutterPluginUtils.getFlutterSourceDirectory(project) + intermediateDir = + project.file( + project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/") + ) + frontendServerStarterPath = frontendServerStarterPathValue + extraFrontEndOptions = extraFrontEndOptionsValue + extraGenSnapshotOptions = extraGenSnapshotOptionsValue + splitDebugInfo = splitDebugInfoValue + treeShakeIcons = treeShakeIconsOptionsValue + dartObfuscation = dartObfuscationValue + dartDefines = dartDefinesValue + performanceMeasurementFile = performanceMeasurementFileValue + codeSizeDirectory = codeSizeDirectoryValue + deferredComponents = deferredComponentsValue + validateDeferredComponents = validateDeferredComponentsValue + flavor = flavorValue + } + val compileTask: FlutterTask = compileTaskProvider.get() + val libJar: File = + project.file( + project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar") + ) + val packJniLibsTaskProvider: TaskProvider = + project.tasks.register( + "packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", + Jar::class.java + ) { + destinationDirectory.set(libJar.parentFile) + archiveFileName.set(libJar.name) + dependsOn(compileTask) + targetPlatforms.forEach { targetPlatform -> + val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform] + from("${compileTask.intermediateDir}/$abi") { + include("*.so") + // Move `app.so` to `lib//libapp.so` + rename { filename: String -> "lib/$abi/lib$filename" } + } + // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble. + // The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'. + val buildDir: String = + "${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build" + val nativeAssetsDir: String = + "$buildDir/native_assets/android/jniLibs/lib" + from("$nativeAssetsDir/$abi") { + include("*.so") + rename { filename: String -> "lib/$abi/$filename" } + } + } + } + val packJniLibsTask: Task = packJniLibsTaskProvider.get() + FlutterPluginUtils.addApiDependencies( + project, + variant.name, + project.files({ + packJniLibsTask + }) + ) + val copyFlutterAssetsTaskProvider: TaskProvider = + project.tasks.register( + "copyFlutterAssets${variant.name.capitalize()}", + Copy::class.java + ) { + dependsOn(compileTask) + with(compileTask.assets) + // TODO(gmackall): Replace with filePermissions.user.read/write = true once + // minimum supported Gradle version is 8.3. + fileMode = 420 // corresponds to unix 0644 in base 8 + if (isUsedAsSubproject) { + // TODO(gmackall): above is always false, can delete + dependsOn(packageAssets) + dependsOn(cleanPackageAssets) + into(packageAssets!!.outputs) + } + val mergeAssets = + try { + variant.mergeAssetsProvider.get() + } catch (e: IllegalStateException) { + variant.mergeAssets + } + dependsOn(mergeAssets) + dependsOn("clean${mergeAssets.name.capitalize()}") + mergeAssets.mustRunAfter("clean${mergeAssets.name.capitalize()}") + into(mergeAssets.outputDir) + } + val copyFlutterAssetsTask: Task = copyFlutterAssetsTaskProvider.get() + if (!isUsedAsSubproject) { + val variantOutput: BaseVariantOutput = variant.outputs.first() + val processResources = + try { + variantOutput.processResourcesProvider.get() + } catch (e: IllegalStateException) { + variantOutput.processResources + } + processResources.dependsOn(copyFlutterAssetsTask) + } + // The following tasks use the output of copyFlutterAssetsTask, + // so it's necessary to declare it as an dependency since Gradle 8. + // See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency. + val tasksToCheck = + listOf( + "compress${variant.name.capitalize()}Assets", + "bundle${variant.name.capitalize()}Aar", + "bundle${variant.name.capitalize()}LocalLintAar" + ) + tasksToCheck.forEach { taskTocheck -> + try { + project.tasks.named(taskTocheck).configure { + dependsOn(copyFlutterAssetsTask) + } + } catch (ignored: UnknownTaskException) { + // ignored + } + } + return copyFlutterAssetsTask + } + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt index d4fe426e26..8f8c0bda56 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt @@ -25,7 +25,6 @@ object FlutterPluginConstants { const val INTERMEDIATES_DIR = "intermediates" const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL" const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com" - const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config" /** Maps platforms to ABI architectures. */ @JvmStatic val PLATFORM_ARCH_MAP = @@ -42,7 +41,7 @@ object FlutterPluginConstants { * Otherwise, the Play Store will complain that the APK variants have the same version. */ @JvmStatic val ABI_VERSION = - mapOf( // Explicit type for clarity, though inferred + mapOf( ARCH_ARM32 to 1, ARCH_ARM64 to 2, ARCH_X86 to 3, diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 1dcfb3b4fc..fc1fd7a454 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -10,6 +10,7 @@ import com.android.build.gradle.api.ApplicationVariant import com.android.build.gradle.api.BaseVariantOutput import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.builder.model.BuildType +import com.flutter.gradle.plugins.PluginHandler import groovy.lang.Closure import groovy.util.Node import groovy.util.XmlParser @@ -139,19 +140,6 @@ object FlutterPluginUtils { // TODO(54566): Can remove this function and its call sites once resolved. - /** - * Returns `true` if the given project is a plugin project having an `android` directory - * containing a `build.gradle` or `build.gradle.kts` file. - */ - @JvmStatic - @JvmName("pluginSupportsAndroidPlatform") - internal fun pluginSupportsAndroidPlatform(project: Project): Boolean { - val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle") - val buildGradleKts = - File(File(project.projectDir.parentFile, "android"), "build.gradle.kts") - 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 @@ -406,7 +394,7 @@ object FlutterPluginUtils { return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode } - private fun getAndroidExtension(project: Project): BaseExtension { + internal fun getAndroidExtension(project: Project): BaseExtension { // Common supertype of the android extension types. // But maybe this should be https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/TestedExtension. return project.extensions.findByType(BaseExtension::class.java)!! @@ -611,7 +599,7 @@ object FlutterPluginUtils { // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings. gradleProjectAndroidExtension.externalNativeBuild.cmake.path( - "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt" + "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt" ) // AGP defaults to outputting build artifacts in `android/app/.cxx`. This directory is a @@ -656,7 +644,7 @@ object FlutterPluginUtils { internal fun addFlutterDependencies( project: Project, buildType: BuildType, - pluginList: List>, + pluginHandler: PluginHandler, engineVersion: String ) { val flutterBuildMode: String = buildModeFor(buildType) @@ -676,11 +664,9 @@ object FlutterPluginUtils { // embedding. val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List> = if (flutterBuildMode == "release") { - getPluginListWithoutDevDependencies( - pluginList - ) + pluginHandler.getPluginListWithoutDevDependencies() } else { - pluginList + pluginHandler.getPluginList() } if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) { @@ -702,143 +688,6 @@ object FlutterPluginUtils { } } - /** - * Gets the list of plugins (as map) that support the Android platform and are dependencies of the - * Android project excluding dev dependencies. - * - * 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/scripts/native_plugin_loader.gradle.kts - */ - private fun getPluginListWithoutDevDependencies(pluginList: List>): List> = - pluginList.filter { pluginObject -> pluginObject["dev_dependency"] == false } - - /** - * 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. - */ - @JvmStatic - @JvmName("configurePluginDependencies") - internal fun configurePluginDependencies( - project: Project, - pluginObject: Map - ) { - val pluginName: String = - requireNotNull(pluginObject["name"] as? String) { - "Missing valid \"name\" property for plugin object: $pluginObject" - } - val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return - - getAndroidExtension(project).buildTypes.forEach { buildType -> - val flutterBuildMode: String = buildModeFor(buildType) - if (flutterBuildMode == "release" && (pluginObject["dev_dependency"] as? Boolean == true)) { - // This plugin is a dev dependency will not be included in the - // release build, so no need to add its dependencies. - return@forEach - } - val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>) - dependencies.forEach innerForEach@{ pluginDependencyName -> - check(pluginDependencyName is String) - if (pluginDependencyName.isEmpty()) { - return@innerForEach - } - - val dependencyProject = - project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach - pluginProject.afterEvaluate { - pluginProject.dependencies.add("implementation", dependencyProject) - } - } - } - } - - /** - * Performs configuration related to the plugin's Gradle [Project], including - * 1. Adding the plugin itself as a dependency to the main project. - * 2. Adding the main project's build types to the plugin's build types. - * 3. Adding a dependency on the Flutter embedding to the plugin. - * - * Should only be called on plugins that support the Android platform. - */ - @JvmStatic - @JvmName("configurePluginProject") - internal fun configurePluginProject( - project: Project, - pluginObject: Map, - engineVersion: String - ) { - // TODO(gmackall): should guard this with a pluginObject.contains(). - val pluginName = - requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" } - val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return - - // Apply the "flutter" Gradle extension to plugins so that they can use it's vended - // compile/target/min sdk values. - pluginProject.extensions.create("flutter", FlutterExtension::class.java) - - // Add plugin dependency to the app project. We only want to add dependency - // for dev dependencies in non-release builds. - project.afterEvaluate { - getAndroidExtension(project).buildTypes.forEach { buildType -> - if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") { - project.dependencies.add("${buildType.name}Api", pluginProject) - } - } - } - - // Wait until the Android plugin loaded. - pluginProject.afterEvaluate { - // Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion. - val projectCompileSdkVersion: String = getCompileSdkFromProject(project) - val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject) - // TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong - // when comparing preview versions (against non preview, and also in the - // case of alphabet reset which happened with "Baklava". - if (pluginCompileSdkVersion > projectCompileSdkVersion) { - project.logger.quiet("Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher.") - project.logger.quiet( - "For more information about build configuration, see ${FlutterPluginConstants.WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}." - ) - } - - getAndroidExtension(project).buildTypes.forEach { buildType -> - addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion) - } - } - } - - private fun addEmbeddingDependencyToPlugin( - project: Project, - pluginProject: Project, - buildType: BuildType, - engineVersion: String - ) { - val flutterBuildMode: String = buildModeFor(buildType) - // TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above - // 3.5. We should try to remove it. - // In AGP 3.5, the embedding must be added as an API implementation, - // so java8 features are desugared against the runtime classpath. - // For more, see https://github.com/flutter/flutter/issues/40126 - if (!supportsBuildMode(pluginProject, flutterBuildMode)) { - return - } - if (!pluginProject.hasProperty("android")) { - return - } - - // Copy build types from the app to the plugin. - // This allows to build apps with plugins and custom build types or flavors. - getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes) - - // The embedding is API dependency of the plugin, so the AGP is able to desugar - // default method implementations when the interface is implemented by a plugin. - // - // See https://issuetracker.google.com/139821726, and - // https://github.com/flutter/flutter/issues/72185 for more details. - addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") - } - // ------------------ Task adders (a subset of the above category) // Add a task that can be called on flutter projects that prints the Java version used in Gradle. diff --git a/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt index df90089516..060456c254 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt @@ -17,24 +17,21 @@ import java.io.File */ 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")!! + ): List> { + val nativePluginLoader = extraProperties.get("nativePluginLoader")!! @Suppress("UNCHECKED_CAST") - val pluginList: List> = - nativePluginLoader!!::class + val pluginList: List> = + nativePluginLoader::class .members .firstOrNull { it.name == "getPlugins" } - ?.call(nativePluginLoader, flutterProjectRoot) as List> + ?.call(nativePluginLoader, flutterProjectRoot) as List> return pluginList } @@ -42,16 +39,15 @@ object NativePluginLoaderReflectionBridge { /** * 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")!! + val nativePluginLoader = extraProperties.get("nativePluginLoader")!! @Suppress("UNCHECKED_CAST") val dependenciesMetadata: Map = - nativePluginLoader!!::class + nativePluginLoader::class .members .firstOrNull { it.name == "dependenciesMetadata" } ?.call(nativePluginLoader, flutterProjectRoot) as Map diff --git a/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt b/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt new file mode 100644 index 0000000000..b452435200 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt @@ -0,0 +1,326 @@ +// 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.plugins + +import androidx.annotation.VisibleForTesting +import com.android.builder.model.BuildType +import com.flutter.gradle.FlutterExtension +import com.flutter.gradle.FlutterPluginUtils +import com.flutter.gradle.FlutterPluginUtils.addApiDependencies +import com.flutter.gradle.FlutterPluginUtils.buildModeFor +import com.flutter.gradle.FlutterPluginUtils.getAndroidExtension +import com.flutter.gradle.FlutterPluginUtils.getCompileSdkFromProject +import com.flutter.gradle.FlutterPluginUtils.supportsBuildMode +import com.flutter.gradle.NativePluginLoaderReflectionBridge +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import java.io.File +import java.io.FileNotFoundException +import java.nio.charset.StandardCharsets + +/** + * Handles interactions with the flutter plugins (not Gradle plugins) used by the Flutter project, + * such as retrieving them as a list and configuring them as Gradle dependencies of the main Gradle + * project. + */ +class PluginHandler( + val project: Project +) { + private var pluginList: List>? = null + private var pluginDependencies: List>? = null + + /** + * Gets the list of plugins (as map) that support the Android platform. + * + * The map contains the following key - value pairs: + * `name` - the plugins name (String), + * `path` - it's path (String), + * `dependencies` - a list of its dependencies names (List) + * `dev_dependency` - a boolean indicating whether the plugin is a dev dependency (Boolean) + * `native_build` - a boolean indicating whether the plugin has native code (Boolean) + * + * This format is defined in packages/flutter_tools/lib/src/flutter_plugins.dart, in the + * _createPluginMapOfPlatform method. + * See also [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts + */ + internal fun getPluginList(): List> { + if (pluginList == null) { + pluginList = + NativePluginLoaderReflectionBridge.getPlugins( + project.extraProperties, + FlutterPluginUtils.getFlutterSourceDirectory(project) + ) + } + 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 fun getPluginDependencies(): List> { + if (pluginDependencies == null) { + val meta: Map = + NativePluginLoaderReflectionBridge.getDependenciesMetadata( + project.extraProperties, + FlutterPluginUtils.getFlutterSourceDirectory(project) + ) + check(meta["dependencyGraph"] is List<*>) + @Suppress("UNCHECKED_CAST") + pluginDependencies = meta["dependencyGraph"] as List> + } + return pluginDependencies!! + } + + // 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 fun configureLegacyPluginEachProjects(engineVersionValue: String) { + try { + // Read the contents of the settings.gradle file. + // Remove block/line comments + var settingsText = + FlutterPluginUtils + .getSettingsGradleFileFromProjectDir( + project.projectDir, + project.logger + ).readText(StandardCharsets.UTF_8) + settingsText = + settingsText + .replace(Regex("""(?s)/\*.*?\*/"""), "") + .replace(Regex("""(?m)//.*$"""), "") + if (!settingsText.contains("'.flutter-plugins'")) { + return + } + } catch (ignored: FileNotFoundException) { + throw GradleException( + "settings.gradle/settings.gradle.kts does not exist: " + + FlutterPluginUtils + .getSettingsGradleFileFromProjectDir( + project.projectDir, + project.logger + ).absolutePath + ) + } + // TODO(matanlurey): https://github.com/flutter/flutter/issues/48918. + project.logger.quiet( + legacyFlutterPluginsWarning + ) + val deps: List> = getPluginDependencies() + val pluginsNameSet = HashSet() + getPluginList().mapTo(pluginsNameSet) { plugin -> plugin["name"] as String } + deps.filterNot { plugin -> pluginsNameSet.contains(plugin["name"]) } + deps.forEach { plugin: Map -> + val pluginProject = project.rootProject.findProject(":${plugin["name"]}") + if (pluginProject == null) { + // Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`. + project.logger.error( + "Plugin project :${plugin["name"]} listed, but not found. Please fix your settings.gradle/settings.gradle.kts." + ) + } else if (pluginSupportsAndroidPlatform(project)) { + // 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(project, plugin, engineVersionValue) + } else { + // Plugin has no or an empty `android` folder. No action required. + } + } + } + + internal fun configurePlugins(engineVersionValue: String) { + configureLegacyPluginEachProjects(engineVersionValue) + val pluginList: List> = getPluginList() + pluginList.forEach { plugin: Map -> + configurePluginProject( + project, + plugin, + engineVersionValue + ) + } + pluginList.forEach { plugin: Map -> + configurePluginDependencies(project, plugin) + } + } + + /** + * Gets the list of plugins (as map) that support the Android platform and are dependencies of the + * Android project excluding dev dependencies. + * + * 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/scripts/native_plugin_loader.gradle.kts + */ + internal fun getPluginListWithoutDevDependencies(): List> = + getPluginList().filter { pluginObject -> pluginObject["dev_dependency"] == false } + + companion object { + /** + * Flutter Docs Website URLs for help messages. + */ + private const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config" + + @VisibleForTesting internal val legacyFlutterPluginsWarning = + """ + Warning: This project is still reading the deprecated '.flutter-plugins. file. + In an upcoming stable release support for this file will be completely removed and your build will fail. + See https:/flutter.dev/to/flutter-plugins-configuration. + """.trimIndent() + + /** + * Performs configuration related to the plugin's Gradle [Project], including + * 1. Adding the plugin itself as a dependency to the main project. + * 2. Adding the main project's build types to the plugin's build types. + * 3. Adding a dependency on the Flutter embedding to the plugin. + * + * Should only be called on plugins that support the Android platform. + */ + private fun configurePluginProject( + project: Project, + pluginObject: Map, + engineVersion: String + ) { + val pluginName = + requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" } + val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return + + // Apply the "flutter" Gradle extension to plugins so that they can use it's vended + // compile/target/min sdk values. + pluginProject.extensions.create("flutter", FlutterExtension::class.java) + + // Add plugin dependency to the app project. We only want to add dependency + // for dev dependencies in non-release builds. + project.afterEvaluate { + getAndroidExtension(project).buildTypes.forEach { buildType -> + if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") { + project.dependencies.add("${buildType.name}Api", pluginProject) + } + } + } + + // Wait until the Android plugin loaded. + pluginProject.afterEvaluate { + // Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion. + val projectCompileSdkVersion: String = getCompileSdkFromProject(project) + val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject) + // TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong + // when comparing preview versions (against non preview, and also in the + // case of alphabet reset which happened with "Baklava". + if (pluginCompileSdkVersion > projectCompileSdkVersion) { + project.logger.quiet( + "Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher." + ) + project.logger.quiet( + "For more information about build configuration, see ${WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}." + ) + } + + getAndroidExtension(project).buildTypes.forEach { buildType -> + addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion) + } + } + } + + private fun addEmbeddingDependencyToPlugin( + project: Project, + pluginProject: Project, + buildType: BuildType, + engineVersion: String + ) { + val flutterBuildMode: String = buildModeFor(buildType) + // TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above + // 3.5. We should try to remove it. + // In AGP 3.5, the embedding must be added as an API implementation, + // so java8 features are desugared against the runtime classpath. + // For more, see https://github.com/flutter/flutter/issues/40126 + if (!supportsBuildMode(pluginProject, flutterBuildMode)) { + return + } + if (!pluginProject.hasProperty("android")) { + return + } + + // Copy build types from the app to the plugin. + // This allows to build apps with plugins and custom build types or flavors. + getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes) + + // The embedding is API dependency of the plugin, so the AGP is able to desugar + // default method implementations when the interface is implemented by a plugin. + // + // See https://issuetracker.google.com/139821726, and + // https://github.com/flutter/flutter/issues/72185 for more details. + addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") + } + + /** + * Returns `true` if the given project is a plugin project having an `android` directory + * containing a `build.gradle` or `build.gradle.kts` file. + */ + internal fun pluginSupportsAndroidPlatform(project: Project): Boolean { + val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle") + val buildGradleKts = + File(File(project.projectDir.parentFile, "android"), "build.gradle.kts") + 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 fun configurePluginDependencies( + project: Project, + pluginObject: Map + ) { + val pluginName: String = + requireNotNull(pluginObject["name"] as? String) { + "Missing valid \"name\" property for plugin object: $pluginObject" + } + val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return + + getAndroidExtension(project).buildTypes.forEach { buildType -> + val flutterBuildMode: String = buildModeFor(buildType) + if (flutterBuildMode == "release" && (pluginObject["dev_dependency"] as? Boolean == true)) { + // This plugin is a dev dependency will not be included in the + // release build, so no need to add its dependencies. + return@forEach + } + val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>) + dependencies.forEach innerForEach@{ pluginDependencyName -> + check(pluginDependencyName is String) + if (pluginDependencyName.isEmpty()) { + return@innerForEach + } + + val dependencyProject = + project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach + pluginProject.afterEvaluate { + // this.dependencies.add("implementation", dependencyProject) + pluginProject.dependencies.add("implementation", dependencyProject) + } + } + } + } + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt similarity index 99% rename from packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt rename to packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt index 62e74dddf7..cb9d45d892 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package com.flutter.gradle +package com.flutter.gradle.tasks import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input diff --git a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt similarity index 99% rename from packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt rename to packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt index fa47257a0e..bfb0bcc5d3 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package com.flutter.gradle +package com.flutter.gradle.tasks import androidx.annotation.VisibleForTesting import org.gradle.api.Action diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt similarity index 98% rename from packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt rename to packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt index 3ff14a5247..86e5ad0bb4 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package com.flutter.gradle +package com.flutter.gradle.tasks import org.gradle.api.file.CopySpec import org.gradle.api.file.FileCollection diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt similarity index 97% rename from packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt rename to packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt index a99a9ea811..8909602d02 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package com.flutter.gradle +package com.flutter.gradle.tasks +import com.flutter.gradle.FlutterPluginConstants import org.gradle.api.Project import org.gradle.api.file.CopySpec import org.gradle.api.file.FileCollection diff --git a/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt b/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt similarity index 100% rename from packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt rename to packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt 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 index 38fc5889bc..eab9482ed4 100644 --- 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 @@ -72,9 +72,63 @@ class NativePluginLoader { */ fun getDependenciesMetadata(flutterSourceDirectory: File): Map? { // Consider a `.flutter-plugins-dependencies` file with the following content: - // { ... (example content as in the original Groovy code) ... } + // { + // "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`. - // ... (rest of the comment as in the original Groovy code) ... + // `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 != null) { return parsedFlutterPluginsDependencies } diff --git a/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt index 18604feb3c..e0d6b0ef18 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt @@ -1,4 +1,9 @@ +// 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.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationExtension import com.flutter.gradle.BaseApplicationNameHandler.GRADLE_BASE_APPLICATION_NAME_PROPERTY diff --git a/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt index 3310814489..ae9625a2f3 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.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.internal.impldep.org.junit.Assert.assertThrows diff --git a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt index 0ad3930ea0..8f11bb7dbe 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.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.AndroidPluginVersion diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.kt index cc58d29b13..6c78f5c6d9 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.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/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt new file mode 100644 index 0000000000..8d8f917675 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt @@ -0,0 +1,78 @@ +package com.flutter.gradle + +import com.android.build.api.dsl.ApplicationDefaultConfig +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.gradle.AbstractAppExtension +import com.android.build.gradle.BaseExtension +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.writeText +import kotlin.test.Test + +class FlutterPluginTest { + @Test + fun `FlutterPlugin apply() adds expected tasks`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("project-dir").resolve("android").resolve("app") + projectDir.toFile().mkdirs() + val settingsFile = projectDir.parent.resolve("settings.gradle") + settingsFile.writeText("empty for now") + val fakeFlutterSdkDir = tempDir.resolve("fake-flutter-sdk") + fakeFlutterSdkDir.toFile().mkdirs() + val fakeCacheDir = fakeFlutterSdkDir.resolve("bin").resolve("cache") + fakeCacheDir.toFile().mkdirs() + val fakeEngineStampFile = fakeCacheDir.resolve("engine.stamp") + fakeEngineStampFile.writeText(FAKE_ENGINE_STAMP) + val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm") + fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM) + val project = mockk(relaxed = true) + val mockAbstractAppExtension = mockk(relaxed = true) + every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension + every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension + every { project.extensions.findByName("android") } returns mockAbstractAppExtension + every { project.projectDir } returns projectDir.toFile() + every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() + every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() + val flutterExtension = FlutterExtension() + every { project.extensions.create("flutter", any>()) } returns flutterExtension + every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension + val mockBaseExtension = mockk(relaxed = true) + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + val mockApplicationExtension = mockk(relaxed = true) + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension + val mockApplicationDefaultConfig = mockk(relaxed = true) + every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig + every { project.rootProject } returns project + every { project.state.failure } returns null + val mockDirectory = mockk(relaxed = true) + every { project.layout.buildDirectory.get() } returns mockDirectory + val mockAndroidSourceSet = mockk(relaxed = true) + every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet + // mock return of NativePluginLoaderReflectionBridge.getPlugins + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf() + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.file(flutterExtension.source!!) } returns mockk() + val flutterPlugin = FlutterPlugin() + flutterPlugin.apply(project) + + verify { project.tasks.register("generateLockfiles", any()) } + verify { project.tasks.register("javaVersion", any()) } + verify { project.tasks.register("printBuildVariants", any()) } + } + + companion object { + const val FAKE_ENGINE_STAMP = "901b0f1afe77c3555abee7b86a26aaa37f131379" + const val FAKE_ENGINE_REALM = "made_up_realm" + } +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 38c655a3c8..0fc36ec38a 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.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 @@ -8,25 +12,26 @@ import com.android.build.gradle.internal.dsl.CmakeOptions import com.android.build.gradle.internal.dsl.DefaultConfig import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.builder.model.BuildType +import com.flutter.gradle.plugins.PluginHandler import io.mockk.called import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.slot import io.mockk.verify import org.gradle.api.Action import org.gradle.api.DomainObjectCollection import org.gradle.api.DomainObjectSet import org.gradle.api.GradleException -import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.UnknownTaskException -import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty import org.gradle.api.logging.Logger import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.io.TempDir import java.io.File @@ -36,12 +41,10 @@ import kotlin.io.path.createDirectory import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue class FlutterPluginUtilsTest { companion object { - val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" + const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" val devDependency: Map = mapOf( @@ -219,45 +222,6 @@ class FlutterPluginUtilsTest { assertEquals(true, result) } - // pluginSupportsAndroidPlatform - @Test - fun `pluginSupportsAndroidPlatform returns true when android directory exists with gradle build file`( - @TempDir tempDir: Path - ) { - val projectDir = tempDir.resolve("my-plugin") - projectDir.toFile().mkdirs() - - val androidDir = tempDir.resolve("android") - androidDir.toFile().mkdirs() - File(androidDir.toFile(), "build.gradle").createNewFile() - - val mockProject = - mockk { - every { this@mockk.projectDir } returns projectDir.toFile() - } - - assertTrue { - FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject) - } // Replace YourClass with the actual class containing the method - } - - @Test - fun `pluginSupportsAndroidPlatform returns false when gradle build file does not exist`( - @TempDir tempDir: Path - ) { - val projectDir = tempDir.resolve("my-plugin") - projectDir.toFile().mkdirs() - - val mockProject = - mockk { - every { this@mockk.projectDir } returns projectDir.toFile() - } - - assertFalse { - FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject) - } // Replace YourClass with the actual class containing the method - } - // settingsGradleFile @Test fun `settingsGradleFile returns groovy settings gradle file when it exists`( @@ -872,7 +836,7 @@ class FlutterPluginUtilsTest { verify(exactly = 1) { mockCmakeOptions.path } - verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt") } + verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt") } verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) } verify(exactly = 1) { mockDefaultConfig.externalNativeBuild.cmake.arguments( @@ -888,6 +852,12 @@ class FlutterPluginUtilsTest { @Test fun `addFlutterDependencies returns early if buildMode is not supported`() { val project = mockk() + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency val buildType: BuildType = mockk() every { buildType.name } returns "debug" every { buildType.isDebuggable } returns true @@ -899,7 +869,7 @@ class FlutterPluginUtilsTest { FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginList = pluginListWithoutDevDependency, + pluginHandler = pluginHandler, engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" ) @@ -914,8 +884,14 @@ class FlutterPluginUtilsTest { @Test fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() { val project = mockk() + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithoutDevDependency val buildType: BuildType = mockk() - val engineVersion = exampleEngineVersion + val engineVersion = EXAMPLE_ENGINE_VERSION every { buildType.name } returns "debug" every { buildType.isDebuggable } returns true every { project.hasProperty("local-engine-repo") } returns false @@ -927,7 +903,7 @@ class FlutterPluginUtilsTest { FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginList = pluginListWithoutDevDependency, + pluginHandler = pluginHandler, engineVersion = engineVersion ) @@ -945,8 +921,15 @@ class FlutterPluginUtilsTest { @Test fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() { val project = mockk() + val pluginListWithSingleDevDependency = listOf(devDependency) + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency val buildType: BuildType = mockk() - val engineVersion = exampleEngineVersion + val engineVersion = EXAMPLE_ENGINE_VERSION every { buildType.name } returns "release" every { buildType.isDebuggable } returns false every { project.hasProperty("local-engine-repo") } returns false @@ -955,12 +938,10 @@ class FlutterPluginUtilsTest { every { project.configurations.named("api") } returns mockk() every { project.dependencies.add(any(), any()) } returns mockk() - val pluginListWithSingleDevDependency = listOf(devDependency) - FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginList = pluginListWithSingleDevDependency, + pluginHandler = pluginHandler, engineVersion = engineVersion ) @@ -994,8 +975,15 @@ class FlutterPluginUtilsTest { @Test fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() { val project = mockk() + val pluginListWithSingleDevDependency = listOf(devDependency) + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns pluginListWithSingleDevDependency val buildType: BuildType = mockk() - val engineVersion = exampleEngineVersion + val engineVersion = EXAMPLE_ENGINE_VERSION every { buildType.name } returns "debug" every { buildType.isDebuggable } returns true every { project.hasProperty("local-engine-repo") } returns false @@ -1004,12 +992,10 @@ class FlutterPluginUtilsTest { every { project.configurations.named("api") } returns mockk() every { project.dependencies.add(any(), any()) } returns mockk() - val pluginListWithSingleDevDependency = listOf(devDependency) - FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginList = pluginListWithSingleDevDependency, + pluginHandler = pluginHandler, engineVersion = engineVersion ) @@ -1034,168 +1020,6 @@ class FlutterPluginUtilsTest { } } - // configurePluginDependencies TODO - @Test - fun `configurePluginDependencies throws IllegalArgumentException when plugin has no name`() { - val project = mockk() - val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() - pluginWithoutName.remove("name") - assertThrows { - FlutterPluginUtils.configurePluginDependencies( - project = project, - pluginObject = pluginWithoutName - ) - } - } - - @Test - fun `configurePluginDependencies throws IllegalArgumentException when plugin has null dependencies`() { - val project = mockk() - val pluginProject = mockk() - val mockBuildType = mockk() - val pluginWithNullDependencies: MutableMap = cameraDependency.toMutableMap() - pluginWithNullDependencies["dependencies"] = null - every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject - every { - project.extensions - .findByType(BaseExtension::class.java)!! - .buildTypes - .iterator() - } returns - mutableListOf( - mockBuildType - ).iterator() - every { mockBuildType.name } returns "debug" - every { mockBuildType.isDebuggable } returns true - - assertThrows { - FlutterPluginUtils.configurePluginDependencies( - project = project, - pluginObject = pluginWithNullDependencies - ) - } - } - - @Test - fun `configurePluginDependencies adds plugin dependencies`() { - val project = mockk() - val pluginProject = mockk() - val pluginDependencyProject = mockk() - val mockBuildType = mockk() - val pluginWithDependencies: MutableMap = cameraDependency.toMutableMap() - pluginWithDependencies["dependencies"] = - listOf(flutterPluginAndroidLifecycleDependency["name"]) - every { project.rootProject.findProject(":${pluginWithDependencies["name"]}") } returns pluginProject - every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject - every { - project.extensions - .findByType(BaseExtension::class.java)!! - .buildTypes - .iterator() - } returns - mutableListOf( - mockBuildType - ).iterator() - every { mockBuildType.name } returns "debug" - every { mockBuildType.isDebuggable } returns true - val captureActionSlot = slot>() - every { pluginProject.afterEvaluate(any>()) } returns Unit - val mockDependencyHandler = mockk() - every { pluginProject.dependencies } returns mockDependencyHandler - every { mockDependencyHandler.add(any(), any()) } returns mockk() - - FlutterPluginUtils.configurePluginDependencies( - project = project, - pluginObject = pluginWithDependencies - ) - - verify { pluginProject.afterEvaluate(capture(captureActionSlot)) } - captureActionSlot.captured.execute(pluginDependencyProject) - verify { mockDependencyHandler.add("implementation", pluginDependencyProject) } - } - - // configurePluginProject - @Test - fun `configurePluginProject throws IllegalArgumentException when plugin has no name`() { - val project = mockk() - val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() - pluginWithoutName.remove("name") - - assertThrows { - FlutterPluginUtils.configurePluginProject( - project = project, - pluginObject = pluginWithoutName, - engineVersion = exampleEngineVersion - ) - } - } - - @Test - fun `configurePluginProject adds plugin project`() { - val project = mockk() - val pluginProject = mockk() - val mockBuildType = mockk() - val mockLogger = mockk() - every { project.logger } returns mockLogger - every { pluginProject.hasProperty("local-engine-repo") } returns false - every { pluginProject.hasProperty("android") } returns true - every { mockBuildType.name } returns "debug" - every { mockBuildType.isDebuggable } returns true - every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject - every { pluginProject.extensions.create(any(), any>()) } returns mockk() - val captureActionSlot = slot>() - val capturePluginActionSlot = slot>() - every { project.afterEvaluate(any>()) } returns Unit - every { pluginProject.afterEvaluate(any>()) } returns Unit - - val mockProjectBuildTypes = - mockk>() - val mockPluginProjectBuildTypes = - mockk>() - every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes - every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes - every { mockPluginProjectBuildTypes.addAll(any()) } returns true - every { pluginProject.configurations.named(any()) } returns mockk() - every { pluginProject.dependencies.add(any(), any()) } returns mockk() - - every { - project.extensions - .findByType(BaseExtension::class.java)!! - .buildTypes - .iterator() - } returns - mutableListOf( - mockBuildType - ).iterator() andThen - mutableListOf( // can't return the same iterator as it is stateful - mockBuildType - ).iterator() - every { project.dependencies.add(any(), any()) } returns mockk() - every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" - every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" - - FlutterPluginUtils.configurePluginProject( - project = project, - pluginObject = cameraDependency, - engineVersion = exampleEngineVersion - ) - - verify { project.afterEvaluate(capture(captureActionSlot)) } - verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) } - captureActionSlot.captured.execute(project) - capturePluginActionSlot.captured.execute(pluginProject) - verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) } - verify { - pluginProject.dependencies.add( - "debugApi", - "io.flutter:flutter_embedding_debug:$exampleEngineVersion" - ) - } - verify { project.dependencies.add("debugApi", pluginProject) } - verify { mockLogger wasNot called } - verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) } - } - // addTaskForJavaVersion @Test fun `addTaskForJavaVersion adds task for Java version`() { diff --git a/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt index 836c6197d8..b95f295a47 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.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 kotlin.test.Test diff --git a/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.kt index c665907b39..d1230549a7 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.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 kotlin.test.Test diff --git a/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt new file mode 100644 index 0000000000..29d40d1573 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt @@ -0,0 +1,443 @@ +// 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.plugins + +import com.android.build.gradle.BaseExtension +import com.flutter.gradle.FlutterExtension +import com.flutter.gradle.FlutterPluginUtilsTest.Companion.EXAMPLE_ENGINE_VERSION +import com.flutter.gradle.FlutterPluginUtilsTest.Companion.cameraDependency +import com.flutter.gradle.FlutterPluginUtilsTest.Companion.flutterPluginAndroidLifecycleDependency +import com.flutter.gradle.FlutterPluginUtilsTest.Companion.pluginListWithDevDependency +import com.flutter.gradle.FlutterPluginUtilsTest.Companion.pluginListWithoutDevDependency +import com.flutter.gradle.NativePluginLoaderReflectionBridge +import io.mockk.called +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.verify +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.api.logging.Logger +import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class PluginHandlerTest { + // getPluginListWithoutDevDependencies + @Test + fun `getPluginListWithoutDevDependencies removes dev dependencies from list`() { + val project = mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + every { + NativePluginLoaderReflectionBridge.getPlugins( + any(), + any() + ) + } returns pluginListWithDevDependency + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + val result = pluginHandler.getPluginListWithoutDevDependencies() + assertEquals(pluginListWithoutDevDependency, result) + } + + @Test + fun `getPluginListWithoutDevDependencies does not modify list without dev dependencies`() { + val project = mockk() + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + every { + NativePluginLoaderReflectionBridge.getPlugins( + any(), + any() + ) + } returns pluginListWithoutDevDependency + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + val result = pluginHandler.getPluginListWithoutDevDependencies() + assertEquals(pluginListWithoutDevDependency, result) + } + + // getPluginList skipped as it is a wrapper around a single reflection call + + // pluginSupportsAndroidPlatform + @Test + fun `pluginSupportsAndroidPlatform returns true when android directory exists with gradle build file`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + + val androidDir = tempDir.resolve("android") + androidDir.toFile().mkdirs() + File(androidDir.toFile(), "build.gradle").createNewFile() + + val mockProject = + mockk { + every { this@mockk.projectDir } returns projectDir.toFile() + } + + assertTrue { + PluginHandler.pluginSupportsAndroidPlatform(mockProject) + } // Replace YourClass with the actual class containing the method + } + + @Test + fun `pluginSupportsAndroidPlatform returns false when gradle build file does not exist`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + + val mockProject = + mockk { + every { this@mockk.projectDir } returns projectDir.toFile() + } + + assertFalse { + PluginHandler.pluginSupportsAndroidPlatform(mockProject) + } + } + + @Test + fun `configurePlugins throws IllegalArgumentException when plugin has no name`( + @TempDir tempDir: Path + ) { + val project = mockk() + + // configuration for configureLegacyPluginEachProjects + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + every { project.projectDir } returns projectDir.toFile() + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + val mockLogger = mockk() + every { project.logger } returns mockLogger + + val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() + pluginWithoutName.remove("name") + + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf( + pluginWithoutName + ) + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + val pluginHandler = PluginHandler(project) + assertThrows { + pluginHandler.configurePlugins( + engineVersionValue = EXAMPLE_ENGINE_VERSION + ) + } + } + + @Test + fun `configurePlugins adds plugin project and configures its dependencies`( + @TempDir tempDir: Path + ) { + val project = mockk() + + // configuration for configureLegacyPluginEachProjects + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + every { project.projectDir } returns projectDir.toFile() + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + val mockLogger = mockk() + every { project.logger } returns mockLogger + + val pluginProject = mockk() + val pluginDependencyProject = mockk() + val mockBuildType = mockk() + every { pluginProject.hasProperty("local-engine-repo") } returns false + every { pluginProject.hasProperty("android") } returns true + every { mockBuildType.name } returns "debug" + every { mockBuildType.isDebuggable } returns true + every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject + every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject + every { pluginProject.extensions.create(any(), any>()) } returns mockk() + val captureActionSlot = slot>() + val capturePluginActionSlot = mutableListOf>() + every { project.afterEvaluate(any>()) } returns Unit + every { pluginProject.afterEvaluate(any>()) } returns Unit + + val mockProjectBuildTypes = + mockk>() + val mockPluginProjectBuildTypes = + mockk>() + every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes + every { mockPluginProjectBuildTypes.addAll(any()) } returns true + every { pluginProject.configurations.named(any()) } returns mockk() + every { pluginProject.dependencies.add(any(), any()) } returns mockk() + + every { + project.extensions + .findByType(BaseExtension::class.java)!! + .buildTypes + .iterator() + } returns + mutableListOf( + mockBuildType + ).iterator() andThen + mutableListOf( // can't return the same iterator as it is stateful + mockBuildType + ).iterator() andThen + mutableListOf( // and again + mockBuildType + ).iterator() + every { project.dependencies.add(any(), any()) } returns mockk() + every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + val pluginWithDependencies: MutableMap = cameraDependency.toMutableMap() + pluginWithDependencies["dependencies"] = + listOf(flutterPluginAndroidLifecycleDependency["name"]) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf( + pluginWithDependencies + ) + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + pluginHandler.configurePlugins( + engineVersionValue = EXAMPLE_ENGINE_VERSION + ) + + verify { project.afterEvaluate(capture(captureActionSlot)) } + verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) } + captureActionSlot.captured.execute(project) + capturePluginActionSlot[0].execute(pluginProject) + capturePluginActionSlot[1].execute(pluginProject) + verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) } + verify { + pluginProject.dependencies.add( + "debugApi", + "io.flutter:flutter_embedding_debug:$EXAMPLE_ENGINE_VERSION" + ) + } + verify { project.dependencies.add("debugApi", pluginProject) } + verify { mockLogger wasNot called } + verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) } + + verify { pluginProject.dependencies.add("implementation", pluginDependencyProject) } + } + + @Test + fun `configurePlugins throws IllegalArgumentException when plugin has null dependencies`( + @TempDir tempDir: Path + ) { + val project = mockk() + + // configuration for configureLegacyPluginEachProjects + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + every { project.projectDir } returns projectDir.toFile() + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + val mockLogger = mockk() + every { project.logger } returns mockLogger + + val pluginProject = mockk() + val mockBuildType = mockk() + every { pluginProject.hasProperty("local-engine-repo") } returns false + every { pluginProject.hasProperty("android") } returns true + every { mockBuildType.name } returns "debug" + every { mockBuildType.isDebuggable } returns true + val pluginWithNullDependencies: MutableMap = cameraDependency.toMutableMap() + pluginWithNullDependencies["dependencies"] = null + every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject + every { pluginProject.extensions.create(any(), any>()) } returns mockk() + every { project.afterEvaluate(any>()) } returns Unit + every { pluginProject.afterEvaluate(any>()) } returns Unit + + val mockProjectBuildTypes = + mockk>() + val mockPluginProjectBuildTypes = + mockk>() + every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes + every { mockPluginProjectBuildTypes.addAll(any()) } returns true + every { pluginProject.configurations.named(any()) } returns mockk() + every { pluginProject.dependencies.add(any(), any()) } returns mockk() + + every { + project.extensions + .findByType(BaseExtension::class.java)!! + .buildTypes + .iterator() + } returns + mutableListOf( + mockBuildType + ).iterator() andThen + mutableListOf( // can't return the same iterator as it is stateful + mockBuildType + ).iterator() andThen + mutableListOf( // and again + mockBuildType + ).iterator() + every { project.dependencies.add(any(), any()) } returns mockk() + every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf( + pluginWithNullDependencies + ) + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + assertThrows { + pluginHandler.configurePlugins( + engineVersionValue = EXAMPLE_ENGINE_VERSION + ) + } + } + + @Test + fun `configurePlugins works for old flutter-plugins file`( + @TempDir tempDir: Path + ) { + val project = mockk() + + // configuration for configureLegacyPluginEachProjects + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + every { project.projectDir } returns projectDir.toFile() + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + settingsGradle.writeText("def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')") + val mockLogger = mockk() + every { project.logger } returns mockLogger + every { mockLogger.quiet(any()) } returns Unit + + val pluginProject = mockk() + val pluginDependencyProject = mockk() + val mockBuildType = mockk() + every { pluginProject.hasProperty("local-engine-repo") } returns false + every { pluginProject.hasProperty("android") } returns true + every { mockBuildType.name } returns "debug" + every { mockBuildType.isDebuggable } returns true + every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject + every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject + every { pluginProject.extensions.create(any(), any>()) } returns mockk() + val captureActionSlot = slot>() + val capturePluginActionSlot = mutableListOf>() + every { project.afterEvaluate(any>()) } returns Unit + every { pluginProject.afterEvaluate(any>()) } returns Unit + + val mockProjectBuildTypes = + mockk>() + val mockPluginProjectBuildTypes = + mockk>() + every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes + every { mockPluginProjectBuildTypes.addAll(any()) } returns true + every { pluginProject.configurations.named(any()) } returns mockk() + every { pluginProject.dependencies.add(any(), any()) } returns mockk() + + every { + project.extensions + .findByType(BaseExtension::class.java)!! + .buildTypes + .iterator() + } returns + mutableListOf( + mockBuildType + ).iterator() andThen + mutableListOf( // can't return the same iterator as it is stateful + mockBuildType + ).iterator() andThen + mutableListOf( // and again + mockBuildType + ).iterator() + every { project.dependencies.add(any(), any()) } returns mockk() + every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + + val pluginHandler = PluginHandler(project) + mockkObject(NativePluginLoaderReflectionBridge) + // mock return of NativePluginLoaderReflectionBridge.getPlugins + val pluginWithDependencies: MutableMap = cameraDependency.toMutableMap() + pluginWithDependencies["dependencies"] = + listOf(flutterPluginAndroidLifecycleDependency["name"]) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf( + pluginWithDependencies + ) + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge.getPlugins + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + + val dependencyGraph = + listOf>( + mapOf( + "name" to cameraDependency["name"], + "dependencies" to listOf(flutterPluginAndroidLifecycleDependency["name"]) + ), + mapOf( + "name" to flutterPluginAndroidLifecycleDependency["name"], + "dependencies" to listOf() + ) + ) + + every { NativePluginLoaderReflectionBridge.getDependenciesMetadata(any(), any()) } returns + mapOf("dependencyGraph" to dependencyGraph) + + pluginHandler.configurePlugins( + engineVersionValue = EXAMPLE_ENGINE_VERSION + ) + + verify { project.afterEvaluate(capture(captureActionSlot)) } + verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) } + captureActionSlot.captured.execute(project) + capturePluginActionSlot[0].execute(pluginProject) + capturePluginActionSlot[1].execute(pluginProject) + verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) } + verify { + pluginProject.dependencies.add( + "debugApi", + "io.flutter:flutter_embedding_debug:$EXAMPLE_ENGINE_VERSION" + ) + } + verify { project.dependencies.add("debugApi", pluginProject) } + verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) } + + verify { pluginProject.dependencies.add("implementation", pluginDependencyProject) } + verify { mockLogger.quiet(PluginHandler.legacyFlutterPluginsWarning) } + } +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/tasks/BaseFlutterTaskHelperTest.kt similarity index 99% rename from packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt rename to packages/flutter_tools/gradle/src/test/kotlin/tasks/BaseFlutterTaskHelperTest.kt index 35645393a0..92565f11d6 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/tasks/BaseFlutterTaskHelperTest.kt @@ -1,5 +1,10 @@ -package com.flutter.gradle +// 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.tasks + +import com.flutter.gradle.DependencyVersionChecker import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/tasks/FlutterTaskHelperTest.kt similarity index 96% rename from packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt rename to packages/flutter_tools/gradle/src/test/kotlin/tasks/FlutterTaskHelperTest.kt index 53b279fdbc..433e9d6fd4 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/tasks/FlutterTaskHelperTest.kt @@ -1,5 +1,10 @@ -package com.flutter.gradle +// 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.tasks + +import com.flutter.gradle.FlutterPluginConstants import io.mockk.every import io.mockk.mockk import io.mockk.slot diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index d49fd74796..e0c644fd0c 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -42,7 +42,7 @@ import 'migrations/top_level_gradle_build_file_migration.dart'; /// The regex to grab variant names from printBuildVariants gradle task /// -/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt /// /// The expected output from the task should be similar to: /// diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 4d14c14d4d..4fdeb11d04 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -917,7 +917,7 @@ const String kAndroidArchs = 'AndroidArchs'; /// /// If not provided, defaults to `minSdkVersion` from gradle_utils.dart. /// -/// This is passed in by flutter.groovy's invocation of `flutter assemble`. +/// This is passed in by the Flutter Gradle plugin's invocation of `flutter assemble`. /// /// For more info, see: /// https://developer.android.com/ndk/guides/sdk-versions#minsdkversion