From 2f72db6c7f7b9aaf5c125e67bf89154865404718 Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" <98614782+auto-submit[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 02:51:47 +0000 Subject: [PATCH] Reverts "Convert the Flutter Gradle Plugin entirely to Kotlin source (#166114)" (#166666) Reverts: flutter/flutter#166114 Initiated by: gmackall Reason for reverting: a failing postsubmit Original PR Author: gmackall Reviewed By: {bartekpacia, reidbaker} This change reverts the following previous change: Finishes the conversion of the Flutter Gradle plugin from Groovy to Kotlin. Fixes https://github.com/flutter/flutter/issues/121541. Fixes https://github.com/flutter/flutter/issues/166287. ## 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: auto-submit[bot] --- .../flutter_tools/gradle/build.gradle.kts | 2 +- .../main/{scripts => groovy}/CMakeLists.txt | 0 .../gradle/src/main/groovy/flutter.groovy | 722 +++++++++++++++++ .../kotlin/{tasks => }/BaseFlutterTask.kt | 2 +- .../{tasks => }/BaseFlutterTaskHelper.kt | 2 +- .../gradle/src/main/kotlin/FlutterPlugin.kt | 741 ------------------ .../src/main/kotlin/FlutterPluginConstants.kt | 3 +- .../src/main/kotlin/FlutterPluginUtils.kt | 163 +++- .../main/kotlin/{tasks => }/FlutterTask.kt | 2 +- .../kotlin/{tasks => }/FlutterTaskHelper.kt | 3 +- .../NativePluginLoaderReflectionBridge.kt | 18 +- .../src/main/kotlin/plugins/PluginHandler.kt | 326 -------- .../scripts/native_plugin_loader.gradle.kts | 58 +- .../kotlin/BaseApplicationNameHandlerTest.kt | 5 - .../{tasks => }/BaseFlutterTaskHelperTest.kt | 7 +- .../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 +++++- .../{tasks => }/FlutterTaskHelperTest.kt | 7 +- .../src/test/kotlin/IntentFilterCheckTest.kt | 4 - .../src/test/kotlin/VersionUtilsTest.kt | 4 - .../test/kotlin/plugins/PluginHandlerTest.kt | 443 ----------- .../flutter_tools/lib/src/android/gradle.dart | 2 +- .../flutter_tools/lib/src/build_info.dart | 2 +- 26 files changed, 1121 insertions(+), 1745 deletions(-) rename packages/flutter_tools/gradle/src/main/{scripts => groovy}/CMakeLists.txt (100%) create mode 100644 packages/flutter_tools/gradle/src/main/groovy/flutter.groovy rename packages/flutter_tools/gradle/src/main/kotlin/{tasks => }/BaseFlutterTask.kt (99%) rename packages/flutter_tools/gradle/src/main/kotlin/{tasks => }/BaseFlutterTaskHelper.kt (99%) delete mode 100644 packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt rename packages/flutter_tools/gradle/src/main/kotlin/{tasks => }/FlutterTask.kt (98%) rename packages/flutter_tools/gradle/src/main/kotlin/{tasks => }/FlutterTaskHelper.kt (97%) delete mode 100644 packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt rename packages/flutter_tools/gradle/src/test/kotlin/{tasks => }/BaseFlutterTaskHelperTest.kt (99%) delete mode 100644 packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt rename packages/flutter_tools/gradle/src/test/kotlin/{tasks => }/FlutterTaskHelperTest.kt (96%) delete mode 100644 packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 0797f58275..b3863fd450 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 = "com.flutter.gradle.FlutterPlugin" + implementationClass = "FlutterPlugin" } // The "flutterAppPluginLoaderPlugin" name isn't used anywhere. create("flutterAppPluginLoaderPlugin") { diff --git a/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt b/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt similarity index 100% rename from packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt rename to packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy new file mode 100644 index 0000000000..24fe304665 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -0,0 +1,722 @@ +/* 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/tasks/BaseFlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt similarity index 99% rename from packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt rename to packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt index cb9d45d892..62e74dddf7 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/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.tasks +package com.flutter.gradle import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt similarity index 99% rename from packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt rename to packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt index bfb0bcc5d3..fa47257a0e 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/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.tasks +package com.flutter.gradle import androidx.annotation.VisibleForTesting import org.gradle.api.Action diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt deleted file mode 100644 index d6d75a72ce..0000000000 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ /dev/null @@ -1,741 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -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() - 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 8f8c0bda56..d4fe426e26 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt @@ -25,6 +25,7 @@ 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 = @@ -41,7 +42,7 @@ object FlutterPluginConstants { * Otherwise, the Play Store will complain that the APK variants have the same version. */ @JvmStatic val ABI_VERSION = - mapOf( + mapOf( // Explicit type for clarity, though inferred 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 fc1fd7a454..1dcfb3b4fc 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -10,7 +10,6 @@ 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 @@ -140,6 +139,19 @@ 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 @@ -394,7 +406,7 @@ object FlutterPluginUtils { return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode } - internal fun getAndroidExtension(project: Project): BaseExtension { + private 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)!! @@ -599,7 +611,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/scripts/CMakeLists.txt" + "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt" ) // AGP defaults to outputting build artifacts in `android/app/.cxx`. This directory is a @@ -644,7 +656,7 @@ object FlutterPluginUtils { internal fun addFlutterDependencies( project: Project, buildType: BuildType, - pluginHandler: PluginHandler, + pluginList: List>, engineVersion: String ) { val flutterBuildMode: String = buildModeFor(buildType) @@ -664,9 +676,11 @@ object FlutterPluginUtils { // embedding. val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List> = if (flutterBuildMode == "release") { - pluginHandler.getPluginListWithoutDevDependencies() + getPluginListWithoutDevDependencies( + pluginList + ) } else { - pluginHandler.getPluginList() + pluginList } if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) { @@ -688,6 +702,143 @@ 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/tasks/FlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt similarity index 98% rename from packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt rename to packages/flutter_tools/gradle/src/main/kotlin/FlutterTask.kt index 86e5ad0bb4..3ff14a5247 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/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.tasks +package com.flutter.gradle import org.gradle.api.file.CopySpec import org.gradle.api.file.FileCollection diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt similarity index 97% rename from packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt rename to packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt index 8909602d02..a99a9ea811 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterTaskHelper.kt @@ -2,9 +2,8 @@ // 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 +package com.flutter.gradle -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/kotlin/NativePluginLoaderReflectionBridge.kt b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt index 060456c254..df90089516 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/NativePluginLoaderReflectionBridge.kt @@ -17,21 +17,24 @@ 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> { - val nativePluginLoader = extraProperties.get("nativePluginLoader")!! + ): List> { + 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 } @@ -39,15 +42,16 @@ 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 { - val nativePluginLoader = extraProperties.get("nativePluginLoader")!! + 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 deleted file mode 100644 index b452435200..0000000000 --- a/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -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/scripts/native_plugin_loader.gradle.kts b/packages/flutter_tools/gradle/src/main/scripts/native_plugin_loader.gradle.kts index eab9482ed4..38fc5889bc 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,63 +72,9 @@ class NativePluginLoader { */ fun getDependenciesMetadata(flutterSourceDirectory: File): Map? { // Consider a `.flutter-plugins-dependencies` file with the following content: - // { - // "plugins": { - // "android": [ - // { - // "name": "plugin-a", - // "path": "/path/to/plugin-a", - // "dependencies": ["plugin-b", "plugin-c"], - // "native_build": true - // "dev_dependency": false - // }, - // { - // "name": "plugin-b", - // "path": "/path/to/plugin-b", - // "dependencies": ["plugin-c"], - // "native_build": true - // "dev_dependency": false - // }, - // { - // "name": "plugin-c", - // "path": "/path/to/plugin-c", - // "dependencies": [], - // "native_build": true - // "dev_dependency": false - // }, - // { - // "name": "plugin-d", - // "path": "/path/to/plugin-d", - // "dependencies": [], - // "native_build": true - // "dev_dependency": true - // }, - // ], - // }, - // "dependencyGraph": [ - // { - // "name": "plugin-a", - // "dependencies": ["plugin-b","plugin-c"] - // }, - // { - // "name": "plugin-b", - // "dependencies": ["plugin-c"] - // }, - // { - // "name": "plugin-c", - // "dependencies": [] - // }, - // { - // "name": "plugin-d", - // "dependencies": [] - // } - // ] - // } + // { ... (example content as in the original Groovy code) ... } // This means, `plugin-a` depends on `plugin-b` and `plugin-c`. - // `plugin-b` depends on `plugin-c`. - // `plugin-c` doesn't depend on anything. - // `plugin-d` also doesn't depend on anything, but it is a dev - // dependency to the Flutter project, so it is marked as such. + // ... (rest of the comment as in the original Groovy code) ... 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 e0d6b0ef18..18604feb3c 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt @@ -1,9 +1,4 @@ -// 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/tasks/BaseFlutterTaskHelperTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt similarity index 99% rename from packages/flutter_tools/gradle/src/test/kotlin/tasks/BaseFlutterTaskHelperTest.kt rename to packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt index 92565f11d6..35645393a0 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/tasks/BaseFlutterTaskHelperTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/BaseFlutterTaskHelperTest.kt @@ -1,10 +1,5 @@ -// 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 -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/DeeplinkTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt index ae9625a2f3..3310814489 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt @@ -1,7 +1,3 @@ -// 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 8f11bb7dbe..0ad3930ea0 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt @@ -1,7 +1,3 @@ -// 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 6c78f5c6d9..cc58d29b13 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterExtensionTest.kt @@ -1,7 +1,3 @@ -// 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 deleted file mode 100644 index 8d8f917675..0000000000 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -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 0fc36ec38a..38c655a3c8 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -1,7 +1,3 @@ -// 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 @@ -12,26 +8,25 @@ 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 @@ -41,10 +36,12 @@ 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 { - const val EXAMPLE_ENGINE_VERSION = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" + val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" val devDependency: Map = mapOf( @@ -222,6 +219,45 @@ 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`( @@ -836,7 +872,7 @@ class FlutterPluginUtilsTest { verify(exactly = 1) { mockCmakeOptions.path } - verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt") } + verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt") } verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) } verify(exactly = 1) { mockDefaultConfig.externalNativeBuild.cmake.arguments( @@ -852,12 +888,6 @@ 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 @@ -869,7 +899,7 @@ class FlutterPluginUtilsTest { FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginHandler = pluginHandler, + pluginList = pluginListWithoutDevDependency, engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23" ) @@ -884,14 +914,8 @@ 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 = EXAMPLE_ENGINE_VERSION + val engineVersion = exampleEngineVersion every { buildType.name } returns "debug" every { buildType.isDebuggable } returns true every { project.hasProperty("local-engine-repo") } returns false @@ -903,7 +927,7 @@ class FlutterPluginUtilsTest { FlutterPluginUtils.addFlutterDependencies( project = project, buildType = buildType, - pluginHandler = pluginHandler, + pluginList = pluginListWithoutDevDependency, engineVersion = engineVersion ) @@ -921,15 +945,8 @@ 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 = EXAMPLE_ENGINE_VERSION + val engineVersion = exampleEngineVersion every { buildType.name } returns "release" every { buildType.isDebuggable } returns false every { project.hasProperty("local-engine-repo") } returns false @@ -938,10 +955,12 @@ 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, - pluginHandler = pluginHandler, + pluginList = pluginListWithSingleDevDependency, engineVersion = engineVersion ) @@ -975,15 +994,8 @@ 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 = EXAMPLE_ENGINE_VERSION + val engineVersion = exampleEngineVersion every { buildType.name } returns "debug" every { buildType.isDebuggable } returns true every { project.hasProperty("local-engine-repo") } returns false @@ -992,10 +1004,12 @@ 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, - pluginHandler = pluginHandler, + pluginList = pluginListWithSingleDevDependency, engineVersion = engineVersion ) @@ -1020,6 +1034,168 @@ 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/tasks/FlutterTaskHelperTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt similarity index 96% rename from packages/flutter_tools/gradle/src/test/kotlin/tasks/FlutterTaskHelperTest.kt rename to packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt index 433e9d6fd4..53b279fdbc 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/tasks/FlutterTaskHelperTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterTaskHelperTest.kt @@ -1,10 +1,5 @@ -// 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 -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/gradle/src/test/kotlin/IntentFilterCheckTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt index b95f295a47..836c6197d8 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/IntentFilterCheckTest.kt @@ -1,7 +1,3 @@ -// 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 d1230549a7..c665907b39 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/VersionUtilsTest.kt @@ -1,7 +1,3 @@ -// 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 deleted file mode 100644 index 29d40d1573..0000000000 --- a/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -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/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index e0c644fd0c..d49fd74796 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/kotlin/FlutterPluginUtils.kt +/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy /// /// 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 4fdeb11d04..4d14c14d4d 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 the Flutter Gradle plugin's invocation of `flutter assemble`. +/// This is passed in by flutter.groovy's invocation of `flutter assemble`. /// /// For more info, see: /// https://developer.android.com/ndk/guides/sdk-versions#minsdkversion