flutter.gradle 20.6 KB
Newer Older
1 2
import java.nio.file.Path
import java.nio.file.Paths
3 4

import com.android.builder.model.AndroidProject
5
import org.apache.tools.ant.taskdefs.condition.Os
6 7 8 9 10
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
11
import org.gradle.api.file.CopySpec
12 13
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.Copy
14
import org.gradle.api.tasks.InputFiles
15 16
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
17
import org.gradle.api.tasks.bundling.Jar
18

19
buildscript {
20
    repositories {
21
        jcenter()
22
        maven {
23
            url 'https://dl.google.com/dl/android/maven2'
24
        }
25 26
    }
    dependencies {
27
        classpath 'com.android.tools.build:gradle:3.0.1'
28
    }
29 30 31 32
}

apply plugin: FlutterPlugin

33
class FlutterPlugin implements Plugin<Project> {
34
    private File flutterRoot
35
    private File flutterExecutable
36
    private String localEngine
37
    private String localEngineSrcPath
38 39
    private Properties localProperties

40
    private File flutterJar
41
    private File flutterX86Jar
42 43 44 45 46 47 48
    private File debugFlutterJar
    private File profileFlutterJar
    private File releaseFlutterJar

    private Properties readPropertiesIfExist(File propertiesFile) {
        Properties result = new Properties()
        if (propertiesFile.exists()) {
49
            propertiesFile.withReader('UTF-8') { reader -> result.load(reader) }
50 51 52 53
        }
        return result
    }

54 55
    private String resolveProperty(Project project, String name, String defaultValue) {
        if (localProperties == null) {
56
            localProperties = readPropertiesIfExist(project.rootProject.file("local.properties"))
57
        }
58
        String result
59 60 61 62 63 64 65 66 67 68 69
        if (project.hasProperty(name)) {
            result = project.property(name)
        }
        if (result == null) {
            result = localProperties.getProperty(name)
        }
        if (result == null) {
            result = defaultValue
        }
        return result
    }
70 71 72

    @Override
    void apply(Project project) {
73 74 75 76
        // Add a 'profile' build type
        project.android.buildTypes {
            profile {
                initWith debug
77 78 79
                if (it.hasProperty('matchingFallbacks')) {
                    matchingFallbacks = ['debug', 'release']
                }
80 81
            }
        }
82

83
        String flutterRootPath = resolveProperty(project, "flutter.sdk", System.env.FLUTTER_ROOT)
84
        if (flutterRootPath == null) {
85
            throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
86
        }
87 88
        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot.isDirectory()) {
89 90 91
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

92 93 94
        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();

95 96 97 98 99 100
        if (project.hasProperty('localEngineOut')) {
            String engineOutPath = project.property('localEngineOut')
            File engineOut = project.file(engineOutPath)
            if (!engineOut.isDirectory()) {
                throw new GradleException('localEngineOut must point to a local engine build')
            }
101
            flutterJar = Paths.get(engineOut.absolutePath, "flutter.jar").toFile()
102
            if (!flutterJar.isFile()) {
103
                throw new GradleException('Local engine build does not contain flutter.jar')
104
            }
105 106 107 108

            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent

109
            project.dependencies {
110 111 112 113 114
                if (project.getConfigurations().findByName("implementation")) {
                    implementation project.files(flutterJar)
                } else {
                    compile project.files(flutterJar)
                }
115
            }
116 117
        } else {
            Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
118 119 120 121 122 123 124 125
            String targetArch = 'arm'
            if (project.hasProperty('target-platform') &&
                project.property('target-platform') == 'android-arm64') {
              targetArch = 'arm64'
            }
            debugFlutterJar = baseEnginePath.resolve("android-${targetArch}").resolve("flutter.jar").toFile()
            profileFlutterJar = baseEnginePath.resolve("android-${targetArch}-profile").resolve("flutter.jar").toFile()
            releaseFlutterJar = baseEnginePath.resolve("android-${targetArch}-release").resolve("flutter.jar").toFile()
126
            if (!debugFlutterJar.isFile()) {
127
                project.exec {
128
                    executable flutterExecutable.absolutePath
129
                    args "--suppress-analytics"
130 131
                    args "precache"
                }
132 133
                if (!debugFlutterJar.isFile()) {
                    throw new GradleException("Unable to find flutter.jar in SDK: ${debugFlutterJar}")
134 135
                }
            }
136 137

            // Add x86/x86_64 native library. Debug mode only, for now.
138
            flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar")
139 140 141
            project.tasks.create("flutterBuildX86Jar", Jar) {
                destinationDir flutterX86Jar.parentFile
                archiveName flutterX86Jar.name
142
                from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") {
143 144
                    into "lib/x86"
                }
145
                from("${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so") {
146 147 148
                    into "lib/x86_64"
                }
            }
149
            // Add flutter.jar dependencies to all <buildType>Implementation configurations, including custom ones
150
            // added after applying the Flutter plugin.
151 152
            project.android.buildTypes.each { addFlutterJarImplementationDependency(project, it) }
            project.android.buildTypes.whenObjectAdded { addFlutterJarImplementationDependency(project, it) }
153 154
        }

155 156
        project.extensions.create("flutter", FlutterExtension)
        project.afterEvaluate this.&addFlutterTask
157 158 159 160 161 162 163 164

        File pluginsFile = new File(project.rootProject.projectDir.parentFile, '.flutter-plugins')
        Properties plugins = readPropertiesIfExist(pluginsFile)

        plugins.each { name, _ ->
            def pluginProject = project.rootProject.findProject(":$name")
            if (pluginProject != null) {
                project.dependencies {
165 166 167 168 169
                    if (project.getConfigurations().findByName("implementation")) {
                        implementation pluginProject
                    } else {
                        compile pluginProject
                    }
170
                }
171
                pluginProject.afterEvaluate this.&addFlutterJarCompileOnlyDependency
172 173 174 175 176 177
            } else {
                project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
            }
        }
    }

178
    private void addFlutterJarCompileOnlyDependency(Project project) {
179 180 181
        if (project.state.failure) {
            return
        }
182 183
        project.dependencies {
            if (flutterJar != null) {
184 185 186 187 188
                if (project.getConfigurations().findByName("compileOnly")) {
                    compileOnly project.files(flutterJar)
                } else {
                    provided project.files(flutterJar)
                }
189
            } else {
190 191 192 193 194 195 196
                if (project.getConfigurations().findByName("debugCompileOnly")) {
                    debugCompileOnly project.files(debugFlutterJar)
                    releaseCompileOnly project.files(releaseFlutterJar)
                } else {
                    debugProvided project.files(debugFlutterJar)
                    releaseProvided project.files(releaseFlutterJar)
                }
197 198
            }
        }
199 200
    }

201
    /**
202
     * Adds suitable flutter.jar implementation dependencies to the specified buildType.
203 204 205
     *
     * Note: The BuildType DSL type is not public, and is therefore omitted from the signature.
     */
206
    private void addFlutterJarImplementationDependency(Project project, buildType) {
207
        project.dependencies {
208 209 210 211 212 213 214
            String configuration;
            if (project.getConfigurations().findByName("implementation")) {
                configuration = buildType.name + "Implementation";
            } else {
                configuration = buildType.name + "Compile";
            }
            add(configuration, project.files {
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
                String buildMode = buildModeFor(buildType)
                if (buildMode == "debug") {
                    [flutterX86Jar, debugFlutterJar]
                } else if (buildMode == "profile") {
                    profileFlutterJar
                } else {
                    releaseFlutterJar
                }
            })
        }
    }

    /**
     * Returns a Flutter build mode suitable for the specified Android buildType.
     *
     * Note: The BuildType DSL type is not public, and is therefore omitted from the signature.
     *
     * @return "debug", "profile", or "release" (fall-back).
     */
    private static String buildModeFor(buildType) {
        if (buildType.name == "profile") {
            return "profile"
        } else if (buildType.debuggable) {
            return "debug"
        }
        return "release"
    }

243
    private void addFlutterTask(Project project) {
244 245 246
        if (project.state.failure) {
            return
        }
247 248 249 250
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }

251
        String target = project.flutter.target
252 253 254
        if (target == null) {
            target = 'lib/main.dart'
        }
255 256 257
        if (project.hasProperty('target')) {
            target = project.property('target')
        }
258

259 260
        Boolean previewDart2Value = false
        if (project.hasProperty('preview-dart-2')) {
261
            previewDart2Value = project.property('preview-dart-2').toBoolean()
262
        }
263 264 265 266 267 268 269 270
        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')
        }
271 272
        Boolean trackWidgetCreationValue = false
        if (project.hasProperty('track-widget-creation')) {
273
            trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
274
        }
275

276 277 278 279 280 281 282 283
        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')
        }
284 285
        Boolean preferSharedLibraryValue = false
        if (project.hasProperty('prefer-shared-library')) {
286
            preferSharedLibraryValue = project.property('prefer-shared-library').toBoolean()
287
        }
288 289 290 291
        String targetPlatformValue = null
        if (project.hasProperty('target-platform')) {
            targetPlatformValue = project.property('target-platform')
        }
292

293
        def addFlutterDeps = { variant ->
294 295
            String flutterBuildMode = buildModeFor(variant.buildType)
            if (flutterBuildMode == 'debug' && project.tasks.findByName('flutterBuildX86Jar')) {
296 297 298 299
                Task task = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac")
                if (task) {
                    task.dependsOn project.flutterBuildX86Jar
                }
300 301 302 303
                task = project.tasks.findByName("compile${variant.name.capitalize()}Kotlin")
                if (task) {
                    task.dependsOn project.flutterBuildX86Jar
                }
304 305
            }

306
            FlutterTask flutterTask = project.tasks.create(name: "flutterBuild${variant.name.capitalize()}", type: FlutterTask) {
307
                flutterRoot this.flutterRoot
308
                flutterExecutable this.flutterExecutable
309
                buildMode flutterBuildMode
310
                localEngine this.localEngine
311
                localEngineSrcPath this.localEngineSrcPath
312
                targetPath target
313
                previewDart2 previewDart2Value
314 315
                fileSystemRoots fileSystemRootsValue
                fileSystemScheme fileSystemSchemeValue
316
                trackWidgetCreation trackWidgetCreationValue
317
                preferSharedLibrary preferSharedLibraryValue
318
                targetPlatform targetPlatformValue
319 320
                sourceDir project.file(project.flutter.source)
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
321 322
                extraFrontEndOptions extraFrontEndOptionsValue
                extraGenSnapshotOptions extraGenSnapshotOptionsValue
323 324
            }

325
            Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
326 327
                dependsOn flutterTask
                dependsOn variant.mergeAssets
328
                dependsOn "clean${variant.mergeAssets.name.capitalize()}"
329
                into variant.mergeAssets.outputDir
330
                with flutterTask.assets
331
            }
332
            variant.outputs[0].processResources.dependsOn(copyFlutterAssetsTask)
333
        }
334 335 336 337 338
        if (project.android.hasProperty("applicationVariants")) {
            project.android.applicationVariants.all addFlutterDeps
        } else {
            project.android.libraryVariants.all addFlutterDeps
        }
339 340 341 342 343
    }
}

class FlutterExtension {
    String source
344
    String target
345 346
}

347
abstract class BaseFlutterTask extends DefaultTask {
348
    File flutterRoot
349
    File flutterExecutable
350 351
    String buildMode
    String localEngine
352
    String localEngineSrcPath
353
    @Input
354
    String targetPath
355
    @Optional @Input
356
    Boolean previewDart2
357
    @Optional @Input
358 359 360 361
    String[] fileSystemRoots
    @Optional @Input
    String fileSystemScheme
    @Optional @Input
362 363
    Boolean trackWidgetCreation
    @Optional @Input
364
    Boolean preferSharedLibrary
365 366
    @Optional @Input
    String targetPlatform
367
    File sourceDir
368
    File intermediateDir
369 370 371 372
    @Optional @Input
    String extraFrontEndOptions
    @Optional @Input
    String extraGenSnapshotOptions
373

374 375
    @OutputFiles
    FileCollection getDependenciesFiles() {
376
        if (buildMode != 'debug') {
377 378 379 380 381 382 383 384 385
            // For AOT builds, include the gen_snapshot depfile.
            FileCollection depfiles = project.files("${intermediateDir}/snapshot.d")
            if (previewDart2) {
                // For Dart 2, also 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")
            }
            return depfiles
386
        }
387
        return project.files("${intermediateDir}/snapshot_blob.bin.d")
388 389
    }

390
    void buildBundle() {
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }

        intermediateDir.mkdirs()

        if (buildMode != "debug") {
            project.exec {
                executable flutterExecutable.absolutePath
                workingDir sourceDir
                if (localEngine != null) {
                    args "--local-engine", localEngine
                    args "--local-engine-src-path", localEngineSrcPath
                }
                args "build", "aot"
406
                args "--suppress-analytics"
407 408 409 410
                args "--quiet"
                args "--target", targetPath
                args "--target-platform", "android-arm"
                args "--output-dir", "${intermediateDir}"
411 412
                if (previewDart2) {
                    args "--preview-dart-2"
413
                } else {
414 415
                    args "--no-preview-dart-2"
                }
416 417 418
                if (trackWidgetCreation) {
                    args "--track-widget-creation"
                }
419
                if (extraFrontEndOptions != null) {
420
                    args "--extra-front-end-options", "${extraFrontEndOptions}"
421 422
                }
                if (extraGenSnapshotOptions != null) {
423 424 425 426
                    args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
                }
                if (preferSharedLibrary) {
                    args "--prefer-shared-library"
427
                }
428 429 430
                if (targetPlatform != null) {
                    args "--target-platform", "${targetPlatform}"
                }
431 432 433
                args "--${buildMode}"
            }
        }
434

435 436 437 438 439 440 441
        project.exec {
            executable flutterExecutable.absolutePath
            workingDir sourceDir
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
442
            args "build", "bundle"
443
            args "--suppress-analytics"
444
            args "--target", targetPath
445 446
            if (previewDart2) {
                args "--preview-dart-2"
447 448
            } else {
                args "--no-preview-dart-2"
449 450 451 452 453 454 455 456 457
            }
            if (fileSystemRoots != null) {
                for (root in fileSystemRoots) {
                    args "--filesystem-root", root
                }
            }
            if (fileSystemScheme != null) {
                args "--filesystem-scheme", fileSystemScheme
            }
458 459 460
            if (trackWidgetCreation) {
                args "--track-widget-creation"
            }
461 462 463
            if (buildMode != "debug") {
                args "--precompiled"
            } else {
464
                args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
465 466 467
                if (!previewDart2) {
                    args "--snapshot", "${intermediateDir}/snapshot_blob.bin"
                }
468
            }
469
            args "--asset-dir", "${intermediateDir}/flutter_assets"
470 471 472 473 474
        }
    }
}

class FlutterTask extends BaseFlutterTask {
475
    @OutputDirectory
476 477 478
    File getOutputDirectory() {
        return intermediateDir
    }
479

480 481
    CopySpec getAssets() {
        return project.copySpec {
482
            from "${intermediateDir}"
483

484
            include "flutter_assets/**" // the working dir and its files
485

486
            if (buildMode != 'debug') {
487
                if (preferSharedLibrary) {
488
                    include "app.so"
489 490 491 492 493 494
                } else {
                    include "vm_snapshot_data"
                    include "vm_snapshot_instr"
                    include "isolate_snapshot_data"
                    include "isolate_snapshot_instr"
                }
495
            }
496
        }
497 498
    }

499 500 501 502 503 504 505 506 507 508 509
    FileCollection readDependencies(File dependenciesFile) {
        if (dependenciesFile.exists()) {
            try {
                // Dependencies file has Makefile syntax:
                //   <target> <files>: <source> <files> <separated> <by> <space>
                String depText = dependenciesFile.text
                return project.files(depText.split(': ')[1].split())
            } catch (Exception e) {
                logger.error("Error reading dependency file ${dependenciesFile}: ${e}")
            }
        }
510
        return project.files()
511 512
    }

513 514
    @InputFiles
    FileCollection getSourceFiles() {
515 516 517 518 519
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
          sources += readDependencies(depfile)
        }
        if (!sources.isEmpty()) {
520 521
            // We have a dependencies file. Add a dependency on gen_snapshot as well, since the
            // snapshots have to be rebuilt if it changes.
522
            sources += readDependencies(project.file("${intermediateDir}/gen_snapshot.d"))
523
            if (previewDart2) {
524
                sources += readDependencies(project.file("${intermediateDir}/frontend_server.d"))
525
            }
526
            if (localEngineSrcPath != null) {
527
                sources += project.files("$localEngineSrcPath/$localEngine")
528
            }
529
            // Finally, add a dependency on pubspec.yaml as well.
530
            return sources + project.files('pubspec.yaml')
531 532 533 534 535 536 537
        }
        // No dependencies file (or problems parsing it). Fall back to source files.
        return project.fileTree(
                dir: sourceDir,
                exclude: ['android', 'ios'],
                include: ['**/*.dart', 'pubspec.yaml']
        )
538 539
    }

540 541
    @TaskAction
    void build() {
542
        buildBundle()
543 544
    }
}