Skip to content

Commit bd16603

Browse files
gmackallGray Mackall
andauthored
Port FlutterTask from Groovy to Kotlin (#165244)
1. Ports `FlutterTask` from groovy to kotlin 2. Refactors `BaseFlutterTask` a bit, to follow a paradigm that methods on `BaseFlutterTask` and `FlutterTask` will contain no logic, and instead be set equal to a method with the same name in `BaseFlutterTaskHelper` and `FlutterTaskHelper` respectively, and that both of those helpers will be stateless. This allows us to somewhat get around the fact that you cannot create a `Task` instance in testing, because testing the logic of the task is the same as testing the logic of the helper. 3. Also creates a `FlutterPluginConstants` object to hold the constants from `FlutterPlugin`. I think ideally this will be collapsed back in to `FlutterPlugin` when the conversion is completed, but it needs to be moved for now so it can be referenced in Kotlin code (this pr needs it). Fixes flutter/flutter#162112 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Gray Mackall <[email protected]>
1 parent f7d11dd commit bd16603

File tree

11 files changed

+470
-329
lines changed

11 files changed

+470
-329
lines changed

packages/flutter_tools/gradle/src/main/groovy/flutter.groovy

Lines changed: 11 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import com.android.build.gradle.tasks.PackageAndroidArtifact
1111
import com.android.build.gradle.tasks.ProcessAndroidResources
1212
import com.android.builder.model.BuildType
1313
import com.flutter.gradle.BaseApplicationNameHandler
14-
import com.flutter.gradle.BaseFlutterTask
1514
import com.flutter.gradle.Deeplink
1615
import com.flutter.gradle.DependencyVersionChecker
1716
import com.flutter.gradle.FlutterExtension
17+
import com.flutter.gradle.FlutterPluginConstants
18+
import com.flutter.gradle.FlutterTask
1819
import com.flutter.gradle.FlutterPluginUtils
1920
import com.flutter.gradle.IntentFilterCheck
2021
import com.flutter.gradle.VersionUtils
@@ -29,64 +30,14 @@ import org.gradle.api.Project
2930
import org.gradle.api.Plugin
3031
import org.gradle.api.Task
3132
import org.gradle.api.UnknownTaskException
32-
import org.gradle.api.file.CopySpec
33-
import org.gradle.api.file.FileCollection
3433
import org.gradle.api.tasks.Copy
35-
import org.gradle.api.tasks.InputFiles
36-
import org.gradle.api.tasks.Internal
37-
import org.gradle.api.tasks.OutputDirectory
38-
import org.gradle.api.tasks.OutputFiles
39-
import org.gradle.api.tasks.TaskAction
4034
import org.gradle.api.tasks.TaskProvider
4135
import org.gradle.api.tasks.bundling.Jar
4236
import org.gradle.internal.os.OperatingSystem
4337

4438

4539
class FlutterPlugin implements Plugin<Project> {
4640

47-
private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
48-
49-
/** The platforms that can be passed to the `--Ptarget-platform` flag. */
50-
private static final String PLATFORM_ARM32 = "android-arm"
51-
private static final String PLATFORM_ARM64 = "android-arm64"
52-
private static final String PLATFORM_X86 = "android-x86"
53-
private static final String PLATFORM_X86_64 = "android-x64"
54-
55-
/** The ABI architectures supported by Flutter. */
56-
private static final String ARCH_ARM32 = "armeabi-v7a"
57-
private static final String ARCH_ARM64 = "arm64-v8a"
58-
private static final String ARCH_X86 = "x86"
59-
private static final String ARCH_X86_64 = "x86_64"
60-
61-
private static final String INTERMEDIATES_DIR = "intermediates"
62-
63-
/** Maps platforms to ABI architectures. */
64-
private static final Map PLATFORM_ARCH_MAP = [
65-
(PLATFORM_ARM32) : ARCH_ARM32,
66-
(PLATFORM_ARM64) : ARCH_ARM64,
67-
(PLATFORM_X86) : ARCH_X86,
68-
(PLATFORM_X86_64) : ARCH_X86_64,
69-
]
70-
71-
/**
72-
* The version code that gives each ABI a value.
73-
* For each APK variant, use the following versions to override the version of the Universal APK.
74-
* Otherwise, the Play Store will complain that the APK variants have the same version.
75-
*/
76-
private static final Map<String, Integer> ABI_VERSION = [
77-
(ARCH_ARM32) : 1,
78-
(ARCH_ARM64) : 2,
79-
(ARCH_X86) : 3,
80-
(ARCH_X86_64) : 4,
81-
]
82-
83-
/** When split is enabled, multiple APKs are generated per each ABI. */
84-
private static final List DEFAULT_PLATFORMS = [
85-
PLATFORM_ARM32,
86-
PLATFORM_ARM64,
87-
PLATFORM_X86_64,
88-
]
89-
9041
private final static String propLocalEngineRepo = "local-engine-repo"
9142
private final static String propProcessResourcesProvider = "processResourcesProvider"
9243

@@ -156,7 +107,7 @@ class FlutterPlugin implements Plugin<Project> {
156107
}
157108

158109
// Configure the Maven repository.
159-
String hostedRepository = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: DEFAULT_MAVEN_HOST
110+
String hostedRepository = System.getenv(FlutterPluginConstants.FLUTTER_STORAGE_BASE_URL) ?: FlutterPluginConstants.DEFAULT_MAVEN_HOST
160111
String repository = FlutterPluginUtils.shouldProjectUseLocalEngine(project)
161112
? project.property(propLocalEngineRepo)
162113
: "$hostedRepository/${engineRealm}download.flutter.io"
@@ -212,7 +163,7 @@ class FlutterPlugin implements Plugin<Project> {
212163
}
213164

214165
getTargetPlatforms().each { targetArch ->
215-
String abiValue = PLATFORM_ARCH_MAP[targetArch]
166+
String abiValue = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
216167
project.android {
217168
if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
218169
splits {
@@ -495,7 +446,7 @@ class FlutterPlugin implements Plugin<Project> {
495446
}
496447
List<String> platforms = getTargetPlatforms().collect()
497448
platforms.each { platform ->
498-
String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
449+
String arch = FlutterPluginUtils.formatPlatformString(platform)
499450
// Add the `libflutter.so` dependency.
500451
FlutterPluginUtils.addApiDependencies(project, buildType.name,
501452
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
@@ -851,10 +802,10 @@ class FlutterPlugin implements Plugin<Project> {
851802
private List<String> getTargetPlatforms() {
852803
final String propTargetPlatform = "target-platform"
853804
if (!project.hasProperty(propTargetPlatform)) {
854-
return DEFAULT_PLATFORMS
805+
return FlutterPluginConstants.DEFAULT_PLATFORMS
855806
}
856807
return project.property(propTargetPlatform).split(",").collect {
857-
if (!PLATFORM_ARCH_MAP[it]) {
808+
if (!FlutterPluginConstants.PLATFORM_ARCH_MAP[it]) {
858809
throw new GradleException("Invalid platform: $it.")
859810
}
860811
return it
@@ -952,7 +903,7 @@ class FlutterPlugin implements Plugin<Project> {
952903
// for only the output APK, not for the variant itself. Skipping this step simply
953904
// causes Gradle to use the value of variant.versionCode for the APK.
954905
// For more, see https://developer.android.com/studio/build/configure-apk-splits
955-
Integer abiVersionCode = ABI_VERSION[output.getFilter(OutputFile.ABI)]
906+
Integer abiVersionCode = FlutterPluginConstants.ABI_VERSION[output.getFilter(OutputFile.ABI)]
956907
if (abiVersionCode != null) {
957908
output.versionCodeOverride =
958909
abiVersionCode * 1000 + variant.versionCode
@@ -1002,7 +953,7 @@ class FlutterPlugin implements Plugin<Project> {
1002953
trackWidgetCreation(trackWidgetCreationValue)
1003954
targetPlatformValues = targetPlatforms
1004955
sourceDir(FlutterPluginUtils.getFlutterSourceDirectory(project))
1005-
intermediateDir(project.file(project.layout.buildDirectory.dir("$INTERMEDIATES_DIR/flutter/${variant.name}/")))
956+
intermediateDir(project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/")))
1006957
frontendServerStarterPath(frontendServerStarterPathValue)
1007958
extraFrontEndOptions(extraFrontEndOptionsValue)
1008959
extraGenSnapshotOptions(extraGenSnapshotOptionsValue)
@@ -1017,13 +968,13 @@ class FlutterPlugin implements Plugin<Project> {
1017968
flavor(flavorValue)
1018969
}
1019970
Task compileTask = compileTaskProvider.get()
1020-
File libJar = project.file(project.layout.buildDirectory.dir("$INTERMEDIATES_DIR/flutter/${variant.name}/libs.jar"))
971+
File libJar = project.file(project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar"))
1021972
TaskProvider<Jar> packJniLibsTaskProvider = project.tasks.register("packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", Jar) {
1022973
destinationDirectory = libJar.parentFile
1023974
archiveFileName = libJar.name
1024975
dependsOn(compileTask)
1025976
targetPlatforms.each { targetPlatform ->
1026-
String abi = PLATFORM_ARCH_MAP[targetPlatform]
977+
String abi = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
1027978
from("${compileTask.intermediateDir}/${abi}") {
1028979
include("*.so")
1029980
// Move `app.so` to `lib/<abi>/libapp.so`
@@ -1212,74 +1163,3 @@ class FlutterPlugin implements Plugin<Project> {
12121163
detectLowCompileSdkVersionOrNdkVersion()
12131164
}
12141165
}
1215-
1216-
class FlutterTask extends BaseFlutterTask {
1217-
1218-
@OutputDirectory
1219-
File getOutputDirectory() {
1220-
return intermediateDir
1221-
}
1222-
1223-
@Internal
1224-
String getAssetsDirectory() {
1225-
return "${outputDirectory}/flutter_assets"
1226-
}
1227-
1228-
@Internal
1229-
CopySpec getAssets() {
1230-
return project.copySpec {
1231-
from("${intermediateDir}")
1232-
include("flutter_assets/**") // the working dir and its files
1233-
}
1234-
}
1235-
1236-
@Internal
1237-
CopySpec getSnapshots() {
1238-
return project.copySpec {
1239-
from("${intermediateDir}")
1240-
1241-
if (buildMode == "release" || buildMode == "profile") {
1242-
targetPlatformValues.each {
1243-
include("${PLATFORM_ARCH_MAP[targetArch]}/app.so")
1244-
}
1245-
}
1246-
}
1247-
}
1248-
1249-
FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
1250-
if (dependenciesFile.exists()) {
1251-
// Dependencies file has Makefile syntax:
1252-
// <target> <files>: <source> <files> <separated> <by> <non-escaped space>
1253-
String depText = dependenciesFile.text
1254-
// So we split list of files by non-escaped(by backslash) space,
1255-
def matcher = depText.split(": ")[inputs ? 1 : 0] =~ /(\\ |\S)+/
1256-
// then we replace all escaped spaces with regular spaces
1257-
def depList = matcher.collect{ it[0].replaceAll("\\\\ ", " ") }
1258-
return project.files(depList)
1259-
}
1260-
return project.files()
1261-
}
1262-
1263-
@InputFiles
1264-
FileCollection getSourceFiles() {
1265-
FileCollection sources = project.files()
1266-
for (File depfile in getDependenciesFiles()) {
1267-
sources += readDependencies(depfile, true)
1268-
}
1269-
return sources + project.files("pubspec.yaml")
1270-
}
1271-
1272-
@OutputFiles
1273-
FileCollection getOutputFiles() {
1274-
FileCollection sources = project.files()
1275-
for (File depfile in getDependenciesFiles()) {
1276-
sources += readDependencies(depfile, false)
1277-
}
1278-
return sources
1279-
}
1280-
1281-
@TaskAction
1282-
void build() {
1283-
buildBundle()
1284-
}
1285-
}

packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTask.kt

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
package com.flutter.gradle
66

77
import org.gradle.api.DefaultTask
8-
import org.gradle.api.file.FileCollection
9-
import org.gradle.api.logging.LogLevel
108
import org.gradle.api.tasks.Input
119
import org.gradle.api.tasks.Internal
1210
import org.gradle.api.tasks.Optional
1311
import org.gradle.api.tasks.OutputFiles
1412
import java.io.File
1513

16-
abstract class BaseFlutterTask : DefaultTask() {
14+
// IMPORTANT: Do not add logic to the methods in this class directly,
15+
// instead add logic to [BaseFlutterTaskHelper].
16+
17+
/**
18+
* Base implementation of a Gradle task. Gradle tasks can not be instantiated for testing,
19+
* so this class delegates all logic to [BaseFlutterTaskHelper].
20+
*/
21+
open class BaseFlutterTask : DefaultTask() {
1722
@Internal
1823
var flutterRoot: File? = null
1924

@@ -132,21 +137,12 @@ abstract class BaseFlutterTask : DefaultTask() {
132137
* @return the dependency file(s) based on the current intermediate directory path.
133138
*/
134139
@OutputFiles
135-
fun getDependenciesFiles(): FileCollection {
136-
val helper = BaseFlutterTaskHelper(baseFlutterTask = this)
137-
val depFiles = helper.getDependenciesFiles()
138-
return depFiles
139-
}
140+
fun getDependenciesFiles() = BaseFlutterTaskHelper.getDependenciesFiles(baseFlutterTask = this)
140141

141142
/**
142143
* Builds a Flutter Android application bundle by verifying the Flutter source directory,
143144
* creating an intermediate build directory if necessary, and running flutter assemble by
144145
* configuring and executing with a set of build configurations.
145146
*/
146-
fun buildBundle() {
147-
val helper = BaseFlutterTaskHelper(baseFlutterTask = this)
148-
helper.checkPreConditions()
149-
logging.captureStandardError(LogLevel.ERROR)
150-
project.exec(helper.createExecSpecActionFromTask())
151-
}
147+
fun buildBundle() = BaseFlutterTaskHelper.buildBundle(baseFlutterTask = this)
152148
}

packages/flutter_tools/gradle/src/main/kotlin/BaseFlutterTaskHelper.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import androidx.annotation.VisibleForTesting
44
import org.gradle.api.Action
55
import org.gradle.api.GradleException
66
import org.gradle.api.file.FileCollection
7+
import org.gradle.api.logging.LogLevel
78
import org.gradle.api.tasks.OutputFiles
89
import org.gradle.process.ExecSpec
910
import java.nio.file.Paths
1011

11-
class BaseFlutterTaskHelper(
12-
private var baseFlutterTask: BaseFlutterTask
13-
) {
12+
/**
13+
* Stateless object to contain the logic used in [BaseFlutterTask]. Any required state should be stored
14+
* on [BaseFlutterTask] instead, while any logic needed by [BaseFlutterTask] should be added here.
15+
*/
16+
object BaseFlutterTaskHelper {
1417
@VisibleForTesting
15-
internal var gradleErrorMessage = "Invalid Flutter source directory: ${baseFlutterTask.sourceDir}"
18+
internal fun getGradleErrorMessage(baseFlutterTask: BaseFlutterTask): String =
19+
"Invalid Flutter source directory: ${baseFlutterTask.sourceDir}"
1620

1721
/**
1822
* Gets the dependency file(s) that tracks the dependencies or input files used for a specific
@@ -22,7 +26,7 @@ class BaseFlutterTaskHelper(
2226
*/
2327
@OutputFiles
2428
@VisibleForTesting
25-
internal fun getDependenciesFiles(): FileCollection {
29+
internal fun getDependenciesFiles(baseFlutterTask: BaseFlutterTask): FileCollection {
2630
var depfiles: FileCollection = baseFlutterTask.project.files()
2731

2832
// TODO(jesswon): During cleanup determine if .../flutter_build.d is ever a directory and refactor accordingly
@@ -38,9 +42,9 @@ class BaseFlutterTaskHelper(
3842
* @throws GradleException if sourceDir is null or is not a directory
3943
*/
4044
@VisibleForTesting
41-
internal fun checkPreConditions() {
45+
internal fun checkPreConditions(baseFlutterTask: BaseFlutterTask) {
4246
if (baseFlutterTask.sourceDir == null || !baseFlutterTask.sourceDir!!.isDirectory) {
43-
throw GradleException(gradleErrorMessage)
47+
throw GradleException(getGradleErrorMessage(baseFlutterTask))
4448
}
4549
baseFlutterTask.intermediateDir!!.mkdirs()
4650
}
@@ -77,8 +81,7 @@ class BaseFlutterTaskHelper(
7781
*
7882
* @return an Action<ExecSpec> of build processes and options to be executed.
7983
*/
80-
@VisibleForTesting
81-
internal fun createExecSpecActionFromTask(): Action<ExecSpec> =
84+
internal fun createExecSpecActionFromTask(baseFlutterTask: BaseFlutterTask): Action<ExecSpec> =
8285
Action<ExecSpec> {
8386
executable(baseFlutterTask.flutterExecutable!!.absolutePath)
8487
workingDir(baseFlutterTask.sourceDir)
@@ -146,4 +149,10 @@ class BaseFlutterTaskHelper(
146149
args("-dMinSdkVersion=${baseFlutterTask.minSdkVersion}")
147150
args(generateRuleNames(baseFlutterTask))
148151
}
152+
153+
fun buildBundle(baseFlutterTask: BaseFlutterTask) {
154+
checkPreConditions(baseFlutterTask)
155+
baseFlutterTask.logging.captureStandardError(LogLevel.ERROR)
156+
baseFlutterTask.project.exec(createExecSpecActionFromTask(baseFlutterTask = baseFlutterTask))
157+
}
149158
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.flutter.gradle
2+
3+
// TODO(gmackall): this should be collapsed back into the core FlutterPlugin once the Groovy to
4+
// kotlin conversion is complete.
5+
object FlutterPluginConstants {
6+
/** The platforms that can be passed to the `--Ptarget-platform` flag. */
7+
private const val PLATFORM_ARM32 = "android-arm"
8+
private const val PLATFORM_ARM64 = "android-arm64"
9+
private const val PLATFORM_X86 = "android-x86"
10+
private const val PLATFORM_X86_64 = "android-x64"
11+
12+
/** The ABI architectures supported by Flutter. */
13+
private const val ARCH_ARM32 = "armeabi-v7a"
14+
private const val ARCH_ARM64 = "arm64-v8a"
15+
private const val ARCH_X86 = "x86"
16+
private const val ARCH_X86_64 = "x86_64"
17+
18+
const val INTERMEDIATES_DIR = "intermediates"
19+
const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL"
20+
const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
21+
22+
/** Maps platforms to ABI architectures. */
23+
@JvmStatic val PLATFORM_ARCH_MAP =
24+
mapOf(
25+
PLATFORM_ARM32 to ARCH_ARM32,
26+
PLATFORM_ARM64 to ARCH_ARM64,
27+
PLATFORM_X86 to ARCH_X86,
28+
PLATFORM_X86_64 to ARCH_X86_64
29+
)
30+
31+
/**
32+
* The version code that gives each ABI a value.
33+
* For each APK variant, use the following versions to override the version of the Universal APK.
34+
* Otherwise, the Play Store will complain that the APK variants have the same version.
35+
*/
36+
@JvmStatic val ABI_VERSION =
37+
mapOf<String, Int>( // Explicit type for clarity, though inferred
38+
ARCH_ARM32 to 1,
39+
ARCH_ARM64 to 2,
40+
ARCH_X86 to 3,
41+
ARCH_X86_64 to 4
42+
)
43+
44+
/** When split is enabled, multiple APKs are generated per each ABI. */
45+
@JvmStatic val DEFAULT_PLATFORMS =
46+
listOf(
47+
PLATFORM_ARM32,
48+
PLATFORM_ARM64,
49+
PLATFORM_X86_64
50+
)
51+
}

0 commit comments

Comments
 (0)