flutter.gradle 36.6 KB
Newer Older
1 2 3 4 5
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import static groovy.io.FileType.FILES
6 7

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

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

Dan Field's avatar
Dan Field committed
37 38 39 40 41 42 43
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

44 45
apply plugin: FlutterPlugin

46
class FlutterPlugin implements Plugin<Project> {
47 48
    private static final String MAVEN_REPO      = "http://download.flutter.io";

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 89 90 91
    // 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"

92
    private Project project
93
    private Map baseJar = [:]
94
    private File flutterRoot
95
    private File flutterExecutable
96
    private String localEngine
97
    private String localEngineSrcPath
98
    private Properties localProperties
99
    private File flutterJar
100
    private String engineVersion
101

102 103
    @Override
    void apply(Project project) {
104 105
        this.project = project

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

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

147 148 149
        engineVersion = Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version")
                .toFile().text.trim()

150 151 152
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();

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

163
        if (shouldShrinkResources(project)) {
164 165 166 167
            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
168 169
                    // Enables code shrinking, obfuscation, and optimization for only
                    // your project's release build type.
170
                    minifyEnabled true
Emmanuel Garcia's avatar
Emmanuel Garcia committed
171 172
                    // Enables resource shrinking, which is performed by the
                    // Android Gradle plugin.
173 174
                    // NOTE: The resource shrinker can't be used for libraries.
                    shrinkResources isBuiltAsApp(project)
175 176 177 178 179 180 181
                    // 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"
                }
            }
        }

182
        if (useLocalEngine()) {
183 184
            String engineOutPath = project.property('localEngineOut')
            File engineOut = project.file(engineOutPath)
185 186 187
            if (!engineOut.isDirectory()) {
                throw new GradleException('localEngineOut must point to a local engine build')
            }
188
            Path baseEnginePath = Paths.get(engineOut.absolutePath)
189
            flutterJar = baseEnginePath.resolve("flutter.jar").toFile()
190
            if (!flutterJar.isFile()) {
191
                throw new GradleException("Local engine jar not found: $flutterJar")
192
            }
193 194
            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent
195 196
            // The local engine is built for one of the build type.
            // However, we use the same engine for each of the build types.
197
            project.android.buildTypes.each {
198
                addApiDependencies(project, it.name, project.files {
199
                    flutterJar
200 201
                })
            }
202
        } else {
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
            project.android.buildTypes.each this.&addFlutterDependencies
            project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
        }
    }

    /**
     * Adds the dependencies required by the Flutter project.
     * This includes:
     *    1. The embedding
     *    2. libflutter.so
     */
    void addFlutterDependencies(buildType) {
        project.rootProject.allprojects {
            repositories {
                maven {
                    url MAVEN_REPO
219
                }
220
            }
221 222 223 224 225
        }
        String flutterBuildMode = buildModeFor(buildType)
        // Add the embedding dependency.
        addApiDependencies(project, buildType.name,
                "io.flutter:flutter_embedding_$flutterBuildMode:1.0.0-$engineVersion")
226

227 228 229 230 231 232 233 234 235 236 237
        List<String> platforms = getTargetPlatforms().collect()
        // Debug mode includes x86 and x64, which are commonly used in emulators.
        if (flutterBuildMode == "debug") {
            platforms.add("android-x86")
            platforms.add("android-x64")
        }
        platforms.each { platform ->
            String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
            // Add the `libflutter.so` dependency.
            addApiDependencies(project, buildType.name,
                    "io.flutter:${arch}_$flutterBuildMode:1.0.0-$engineVersion")
238
        }
239 240 241 242 243
    }

    /**
     * Returns the directory where the plugins are built.
     */
244
    private File getPluginBuildDir() {
245 246 247 248 249 250
        // 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
    }
251

252 253 254 255 256 257 258 259 260
    /**
     * 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()) {
261
            getPluginList().each this.&configurePluginProject
262 263
            return
        }
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
        if (useLocalEngine()) {
            throw new GradleException("Local engine isn't supported when building the plugins as AAR")
        }
        List<Project> projects = [project]
        // Module projects set the `hostProjects` extra property in `include_flutter.groovy`.
        // This is required to set the local repository in each host app project.
        if (project.ext.has("hostProjects")) {
            projects.addAll(project.ext.get("hostProjects"))
        }
        // Configure the repository for the plugins.
        projects.each { hostProject ->
            hostProject.repositories {
                maven {
                    url "${getPluginBuildDir()}/outputs/repo"
                }
279 280
            }
        }
281 282
        getPluginList().each { pluginName, pluginPath ->
            configurePluginAar(pluginName, pluginPath, project)
283 284 285
        }
    }

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
    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 .
    private void configurePluginProject(String name, String _) {
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
        Project pluginProject = project.rootProject.findProject(":$name")
        if (pluginProject == null) {
            project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
            return
        }
        // Add plugin dependency to the app project.
        project.dependencies {
            if (project.getConfigurations().findByName("implementation")) {
                implementation pluginProject
            } else {
                compile pluginProject
            }
        }
        Closure addEmbeddingCompileOnlyDependency = { buildType ->
            String flutterBuildMode = buildModeFor(buildType)
335 336 337
            // 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
338
            if (flutterJar) {
339
                addApiDependencies(
340 341 342 343 344 345 346 347
                    pluginProject,
                    buildType.name,
                    project.files {
                        flutterJar
                    }
                )
                return
            }
348
            addApiDependencies(
349 350 351 352
                pluginProject,
                buildType.name,
                "io.flutter:flutter_embedding_$flutterBuildMode:1.0.0-$engineVersion",
                {
353 354
                    // Include the embedding transitive dependencies since plugins may depend on them.
                    transitive = true
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
                }
            )
        }
        pluginProject.afterEvaluate {
            pluginProject.android.buildTypes {
                profile {
                    initWith debug
                }
            }
            pluginProject.android.buildTypes.each addEmbeddingCompileOnlyDependency
            pluginProject.android.buildTypes.whenObjectAdded addEmbeddingCompileOnlyDependency
        }
    }

    private Properties getPluginList() {
370
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
371 372 373 374 375 376 377 378 379 380 381 382
        Properties allPlugins = readPropertiesIfExist(pluginsFile)
        Properties androidPlugins = new Properties()
        allPlugins.each { name, path ->
            File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
            if (editableAndroidProject.exists()) {
                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
383 384 385 386 387 388 389 390 391
    }

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

392
    private String resolveProperty(String name, String defaultValue) {
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
        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
    }

417
    private List<String> getTargetPlatforms() {
418 419 420 421 422 423 424 425 426 427 428
        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
        }
    }

429
    private Boolean shouldSplitPerAbi() {
430 431 432 433 434 435
        if (project.hasProperty('split-per-abi')) {
            return project.property('split-per-abi').toBoolean()
        }
        return false;
    }

436
    private Boolean useLocalEngine() {
437 438 439
        return project.hasProperty('localEngineOut')
    }

440
    private Boolean isVerbose() {
441 442 443 444 445 446
        if (project.hasProperty('verbose')) {
            return project.property('verbose').toBoolean()
        }
        return false
    }

447 448 449
    private static Boolean shouldShrinkResources(Project project) {
        if (project.hasProperty("shrink")) {
            return project.property("shrink").toBoolean()
450 451 452 453
        }
        return false
    }

454 455 456 457 458 459
    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");
    }

460 461 462 463
    private static Boolean buildPluginAsAar() {
        return System.getProperty('build-plugins-as-aars') == 'true'
    }

464
    private void addCompileOnlyDependency(Project project, String variantName, Object dependency, Closure config = null) {
465 466 467
        if (project.state.failure) {
            return
        }
468 469 470 471 472
        String configuration;
        if (project.getConfigurations().findByName("compileOnly")) {
            configuration = "${variantName}CompileOnly";
        } else {
            configuration = "${variantName}Provided";
473
        }
474
        project.dependencies.add(configuration, dependency, config)
475 476
    }

477 478 479 480 481 482 483
    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";
484
        }
485
        project.dependencies.add(configuration, dependency, config)
486 487 488 489 490 491 492
    }

    /**
     * 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.
     *
493
     * @return "debug", "profile", or "release" (fall-back).
494 495 496 497 498 499 500 501 502 503
     */
    private static String buildModeFor(buildType) {
        if (buildType.name == "profile") {
            return "profile"
        } else if (buildType.debuggable) {
            return "debug"
        }
        return "release"
    }

504 505 506 507 508 509 510 511 512
    private static String getEngineArtifactDirName(buildType, targetArch) {
        if (buildType.name == "profile") {
            return "${targetArch}-profile"
        } else if (buildType.debuggable) {
            return "${targetArch}"
        }
        return "${targetArch}-release"
    }

513
    private void addFlutterTasks(Project project) {
514 515 516
        if (project.state.failure) {
            return
        }
517 518 519
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }
520
        String target = project.flutter.target
521 522 523
        if (target == null) {
            target = 'lib/main.dart'
        }
524 525 526
        if (project.hasProperty('target')) {
            target = project.property('target')
        }
527 528 529 530 531 532 533 534
        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')
        }
535 536
        Boolean trackWidgetCreationValue = false
        if (project.hasProperty('track-widget-creation')) {
537
            trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
538
        }
539
        String compilationTraceFilePathValue = null
540 541
        if (project.hasProperty('compilation-trace-file')) {
            compilationTraceFilePathValue = project.property('compilation-trace-file')
542
        }
543 544 545 546 547 548 549 550 551 552 553
        Boolean createPatchValue = false
        if (project.hasProperty('patch')) {
            createPatchValue = project.property('patch').toBoolean()
        }
        Integer buildNumberValue = null
        if (project.hasProperty('build-number')) {
            buildNumberValue = project.property('build-number').toInteger()
        }
        String baselineDirValue = null
        if (project.hasProperty('baseline-dir')) {
            baselineDirValue = project.property('baseline-dir')
554
        }
555 556 557 558 559 560 561 562
        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')
        }
563
        def targetPlatforms = getTargetPlatforms()
564
        def addFlutterDeps = { variant ->
565
            if (shouldSplitPerAbi()) {
566 567 568 569 570 571 572 573 574 575 576 577
                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
                    }
                }
            }
578 579
            String variantBuildMode = buildModeFor(variant.buildType)
            List<FlutterTask> compileTasks = targetPlatforms.collect { targetArch ->
580
                String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name, targetArch.replace('android-', '')])
581
                project.tasks.create(name: taskName, type: FlutterTask) {
582 583
                    flutterRoot this.flutterRoot
                    flutterExecutable this.flutterExecutable
584
                    buildMode variantBuildMode
585 586
                    localEngine this.localEngine
                    localEngineSrcPath this.localEngineSrcPath
587
                    abi PLATFORM_ARCH_MAP[targetArch]
588
                    targetPath target
589
                    verbose isVerbose()
590 591 592 593 594 595 596
                    fileSystemRoots fileSystemRootsValue
                    fileSystemScheme fileSystemSchemeValue
                    trackWidgetCreation trackWidgetCreationValue
                    compilationTraceFilePath compilationTraceFilePathValue
                    createPatch createPatchValue
                    buildNumber buildNumberValue
                    baselineDir baselineDirValue
597
                    targetPlatform targetArch
598
                    sourceDir project.file(project.flutter.source)
599
                    intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/${targetArch}")
600 601 602 603
                    extraFrontEndOptions extraFrontEndOptionsValue
                    extraGenSnapshotOptions extraGenSnapshotOptionsValue
                }
            }
604
            File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
605
            Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
606 607
                destinationDir libJar.parentFile
                archiveName libJar.name
608 609 610
                dependsOn compileTasks
                compileTasks.each { compileTask ->
                    from(compileTask.intermediateDir) {
611
                        include '*.so'
612
                        // Move `app.so` to `lib/<abi>/libapp.so`
613
                        rename { String filename ->
614
                            return "lib/${compileTask.abi}/lib${filename}"
615 616 617
                        }
                    }
                }
618
            }
619
            addApiDependencies(project, variant.name, project.files {
620
                packFlutterAppAotTask
621
            })
622 623
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
624 625 626
            // In add to app scenarios, :flutter is a subproject of another Android app.
            // We know that :flutter is used as a subproject when these tasks exist.
            boolean isUsedAsSubproject = packageAssets && cleanPackageAssets
627
            Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
628
                dependsOn compileTasks
629 630 631 632 633
                compileTasks.each { flutterTask ->
                    // Add flutter_assets.
                    with flutterTask.assets
                }
                if (isUsedAsSubproject) {
634 635 636
                    dependsOn packageAssets
                    dependsOn cleanPackageAssets
                    into packageAssets.outputDir
637
                    return
638
                }
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
                // `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) {
                variant.mergeResources.dependsOn(copyFlutterAssetsTask)
                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)
684
                }
685 686
            }
        }
687 688 689 690 691
        if (project.android.hasProperty("applicationVariants")) {
            project.android.applicationVariants.all addFlutterDeps
        } else {
            project.android.libraryVariants.all addFlutterDeps
        }
692
        configurePlugins()
693 694 695 696 697
    }
}

class FlutterExtension {
    String source
698
    String target
699 700
}

701
abstract class BaseFlutterTask extends DefaultTask {
702
    File flutterRoot
703
    File flutterExecutable
704 705
    String buildMode
    String localEngine
706
    String localEngineSrcPath
707
    @Input
708
    String targetPath
709
    @Optional @Input
710 711
    Boolean verbose
    @Optional @Input
712 713 714
    String[] fileSystemRoots
    @Optional @Input
    String fileSystemScheme
715
    @Input
716 717
    Boolean trackWidgetCreation
    @Optional @Input
718
    String compilationTraceFilePath
719
    @Optional @Input
720 721 722 723 724
    Boolean createPatch
    @Optional @Input
    Integer buildNumber
    @Optional @Input
    String baselineDir
725
    @Optional @Input
726
    String targetPlatform
727 728
    @Input
    String abi
729
    File sourceDir
730
    File intermediateDir
731 732 733 734
    @Optional @Input
    String extraFrontEndOptions
    @Optional @Input
    String extraGenSnapshotOptions
735

736 737
    @OutputFiles
    FileCollection getDependenciesFiles() {
738
        FileCollection depfiles = project.files()
739 740 741 742 743

        // Include the kernel compiler depfile, since kernel compile is the
        // first stage of AOT build in this mode, and it includes all the Dart
        // sources.
        depfiles += project.files("${intermediateDir}/kernel_compile.d")
744 745 746 747 748

        // Include Core JIT kernel compiler depfile, since kernel compile is
        // the first stage of JIT builds in this mode, and it includes all the
        // Dart sources.
        depfiles += project.files("${intermediateDir}/snapshot_blob.bin.d")
749
        return depfiles
750 751
    }

752
    void buildBundle() {
753 754 755 756 757 758
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }

        intermediateDir.mkdirs()

759
        if (buildMode == "profile" || buildMode == "release") {
760 761 762 763 764 765 766 767
            project.exec {
                executable flutterExecutable.absolutePath
                workingDir sourceDir
                if (localEngine != null) {
                    args "--local-engine", localEngine
                    args "--local-engine-src-path", localEngineSrcPath
                }
                args "build", "aot"
768
                args "--suppress-analytics"
769 770 771
                args "--quiet"
                args "--target", targetPath
                args "--output-dir", "${intermediateDir}"
772
                args "--target-platform", "${targetPlatform}"
773 774 775
                if (trackWidgetCreation) {
                    args "--track-widget-creation"
                }
776
                if (extraFrontEndOptions != null) {
777
                    args "--extra-front-end-options", "${extraFrontEndOptions}"
778 779
                }
                if (extraGenSnapshotOptions != null) {
780 781
                    args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
                }
782 783 784
                args "--${buildMode}"
            }
        }
785

786 787 788
        project.exec {
            executable flutterExecutable.absolutePath
            workingDir sourceDir
789

790 791 792 793
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
794
            args "build", "bundle"
795
            args "--target", targetPath
796
            args "--target-platform", "${targetPlatform}"
797 798 799
            if (verbose) {
                args "--verbose"
            }
800 801 802 803 804 805 806 807
            if (fileSystemRoots != null) {
                for (root in fileSystemRoots) {
                    args "--filesystem-root", root
                }
            }
            if (fileSystemScheme != null) {
                args "--filesystem-scheme", fileSystemScheme
            }
808 809 810
            if (trackWidgetCreation) {
                args "--track-widget-creation"
            }
811
            if (compilationTraceFilePath != null) {
812
                args "--compilation-trace-file", compilationTraceFilePath
813
            }
814 815 816 817 818 819 820 821 822
            if (createPatch) {
                args "--patch"
                args "--build-number", project.android.defaultConfig.versionCode
                if (buildNumber != null) {
                    assert buildNumber == project.android.defaultConfig.versionCode
                }
            }
            if (baselineDir != null) {
                args "--baseline-dir", baselineDir
823
            }
824 825 826
            if (extraFrontEndOptions != null) {
                args "--extra-front-end-options", "${extraFrontEndOptions}"
            }
827 828 829
            if (extraGenSnapshotOptions != null) {
                args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
            }
830
            if (buildMode == "release" || buildMode == "profile") {
831
                args "--precompiled"
832 833
            } else {
                args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
834
            }
835
            args "--asset-dir", "${intermediateDir}/flutter_assets"
836 837 838
            if (buildMode == "debug") {
                args "--debug"
            }
839
            if (buildMode == "profile") {
840 841
                args "--profile"
            }
842
            if (buildMode == "release") {
843 844
                args "--release"
            }
845 846 847 848 849
        }
    }
}

class FlutterTask extends BaseFlutterTask {
850
    @OutputDirectory
851 852 853
    File getOutputDirectory() {
        return intermediateDir
    }
854

855 856
    CopySpec getAssets() {
        return project.copySpec {
857 858
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
859 860 861 862 863 864
        }
    }

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

866
            if (buildMode == 'release' || buildMode == 'profile') {
867
                include "app.so"
868
            }
869
        }
870 871
    }

872 873 874 875
    FileCollection readDependencies(File dependenciesFile) {
        if (dependenciesFile.exists()) {
            try {
                // Dependencies file has Makefile syntax:
876
                //   <target> <files>: <source> <files> <separated> <by> <non-escaped space>
877
                String depText = dependenciesFile.text
878 879 880 881 882
                // So we split list of files by non-escaped(by backslash) space,
                def matcher = depText.split(': ')[1] =~ /(\\ |[^\s])+/
                // then we replace all escaped spaces with regular spaces
                def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")}
                return project.files(depList)
883 884 885 886
            } catch (Exception e) {
                logger.error("Error reading dependency file ${dependenciesFile}: ${e}")
            }
        }
887
        return project.files()
888 889
    }

890 891
    @InputFiles
    FileCollection getSourceFiles() {
892 893 894 895 896
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile)
        }
        if (!sources.isEmpty()) {
897 898
            // We have a dependencies file. Add a dependency on gen_snapshot as well, since the
            // snapshots have to be rebuilt if it changes.
899
            sources += readDependencies(project.file("${intermediateDir}/gen_snapshot.d"))
900
            sources += readDependencies(project.file("${intermediateDir}/frontend_server.d"))
901
            if (localEngineSrcPath != null) {
902
                sources += project.files("$localEngineSrcPath/$localEngine")
903
            }
904
            // Finally, add a dependency on pubspec.yaml as well.
905
            return sources + project.files('pubspec.yaml')
906 907 908 909 910 911 912
        }
        // No dependencies file (or problems parsing it). Fall back to source files.
        return project.fileTree(
                dir: sourceDir,
                exclude: ['android', 'ios'],
                include: ['**/*.dart', 'pubspec.yaml']
        )
913 914
    }

915 916
    @TaskAction
    void build() {
917
        buildBundle()
918 919
    }
}