flutter.gradle 53.1 KB
Newer Older
1 2 3 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.

5
import static groovy.io.FileType.FILES
6 7

import com.android.builder.model.AndroidProject
8
import com.android.build.OutputFile
9
import groovy.json.JsonSlurper
10 11
import java.nio.file.Path
import java.nio.file.Paths
12 13
import java.util.regex.Matcher
import java.util.regex.Pattern
14
import org.apache.tools.ant.taskdefs.condition.Os
15 16 17 18 19
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
20
import org.gradle.api.file.CopySpec
21 22
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.Copy
23
import org.gradle.api.tasks.InputFiles
24 25
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
26
import org.gradle.api.tasks.bundling.Jar
27
import org.gradle.internal.os.OperatingSystem
28
import org.gradle.util.VersionNumber
29

30 31 32
/** For apps only. Provides the flutter extension used in app/build.gradle. */
class FlutterExtension {
    /** Sets the compileSdkVersion used by default in Flutter app projects. */
33
    static int compileSdkVersion = 31
34 35 36 37 38 39 40

    /** 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

Daco Harkes's avatar
Daco Harkes committed
41 42 43 44 45 46
    /**
     * Sets the ndkVersion used by default in Flutter app projects.
     * Chosen as default version of the AGP version below.
     */
    static String ndkVersion = "21.1.6352462"

47 48 49 50
    /**
     * 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.
     */
51
    String source
52 53 54 55 56

    /** Allows to override the target file. Otherwise, the target is lib/main.dart. */
    String target
}

57
buildscript {
58
    repositories {
59
        google()
60
        mavenCentral()
61 62
    }
    dependencies {
Daco Harkes's avatar
Daco Harkes committed
63
        /* When bumping, also update ndkVersion above. */
64
        classpath 'com.android.tools.build:gradle:4.1.0'
65
    }
66 67
}

68 69 70 71 72
/**
 * 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.
 */
Dan Field's avatar
Dan Field committed
73 74
android {
    compileOptions {
75 76
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
Dan Field's avatar
Dan Field committed
77 78 79
    }
}

80 81
apply plugin: FlutterPlugin

82
class FlutterPlugin implements Plugin<Project> {
83
    private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com";
84

85
    /** The platforms that can be passed to the `--Ptarget-platform` flag. */
86 87 88 89 90
    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";

91
    /** The ABI architectures supported by Flutter. */
92 93 94 95 96
    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";

97
    /** Maps platforms to ABI architectures. */
98 99 100 101 102 103 104
    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,
    ]

105 106 107 108 109
    /**
     * 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.
     */
110 111 112 113 114 115 116
    private static final Map ABI_VERSION = [
        (ARCH_ARM32)        : 1,
        (ARCH_ARM64)        : 2,
        (ARCH_X86)          : 3,
        (ARCH_X86_64)       : 4,
    ]

117
    /** When split is enabled, multiple APKs are generated per each ABI. */
118 119 120
    private static final List DEFAULT_PLATFORMS = [
        PLATFORM_ARM32,
        PLATFORM_ARM64,
121
        PLATFORM_X86_64,
122 123
    ]

124 125 126 127 128 129 130
    /**
     * 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.
     */
131 132
    static final String FLUTTER_BUILD_PREFIX = "flutterBuild"

133
    private Project project
134
    private Map baseJar = [:]
135
    private File flutterRoot
136
    private File flutterExecutable
137
    private String localEngine
138
    private String localEngineSrcPath
139
    private Properties localProperties
140
    private String engineVersion
141

142 143
    @Override
    void apply(Project project) {
144 145
        this.project = project

146
        def rootProject = project.rootProject
147 148 149 150 151 152 153 154 155 156
        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"
                    }
157 158 159 160
                }
            }
        }

161 162 163 164 165
        // 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"
166
        rootProject.allprojects {
167 168 169 170 171 172 173
            repositories {
                maven {
                    url repository
                }
            }
        }

174
        project.extensions.create("flutter", FlutterExtension)
175 176
        this.addFlutterTasks(project)

177 178 179
        // 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.
180
        if (shouldSplitPerAbi()) {
181 182 183 184 185 186 187 188 189 190 191 192 193
            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
                    }
                }
            }
        }
194 195 196 197 198 199 200 201

        if (project.hasProperty('deferred-component-names')) {
            String[] componentNames = project.property('deferred-component-names').split(',').collect {":${it}"}
            project.android {
                dynamicFeatures = componentNames
            }
        }

202
        getTargetPlatforms().each { targetArch ->
203
            String abiValue = PLATFORM_ARCH_MAP[targetArch]
204
            project.android {
205
                if (shouldSplitPerAbi()) {
206 207 208 209 210
                    splits {
                        abi {
                            include abiValue
                        }
                    }
211 212 213
                }
            }
        }
214

215
        String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
216
        if (flutterRootPath == null) {
217
            throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
218
        }
219 220
        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot.isDirectory()) {
221 222 223
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

224 225 226
        engineVersion = useLocalEngine()
            ? "+" // Match any version since there's only one.
            : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()
227

228 229 230
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();

231 232
        String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                "gradle", "flutter_proguard_rules.pro")
233
        project.android.buildTypes {
234
            // Add profile build type.
235 236 237 238 239 240
            profile {
                initWith debug
                if (it.hasProperty("matchingFallbacks")) {
                    matchingFallbacks = ["debug", "release"]
                }
            }
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
            // 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"
                }
258 259
            }
        }
260

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
        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]
                }
            }
        }

293
        if (useLocalEngine()) {
294 295
            // This is required to pass the local engine to flutter build aot.
            String engineOutPath = project.property('local-engine-out')
296
            File engineOut = project.file(engineOutPath)
297
            if (!engineOut.isDirectory()) {
298
                throw new GradleException('local-engine-out must point to a local engine build')
299
            }
300 301
            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent
302
        }
303
        project.android.buildTypes.all this.&addFlutterDependencies
304 305
    }

Shi-Hao Hong's avatar
Shi-Hao Hong committed
306
    private static Boolean shouldShrinkResources(Project project) {
307 308 309 310 311 312
        if (project.hasProperty("shrink")) {
            return project.property("shrink").toBoolean()
        }
        return true
    }

313 314 315 316 317 318 319
    /**
     * Adds the dependencies required by the Flutter project.
     * This includes:
     *    1. The embedding
     *    2. libflutter.so
     */
    void addFlutterDependencies(buildType) {
320 321 322 323
        String flutterBuildMode = buildModeFor(buildType)
        if (!supportsBuildMode(flutterBuildMode)) {
            return
        }
324 325 326 327 328 329 330 331 332
        // 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")
333 334 335
        }
        List<String> platforms = getTargetPlatforms().collect()
        // Debug mode includes x86 and x64, which are commonly used in emulators.
336
        if (flutterBuildMode == "debug" && !useLocalEngine()) {
337 338 339 340 341 342 343
            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,
344
                    "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
345
        }
346 347 348 349 350
    }

    /**
     * Returns the directory where the plugins are built.
     */
351
    private File getPluginBuildDir() {
352 353 354 355 356 357
        // 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
    }
358

359 360 361 362 363 364 365 366
    /**
     * 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() {
367 368
        getPluginList().each this.&configurePluginProject
        getPluginDependencies().each this.&configurePluginDependencies
369 370
    }

371
    /** Adds the plugin project dependency to the app project. */
372 373
    private void configurePluginProject(String pluginName, String _) {
        Project pluginProject = project.rootProject.findProject(":$pluginName")
374
        if (pluginProject == null) {
375
            project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
376 377 378 379
            return
        }
        // Add plugin dependency to the app project.
        project.dependencies {
380
            api pluginProject
381
        }
382
        Closure addEmbeddingDependencyToPlugin = { buildType ->
383
            String flutterBuildMode = buildModeFor(buildType)
384 385 386
            // 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
387
            if (!supportsBuildMode(flutterBuildMode)) {
388 389
                return
            }
390 391 392
            if (!pluginProject.hasProperty('android')) {
                return
            }
393 394 395 396 397
            // 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}" {}
            }
398 399
            // 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.
400
            //
401 402 403 404 405 406 407
            // 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"
            )
408
        }
409

410
        // Wait until the Android plugin loaded.
411
        pluginProject.afterEvaluate {
412 413 414
            if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
                project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${pluginProject.android.compileSdkVersion.substring(8)}.")
            }
415
            project.android.buildTypes.all addEmbeddingDependencyToPlugin
416 417 418
        }
    }

Daco Harkes's avatar
Daco Harkes committed
419 420
    /**
     * Compares semantic versions ignoring labels.
421
     * 
Daco Harkes's avatar
Daco Harkes committed
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
     * If the versions are equal (ignoring labels), returns one of the two strings arbitrarily.
     *
     * If minor or patch are omitted (non-conformant to semantic versioning), they are considered zero.
     * If the provided versions in both are equal, the longest version string is returned.
     * For example, "2.8.0" vs "2.8" will always consider "2.8.0" to be the most recent version.
     */
    static String mostRecentSemanticVersion(String version1, String version2) {
        List version1Tokenized = version1.tokenize('.')
        List version2Tokenized = version2.tokenize('.')
        def version1numTokens = version1Tokenized.size()
        def version2numTokens = version2Tokenized.size()
        def minNumTokens = Math.min(version1numTokens, version2numTokens)
        for (int i = 0; i < minNumTokens; i++) {
            def num1 = version1Tokenized[i].toInteger()
            def num2 = version2Tokenized[i].toInteger()
            if (num1 > num2) {
                return version1
            }
            if (num2 > num1) {
                return version2
            }
        }
        if (version1numTokens > version2numTokens) {
            return version1
        }
        return version2
    }

    /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
    private void detectLowCompileSdkVersionOrNdkVersion() {
452 453 454
        project.afterEvaluate {
            int projectCompileSdkVersion = project.android.compileSdkVersion.substring(8) as int
            int maxPluginCompileSdkVersion = projectCompileSdkVersion
Daco Harkes's avatar
Daco Harkes committed
455 456 457
            String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
            String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
            String maxPluginNdkVersion = projectNdkVersion
458 459 460 461 462 463 464
            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)
Daco Harkes's avatar
Daco Harkes committed
465 466
                    String pluginNdkVersion = pluginProject.android.ndkVersion ?: ndkVersionIfUnspecified
                    maxPluginNdkVersion = mostRecentSemanticVersion(pluginNdkVersion, maxPluginNdkVersion)
467 468 469 470 471 472

                    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")
                        }
Daco Harkes's avatar
Daco Harkes committed
473 474 475
                        if (maxPluginNdkVersion != projectNdkVersion) {
                            project.logger.error("One or more plugins require a higher Android NDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n  ndkVersion ${maxPluginNdkVersion}\n  ...\n}\n")
                        }
476 477 478
                    }
                }
            }
479 480 481 482 483 484 485 486 487
        }  
    }

    /**
     * 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()
488 489
    }

490 491 492 493 494
    /**
     * 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.
     */
495 496 497
    private void configurePluginDependencies(Object dependencyObject) {
        assert dependencyObject.name instanceof String
        Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
498 499
        if (pluginProject == null ||
            !doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
500 501 502 503 504 505 506 507 508
            return
        }
        assert dependencyObject.dependencies instanceof List
        dependencyObject.dependencies.each { pluginDependencyName ->
            assert pluginDependencyName instanceof String
            if (pluginDependencyName.empty) {
                return
            }
            Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
509 510
            if (dependencyProject == null ||
                !doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
511 512
                return
            }
513 514 515 516 517
            // Wait for the Android plugin to load and add the dependency to the plugin project.
            pluginProject.afterEvaluate {
                pluginProject.dependencies {
                    implementation dependencyProject
                }
518 519 520 521
            }
        }
    }

522
    private Properties getPluginList() {
523 524
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
        Properties allPlugins = readPropertiesIfExist(pluginsFile)
525
        Properties androidPlugins = new Properties()
526 527 528
        allPlugins.each { name, path ->
            if (doesSupportAndroidPlatform(path)) {
                androidPlugins.setProperty(name, path)
529
            }
530 531 532
            // 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
533
        }
534
        return androidPlugins
535 536
    }

537
    /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
    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.
560
        File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
561 562 563
        if (pluginsDependencyFile.exists()) {
            def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
            assert object instanceof Map
564 565
            assert object.dependencyGraph instanceof List
            return object.dependencyGraph
566
        }
567
        return []
568 569
    }

570 571 572 573 574 575 576
    private static String toCammelCase(List<String> parts) {
        if (parts.empty) {
            return ""
        }
        return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}"
    }

577
    private String resolveProperty(String name, String defaultValue) {
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
        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
    }

602
    private List<String> getTargetPlatforms() {
603 604 605 606 607 608 609 610 611 612 613
        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
        }
    }

614
    private Boolean shouldSplitPerAbi() {
615 616 617 618 619 620
        if (project.hasProperty('split-per-abi')) {
            return project.property('split-per-abi').toBoolean()
        }
        return false;
    }

621
    private Boolean useLocalEngine() {
622
        return project.hasProperty('local-engine-repo')
623 624
    }

625
    private Boolean isVerbose() {
626 627 628 629 630 631
        if (project.hasProperty('verbose')) {
            return project.property('verbose').toBoolean()
        }
        return false
    }

632
    /** Whether to build the debug app in "fast-start" mode. */
633 634 635 636 637 638 639
    private Boolean isFastStart() {
        if (project.hasProperty("fast-start")) {
            return project.property("fast-start").toBoolean()
        }
        return false
    }

640 641 642 643 644 645
    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");
    }

646 647 648 649 650
    /**
     * 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.
     */
651 652 653 654 655 656 657 658 659 660
    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
    }

661
    private void addCompileOnlyDependency(Project project, String variantName, Object dependency, Closure config = null) {
662 663 664
        if (project.state.failure) {
            return
        }
665 666 667 668 669
        String configuration;
        if (project.getConfigurations().findByName("compileOnly")) {
            configuration = "${variantName}CompileOnly";
        } else {
            configuration = "${variantName}Provided";
670
        }
671
        project.dependencies.add(configuration, dependency, config)
672 673
    }

674 675 676 677 678 679 680
    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";
681
        }
682
        project.dependencies.add(configuration, dependency, config)
683 684 685 686 687 688 689
    }

    /**
     * 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.
     *
690
     * @return "debug", "profile", or "release" (fall-back).
691 692 693 694 695 696 697 698 699 700
     */
    private static String buildModeFor(buildType) {
        if (buildType.name == "profile") {
            return "profile"
        } else if (buildType.debuggable) {
            return "debug"
        }
        return "release"
    }

701 702 703 704 705 706 707 708 709
    private static String getEngineArtifactDirName(buildType, targetArch) {
        if (buildType.name == "profile") {
            return "${targetArch}-profile"
        } else if (buildType.debuggable) {
            return "${targetArch}"
        }
        return "${targetArch}-release"
    }

710 711
    /**
     * Gets the directory that contains the Flutter source code.
Shi-Hao Hong's avatar
Shi-Hao Hong committed
712
     * This is the directory containing the `android/` directory.
713 714
     */
    private File getFlutterSourceDirectory() {
715 716 717
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }
718 719 720 721 722 723 724
        return project.file(project.flutter.source)
    }

    /**
     * Gets the target file. This is typically `lib/main.dart`.
     */
    private String getFlutterTarget() {
725
        String target = project.flutter.target
726 727 728
        if (target == null) {
            target = 'lib/main.dart'
        }
729 730 731
        if (project.hasProperty('target')) {
            target = project.property('target')
        }
732 733 734
        return target
    }

735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
    /**
     * 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
     */
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
    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
    }

790 791 792 793
    private boolean isFlutterAppProject() {
        return project.android.hasProperty("applicationVariants")
    }

794 795 796 797
    private void addFlutterTasks(Project project) {
        if (project.state.failure) {
            return
        }
798 799 800 801 802 803 804 805
        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')
        }
806
        Boolean trackWidgetCreationValue = true
807
        if (project.hasProperty('track-widget-creation')) {
808
            trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
809
        }
810 811 812 813 814 815 816 817
        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')
        }
818 819 820 821
        String splitDebugInfoValue = null
        if (project.hasProperty('split-debug-info')) {
            splitDebugInfoValue = project.property('split-debug-info')
        }
822 823 824 825
        Boolean dartObfuscationValue = false
        if (project.hasProperty('dart-obfuscation')) {
            dartObfuscationValue = project.property('dart-obfuscation').toBoolean();
        }
826 827 828 829
        Boolean treeShakeIconsOptionsValue = false
        if (project.hasProperty('tree-shake-icons')) {
            treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean()
        }
830 831 832 833
        String dartDefinesValue = null
        if (project.hasProperty('dart-defines')) {
            dartDefinesValue = project.property('dart-defines')
        }
834 835 836 837
        String bundleSkSLPathValue;
        if (project.hasProperty('bundle-sksl-path')) {
            bundleSkSLPathValue = project.property('bundle-sksl-path')
        }
838 839 840 841
        String performanceMeasurementFileValue;
        if (project.hasProperty('performance-measurement-file')) {
            performanceMeasurementFileValue = project.property('performance-measurement-file')
        }
842 843 844 845
        String codeSizeDirectoryValue;
        if (project.hasProperty('code-size-directory')) {
            codeSizeDirectoryValue = project.property('code-size-directory')
        }
846 847 848 849 850 851 852 853
        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()
        }
854
        def targetPlatforms = getTargetPlatforms()
855
        def addFlutterDeps = { variant ->
856
            if (shouldSplitPerAbi()) {
857 858 859 860 861 862 863 864 865 866 867 868
                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
                    }
                }
            }
869
            String variantBuildMode = buildModeFor(variant.buildType)
870 871 872 873 874 875 876
            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
877
                targetPath getFlutterTarget()
878
                verbose isVerbose()
879
                fastStart isFastStart()
880 881 882 883
                fileSystemRoots fileSystemRootsValue
                fileSystemScheme fileSystemSchemeValue
                trackWidgetCreation trackWidgetCreationValue
                targetPlatformValues = targetPlatforms
884
                sourceDir getFlutterSourceDirectory()
885 886 887
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/")
                extraFrontEndOptions extraFrontEndOptionsValue
                extraGenSnapshotOptions extraGenSnapshotOptionsValue
888
                splitDebugInfo splitDebugInfoValue
889
                treeShakeIcons treeShakeIconsOptionsValue
890
                dartObfuscation dartObfuscationValue
891
                dartDefines dartDefinesValue
892
                bundleSkSLPath bundleSkSLPathValue
893
                performanceMeasurementFile performanceMeasurementFileValue
894
                codeSizeDirectory codeSizeDirectoryValue
895 896
                deferredComponents deferredComponentsValue
                validateDeferredComponents validateDeferredComponentsValue
897 898 899
                doLast {
                    project.exec {
                        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
900
                            commandLine('cmd', '/c', "attrib -r ${assetsDirectory}/* /s")
901 902 903 904 905
                        } else {
                            commandLine('chmod', '-R', 'u+w', assetsDirectory)
                        }
                    }
                }
906
            }
907
            File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
908
            Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
909 910
                destinationDir libJar.parentFile
                archiveName libJar.name
911 912 913 914 915
                dependsOn compileTask
                targetPlatforms.each { targetPlatform ->
                    String abi = PLATFORM_ARCH_MAP[targetPlatform]
                    from("${compileTask.intermediateDir}/${abi}") {
                        include "*.so"
916
                        // Move `app.so` to `lib/<abi>/libapp.so`
917
                        rename { String filename ->
918
                            return "lib/${abi}/lib${filename}"
919 920 921
                        }
                    }
                }
922
            }
923
            addApiDependencies(project, variant.name, project.files {
924
                packFlutterAppAotTask
925
            })
926 927 928 929
            // 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.
930 931
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
932 933 934 935 936
            boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
            Task copyFlutterAssetsTask = project.tasks.create(
                name: "copyFlutterAssets${variant.name.capitalize()}",
                type: Copy,
            ) {
937 938
                dependsOn compileTask
                with compileTask.assets
939
                if (isUsedAsSubproject) {
940 941 942
                    dependsOn packageAssets
                    dependsOn cleanPackageAssets
                    into packageAssets.outputDir
943
                    return
944
                }
945 946 947 948 949 950 951 952 953
                // `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) {
954 955 956 957
                def variantOutput = variant.outputs.first()
                def processResources = variantOutput.hasProperty("processResourcesProvider") ?
                    variantOutput.processResourcesProvider.get() : variantOutput.processResources
                processResources.dependsOn(copyFlutterAssetsTask)
958
            }
959
            return copyFlutterAssetsTask
960
        } // end def addFlutterDeps
Shi-Hao Hong's avatar
Shi-Hao Hong committed
961

962
        if (isFlutterAppProject()) {
963
            project.android.applicationVariants.all { variant ->
964 965 966 967 968 969 970 971 972 973
                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)

974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
                // 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"
                            }
                        }
                    }
                }
            }
1011
            configurePlugins()
Daco Harkes's avatar
Daco Harkes committed
1012
            detectLowCompileSdkVersionOrNdkVersion()
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
            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  |
                    // | ----------------- | ----------------------------- |
1035
                    // |   freeRelease     |   release                      |
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
                    // |   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)
                }
            }
1062
        }
1063
        configurePlugins()
Daco Harkes's avatar
Daco Harkes committed
1064
        detectLowCompileSdkVersionOrNdkVersion()
1065 1066 1067
    }
}

1068
abstract class BaseFlutterTask extends DefaultTask {
1069
    @Internal
1070
    File flutterRoot
1071
    @Internal
1072
    File flutterExecutable
1073
    @Input
1074
    String buildMode
1075
    @Optional @Input
1076
    String localEngine
1077
    @Optional @Input
1078
    String localEngineSrcPath
1079
    @Optional @Input
1080 1081
    Boolean fastStart
    @Input
1082
    String targetPath
1083
    @Optional @Internal
1084 1085
    Boolean verbose
    @Optional @Input
1086 1087 1088
    String[] fileSystemRoots
    @Optional @Input
    String fileSystemScheme
1089
    @Input
1090 1091
    Boolean trackWidgetCreation
    @Optional @Input
1092
    List<String> targetPlatformValues
1093
    @Internal
1094
    File sourceDir
1095
    @Internal
1096
    File intermediateDir
1097 1098 1099 1100
    @Optional @Input
    String extraFrontEndOptions
    @Optional @Input
    String extraGenSnapshotOptions
1101
    @Optional @Input
1102 1103
    String splitDebugInfo
    @Optional @Input
1104
    Boolean treeShakeIcons
1105 1106
    @Optional @Input
    Boolean dartObfuscation
1107 1108
    @Optional @Input
    String dartDefines
1109 1110
    @Optional @Input
    String bundleSkSLPath
1111 1112
    @Optional @Input
    String codeSizeDirectory;
1113
    @Optional @Input
1114
    String performanceMeasurementFile;
1115 1116 1117 1118
    @Optional @Input
    Boolean deferredComponents
    @Optional @Input
    Boolean validateDeferredComponents
1119

1120 1121
    @OutputFiles
    FileCollection getDependenciesFiles() {
1122
        FileCollection depfiles = project.files()
1123

1124 1125
        // Includes all sources used in the flutter compilation.
        depfiles += project.files("${intermediateDir}/flutter_build.d")
1126
        return depfiles
1127 1128
    }

1129
    void buildBundle() {
1130 1131 1132 1133 1134 1135
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }

        intermediateDir.mkdirs()

1136 1137 1138 1139 1140 1141
        // 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") {
1142
            ruleNames = ["debug_android_application"]
1143 1144
        } else if (deferredComponents) {
            ruleNames = targetPlatformValues.collect { "android_aot_deferred_components_bundle_${buildMode}_$it" }
1145
        } else {
1146
            ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
1147
        }
1148
        project.exec {
1149
            logging.captureStandardError LogLevel.ERROR
1150 1151 1152 1153 1154 1155
            executable flutterExecutable.absolutePath
            workingDir sourceDir
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
1156 1157
            if (verbose) {
                args "--verbose"
1158 1159
            } else {
                args "--quiet"
1160
            }
1161
            args "assemble"
1162
            args "--no-version-check"
1163 1164
            args "--depfile", "${intermediateDir}/flutter_build.d"
            args "--output", "${intermediateDir}"
1165 1166 1167
            if (performanceMeasurementFile != null) {
                args "--performance-measurement-file=${performanceMeasurementFile}"
            }
1168 1169 1170 1171 1172
            if (!fastStart || buildMode != "debug") {
                args "-dTargetFile=${targetPath}"
            } else {
                args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
            }
1173
            args "-dTargetPlatform=android"
1174
            args "-dBuildMode=${buildMode}"
1175 1176 1177
            if (trackWidgetCreation != null) {
                args "-dTrackWidgetCreation=${trackWidgetCreation}"
            }
1178 1179 1180
            if (splitDebugInfo != null) {
                args "-dSplitDebugInfo=${splitDebugInfo}"
            }
1181 1182 1183
            if (treeShakeIcons == true) {
                args "-dTreeShakeIcons=true"
            }
1184 1185 1186
            if (dartObfuscation == true) {
                args "-dDartObfuscation=true"
            }
1187
            if (dartDefines != null) {
1188
                args "--DartDefines=${dartDefines}"
1189
            }
1190
            if (bundleSkSLPath != null) {
1191
                args "-dBundleSkSLPath=${bundleSkSLPath}"
1192
            }
1193 1194 1195
            if (codeSizeDirectory != null) {
                args "-dCodeSizeDirectory=${codeSizeDirectory}"
            }
1196
            if (extraGenSnapshotOptions != null) {
1197
                args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
1198
            }
1199 1200 1201
            if (extraFrontEndOptions != null) {
                args "--ExtraFrontEndOptions=${extraFrontEndOptions}"
            }
1202
            args ruleNames
1203 1204 1205 1206 1207
        }
    }
}

class FlutterTask extends BaseFlutterTask {
1208
    @OutputDirectory
1209 1210 1211
    File getOutputDirectory() {
        return intermediateDir
    }
1212

1213
    @Internal
1214 1215 1216 1217
    String getAssetsDirectory() {
        return "${outputDirectory}/flutter_assets"
    }

1218
    @Internal
1219 1220
    CopySpec getAssets() {
        return project.copySpec {
1221 1222
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
1223 1224 1225
        }
    }

1226
    @Internal
1227 1228 1229
    CopySpec getSnapshots() {
        return project.copySpec {
            from "${intermediateDir}"
1230

1231
            if (buildMode == 'release' || buildMode == 'profile') {
1232 1233 1234
                targetPlatformValues.each {
                    include "${PLATFORM_ARCH_MAP[targetArch]}/app.so"
                }
1235
            }
1236
        }
1237 1238
    }

1239
    FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
1240 1241 1242 1243 1244
      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,
1245
        def matcher = depText.split(': ')[inputs ? 1 : 0] =~ /(\\ |[^\s])+/
1246 1247 1248 1249 1250
        // then we replace all escaped spaces with regular spaces
        def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")}
        return project.files(depList)
      }
      return project.files();
1251 1252
    }

1253 1254
    @InputFiles
    FileCollection getSourceFiles() {
1255 1256
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
1257
          sources += readDependencies(depfile, true)
1258
        }
1259
        return sources + project.files('pubspec.yaml')
1260 1261
    }

1262 1263 1264 1265 1266 1267 1268 1269 1270
    @OutputFiles
    FileCollection getOutputFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile, false)
        }
        return sources
    }

1271 1272
    @TaskAction
    void build() {
1273
        buildBundle()
1274 1275
    }
}