mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
995 lines
41 KiB
Groovy
995 lines
41 KiB
Groovy
import static groovy.io.FileType.FILES
|
|
|
|
import com.android.builder.model.AndroidProject
|
|
import com.android.build.OutputFile
|
|
import groovy.json.JsonSlurper
|
|
import java.nio.file.Path
|
|
import java.nio.file.Paths
|
|
import java.util.regex.Matcher
|
|
import java.util.regex.Pattern
|
|
import org.apache.tools.ant.taskdefs.condition.Os
|
|
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
|
|
import org.gradle.api.file.CopySpec
|
|
import org.gradle.api.file.FileCollection
|
|
import org.gradle.api.tasks.Copy
|
|
import org.gradle.api.tasks.InputFiles
|
|
import org.gradle.api.tasks.OutputDirectory
|
|
import org.gradle.api.tasks.TaskAction
|
|
import org.gradle.api.tasks.bundling.Jar
|
|
|
|
buildscript {
|
|
repositories {
|
|
google()
|
|
jcenter()
|
|
}
|
|
dependencies {
|
|
classpath 'com.android.tools.build:gradle:3.5.0'
|
|
}
|
|
}
|
|
|
|
android {
|
|
compileOptions {
|
|
sourceCompatibility 1.8
|
|
targetCompatibility 1.8
|
|
}
|
|
}
|
|
|
|
apply plugin: FlutterPlugin
|
|
|
|
class FlutterPlugin implements Plugin<Project> {
|
|
private static final String MAVEN_REPO = "https://storage.googleapis.com/download.flutter.io";
|
|
|
|
// 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"
|
|
|
|
private Project project
|
|
private Map baseJar = [:]
|
|
private File flutterRoot
|
|
private File flutterExecutable
|
|
private String localEngine
|
|
private String localEngineSrcPath
|
|
private Properties localProperties
|
|
private String engineVersion
|
|
|
|
@Override
|
|
void apply(Project project) {
|
|
this.project = project
|
|
|
|
project.extensions.create("flutter", FlutterExtension)
|
|
project.afterEvaluate this.&addFlutterTasks
|
|
// 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.
|
|
if (shouldSplitPerAbi()) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
getTargetPlatforms().each { targetArch ->
|
|
String abiValue = PLATFORM_ARCH_MAP[targetArch]
|
|
project.android {
|
|
if (shouldSplitPerAbi()) {
|
|
splits {
|
|
abi {
|
|
include abiValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
|
|
if (flutterRootPath == null) {
|
|
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
|
|
}
|
|
flutterRoot = project.file(flutterRootPath)
|
|
if (!flutterRoot.isDirectory()) {
|
|
throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
|
|
}
|
|
|
|
engineVersion = useLocalEngine()
|
|
? "+" // Match any version since there's only one.
|
|
: "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()
|
|
|
|
String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
|
|
flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
|
|
|
|
// Add custom build types.
|
|
project.android.buildTypes {
|
|
profile {
|
|
initWith debug
|
|
if (it.hasProperty("matchingFallbacks")) {
|
|
matchingFallbacks = ["debug", "release"]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldShrinkResources(project)) {
|
|
String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
|
|
"gradle", "flutter_proguard_rules.pro")
|
|
project.android.buildTypes {
|
|
release {
|
|
// Enables code shrinking, obfuscation, and optimization for only
|
|
// your project's release build type.
|
|
minifyEnabled true
|
|
// Enables resource shrinking, which is performed by the
|
|
// Android Gradle plugin.
|
|
// NOTE: The resource shrinker can't be used for libraries.
|
|
shrinkResources isBuiltAsApp(project)
|
|
// Fallback to `android/app/proguard-rules.pro`.
|
|
// This way, custom Proguard rules can be configured as needed.
|
|
proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
|
|
}
|
|
}
|
|
}
|
|
if (useLocalEngine()) {
|
|
// This is required to pass the local engine to flutter build aot.
|
|
String engineOutPath = project.property('local-engine-out')
|
|
File engineOut = project.file(engineOutPath)
|
|
if (!engineOut.isDirectory()) {
|
|
throw new GradleException('local-engine-out must point to a local engine build')
|
|
}
|
|
localEngine = engineOut.name
|
|
localEngineSrcPath = engineOut.parentFile.parent
|
|
}
|
|
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) {
|
|
String flutterBuildMode = buildModeFor(buildType)
|
|
if (!supportsBuildMode(flutterBuildMode)) {
|
|
return
|
|
}
|
|
String repository = useLocalEngine()
|
|
? project.property('local-engine-repo')
|
|
: MAVEN_REPO
|
|
|
|
project.rootProject.allprojects {
|
|
repositories {
|
|
maven {
|
|
url repository
|
|
}
|
|
}
|
|
}
|
|
// Add the embedding dependency.
|
|
addApiDependencies(project, buildType.name,
|
|
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
|
|
|
|
List<String> platforms = getTargetPlatforms().collect()
|
|
// Debug mode includes x86 and x64, which are commonly used in emulators.
|
|
if (flutterBuildMode == "debug" && !useLocalEngine()) {
|
|
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:$engineVersion")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the directory where the plugins are built.
|
|
*/
|
|
private File getPluginBuildDir() {
|
|
// 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
|
|
}
|
|
|
|
/**
|
|
* 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()) {
|
|
getPluginList().each this.&configurePluginProject
|
|
getPluginDependencies().each this.&configurePluginDependencies
|
|
return
|
|
}
|
|
project.repositories {
|
|
maven {
|
|
url "${getPluginBuildDir()}/outputs/repo"
|
|
}
|
|
}
|
|
getPluginList().each { pluginName, pluginPath ->
|
|
configurePluginAar(pluginName, pluginPath, project)
|
|
}
|
|
}
|
|
|
|
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 pluginName, String _) {
|
|
Project pluginProject = project.rootProject.findProject(":$pluginName")
|
|
if (pluginProject == null) {
|
|
project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
|
|
return
|
|
}
|
|
// Add plugin dependency to the app project.
|
|
project.dependencies {
|
|
implementation pluginProject
|
|
}
|
|
Closure addEmbeddingCompileOnlyDependency = { buildType ->
|
|
String flutterBuildMode = buildModeFor(buildType)
|
|
// 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
|
|
if (!supportsBuildMode(flutterBuildMode)) {
|
|
return
|
|
}
|
|
addApiDependencies(
|
|
pluginProject,
|
|
buildType.name,
|
|
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
|
|
)
|
|
}
|
|
pluginProject.afterEvaluate {
|
|
pluginProject.android.buildTypes {
|
|
profile {
|
|
initWith debug
|
|
}
|
|
}
|
|
pluginProject.android.buildTypes.each addEmbeddingCompileOnlyDependency
|
|
pluginProject.android.buildTypes.whenObjectAdded addEmbeddingCompileOnlyDependency
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// 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}")
|
|
if (pluginProject == null ||
|
|
!doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
|
|
return
|
|
}
|
|
assert dependencyObject.dependencies instanceof List
|
|
dependencyObject.dependencies.each { pluginDependencyName ->
|
|
assert pluginDependencyName instanceof String
|
|
if (pluginDependencyName.empty) {
|
|
return
|
|
}
|
|
Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
|
|
if (dependencyProject == null ||
|
|
!doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
|
|
return
|
|
}
|
|
// Wait for the Android plugin to load and add the dependency to the plugin project.
|
|
pluginProject.afterEvaluate {
|
|
pluginProject.dependencies {
|
|
implementation dependencyProject
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Properties getPluginList() {
|
|
File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
|
|
Properties allPlugins = readPropertiesIfExist(pluginsFile)
|
|
Properties androidPlugins = new Properties()
|
|
allPlugins.each { name, path ->
|
|
if (doesSupportAndroidPlatform(path)) {
|
|
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
|
|
}
|
|
|
|
// 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 []
|
|
}
|
|
|
|
private static String toCammelCase(List<String> parts) {
|
|
if (parts.empty) {
|
|
return ""
|
|
}
|
|
return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}"
|
|
}
|
|
|
|
private String resolveProperty(String name, String defaultValue) {
|
|
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
|
|
}
|
|
|
|
private List<String> getTargetPlatforms() {
|
|
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
|
|
}
|
|
}
|
|
|
|
private Boolean shouldSplitPerAbi() {
|
|
if (project.hasProperty('split-per-abi')) {
|
|
return project.property('split-per-abi').toBoolean()
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private Boolean useLocalEngine() {
|
|
return project.hasProperty('local-engine-repo')
|
|
}
|
|
|
|
private Boolean isVerbose() {
|
|
if (project.hasProperty('verbose')) {
|
|
return project.property('verbose').toBoolean()
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
|
|
private static Boolean shouldShrinkResources(Project project) {
|
|
if (project.hasProperty("shrink")) {
|
|
return project.property("shrink").toBoolean()
|
|
}
|
|
return false
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
private static Boolean buildPluginAsAar() {
|
|
return System.getProperty('build-plugins-as-aars') == 'true'
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
private void addCompileOnlyDependency(Project project, String variantName, Object dependency, Closure config = null) {
|
|
if (project.state.failure) {
|
|
return
|
|
}
|
|
String configuration;
|
|
if (project.getConfigurations().findByName("compileOnly")) {
|
|
configuration = "${variantName}CompileOnly";
|
|
} else {
|
|
configuration = "${variantName}Provided";
|
|
}
|
|
project.dependencies.add(configuration, dependency, config)
|
|
}
|
|
|
|
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";
|
|
}
|
|
project.dependencies.add(configuration, dependency, config)
|
|
}
|
|
|
|
/**
|
|
* 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"
|
|
}
|
|
|
|
private static String getEngineArtifactDirName(buildType, targetArch) {
|
|
if (buildType.name == "profile") {
|
|
return "${targetArch}-profile"
|
|
} else if (buildType.debuggable) {
|
|
return "${targetArch}"
|
|
}
|
|
return "${targetArch}-release"
|
|
}
|
|
|
|
private void addFlutterTasks(Project project) {
|
|
if (project.state.failure) {
|
|
return
|
|
}
|
|
if (project.flutter.source == null) {
|
|
throw new GradleException("Must provide Flutter source directory")
|
|
}
|
|
String target = project.flutter.target
|
|
if (target == null) {
|
|
target = 'lib/main.dart'
|
|
}
|
|
if (project.hasProperty('target')) {
|
|
target = project.property('target')
|
|
}
|
|
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')
|
|
}
|
|
Boolean trackWidgetCreationValue = false
|
|
if (project.hasProperty('track-widget-creation')) {
|
|
trackWidgetCreationValue = project.property('track-widget-creation').toBoolean()
|
|
}
|
|
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')
|
|
}
|
|
String splitDebugInfoValue = null
|
|
if (project.hasProperty('split-debug-info')) {
|
|
splitDebugInfoValue = project.property('split-debug-info')
|
|
}
|
|
Boolean dartObfuscationValue = false
|
|
if (project.hasProperty('dart-obfuscation')) {
|
|
dartObfuscationValue = project.property('dart-obfuscation').toBoolean();
|
|
}
|
|
Boolean treeShakeIconsOptionsValue = false
|
|
if (project.hasProperty('tree-shake-icons')) {
|
|
treeShakeIconsOptionsValue = project.property('tree-shake-icons').toBoolean()
|
|
}
|
|
String dartDefinesValue = null
|
|
if (project.hasProperty('dart-defines')) {
|
|
dartDefinesValue = project.property('dart-defines')
|
|
}
|
|
def targetPlatforms = getTargetPlatforms()
|
|
def addFlutterDeps = { variant ->
|
|
if (shouldSplitPerAbi()) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
String variantBuildMode = buildModeFor(variant.buildType)
|
|
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()
|
|
fastStart isFastStart()
|
|
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
|
|
splitDebugInfo splitDebugInfoValue
|
|
treeShakeIcons treeShakeIconsOptionsValue
|
|
dartObfuscation dartObfuscationValue
|
|
dartDefines dartDefinesValue
|
|
doLast {
|
|
project.exec {
|
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
|
commandLine('cmd', '/c', "attrib -r ${assetsDirectory}/* /s")
|
|
} else {
|
|
commandLine('chmod', '-R', 'u+w', assetsDirectory)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
File libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
|
|
Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
|
|
destinationDir libJar.parentFile
|
|
archiveName libJar.name
|
|
dependsOn compileTask
|
|
targetPlatforms.each { targetPlatform ->
|
|
String abi = PLATFORM_ARCH_MAP[targetPlatform]
|
|
from("${compileTask.intermediateDir}/${abi}") {
|
|
include "*.so"
|
|
// Move `app.so` to `lib/<abi>/libapp.so`
|
|
rename { String filename ->
|
|
return "lib/${abi}/lib${filename}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addApiDependencies(project, variant.name, project.files {
|
|
packFlutterAppAotTask
|
|
})
|
|
// 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.
|
|
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
|
|
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
|
|
boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar
|
|
Task copyFlutterAssetsTask = project.tasks.create(
|
|
name: "copyFlutterAssets${variant.name.capitalize()}",
|
|
type: Copy,
|
|
) {
|
|
dependsOn compileTask
|
|
with compileTask.assets
|
|
if (isUsedAsSubproject) {
|
|
dependsOn packageAssets
|
|
dependsOn cleanPackageAssets
|
|
into packageAssets.outputDir
|
|
return
|
|
}
|
|
// `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) {
|
|
def variantOutput = variant.outputs.first()
|
|
def processResources = variantOutput.hasProperty("processResourcesProvider") ?
|
|
variantOutput.processResourcesProvider.get() : variantOutput.processResources
|
|
processResources.dependsOn(copyFlutterAssetsTask)
|
|
return
|
|
}
|
|
// Flutter module included as a subproject in add to app.
|
|
String hostAppProjectName = project.rootProject.hasProperty('flutter.hostAppProjectName') ? project.rootProject.property('flutter.hostAppProjectName') : "app"
|
|
Project appProject = project.rootProject.findProject(":${hostAppProjectName}")
|
|
assert appProject != null : "Project :${hostAppProjectName} doesn't exist. To custom the host app project name, set `org.gradle.project.flutter.hostAppProjectName=<project-name>` in gradle.properties."
|
|
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(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets")
|
|
assert mergeAssets
|
|
mergeAssets.dependsOn(copyFlutterAssetsTask)
|
|
}
|
|
}
|
|
}
|
|
if (project.android.hasProperty("applicationVariants")) {
|
|
project.android.applicationVariants.all { variant ->
|
|
addFlutterDeps(variant)
|
|
// Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
|
|
// can discover them. By default, this is `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`.
|
|
//
|
|
// The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.apk`.
|
|
// Where:
|
|
// * `abi` can be `armeabi-v7a|arm64-v8a|x86|x86_64` only if the flag `split-per-abi` is set.
|
|
// * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called.
|
|
// * `build-mode` can be `release|debug|profile`.
|
|
variant.outputs.all { output ->
|
|
// `assemble` became `assembleProvider` in AGP 3.3.0.
|
|
def assembleTask = variant.hasProperty("assembleProvider")
|
|
? variant.assembleProvider.get()
|
|
: variant.assemble
|
|
assembleTask.doLast {
|
|
// `packageApplication` became `packageApplicationProvider` in AGP 3.3.0.
|
|
def outputDirectory = variant.hasProperty("packageApplicationProvider")
|
|
? variant.packageApplicationProvider.get().outputDirectory
|
|
: variant.packageApplication.outputDirectory
|
|
// `outputDirectory` is a `DirectoryProperty` in AGP 4.1.
|
|
String outputDirectoryStr = outputDirectory.metaClass.respondsTo(outputDirectory, "get")
|
|
? outputDirectory.get()
|
|
: outputDirectory
|
|
String filename = "app"
|
|
String abi = output.getFilter(OutputFile.ABI)
|
|
if (abi != null && !abi.isEmpty()) {
|
|
filename += "-${abi}"
|
|
}
|
|
if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
|
|
filename += "-${variant.flavorName.toLowerCase()}"
|
|
}
|
|
filename += "-${buildModeFor(variant.buildType)}"
|
|
project.copy {
|
|
from new File("$outputDirectoryStr/${output.outputFileName}")
|
|
into new File("${project.buildDir}/outputs/flutter-apk");
|
|
rename {
|
|
return "${filename}.apk"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
project.android.libraryVariants.all addFlutterDeps
|
|
}
|
|
configurePlugins()
|
|
}
|
|
}
|
|
|
|
class FlutterExtension {
|
|
String source
|
|
String target
|
|
}
|
|
|
|
abstract class BaseFlutterTask extends DefaultTask {
|
|
File flutterRoot
|
|
File flutterExecutable
|
|
String buildMode
|
|
@Optional @Input
|
|
String localEngine
|
|
@Optional @Input
|
|
String localEngineSrcPath
|
|
@Input
|
|
Boolean fastStart
|
|
@Input
|
|
String targetPath
|
|
@Optional
|
|
Boolean verbose
|
|
@Optional @Input
|
|
String[] fileSystemRoots
|
|
@Optional @Input
|
|
String fileSystemScheme
|
|
@Input
|
|
Boolean trackWidgetCreation
|
|
@Optional @Input
|
|
List<String> targetPlatformValues
|
|
File sourceDir
|
|
File intermediateDir
|
|
@Optional @Input
|
|
String extraFrontEndOptions
|
|
@Optional @Input
|
|
String extraGenSnapshotOptions
|
|
@Optional @Input
|
|
String splitDebugInfo
|
|
@Optional @Input
|
|
Boolean treeShakeIcons
|
|
@Optional @Input
|
|
Boolean dartObfuscation
|
|
@Optional @Input
|
|
String dartDefines
|
|
|
|
@OutputFiles
|
|
FileCollection getDependenciesFiles() {
|
|
FileCollection depfiles = project.files()
|
|
|
|
// Includes all sources used in the flutter compilation.
|
|
depfiles += project.files("${intermediateDir}/flutter_build.d")
|
|
return depfiles
|
|
}
|
|
|
|
void buildBundle() {
|
|
if (!sourceDir.isDirectory()) {
|
|
throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
|
|
}
|
|
|
|
intermediateDir.mkdirs()
|
|
|
|
// 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") {
|
|
if (fastStart) {
|
|
ruleNames = ["faststart_android_application"]
|
|
} else {
|
|
ruleNames = ["debug_android_application"]
|
|
}
|
|
} else {
|
|
ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
|
|
}
|
|
project.exec {
|
|
logging.captureStandardError LogLevel.ERROR
|
|
executable flutterExecutable.absolutePath
|
|
workingDir sourceDir
|
|
if (localEngine != null) {
|
|
args "--local-engine", localEngine
|
|
args "--local-engine-src-path", localEngineSrcPath
|
|
}
|
|
if (verbose) {
|
|
args "--verbose"
|
|
} else {
|
|
args "--quiet"
|
|
}
|
|
args "assemble"
|
|
args "--depfile", "${intermediateDir}/flutter_build.d"
|
|
args "--output", "${intermediateDir}"
|
|
if (!fastStart || buildMode != "debug") {
|
|
args "-dTargetFile=${targetPath}"
|
|
} else {
|
|
args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
|
|
}
|
|
args "-dTargetPlatform=android"
|
|
args "-dBuildMode=${buildMode}"
|
|
if (extraFrontEndOptions != null) {
|
|
args "-dExtraFrontEndOptions=${extraFrontEndOptions}"
|
|
}
|
|
if (splitDebugInfo != null) {
|
|
args "-dSplitDebugInfo=${splitDebugInfo}"
|
|
}
|
|
if (treeShakeIcons == true) {
|
|
args "-dTreeShakeIcons=true"
|
|
}
|
|
if (dartObfuscation == true) {
|
|
args "-dDartObfuscation=true"
|
|
}
|
|
if (dartDefines != null) {
|
|
args "--DartDefines=${dartDefines}"
|
|
}
|
|
if (extraGenSnapshotOptions != null) {
|
|
args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
|
|
}
|
|
args ruleNames
|
|
}
|
|
}
|
|
}
|
|
|
|
class FlutterTask extends BaseFlutterTask {
|
|
@OutputDirectory
|
|
File getOutputDirectory() {
|
|
return intermediateDir
|
|
}
|
|
|
|
String getAssetsDirectory() {
|
|
return "${outputDirectory}/flutter_assets"
|
|
}
|
|
|
|
CopySpec getAssets() {
|
|
return project.copySpec {
|
|
from "${intermediateDir}"
|
|
include "flutter_assets/**" // the working dir and its files
|
|
}
|
|
}
|
|
|
|
CopySpec getSnapshots() {
|
|
return project.copySpec {
|
|
from "${intermediateDir}"
|
|
|
|
if (buildMode == 'release' || buildMode == 'profile') {
|
|
targetPlatformValues.each {
|
|
include "${PLATFORM_ARCH_MAP[targetArch]}/app.so"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
|
|
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,
|
|
def matcher = depText.split(': ')[inputs ? 1 : 0] =~ /(\\ |[^\s])+/
|
|
// then we replace all escaped spaces with regular spaces
|
|
def depList = matcher.collect{it[0].replaceAll("\\\\ ", " ")}
|
|
return project.files(depList)
|
|
}
|
|
return project.files();
|
|
}
|
|
|
|
@InputFiles
|
|
FileCollection getSourceFiles() {
|
|
FileCollection sources = project.files()
|
|
for (File depfile in getDependenciesFiles()) {
|
|
sources += readDependencies(depfile, true)
|
|
}
|
|
return sources + project.files('pubspec.yaml')
|
|
}
|
|
|
|
@OutputFiles
|
|
FileCollection getOutputFiles() {
|
|
FileCollection sources = project.files()
|
|
for (File depfile in getDependenciesFiles()) {
|
|
sources += readDependencies(depfile, false)
|
|
}
|
|
return sources
|
|
}
|
|
|
|
@TaskAction
|
|
void build() {
|
|
buildBundle()
|
|
}
|
|
}
|