flutter.gradle 37.8 KB
Newer Older
1
import static groovy.io.FileType.FILES
2 3

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

24
buildscript {
25
    repositories {
26
        google()
27
        jcenter()
28 29
    }
    dependencies {
30
        classpath 'com.android.tools.build:gradle:3.5.0'
31
    }
32 33
}

Dan Field's avatar
Dan Field committed
34 35 36 37 38 39 40
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

41 42
apply plugin: FlutterPlugin

43
class FlutterPlugin implements Plugin<Project> {
44
    private static final String MAVEN_REPO      = "https://storage.googleapis.com/download.flutter.io";
45

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
    // 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.
    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,
    ]

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

89
    private Project project
90
    private Map baseJar = [:]
91
    private File flutterRoot
92
    private File flutterExecutable
93
    private String localEngine
94
    private String localEngineSrcPath
95
    private Properties localProperties
96
    private String engineVersion
97

98 99
    @Override
    void apply(Project project) {
100 101
        this.project = project

102
        project.extensions.create("flutter", FlutterExtension)
103
        project.afterEvaluate this.&addFlutterTasks
104 105 106
        // 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.
107
        if (shouldSplitPerAbi()) {
108 109 110 111 112 113 114 115 116 117 118 119 120
            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
                    }
                }
            }
        }
121
        getTargetPlatforms().each { targetArch ->
122
            String abiValue = PLATFORM_ARCH_MAP[targetArch]
123
            project.android {
124
                if (shouldSplitPerAbi()) {
125 126 127 128 129
                    splits {
                        abi {
                            include abiValue
                        }
                    }
130 131 132
                }
            }
        }
133

134
        String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
135
        if (flutterRootPath == null) {
136
            throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
137
        }
138 139
        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot.isDirectory()) {
140 141 142
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

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

147 148 149
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();

150 151 152 153 154 155 156 157 158 159
        // Add custom build types.
        project.android.buildTypes {
            profile {
                initWith debug
                if (it.hasProperty("matchingFallbacks")) {
                    matchingFallbacks = ["debug", "release"]
                }
            }
        }

160
        if (shouldShrinkResources(project)) {
161 162 163 164
            String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                    "gradle", "flutter_proguard_rules.pro")
            project.android.buildTypes {
                release {
Emmanuel Garcia's avatar
Emmanuel Garcia committed
165 166
                    // Enables code shrinking, obfuscation, and optimization for only
                    // your project's release build type.
167
                    minifyEnabled true
Emmanuel Garcia's avatar
Emmanuel Garcia committed
168 169
                    // Enables resource shrinking, which is performed by the
                    // Android Gradle plugin.
170 171
                    // NOTE: The resource shrinker can't be used for libraries.
                    shrinkResources isBuiltAsApp(project)
172 173 174 175 176 177
                    // 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"
                }
            }
        }
178
        if (useLocalEngine()) {
179 180
            // This is required to pass the local engine to flutter build aot.
            String engineOutPath = project.property('local-engine-out')
181
            File engineOut = project.file(engineOutPath)
182
            if (!engineOut.isDirectory()) {
183
                throw new GradleException('local-engine-out must point to a local engine build')
184
            }
185 186
            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent
187
        }
188 189
        project.android.buildTypes.each this.&addFlutterDependencies
        project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
190 191 192 193 194 195 196 197 198
    }

    /**
     * Adds the dependencies required by the Flutter project.
     * This includes:
     *    1. The embedding
     *    2. libflutter.so
     */
    void addFlutterDependencies(buildType) {
199 200 201 202 203 204 205 206
        String flutterBuildMode = buildModeFor(buildType)
        if (!supportsBuildMode(flutterBuildMode)) {
            return
        }
        String repository = useLocalEngine()
            ? project.property('local-engine-repo')
            : MAVEN_REPO

207 208 209
        project.rootProject.allprojects {
            repositories {
                maven {
210
                    url repository
211
                }
212
            }
213 214 215
        }
        // Add the embedding dependency.
        addApiDependencies(project, buildType.name,
216
                "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
217

218 219
        List<String> platforms = getTargetPlatforms().collect()
        // Debug mode includes x86 and x64, which are commonly used in emulators.
220
        if (flutterBuildMode == "debug" && !useLocalEngine()) {
221 222 223 224 225 226 227
            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,
228
                    "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
229
        }
230 231 232 233 234
    }

    /**
     * Returns the directory where the plugins are built.
     */
235
    private File getPluginBuildDir() {
236 237 238 239 240 241
        // 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
    }
242

243 244 245 246 247 248 249 250 251
    /**
     * 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() {
        if (!buildPluginAsAar()) {
252
            getPluginList().each this.&configurePluginProject
253
            getPluginDependencies().each this.&configurePluginDependencies
254 255
            return
        }
256 257 258
        project.repositories {
            maven {
                url "${getPluginBuildDir()}/outputs/repo"
259 260
            }
        }
261 262
        getPluginList().each { pluginName, pluginPath ->
            configurePluginAar(pluginName, pluginPath, project)
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 293 294 295 296 297 298
    private static final Pattern GROUP_PATTERN = ~/group\s+\'(.+)\'/
    private static final Pattern PROJECT_NAME_PATTERN = ~/rootProject\.name\s+=\s+\'(.+)\'/

    // Adds the plugin AAR dependency to the app project.
    private void configurePluginAar(String pluginName, String pluginPath, Project project) {
        // Extract the group id from the plugin's build.gradle.
        // This is `group '<group-id>'`
        File pluginBuildFile = project.file(Paths.get(pluginPath, "android", "build.gradle"));
        if (!pluginBuildFile.exists()) {
            throw new GradleException("Plugin $pluginName doesn't have the required file $pluginBuildFile.")
        }

        Matcher groupParts = GROUP_PATTERN.matcher(pluginBuildFile.text)
        assert groupParts.count == 1
        assert groupParts.hasGroup()
        String groupId = groupParts[0][1]

        // Extract the artifact name from the plugin's settings.gradle.
        // This is `rootProject.name = '<artifact-name>'`
        File pluginSettings = project.file(Paths.get(pluginPath, "android", "settings.gradle"));
        if (!pluginSettings.exists()) {
            throw new GradleException("Plugin $pluginName doesn't have the required file $pluginSettings.")
        }
        Matcher projectNameParts = PROJECT_NAME_PATTERN.matcher(pluginSettings.text)
        assert projectNameParts.count == 1
        assert projectNameParts.hasGroup()
        String artifactId = "${projectNameParts[0][1]}_release"

        assert !groupId.empty
        project.dependencies.add("api", "$groupId:$artifactId:+")
    }

    // Adds the plugin project dependency to the app project .
299 300
    private void configurePluginProject(String pluginName, String _) {
        Project pluginProject = project.rootProject.findProject(":$pluginName")
301
        if (pluginProject == null) {
302
            project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
303 304 305 306
            return
        }
        // Add plugin dependency to the app project.
        project.dependencies {
307
            implementation pluginProject
308 309 310
        }
        Closure addEmbeddingCompileOnlyDependency = { buildType ->
            String flutterBuildMode = buildModeFor(buildType)
311 312 313
            // 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
314
            if (!supportsBuildMode(flutterBuildMode)) {
315 316
                return
            }
317
            addApiDependencies(
318 319
                pluginProject,
                buildType.name,
320
                "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
321 322 323 324 325 326 327 328 329 330 331 332 333
            )
        }
        pluginProject.afterEvaluate {
            pluginProject.android.buildTypes {
                profile {
                    initWith debug
                }
            }
            pluginProject.android.buildTypes.each addEmbeddingCompileOnlyDependency
            pluginProject.android.buildTypes.whenObjectAdded addEmbeddingCompileOnlyDependency
        }
    }

334 335 336 337 338 339 340 341 342 343
    // Returns `true` if the given path contains an `android/build.gradle` file.
    //
    // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/39657.
    // Android Studio may create empty android directories due to the logic in <app>/android/settings.gradle,
    // which imports all plugins regardless of whether they support the android platform.
    private Boolean doesSupportAndroidPlatform(String path) {
        File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
        return editableAndroidProject.exists()
    }

344 345 346 347 348 349
    // 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}")
350 351
        if (pluginProject == null ||
            !doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
352 353 354 355 356 357 358 359 360
            return
        }
        assert dependencyObject.dependencies instanceof List
        dependencyObject.dependencies.each { pluginDependencyName ->
            assert pluginDependencyName instanceof String
            if (pluginDependencyName.empty) {
                return
            }
            Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
361 362
            if (dependencyProject == null ||
                !doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
363 364 365 366 367 368 369 370 371 372 373
                return
            }
            // Wait for the Android plugin to load and add the dependency to the plugin project.
            pluginProject.afterEvaluate {
                pluginProject.dependencies {
                    implementation dependencyProject
                }
            }
        }
    }

374
    private Properties getPluginList() {
375
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
376 377 378
        Properties allPlugins = readPropertiesIfExist(pluginsFile)
        Properties androidPlugins = new Properties()
        allPlugins.each { name, path ->
379
            if (doesSupportAndroidPlatform(path)) {
380 381 382 383 384 385 386
                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
387 388
    }

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    // 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 []
    }

422 423 424 425 426 427 428
    private static String toCammelCase(List<String> parts) {
        if (parts.empty) {
            return ""
        }
        return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}"
    }

429
    private String resolveProperty(String name, String defaultValue) {
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
        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
    }

454
    private List<String> getTargetPlatforms() {
455 456 457 458 459 460 461 462 463 464 465
        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
        }
    }

466
    private Boolean shouldSplitPerAbi() {
467 468 469 470 471 472
        if (project.hasProperty('split-per-abi')) {
            return project.property('split-per-abi').toBoolean()
        }
        return false;
    }

473
    private Boolean useLocalEngine() {
474
        return project.hasProperty('local-engine-repo')
475 476
    }

477
    private Boolean isVerbose() {
478 479 480 481 482 483
        if (project.hasProperty('verbose')) {
            return project.property('verbose').toBoolean()
        }
        return false
    }

484 485 486 487 488 489 490 491 492
    /// 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
    }


493 494 495
    private static Boolean shouldShrinkResources(Project project) {
        if (project.hasProperty("shrink")) {
            return project.property("shrink").toBoolean()
496 497 498 499
        }
        return false
    }

500 501 502 503 504 505
    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");
    }

506 507 508 509
    private static Boolean buildPluginAsAar() {
        return System.getProperty('build-plugins-as-aars') == 'true'
    }

510 511 512 513 514 515 516 517 518 519 520 521 522
    // 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
    }

523
    private void addCompileOnlyDependency(Project project, String variantName, Object dependency, Closure config = null) {
524 525 526
        if (project.state.failure) {
            return
        }
527 528 529 530 531
        String configuration;
        if (project.getConfigurations().findByName("compileOnly")) {
            configuration = "${variantName}CompileOnly";
        } else {
            configuration = "${variantName}Provided";
532
        }
533
        project.dependencies.add(configuration, dependency, config)
534 535
    }

536 537 538 539 540 541 542
    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";
543
        }
544
        project.dependencies.add(configuration, dependency, config)
545 546 547 548 549 550 551
    }

    /**
     * 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.
     *
552
     * @return "debug", "profile", or "release" (fall-back).
553 554 555 556 557 558 559 560 561 562
     */
    private static String buildModeFor(buildType) {
        if (buildType.name == "profile") {
            return "profile"
        } else if (buildType.debuggable) {
            return "debug"
        }
        return "release"
    }

563 564 565 566 567 568 569 570 571
    private static String getEngineArtifactDirName(buildType, targetArch) {
        if (buildType.name == "profile") {
            return "${targetArch}-profile"
        } else if (buildType.debuggable) {
            return "${targetArch}"
        }
        return "${targetArch}-release"
    }

572
    private void addFlutterTasks(Project project) {
573 574 575
        if (project.state.failure) {
            return
        }
576 577 578
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }
579
        String target = project.flutter.target
580 581 582
        if (target == null) {
            target = 'lib/main.dart'
        }
583 584 585
        if (project.hasProperty('target')) {
            target = project.property('target')
        }
586 587 588 589 590 591 592 593
        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')
        }
594 595
        Boolean trackWidgetCreationValue = false
        if (project.hasProperty('track-widget-creation')) {
596
            trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
597
        }
598 599 600 601 602 603 604 605
        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')
        }
606 607 608 609
        String splitDebugInfoValue = null
        if (project.hasProperty('split-debug-info')) {
            splitDebugInfoValue = project.property('split-debug-info')
        }
610 611 612 613
        Boolean dartObfuscationValue = false
        if (project.hasProperty('dart-obfuscation')) {
            dartObfuscationValue = project.property('dart-obfuscation').toBoolean();
        }
614 615 616 617
        Boolean treeShakeIconsOptionsValue = false
        if (project.hasProperty('tree-shake-icons')) {
            treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean()
        }
618 619 620 621
        String dartDefinesValue = null
        if (project.hasProperty('dart-defines')) {
            dartDefinesValue = project.property('dart-defines')
        }
622
        def targetPlatforms = getTargetPlatforms()
623
        def addFlutterDeps = { variant ->
624
            if (shouldSplitPerAbi()) {
625 626 627 628 629 630 631 632 633 634 635 636
                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
                    }
                }
            }
637
            String variantBuildMode = buildModeFor(variant.buildType)
638 639 640 641 642 643 644 645 646
            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 target
                verbose isVerbose()
647
                fastStart isFastStart()
648 649 650 651 652 653 654 655
                fileSystemRoots fileSystemRootsValue
                fileSystemScheme fileSystemSchemeValue
                trackWidgetCreation trackWidgetCreationValue
                targetPlatformValues = targetPlatforms
                sourceDir project.file(project.flutter.source)
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/")
                extraFrontEndOptions extraFrontEndOptionsValue
                extraGenSnapshotOptions extraGenSnapshotOptionsValue
656
                splitDebugInfo splitDebugInfoValue
657
                treeShakeIcons treeShakeIconsOptionsValue
658
                dartObfuscation dartObfuscationValue
659
                dartDefines dartDefinesValue
660 661 662
                doLast {
                    project.exec {
                        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
663
                            commandLine('cmd', '/c', "attrib -r ${assetsDirectory}/* /s")
664 665 666 667 668
                        } else {
                            commandLine('chmod', '-R', 'u+w', assetsDirectory)
                        }
                    }
                }
669
            }
670
            File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
671
            Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
672 673
                destinationDir libJar.parentFile
                archiveName libJar.name
674 675 676 677 678
                dependsOn compileTask
                targetPlatforms.each { targetPlatform ->
                    String abi = PLATFORM_ARCH_MAP[targetPlatform]
                    from("${compileTask.intermediateDir}/${abi}") {
                        include "*.so"
679
                        // Move `app.so` to `lib/<abi>/libapp.so`
680
                        rename { String filename ->
681
                            return "lib/${abi}/lib${filename}"
682 683 684
                        }
                    }
                }
685
            }
686
            addApiDependencies(project, variant.name, project.files {
687
                packFlutterAppAotTask
688
            })
689 690 691 692
            // 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.
693 694
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
695 696 697 698 699
            boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
            Task copyFlutterAssetsTask = project.tasks.create(
                name: "copyFlutterAssets${variant.name.capitalize()}",
                type: Copy,
            ) {
700 701
                dependsOn compileTask
                with compileTask.assets
702
                if (isUsedAsSubproject) {
703 704 705
                    dependsOn packageAssets
                    dependsOn cleanPackageAssets
                    into packageAssets.outputDir
706
                    return
707
                }
708 709 710 711 712 713 714 715 716
                // `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) {
717 718 719 720
                def variantOutput = variant.outputs.first()
                def processResources = variantOutput.hasProperty("processResourcesProvider") ?
                    variantOutput.processResourcesProvider.get() : variantOutput.processResources
                processResources.dependsOn(copyFlutterAssetsTask)
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
                return
            }
            // Flutter module included as a subproject in add to app.
            Project appProject = project.rootProject.findProject(':app')
            assert appProject != null
            appProject.afterEvaluate {
                assert appProject.android != null
                appProject.android.applicationVariants.all { appProjectVariant ->
                    // 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     |   relese                      |
                    // |   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`.
                    if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
                        return
                    }
                    Task mergeAssets = project
                        .tasks
                        .findByPath(":app:merge${appProjectVariant.name.capitalize()}Assets")
                    assert mergeAssets
                    mergeAssets.dependsOn(copyFlutterAssetsTask)
756
                }
757 758
            }
        }
759 760 761 762 763
        if (project.android.hasProperty("applicationVariants")) {
            project.android.applicationVariants.all addFlutterDeps
        } else {
            project.android.libraryVariants.all addFlutterDeps
        }
764
        configurePlugins()
765 766 767 768 769
    }
}

class FlutterExtension {
    String source
770
    String target
771 772
}

773
abstract class BaseFlutterTask extends DefaultTask {
774
    File flutterRoot
775
    File flutterExecutable
776
    String buildMode
777
    @Optional @Input
778
    String localEngine
779
    @Optional @Input
780
    String localEngineSrcPath
781
    @Input
782 783
    Boolean fastStart
    @Input
784
    String targetPath
785
    @Optional
786 787
    Boolean verbose
    @Optional @Input
788 789 790
    String[] fileSystemRoots
    @Optional @Input
    String fileSystemScheme
791
    @Input
792 793
    Boolean trackWidgetCreation
    @Optional @Input
794
    List<String> targetPlatformValues
795
    File sourceDir
796
    File intermediateDir
797 798 799 800
    @Optional @Input
    String extraFrontEndOptions
    @Optional @Input
    String extraGenSnapshotOptions
801
    @Optional @Input
802 803
    String splitDebugInfo
    @Optional @Input
804
    Boolean treeShakeIcons
805 806
    @Optional @Input
    Boolean dartObfuscation
807 808
    @Optional @Input
    String dartDefines
809

810 811
    @OutputFiles
    FileCollection getDependenciesFiles() {
812
        FileCollection depfiles = project.files()
813

814 815
        // Includes all sources used in the flutter compilation.
        depfiles += project.files("${intermediateDir}/flutter_build.d")
816
        return depfiles
817 818
    }

819
    void buildBundle() {
820 821 822 823 824 825
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }

        intermediateDir.mkdirs()

826 827 828 829 830 831
        // 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") {
832 833 834 835 836
            if (fastStart) {
                ruleNames = ["faststart_android_application"]
            } else {
                ruleNames = ["debug_android_application"]
            }
837
        } else {
838
            ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
839
        }
840 841 842 843 844 845 846
        project.exec {
            executable flutterExecutable.absolutePath
            workingDir sourceDir
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
847 848
            if (verbose) {
                args "--verbose"
849 850
            } else {
                args "--quiet"
851
            }
852 853 854
            args "assemble"
            args "--depfile", "${intermediateDir}/flutter_build.d"
            args "--output", "${intermediateDir}"
855 856 857 858 859
            if (!fastStart || buildMode != "debug") {
                args "-dTargetFile=${targetPath}"
            } else {
                args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
            }
860
            args "-dTargetPlatform=android"
861
            args "-dBuildMode=${buildMode}"
862
            if (extraFrontEndOptions != null) {
863
                args "-dExtraFrontEndOptions=${extraFrontEndOptions}"
864
            }
865 866 867
            if (splitDebugInfo != null) {
                args "-dSplitDebugInfo=${splitDebugInfo}"
            }
868 869 870
            if (treeShakeIcons == true) {
                args "-dTreeShakeIcons=true"
            }
871 872 873
            if (dartObfuscation == true) {
                args "-dDartObfuscation=true"
            }
874 875 876
            if (dartDefines != null) {
                args "-dDartDefines=${dartDefines}"
            }
877
            if (extraGenSnapshotOptions != null) {
878
                args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
879
            }
880
            args ruleNames
881 882 883 884 885
        }
    }
}

class FlutterTask extends BaseFlutterTask {
886
    @OutputDirectory
887 888 889
    File getOutputDirectory() {
        return intermediateDir
    }
890

891 892 893 894
    String getAssetsDirectory() {
        return "${outputDirectory}/flutter_assets"
    }

895 896
    CopySpec getAssets() {
        return project.copySpec {
897 898
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
899 900 901 902 903 904
        }
    }

    CopySpec getSnapshots() {
        return project.copySpec {
            from "${intermediateDir}"
905

906
            if (buildMode == 'release' || buildMode == 'profile') {
907 908 909
                targetPlatformValues.each {
                    include "${PLATFORM_ARCH_MAP[targetArch]}/app.so"
                }
910
            }
911
        }
912 913
    }

914
    FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
915 916 917 918 919
      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,
920
        def matcher = depText.split(': ')[inputs ? 1 : 0] =~ /(\\ |[^\s])+/
921 922 923 924 925
        // then we replace all escaped spaces with regular spaces
        def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")}
        return project.files(depList)
      }
      return project.files();
926 927
    }

928 929
    @InputFiles
    FileCollection getSourceFiles() {
930 931
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
932
          sources += readDependencies(depfile, true)
933
        }
934
        return sources + project.files('pubspec.yaml')
935 936
    }

937 938 939 940 941 942 943 944 945
    @OutputFiles
    FileCollection getOutputFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile, false)
        }
        return sources
    }

946 947
    @TaskAction
    void build() {
948
        buildBundle()
949 950
    }
}