flutter.gradle 19.1 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 23 24
        maven {
            url 'https://maven.google.com'
        }
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
            debugFlutterJar = baseEnginePath.resolve("android-arm").resolve("flutter.jar").toFile()
            profileFlutterJar = baseEnginePath.resolve("android-arm-profile").resolve("flutter.jar").toFile()
            releaseFlutterJar = baseEnginePath.resolve("android-arm-release").resolve("flutter.jar").toFile()
121
            if (!debugFlutterJar.isFile()) {
122
                project.exec {
123
                    executable flutterExecutable.absolutePath
124
                    args "--suppress-analytics"
125 126
                    args "precache"
                }
127 128
                if (!debugFlutterJar.isFile()) {
                    throw new GradleException("Unable to find flutter.jar in SDK: ${debugFlutterJar}")
129 130
                }
            }
131 132

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

150 151
        project.extensions.create("flutter", FlutterExtension)
        project.afterEvaluate this.&addFlutterTask
152 153 154 155 156 157 158 159

        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 {
160 161 162 163 164
                    if (project.getConfigurations().findByName("implementation")) {
                        implementation pluginProject
                    } else {
                        compile pluginProject
                    }
165
                }
166
                pluginProject.afterEvaluate this.&addFlutterJarCompileOnlyDependency
167 168 169 170 171 172
            } else {
                project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
            }
        }
    }

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

196
    /**
197
     * Adds suitable flutter.jar implementation dependencies to the specified buildType.
198 199 200
     *
     * Note: The BuildType DSL type is not public, and is therefore omitted from the signature.
     */
201
    private void addFlutterJarImplementationDependency(Project project, buildType) {
202
        project.dependencies {
203 204 205 206 207 208 209
            String configuration;
            if (project.getConfigurations().findByName("implementation")) {
                configuration = buildType.name + "Implementation";
            } else {
                configuration = buildType.name + "Compile";
            }
            add(configuration, project.files {
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
                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"
    }

238
    private void addFlutterTask(Project project) {
239 240 241
        if (project.state.failure) {
            return
        }
242 243 244 245
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }

246
        String target = project.flutter.target
247 248 249
        if (target == null) {
            target = 'lib/main.dart'
        }
250 251 252
        if (project.hasProperty('target')) {
            target = project.property('target')
        }
253

254 255 256
        Boolean previewDart2Value = false
        if (project.hasProperty('preview-dart-2')) {
            previewDart2Value = project.property('preview-dart-2')
257
        }
258 259 260 261 262
	
        Boolean strongModeValue = false
        if (project.hasProperty('strong')) {
            strongModeValue = project.property('strong')
        }
263

264 265 266 267 268 269 270 271
        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')
        }
272 273 274 275
        Boolean preferSharedLibraryValue = false
        if (project.hasProperty('prefer-shared-library')) {
            preferSharedLibraryValue = project.property('prefer-shared-library')
        }
276

277
        project.android.applicationVariants.all { variant ->
278 279
            String flutterBuildMode = buildModeFor(variant.buildType)
            if (flutterBuildMode == 'debug' && project.tasks.findByName('flutterBuildX86Jar')) {
280 281 282 283
                Task task = project.tasks.findByName("compile${variant.name.capitalize()}JavaWithJavac")
                if (task) {
                    task.dependsOn project.flutterBuildX86Jar
                }
284 285 286 287
                task = project.tasks.findByName("compile${variant.name.capitalize()}Kotlin")
                if (task) {
                    task.dependsOn project.flutterBuildX86Jar
                }
288 289
            }

290
            GenerateDependencies dependenciesTask = project.tasks.create(name: "flutterDependencies${variant.name.capitalize()}", type: GenerateDependencies) {
291 292
                flutterRoot this.flutterRoot
                flutterExecutable this.flutterExecutable
293
                buildMode flutterBuildMode
294 295 296
                localEngine this.localEngine
                localEngineSrcPath this.localEngineSrcPath
                targetPath target
297
                previewDart2 previewDart2Value
298
                strongMode strongModeValue	
299
                preferSharedLibrary preferSharedLibraryValue
300 301 302 303
                sourceDir project.file(project.flutter.source)
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
            }

304
            FlutterTask flutterTask = project.tasks.create(name: "flutterBuild${variant.name.capitalize()}", type: FlutterTask) {
305
                dependsOn dependenciesTask
306
                flutterRoot this.flutterRoot
307
                flutterExecutable this.flutterExecutable
308
                buildMode flutterBuildMode
309
                localEngine this.localEngine
310
                localEngineSrcPath this.localEngineSrcPath
311
                targetPath target
312
                previewDart2 previewDart2Value
313
                strongMode strongModeValue	
314
                preferSharedLibrary preferSharedLibraryValue
315 316
                sourceDir project.file(project.flutter.source)
                intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
317 318
                extraFrontEndOptions extraFrontEndOptionsValue
                extraGenSnapshotOptions extraGenSnapshotOptionsValue
319 320
            }

321
            Task copyFlxTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
322 323 324
                dependsOn flutterTask
                dependsOn variant.mergeAssets
                into variant.mergeAssets.outputDir
325
                with flutterTask.assets
326 327 328 329 330 331 332 333
            }
            variant.outputs[0].processResources.dependsOn(copyFlxTask)
        }
    }
}

class FlutterExtension {
    String source
334
    String target
335 336
}

337
abstract class BaseFlutterTask extends DefaultTask {
338
    File flutterRoot
339
    File flutterExecutable
340 341
    String buildMode
    String localEngine
342
    String localEngineSrcPath
343
    @Input
344
    String targetPath
345
    @Optional @Input
346
    Boolean previewDart2
347
    @Optional @Input
348 349
    Boolean strongMode
    @Optional @Input
350
    Boolean preferSharedLibrary
351
    File sourceDir
352
    File intermediateDir
353 354 355 356
    @Optional @Input
    String extraFrontEndOptions
    @Optional @Input
    String extraGenSnapshotOptions
357 358 359 360 361 362

    @OutputFile
    File getDependenciesFile() {
        if (buildMode != 'debug') {
            return project.file("${intermediateDir}/snapshot.d")
        }
363
        return project.file("${intermediateDir}/snapshot_blob.bin.d")
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
    }

    void buildFlx() {
        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"
382
                args "--suppress-analytics"
383 384 385 386
                args "--quiet"
                args "--target", targetPath
                args "--target-platform", "android-arm"
                args "--output-dir", "${intermediateDir}"
387 388 389
                if (previewDart2) {
                    args "--preview-dart-2"
                }
390 391 392
		if (strongMode) {
                    args "--strong"
                }
393
                if (extraFrontEndOptions != null) {
394
                    args "--extra-front-end-options", "${extraFrontEndOptions}"
395 396
                }
                if (extraGenSnapshotOptions != null) {
397 398 399 400
                    args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
                }
                if (preferSharedLibrary) {
                    args "--prefer-shared-library"
401
                }
402 403 404
                args "--${buildMode}"
            }
        }
405

406 407 408 409 410 411 412 413
        project.exec {
            executable flutterExecutable.absolutePath
            workingDir sourceDir
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
            args "build", "flx"
414
            args "--suppress-analytics"
415
            args "--target", targetPath
416 417
            if (previewDart2) {
                args "--preview-dart-2"
418
            }
419 420 421 422
            if (strongMode) {
                args "--strong"
            }

423 424 425 426
            args "--output-file", "${intermediateDir}/app.flx"
            if (buildMode != "debug") {
                args "--precompiled"
            } else {
427 428 429 430
                if (!previewDart2) {
                    args "--snapshot", "${intermediateDir}/snapshot_blob.bin"
                    args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
                }
431
            }
432
            args "--working-dir", "${intermediateDir}/flutter_assets"
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
        }
    }
}

class GenerateDependencies extends BaseFlutterTask {
    @TaskAction
    void build() {
        File dependenciesFile = getDependenciesFile();
        if (!dependenciesFile.exists()) {
            buildFlx()
        }
    }
}

class FlutterTask extends BaseFlutterTask {
448
    @OutputDirectory
449 450 451
    File getOutputDirectory() {
        return intermediateDir
    }
452

453 454
    CopySpec getAssets() {
        return project.copySpec {
455 456
            from "${intermediateDir}"
            include "flutter_assets/**" // the working dir and its files
457
            if (buildMode != 'debug') {
458 459 460 461 462 463 464 465
	       if (preferSharedLibrary) {
                  include "${intermediateDir}/app.so"
	       } else {
                  include "vm_snapshot_data"
                  include "vm_snapshot_instr"
                  include "isolate_snapshot_data"
                  include "isolate_snapshot_instr"
	       }
466
            }
467
      }
468 469
    }

470 471 472 473 474 475 476 477 478 479 480 481 482 483
    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}")
            }
        }
        return null
    }

484 485
    @InputFiles
    FileCollection getSourceFiles() {
486
        File dependenciesFile = getDependenciesFile()
487 488 489 490 491 492 493 494
        FileCollection sources = readDependencies(dependenciesFile)
        if (sources != null) {
            // We have a dependencies file. Add a dependency on gen_snapshot as well, since the
            // snapshots have to be rebuilt if it changes.
            FileCollection snapshotter = readDependencies(project.file("${intermediateDir}/gen_snapshot.d"))
            if (snapshotter != null) {
                sources = sources.plus(snapshotter)
            }
495 496 497
            if (localEngineSrcPath != null) {
                sources = sources.plus(project.files("$localEngineSrcPath/$localEngine"))
            }
498 499 500 501 502 503 504 505 506
            // Finally, add a dependency on pubspec.yaml as well.
            return sources.plus(project.files('pubspec.yaml'))
        }
        // No dependencies file (or problems parsing it). Fall back to source files.
        return project.fileTree(
                dir: sourceDir,
                exclude: ['android', 'ios'],
                include: ['**/*.dart', 'pubspec.yaml']
        )
507 508
    }

509 510
    @TaskAction
    void build() {
511
        buildFlx()
512 513
    }
}