| // 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 static groovy.io.FileType.FILES |
| |
| import com.android.builder.model.AndroidProject |
| import com.android.build.OutputFile |
| import groovy.json.JsonSlurper |
| import java.nio.file.Path |
| import java.nio.file.Paths |
| import java.util.regex.Matcher |
| import java.util.regex.Pattern |
| import org.apache.tools.ant.taskdefs.condition.Os |
| import org.gradle.api.DefaultTask |
| import org.gradle.api.GradleException |
| import org.gradle.api.Project |
| import org.gradle.api.Plugin |
| import org.gradle.api.Task |
| import org.gradle.api.file.CopySpec |
| import org.gradle.api.file.FileCollection |
| import org.gradle.api.tasks.Copy |
| import org.gradle.api.tasks.InputFiles |
| import org.gradle.api.tasks.OutputDirectory |
| import org.gradle.api.tasks.TaskAction |
| import org.gradle.api.tasks.bundling.Jar |
| import org.gradle.internal.os.OperatingSystem |
| import org.gradle.util.VersionNumber |
| |
| /** For apps only. Provides the flutter extension used in app/build.gradle. */ |
| class FlutterExtension { |
| /** Sets the compileSdkVersion used by default in Flutter app projects. */ |
| static int compileSdkVersion = 31 |
| |
| /** Sets the minSdkVersion used by default in Flutter app projects. */ |
| static int minSdkVersion = 16 |
| |
| /** Sets the targetSdkVersion used by default in Flutter app projects. */ |
| static int targetSdkVersion = 31 |
| |
| /** |
| * Specifies the relative directory to the Flutter project directory. |
| * In an app project, this is ../.. since the app's build.gradle is under android/app. |
| */ |
| String source |
| |
| /** Allows to override the target file. Otherwise, the target is lib/main.dart. */ |
| String target |
| } |
| |
| buildscript { |
| repositories { |
| google() |
| mavenCentral() |
| } |
| dependencies { |
| classpath 'com.android.tools.build:gradle:4.1.0' |
| } |
| } |
| |
| /** |
| * Some apps don't set default compile options. |
| * Apps can change these values in android/app/build.gradle. |
| * This just ensures that default values are set. |
| */ |
| android { |
| compileOptions { |
| sourceCompatibility JavaVersion.VERSION_1_8 |
| targetCompatibility JavaVersion.VERSION_1_8 |
| } |
| } |
| |
| apply plugin: FlutterPlugin |
| |
| class FlutterPlugin implements Plugin<Project> { |
| private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"; |
| |
| /** The platforms that can be passed to the `--Ptarget-platform` flag. */ |
| private static final String PLATFORM_ARM32 = "android-arm"; |
| private static final String PLATFORM_ARM64 = "android-arm64"; |
| private static final String PLATFORM_X86 = "android-x86"; |
| private static final String PLATFORM_X86_64 = "android-x64"; |
| |
| /** The ABI architectures supported by Flutter. */ |
| private static final String ARCH_ARM32 = "armeabi-v7a"; |
| private static final String ARCH_ARM64 = "arm64-v8a"; |
| private static final String ARCH_X86 = "x86"; |
| private static final String ARCH_X86_64 = "x86_64"; |
| |
| /** Maps platforms to ABI architectures. */ |
| private static final Map PLATFORM_ARCH_MAP = [ |
| (PLATFORM_ARM32) : ARCH_ARM32, |
| (PLATFORM_ARM64) : ARCH_ARM64, |
| (PLATFORM_X86) : ARCH_X86, |
| (PLATFORM_X86_64) : ARCH_X86_64, |
| ] |
| |
| /** |
| * The version code that gives each ABI a value. |
| * For each APK variant, use the following versions to override the version of the Universal APK. |
| * Otherwise, the Play Store will complain that the APK variants have the same version. |
| */ |
| private static final Map ABI_VERSION = [ |
| (ARCH_ARM32) : 1, |
| (ARCH_ARM64) : 2, |
| (ARCH_X86) : 3, |
| (ARCH_X86_64) : 4, |
| ] |
| |
| /** When split is enabled, multiple APKs are generated per each ABI. */ |
| private static final List DEFAULT_PLATFORMS = [ |
| PLATFORM_ARM32, |
| PLATFORM_ARM64, |
| PLATFORM_X86_64, |
| ] |
| |
| /** |
| * 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 Map baseJar = [:] |
| private File flutterRoot |
| private File flutterExecutable |
| private String localEngine |
| private String localEngineSrcPath |
| private Properties localProperties |
| private String engineVersion |
| |
| @Override |
| void apply(Project project) { |
| this.project = project |
| |
| def rootProject = project.rootProject |
| if (isFlutterAppProject()) { |
| rootProject.tasks.register('generateLockfiles') { |
| rootProject.subprojects.each { subproject -> |
| def gradlew = (OperatingSystem.current().isWindows()) ? |
| "${rootProject.projectDir}/gradlew.bat" : "${rootProject.projectDir}/gradlew" |
| rootProject.exec { |
| workingDir rootProject.projectDir |
| executable gradlew |
| args ":${subproject.name}:dependencies", "--write-locks" |
| } |
| } |
| } |
| } |
| |
| // Configure the Maven repository. |
| String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST |
| String repository = useLocalEngine() |
| ? project.property('local-engine-repo') |
| : "$hostedRepository/download.flutter.io" |
| rootProject.allprojects { |
| repositories { |
| maven { |
| url repository |
| } |
| } |
| } |
| |
| project.extensions.create("flutter", FlutterExtension) |
| this.addFlutterTasks(project) |
| |
| // 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 (shouldSplitPerAbi()) { |
| 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 |
| } |
| } |
| } |
| } |
| |
| if (project.hasProperty('deferred-component-names')) { |
| String[] componentNames = project.property('deferred-component-names').split(',').collect {":${it}"} |
| project.android { |
| dynamicFeatures = componentNames |
| } |
| } |
| |
| getTargetPlatforms().each { targetArch -> |
| String abiValue = PLATFORM_ARCH_MAP[targetArch] |
| project.android { |
| if (shouldSplitPerAbi()) { |
| splits { |
| abi { |
| include abiValue |
| } |
| } |
| } |
| } |
| } |
| |
| String flutterRootPath = resolveProperty("flutter.sdk", System.env.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 = useLocalEngine() |
| ? "+" // Match any version since there's only one. |
| : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim() |
| |
| String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter" |
| flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile(); |
| |
| 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 (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. |
| // NOTE: The resource shrinker can't be used for libraries. |
| shrinkResources 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.txt"), flutterProguardRules, "proguard-rules.pro" |
| } |
| } |
| } |
| |
| if (project.hasProperty("multidex-enabled") && |
| project.property("multidex-enabled").toBoolean() && |
| project.android.defaultConfig.minSdkVersion <= 20) { |
| String flutterMultidexKeepfile = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", |
| "gradle", "flutter_multidex_keepfile.txt") |
| project.android { |
| defaultConfig { |
| multiDexEnabled true |
| manifestPlaceholders = [applicationName: "io.flutter.app.FlutterMultiDexApplication"] |
| } |
| buildTypes { |
| release { |
| multiDexKeepFile project.file(flutterMultidexKeepfile) |
| } |
| } |
| } |
| project.dependencies { |
| implementation "androidx.multidex:multidex:2.0.1" |
| } |
| } else { |
| String baseApplicationName = "android.app.Application" |
| if (project.hasProperty("base-application-name")) { |
| baseApplicationName = project.property("base-application-name") |
| } |
| project.android { |
| defaultConfig { |
| // Setting to android.app.Application is the same as omitting the attribute. |
| manifestPlaceholders = [applicationName: baseApplicationName] |
| } |
| } |
| } |
| |
| if (useLocalEngine()) { |
| // 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 |
| } |
| project.android.buildTypes.all this.&addFlutterDependencies |
| } |
| |
| private static Boolean shouldShrinkResources(Project project) { |
| if (project.hasProperty("shrink")) { |
| return project.property("shrink").toBoolean() |
| } |
| return true |
| } |
| |
| /** |
| * Adds the dependencies required by the Flutter project. |
| * This includes: |
| * 1. The embedding |
| * 2. libflutter.so |
| */ |
| void addFlutterDependencies(buildType) { |
| String flutterBuildMode = buildModeFor(buildType) |
| if (!supportsBuildMode(flutterBuildMode)) { |
| return |
| } |
| // The embedding is set as an API dependency in a Flutter plugin. |
| // Therefore, don't make the app project depend on the embedding if there are Flutter |
| // plugins. |
| // This prevents duplicated classes when using custom build types. That is, a custom build |
| // type like profile is used, and the plugin and app projects have API dependencies on the |
| // embedding. |
| if (!isFlutterAppProject() || getPluginList().size() == 0) { |
| addApiDependencies(project, buildType.name, |
| "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") |
| } |
| List<String> platforms = getTargetPlatforms().collect() |
| // Debug mode includes x86 and x64, which are commonly used in emulators. |
| if (flutterBuildMode == "debug" && !useLocalEngine()) { |
| platforms.add("android-x86") |
| platforms.add("android-x64") |
| } |
| platforms.each { platform -> |
| String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_") |
| // Add the `libflutter.so` dependency. |
| addApiDependencies(project, buildType.name, |
| "io.flutter:${arch}_$flutterBuildMode:$engineVersion") |
| } |
| } |
| |
| /** |
| * Returns the directory where the plugins are built. |
| */ |
| private File getPluginBuildDir() { |
| // Module projects specify this flag to include plugins in the same repo as the module project. |
| if (project.ext.has("pluginBuildDir")) { |
| return project.ext.get("pluginBuildDir") |
| } |
| return project.buildDir |
| } |
| |
| /** |
| * Configures the Flutter plugin dependencies. |
| * |
| * The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`, |
| * the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location. |
| * Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject. |
| */ |
| private void configurePlugins() { |
| getPluginList().each this.&configurePluginProject |
| getPluginDependencies().each this.&configurePluginDependencies |
| } |
| |
| /** Adds the plugin project dependency to the app project. */ |
| private void configurePluginProject(String pluginName, String _) { |
| Project pluginProject = project.rootProject.findProject(":$pluginName") |
| if (pluginProject == null) { |
| project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.") |
| return |
| } |
| // Add plugin dependency to the app project. |
| project.dependencies { |
| api pluginProject |
| } |
| Closure addEmbeddingDependencyToPlugin = { buildType -> |
| String flutterBuildMode = buildModeFor(buildType) |
| // 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(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. |
| pluginProject.android.buildTypes { |
| "${buildType.name}" {} |
| } |
| // 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" |
| ) |
| } |
| |
| // Wait until the Android plugin loaded. |
| pluginProject.afterEvaluate { |
| if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) { |
| project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${pluginProject.android.compileSdkVersion.substring(8)}.") |
| } |
| project.android.buildTypes.all addEmbeddingDependencyToPlugin |
| } |
| } |
| |
| /** Prints error message and fix for any plugin compileSdkVersion that are higher than the project. */ |
| private void detectLowCompileSdkVersion() { |
| project.afterEvaluate { |
| int projectCompileSdkVersion = project.android.compileSdkVersion.substring(8) as int |
| int maxPluginCompileSdkVersion = projectCompileSdkVersion |
| int numProcessedPlugins = getPluginList().size() |
| |
| getPluginList().each { plugin -> |
| Project pluginProject = project.rootProject.findProject(plugin.key) |
| pluginProject.afterEvaluate { |
| int pluginCompileSdkVersion = pluginProject.android.compileSdkVersion.substring(8) as int |
| maxPluginCompileSdkVersion = Math.max(pluginCompileSdkVersion, maxPluginCompileSdkVersion) |
| |
| numProcessedPlugins-- |
| if (numProcessedPlugins == 0) { |
| if (maxPluginCompileSdkVersion > projectCompileSdkVersion) { |
| project.logger.error("One or more plugins require a higher Android SDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n compileSdkVersion ${maxPluginCompileSdkVersion}\n ...\n}\n") |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns `true` if the given path contains an `android/build.gradle` file. |
| */ |
| private Boolean doesSupportAndroidPlatform(String path) { |
| File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle') |
| return editableAndroidProject.exists() |
| } |
| |
| /** |
| * Add the dependencies on other plugin projects to the plugin project. |
| * A plugin A can depend on plugin B. As a result, this dependency must be surfaced by |
| * making the Gradle plugin project A depend on the Gradle plugin project B. |
| */ |
| private void configurePluginDependencies(Object dependencyObject) { |
| assert dependencyObject.name instanceof String |
| Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}") |
| if (pluginProject == null || |
| !doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) { |
| return |
| } |
| assert dependencyObject.dependencies instanceof List |
| dependencyObject.dependencies.each { pluginDependencyName -> |
| assert pluginDependencyName instanceof String |
| if (pluginDependencyName.empty) { |
| return |
| } |
| Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName") |
| if (dependencyProject == null || |
| !doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) { |
| return |
| } |
| // Wait for the Android plugin to load and add the dependency to the plugin project. |
| pluginProject.afterEvaluate { |
| pluginProject.dependencies { |
| implementation dependencyProject |
| } |
| } |
| } |
| } |
| |
| private Properties getPluginList() { |
| File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins') |
| Properties allPlugins = readPropertiesIfExist(pluginsFile) |
| Properties androidPlugins = new Properties() |
| allPlugins.each { name, path -> |
| if (doesSupportAndroidPlatform(path)) { |
| androidPlugins.setProperty(name, path) |
| } |
| // TODO(amirh): log an error if this plugin was specified to be an Android |
| // plugin according to the new schema, and was missing a build.gradle file. |
| // https://github.com/flutter/flutter/issues/40784 |
| } |
| return androidPlugins |
| } |
| |
| /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */ |
| private List getPluginDependencies() { |
| // Consider a `.flutter-plugins-dependencies` file with the following content: |
| // { |
| // "dependencyGraph": [ |
| // { |
| // "name": "plugin-a", |
| // "dependencies": ["plugin-b","plugin-c"] |
| // }, |
| // { |
| // "name": "plugin-b", |
| // "dependencies": ["plugin-c"] |
| // }, |
| // { |
| // "name": "plugin-c", |
| // "dependencies": []' |
| // } |
| // ] |
| // } |
| // |
| // This means, `plugin-a` depends on `plugin-b` and `plugin-c`. |
| // `plugin-b` depends on `plugin-c`. |
| // `plugin-c` doesn't depend on anything. |
| File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies') |
| if (pluginsDependencyFile.exists()) { |
| def object = new JsonSlurper().parseText(pluginsDependencyFile.text) |
| assert object instanceof Map |
| assert object.dependencyGraph instanceof List |
| return object.dependencyGraph |
| } |
| return [] |
| } |
| |
| private static String toCammelCase(List<String> parts) { |
| if (parts.empty) { |
| return "" |
| } |
| return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}" |
| } |
| |
| private String resolveProperty(String name, String defaultValue) { |
| if (localProperties == null) { |
| localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties")) |
| } |
| String result |
| if (project.hasProperty(name)) { |
| result = project.property(name) |
| } |
| if (result == null) { |
| result = localProperties.getProperty(name) |
| } |
| if (result == null) { |
| result = defaultValue |
| } |
| return result |
| } |
| |
| private static Properties readPropertiesIfExist(File propertiesFile) { |
| Properties result = new Properties() |
| if (propertiesFile.exists()) { |
| propertiesFile.withReader('UTF-8') { reader -> result.load(reader) } |
| } |
| return result |
| } |
| |
| private List<String> getTargetPlatforms() { |
| if (!project.hasProperty('target-platform')) { |
| return DEFAULT_PLATFORMS |
| } |
| return project.property('target-platform').split(',').collect { |
| if (!PLATFORM_ARCH_MAP[it]) { |
| throw new GradleException("Invalid platform: $it.") |
| } |
| return it |
| } |
| } |
| |
| private Boolean shouldSplitPerAbi() { |
| if (project.hasProperty('split-per-abi')) { |
| return project.property('split-per-abi').toBoolean() |
| } |
| return false; |
| } |
| |
| private Boolean useLocalEngine() { |
| return project.hasProperty('local-engine-repo') |
| } |
| |
| private Boolean isVerbose() { |
| if (project.hasProperty('verbose')) { |
| return project.property('verbose').toBoolean() |
| } |
| return false |
| } |
| |
| /** Whether to build the debug app in "fast-start" mode. */ |
| private Boolean isFastStart() { |
| if (project.hasProperty("fast-start")) { |
| return project.property("fast-start").toBoolean() |
| } |
| return false |
| } |
| |
| private static Boolean isBuiltAsApp(Project project) { |
| // Projects are built as applications when the they use the `com.android.application` |
| // plugin. |
| return project.plugins.hasPlugin("com.android.application"); |
| } |
| |
| /** |
| * Returns true if the build mode is supported by the current call to Gradle. |
| * This only relevant when using a local engine. Because the engine |
| * is built for a specific mode, the call to Gradle must match that mode. |
| */ |
| private Boolean supportsBuildMode(String flutterBuildMode) { |
| if (!useLocalEngine()) { |
| return true; |
| } |
| assert project.hasProperty('local-engine-build-mode') |
| // Don't configure dependencies for a build mode that the local engine |
| // doesn't support. |
| return project.property('local-engine-build-mode') == flutterBuildMode |
| } |
| |
| private void addCompileOnlyDependency(Project project, String variantName, Object dependency, Closure config = null) { |
| if (project.state.failure) { |
| return |
| } |
| String configuration; |
| if (project.getConfigurations().findByName("compileOnly")) { |
| configuration = "${variantName}CompileOnly"; |
| } else { |
| configuration = "${variantName}Provided"; |
| } |
| project.dependencies.add(configuration, dependency, config) |
| } |
| |
| private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) { |
| String configuration; |
| // `compile` dependencies are now `api` dependencies. |
| if (project.getConfigurations().findByName("api")) { |
| configuration = "${variantName}Api"; |
| } else { |
| configuration = "${variantName}Compile"; |
| } |
| project.dependencies.add(configuration, dependency, config) |
| } |
| |
| /** |
| * Returns a Flutter build mode suitable for the specified Android buildType. |
| * |
| * Note: The BuildType DSL type is not public, and is therefore omitted from the signature. |
| * |
| * @return "debug", "profile", or "release" (fall-back). |
| */ |
| private static String buildModeFor(buildType) { |
| if (buildType.name == "profile") { |
| return "profile" |
| } else if (buildType.debuggable) { |
| return "debug" |
| } |
| return "release" |
| } |
| |
| private static String getEngineArtifactDirName(buildType, targetArch) { |
| if (buildType.name == "profile") { |
| return "${targetArch}-profile" |
| } else if (buildType.debuggable) { |
| return "${targetArch}" |
| } |
| return "${targetArch}-release" |
| } |
| |
| /** |
| * Gets the directory that contains the Flutter source code. |
| * This is the directory containing the `android/` directory. |
| */ |
| private File getFlutterSourceDirectory() { |
| if (project.flutter.source == null) { |
| throw new GradleException("Must provide Flutter source directory") |
| } |
| return project.file(project.flutter.source) |
| } |
| |
| /** |
| * Gets the target file. This is typically `lib/main.dart`. |
| */ |
| private String getFlutterTarget() { |
| String target = project.flutter.target |
| if (target == null) { |
| target = 'lib/main.dart' |
| } |
| if (project.hasProperty('target')) { |
| target = project.property('target') |
| } |
| return target |
| } |
| |
| /** |
| * In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`. |
| * When building APKs, this causes an issue where building release requires the debug JAR, |
| * but Gradle won't build debug. |
| * |
| * To workaround this issue, only configure the JAR task that is required given the task |
| * from the command line. |
| * |
| * The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the |
| * time of adding this code. Once released, this can be removed. |
| * |
| * Tested cases: |
| * * `./gradlew assembleRelease` |
| * * `./gradlew app:assembleRelease.` |
| * * `./gradlew assemble{flavorName}Release` |
| * * `./gradlew app:assemble{flavorName}Release` |
| * * `./gradlew assemble.` |
| * * `./gradlew app:assemble.` |
| * * `./gradlew bundle.` |
| * * `./gradlew bundleRelease.` |
| * * `./gradlew app:bundleRelease.` |
| * |
| * Related issues: |
| * https://issuetracker.google.com/issues/158060799 |
| * https://issuetracker.google.com/issues/158753935 |
| */ |
| private boolean shouldConfigureFlutterTask(Task assembleTask) { |
| def cliTasksNames = project.gradle.startParameter.taskNames |
| if (cliTasksNames.size() != 1 || !cliTasksNames.first().contains("assemble")) { |
| return true |
| } |
| def taskName = cliTasksNames.first().split(":").last() |
| if (taskName == "assemble") { |
| return true |
| } |
| if (taskName == assembleTask.name) { |
| return true |
| } |
| if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) { |
| return true |
| } |
| if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) { |
| return true |
| } |
| if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) { |
| return true |
| } |
| return false |
| } |
| |
| private Task getAssembleTask(variant) { |
| // `assemble` became `assembleProvider` in AGP 3.3.0. |
| return variant.hasProperty("assembleProvider") ? variant.assembleProvider.get() : variant.assemble |
| } |
| |
| private boolean isFlutterAppProject() { |
| return project.android.hasProperty("applicationVariants") |
| } |
| |
| private void addFlutterTasks(Project project) { |
| if (project.state.failure) { |
| return |
| } |
| String[] fileSystemRootsValue = null |
| if (project.hasProperty('filesystem-roots')) { |
| fileSystemRootsValue = project.property('filesystem-roots').split('\\|') |
| } |
| String fileSystemSchemeValue = null |
| if (project.hasProperty('filesystem-scheme')) { |
| fileSystemSchemeValue = project.property('filesystem-scheme') |
| } |
| Boolean trackWidgetCreationValue = true |
| if (project.hasProperty('track-widget-creation')) { |
| trackWidgetCreationValue = project.property('track-widget-creation').toBoolean() |
| } |
| String extraFrontEndOptionsValue = null |
| if (project.hasProperty('extra-front-end-options')) { |
| extraFrontEndOptionsValue = project.property('extra-front-end-options') |
| } |
| String extraGenSnapshotOptionsValue = null |
| if (project.hasProperty('extra-gen-snapshot-options')) { |
| extraGenSnapshotOptionsValue = project.property('extra-gen-snapshot-options') |
| } |
| String splitDebugInfoValue = null |
| if (project.hasProperty('split-debug-info')) { |
| splitDebugInfoValue = project.property('split-debug-info') |
| } |
| Boolean dartObfuscationValue = false |
| if (project.hasProperty('dart-obfuscation')) { |
| dartObfuscationValue = project.property('dart-obfuscation').toBoolean(); |
| } |
| Boolean treeShakeIconsOptionsValue = false |
| if (project.hasProperty('tree-shake-icons')) { |
| treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean() |
| } |
| String dartDefinesValue = null |
| if (project.hasProperty('dart-defines')) { |
| dartDefinesValue = project.property('dart-defines') |
| } |
| String bundleSkSLPathValue; |
| if (project.hasProperty('bundle-sksl-path')) { |
| bundleSkSLPathValue = project.property('bundle-sksl-path') |
| } |
| String performanceMeasurementFileValue; |
| if (project.hasProperty('performance-measurement-file')) { |
| performanceMeasurementFileValue = project.property('performance-measurement-file') |
| } |
| String codeSizeDirectoryValue; |
| if (project.hasProperty('code-size-directory')) { |
| codeSizeDirectoryValue = project.property('code-size-directory') |
| } |
| Boolean deferredComponentsValue = false |
| if (project.hasProperty('deferred-components')) { |
| deferredComponentsValue = project.property('deferred-components').toBoolean() |
| } |
| Boolean validateDeferredComponentsValue = true |
| if (project.hasProperty('validate-deferred-components')) { |
| validateDeferredComponentsValue = project.property('validate-deferred-components').toBoolean() |
| } |
| def targetPlatforms = getTargetPlatforms() |
| def addFlutterDeps = { variant -> |
| if (shouldSplitPerAbi()) { |
| 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 |
| def abiVersionCode = ABI_VERSION.get(output.getFilter(OutputFile.ABI)) |
| if (abiVersionCode != null) { |
| output.versionCodeOverride = |
| abiVersionCode * 1000 + variant.versionCode |
| } |
| } |
| } |
| String variantBuildMode = buildModeFor(variant.buildType) |
| String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) |
| FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) { |
| flutterRoot this.flutterRoot |
| flutterExecutable this.flutterExecutable |
| buildMode variantBuildMode |
| localEngine this.localEngine |
| localEngineSrcPath this.localEngineSrcPath |
| targetPath getFlutterTarget() |
| verbose isVerbose() |
| fastStart isFastStart() |
| fileSystemRoots fileSystemRootsValue |
| fileSystemScheme fileSystemSchemeValue |
| trackWidgetCreation trackWidgetCreationValue |
| targetPlatformValues = targetPlatforms |
| sourceDir getFlutterSourceDirectory() |
| intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/") |
| extraFrontEndOptions extraFrontEndOptionsValue |
| extraGenSnapshotOptions extraGenSnapshotOptionsValue |
| splitDebugInfo splitDebugInfoValue |
| treeShakeIcons treeShakeIconsOptionsValue |
| dartObfuscation dartObfuscationValue |
| dartDefines dartDefinesValue |
| bundleSkSLPath bundleSkSLPathValue |
| performanceMeasurementFile performanceMeasurementFileValue |
| codeSizeDirectory codeSizeDirectoryValue |
| deferredComponents deferredComponentsValue |
| validateDeferredComponents validateDeferredComponentsValue |
| doLast { |
| project.exec { |
| if (Os.isFamily(Os.FAMILY_WINDOWS)) { |
| commandLine('cmd', '/c', "attrib -r ${assetsDirectory}/* /s") |
| } else { |
| commandLine('chmod', '-R', 'u+w', assetsDirectory) |
| } |
| } |
| } |
| } |
| File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar") |
| Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { |
| destinationDir libJar.parentFile |
| archiveName libJar.name |
| dependsOn compileTask |
| targetPlatforms.each { targetPlatform -> |
| String abi = PLATFORM_ARCH_MAP[targetPlatform] |
| from("${compileTask.intermediateDir}/${abi}") { |
| include "*.so" |
| // Move `app.so` to `lib/<abi>/libapp.so` |
| rename { String filename -> |
| return "lib/${abi}/lib${filename}" |
| } |
| } |
| } |
| } |
| addApiDependencies(project, variant.name, project.files { |
| packFlutterAppAotTask |
| }) |
| // We 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. |
| // We know that `:flutter` is used as a subproject when these tasks exists and we aren't building an AAR. |
| Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets") |
| Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets") |
| boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar |
| Task copyFlutterAssetsTask = project.tasks.create( |
| name: "copyFlutterAssets${variant.name.capitalize()}", |
| type: Copy, |
| ) { |
| dependsOn compileTask |
| with compileTask.assets |
| 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 |
| } |
| if (!isUsedAsSubproject) { |
| def variantOutput = variant.outputs.first() |
| def processResources = variantOutput.hasProperty("processResourcesProvider") ? |
| variantOutput.processResourcesProvider.get() : variantOutput.processResources |
| processResources.dependsOn(copyFlutterAssetsTask) |
| } |
| return copyFlutterAssetsTask |
| } // end def addFlutterDeps |
| |
| if (isFlutterAppProject()) { |
| project.android.applicationVariants.all { variant -> |
| Task assembleTask = getAssembleTask(variant) |
| if (!shouldConfigureFlutterTask(assembleTask)) { |
| return |
| } |
| Task copyFlutterAssetsTask = addFlutterDeps(variant) |
| def variantOutput = variant.outputs.first() |
| def processResources = variantOutput.hasProperty("processResourcesProvider") ? |
| 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 `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`. |
| // |
| // The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.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.all { output -> |
| assembleTask.doLast { |
| // `packageApplication` became `packageApplicationProvider` in AGP 3.3.0. |
| def outputDirectory = variant.hasProperty("packageApplicationProvider") |
| ? variant.packageApplicationProvider.get().outputDirectory |
| : variant.packageApplication.outputDirectory |
| // `outputDirectory` is a `DirectoryProperty` in AGP 4.1. |
| String outputDirectoryStr = outputDirectory.metaClass.respondsTo(outputDirectory, "get") |
| ? outputDirectory.get() |
| : outputDirectory |
| 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 += "-${buildModeFor(variant.buildType)}" |
| project.copy { |
| from new File("$outputDirectoryStr/${output.outputFileName}") |
| into new File("${project.buildDir}/outputs/flutter-apk"); |
| rename { |
| return "${filename}.apk" |
| } |
| } |
| } |
| } |
| } |
| configurePlugins() |
| detectLowCompileSdkVersion() |
| 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 custom the host app project name, set `org.gradle.project.flutter.hostAppProjectName=<project-name>` 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 = getAssembleTask(appProjectVariant) |
| if (!shouldConfigureFlutterTask(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 = buildModeFor(libraryVariant.buildType) |
| if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) { |
| return |
| } |
| if (copyFlutterAssetsTask == null) { |
| copyFlutterAssetsTask = addFlutterDeps(libraryVariant) |
| } |
| Task mergeAssets = project |
| .tasks |
| .findByPath(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets") |
| assert mergeAssets |
| mergeAssets.dependsOn(copyFlutterAssetsTask) |
| } |
| } |
| } |
| configurePlugins() |
| detectLowCompileSdkVersion() |
| } |
| } |
| |
| abstract class BaseFlutterTask extends DefaultTask { |
| @Internal |
| File flutterRoot |
| @Internal |
| File flutterExecutable |
| @Input |
| String buildMode |
| @Optional @Input |
| String localEngine |
| @Optional @Input |
| String localEngineSrcPath |
| @Optional @Input |
| Boolean fastStart |
| @Input |
| String targetPath |
| @Optional @Internal |
| Boolean verbose |
| @Optional @Input |
| String[] fileSystemRoots |
| @Optional @Input |
| String fileSystemScheme |
| @Input |
| Boolean trackWidgetCreation |
| @Optional @Input |
| List<String> targetPlatformValues |
| @Internal |
| File sourceDir |
| @Internal |
| File intermediateDir |
| @Optional @Input |
| String extraFrontEndOptions |
| @Optional @Input |
| String extraGenSnapshotOptions |
| @Optional @Input |
| String splitDebugInfo |
| @Optional @Input |
| Boolean treeShakeIcons |
| @Optional @Input |
| Boolean dartObfuscation |
| @Optional @Input |
| String dartDefines |
| @Optional @Input |
| String bundleSkSLPath |
| @Optional @Input |
| String codeSizeDirectory; |
| @Optional @Input |
| String performanceMeasurementFile; |
| @Optional @Input |
| Boolean deferredComponents |
| @Optional @Input |
| Boolean validateDeferredComponents |
| |
| @OutputFiles |
| FileCollection getDependenciesFiles() { |
| FileCollection depfiles = project.files() |
| |
| // Includes all sources used in the flutter compilation. |
| depfiles += project.files("${intermediateDir}/flutter_build.d") |
| return depfiles |
| } |
| |
| void buildBundle() { |
| if (!sourceDir.isDirectory()) { |
| throw new GradleException("Invalid Flutter source directory: ${sourceDir}") |
| } |
| |
| intermediateDir.mkdirs() |
| |
| // Compute the rule name for flutter assemble. To speed up builds that contain |
| // multiple ABIs, the target name is used to communicate which ones are required |
| // rather than the TargetPlatform. This allows multiple builds to share the same |
| // cache. |
| String[] ruleNames; |
| if (buildMode == "debug") { |
| ruleNames = ["debug_android_application"] |
| } else if (deferredComponents) { |
| ruleNames = targetPlatformValues.collect { "android_aot_deferred_components_bundle_${buildMode}_$it" } |
| } else { |
| ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } |
| } |
| project.exec { |
| logging.captureStandardError LogLevel.ERROR |
| executable flutterExecutable.absolutePath |
| workingDir sourceDir |
| if (localEngine != null) { |
| args "--local-engine", localEngine |
| args "--local-engine-src-path", localEngineSrcPath |
| } |
| if (verbose) { |
| args "--verbose" |
| } else { |
| args "--quiet" |
| } |
| args "assemble" |
| args "--no-version-check" |
| args "--depfile", "${intermediateDir}/flutter_build.d" |
| args "--output", "${intermediateDir}" |
| if (performanceMeasurementFile != null) { |
| args "--performance-measurement-file=${performanceMeasurementFile}" |
| } |
| if (!fastStart || buildMode != "debug") { |
| args "-dTargetFile=${targetPath}" |
| } else { |
| args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}" |
| } |
| args "-dTargetPlatform=android" |
| args "-dBuildMode=${buildMode}" |
| if (trackWidgetCreation != null) { |
| args "-dTrackWidgetCreation=${trackWidgetCreation}" |
| } |
| if (splitDebugInfo != null) { |
| args "-dSplitDebugInfo=${splitDebugInfo}" |
| } |
| if (treeShakeIcons == true) { |
| args "-dTreeShakeIcons=true" |
| } |
| if (dartObfuscation == true) { |
| args "-dDartObfuscation=true" |
| } |
| if (dartDefines != null) { |
| args "--DartDefines=${dartDefines}" |
| } |
| if (bundleSkSLPath != null) { |
| args "-dBundleSkSLPath=${bundleSkSLPath}" |
| } |
| if (codeSizeDirectory != null) { |
| args "-dCodeSizeDirectory=${codeSizeDirectory}" |
| } |
| if (extraGenSnapshotOptions != null) { |
| args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" |
| } |
| if (extraFrontEndOptions != null) { |
| args "--ExtraFrontEndOptions=${extraFrontEndOptions}" |
| } |
| args ruleNames |
| } |
| } |
| } |
| |
| class FlutterTask extends BaseFlutterTask { |
| @OutputDirectory |
| File getOutputDirectory() { |
| return intermediateDir |
| } |
| |
| @Internal |
| String getAssetsDirectory() { |
| return "${outputDirectory}/flutter_assets" |
| } |
| |
| @Internal |
| CopySpec getAssets() { |
| return project.copySpec { |
| from "${intermediateDir}" |
| include "flutter_assets/**" // the working dir and its files |
| } |
| } |
| |
| @Internal |
| CopySpec getSnapshots() { |
| return project.copySpec { |
| from "${intermediateDir}" |
| |
| if (buildMode == 'release' || buildMode == 'profile') { |
| targetPlatformValues.each { |
| include "${PLATFORM_ARCH_MAP[targetArch]}/app.so" |
| } |
| } |
| } |
| } |
| |
| FileCollection readDependencies(File dependenciesFile, Boolean inputs) { |
| if (dependenciesFile.exists()) { |
| // Dependencies file has Makefile syntax: |
| // <target> <files>: <source> <files> <separated> <by> <non-escaped space> |
| String depText = dependenciesFile.text |
| // So we split list of files by non-escaped(by backslash) space, |
| def matcher = depText.split(': ')[inputs ? 1 : 0] =~ /(\\ |[^\s])+/ |
| // then we replace all escaped spaces with regular spaces |
| def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")} |
| return project.files(depList) |
| } |
| return project.files(); |
| } |
| |
| @InputFiles |
| FileCollection getSourceFiles() { |
| FileCollection sources = project.files() |
| for (File depfile in getDependenciesFiles()) { |
| sources += readDependencies(depfile, true) |
| } |
| return sources + project.files('pubspec.yaml') |
| } |
| |
| @OutputFiles |
| FileCollection getOutputFiles() { |
| FileCollection sources = project.files() |
| for (File depfile in getDependenciesFiles()) { |
| sources += readDependencies(depfile, false) |
| } |
| return sources |
| } |
| |
| @TaskAction |
| void build() { |
| buildBundle() |
| } |
| } |