Skip to content

Commit ade5d9f

Browse files
committed
Updated project to use Android Gradle Plugin 9.0.1. Fixes #478
1 parent f766039 commit ade5d9f

36 files changed

Lines changed: 537 additions & 308 deletions

android-library-no-tests/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
plugins {
22
id("com.android.library")
3-
kotlin("android")
43
}
54

65
android {

build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ buildscript {
77

88
plugins {
99
alias(libs.plugins.agp) apply false
10-
alias(libs.plugins.kgp) apply false
1110
alias(libs.plugins.ben.manes.versions)
1211
id "com.osacky.fulladle"
1312
alias(libs.plugins.kotlinter)
@@ -22,7 +21,7 @@ fladle {
2221
}
2322

2423
tasks.wrapper.configure {
25-
gradleVersion = '8.14.3'
24+
gradleVersion = '9.1.0'
2625
}
2726

2827
def isNonStable = { String version ->

fladle-plugin/build.gradle.kts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ plugins {
1818
alias(libs.plugins.vanniktech.publish)
1919
}
2020

21-
// See https://github.com/slackhq/keeper/pull/11#issuecomment-579544375 for context
22-
val isReleaseMode : Boolean = hasProperty("fladle.releaseMode")
23-
2421
dependencies {
2522
compileOnly(gradleApi())
26-
if (isReleaseMode) {
27-
compileOnly(libs.agp)
28-
} else {
29-
implementation(libs.agp)
23+
compileOnly(libs.agp) {
24+
exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-embeddable")
25+
exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-runner")
3026
}
3127
compileOnly(libs.gradle.enterprise)
3228

29+
// AGP must be on the runtime classpath so GradleTestKit's withPluginClasspath()
30+
// can resolve the com.android.application and com.android.library plugins.
31+
runtimeOnly(libs.agp)
32+
3333
testImplementation(gradleTestKit())
3434
testImplementation(libs.junit)
3535
testImplementation(libs.truth)
@@ -106,8 +106,8 @@ tasks.withType(ValidatePlugins::class.java).configureEach {
106106
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach {
107107
compilerOptions {
108108
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
109-
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7)
110-
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7)
109+
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
110+
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
111111
}
112112
}
113113

fladle-plugin/src/main/java/com/osacky/flank/gradle/FladleConfig.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ interface FladleConfig {
5151
@get:Input
5252
val testTargets: ListProperty<String>
5353

54+
// The maximum number of shards. Value will be overwritten by [maxTestShards] if both used in configuration
5455
@Deprecated(
5556
message = "testShards is deprecated. Use maxTestShards instead",
5657
replaceWith = ReplaceWith("maxTestShards"),
5758
)
58-
/**
59-
* The maximum number of shards. Value will be overwritten by [maxTestShards] if both used in configuration
60-
*/
6159
@get:Input
6260
@get:Optional
6361
val testShards: Property<Int>
@@ -473,7 +471,8 @@ interface FladleConfig {
473471

474472
@Internal
475473
fun getPresentProperties() =
476-
this::class.memberProperties
474+
this::class
475+
.memberProperties
477476
.filter {
478477
when (val prop = it.call(this)) {
479478
is Property<*> -> prop.isPresent

fladle-plugin/src/main/java/com/osacky/flank/gradle/FladlePluginDelegate.kt

Lines changed: 145 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.osacky.flank.gradle
22

3-
import com.android.build.gradle.AppExtension
4-
import com.android.build.gradle.TestedExtension
5-
import com.android.build.gradle.internal.tasks.factory.dependsOn
6-
import com.android.builder.model.TestOptions
3+
import com.android.build.api.dsl.ApplicationExtension
4+
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
5+
import com.android.build.api.variant.FilterConfiguration
6+
import com.android.build.api.variant.HasAndroidTest
77
import com.osacky.flank.gradle.validation.checkForExclusionUsage
88
import com.osacky.flank.gradle.validation.validateOptionsUsed
99
import org.gradle.api.GradleException
1010
import org.gradle.api.Project
1111
import org.gradle.api.artifacts.Configuration
12+
import org.gradle.api.plugins.BasePluginExtension
1213
import org.gradle.api.tasks.TaskContainer
1314
import org.gradle.kotlin.dsl.create
1415
import org.gradle.util.GradleVersion
@@ -24,7 +25,10 @@ class FladlePluginDelegate {
2425

2526
target.tasks.register("flankAuth", FlankJavaExec::class.java) {
2627
doFirst {
27-
target.layout.fladleDir.get().asFile.mkdirs()
28+
target.layout.fladleDir
29+
.get()
30+
.asFile
31+
.mkdirs()
2832
}
2933
classpath = project.fladleConfig
3034
args = listOf("auth", "login")
@@ -34,7 +38,6 @@ class FladlePluginDelegate {
3438
}
3539

3640
private fun checkMinimumGradleVersion() {
37-
// Gradle 4.9 is required because we use the lazy task configuration API.
3841
if (GRADLE_MIN_VERSION > GradleVersion.current()) {
3942
throw GradleException("Fladle requires at minimum version $GRADLE_MIN_VERSION. Detected version ${GradleVersion.current()}.")
4043
}
@@ -44,24 +47,22 @@ class FladlePluginDelegate {
4447
project: Project,
4548
base: FlankGradleExtension,
4649
) {
47-
if (GradleVersion.current() > GradleVersion.version("6.1")) {
48-
base.flankVersion.finalizeValueOnRead()
49-
base.flankCoordinates.finalizeValueOnRead()
50-
base.serviceAccountCredentials.finalizeValueOnRead()
50+
base.flankVersion.finalizeValueOnRead()
51+
base.flankCoordinates.finalizeValueOnRead()
52+
base.serviceAccountCredentials.finalizeValueOnRead()
53+
54+
// Register onVariants callbacks before afterEvaluate for APK path detection
55+
project.pluginManager.withPlugin("com.android.application") {
56+
if (!base.debugApk.isPresent || !base.instrumentationApk.isPresent) {
57+
findDebugAndInstrumentationApk(project, base)
58+
}
5159
}
60+
5261
project.afterEvaluate {
5362
// Add Flank dependency to Fladle Configuration
5463
// Must be done afterEvaluate otherwise extension values will not be set.
5564
project.dependencies.add(FLADLE_CONFIG, "${base.flankCoordinates.get()}:${base.flankVersion.get()}")
5665

57-
// Only use automatic apk path detection for 'com.android.application' projects.
58-
project.pluginManager.withPlugin("com.android.application") {
59-
// This doesn't work properly for multiple configs since they likely are inheriting the config from root already. See #60 https://github.com/runningcode/fladle/issues/60
60-
if (!base.debugApk.isPresent || !base.instrumentationApk.isPresent) {
61-
findDebugAndInstrumentationApk(project, base)
62-
}
63-
}
64-
6566
tasks.apply {
6667
createTasksForConfig(base, base, project, "")
6768

@@ -95,22 +96,41 @@ class FladlePluginDelegate {
9596

9697
val writeConfigProps = register("writeConfigProps$name", YamlConfigWriterTask::class.java, base, config, name)
9798

98-
writeConfigProps.dependsOn(validateFladle)
99+
writeConfigProps.configure { dependsOn(validateFladle) }
99100

100101
register("printYml$name") {
101102
description = "Print the flank.yml file to the console."
102103
group = TASK_GROUP
103104
dependsOn(writeConfigProps)
104105
doLast {
105-
println(writeConfigProps.get().fladleConfigFile.get().asFile.readText())
106+
println(
107+
writeConfigProps
108+
.get()
109+
.fladleConfigFile
110+
.get()
111+
.asFile
112+
.readText(),
113+
)
106114
}
107115
}
108116

109117
register("flankDoctor$name", FlankJavaExec::class.java) {
110118
if (useDefaultDir) setUpWorkingDir(configName)
111119
description = "Finds problems with the current configuration."
112120
classpath = project.fladleConfig
113-
args = listOf("firebase", "test", "android", "doctor", "-c", writeConfigProps.get().fladleConfigFile.get().asFile.absolutePath)
121+
args =
122+
listOf(
123+
"firebase",
124+
"test",
125+
"android",
126+
"doctor",
127+
"-c",
128+
writeConfigProps
129+
.get()
130+
.fladleConfigFile
131+
.get()
132+
.asFile.absolutePath,
133+
)
114134
dependsOn(writeConfigProps)
115135
}
116136

@@ -122,31 +142,46 @@ class FladlePluginDelegate {
122142
args =
123143
if (project.hasProperty("dumpShards")) {
124144
listOf(
125-
"firebase", "test", "android", "run", "-c",
126-
writeConfigProps.get().fladleConfigFile.get().asFile.absolutePath, "--dump-shards",
145+
"firebase",
146+
"test",
147+
"android",
148+
"run",
149+
"-c",
150+
writeConfigProps
151+
.get()
152+
.fladleConfigFile
153+
.get()
154+
.asFile.absolutePath,
155+
"--dump-shards",
127156
)
128157
} else {
129158
listOf(
130-
"firebase", "test", "android", "run", "-c",
131-
writeConfigProps.get().fladleConfigFile.get().asFile.absolutePath,
159+
"firebase",
160+
"test",
161+
"android",
162+
"run",
163+
"-c",
164+
writeConfigProps
165+
.get()
166+
.fladleConfigFile
167+
.get()
168+
.asFile.absolutePath,
132169
)
133170
}
134171
if (config.serviceAccountCredentials.isPresent) {
135172
environment(mapOf("GOOGLE_APPLICATION_CREDENTIALS" to config.serviceAccountCredentials.get()))
136173
}
137174
dependsOn(writeConfigProps)
138175
if (config.dependOnAssemble.isPresent && config.dependOnAssemble.get()) {
139-
val testedExtension =
140-
requireNotNull(project.extensions.findByType(TestedExtension::class.java)) { "Could not find TestedExtension in ${project.name}" }
141-
testedExtension.testVariants.configureEach {
142-
if (testedVariant.isExpectedVariant(config)) {
143-
if (testedVariant.assembleProvider.isPresent) {
144-
dependsOn(testedVariant.assembleProvider)
145-
}
146-
if (assembleProvider.isPresent) {
147-
dependsOn(assembleProvider)
148-
}
149-
}
176+
// Find assemble tasks by convention name pattern
177+
val variantName = config.variant.orNull
178+
if (variantName != null) {
179+
val capitalizedVariant = variantName.capitalize()
180+
dependsOn("assemble$capitalizedVariant")
181+
dependsOn("assemble${capitalizedVariant}AndroidTest")
182+
} else {
183+
dependsOn("assembleDebug")
184+
dependsOn("assembleDebugAndroidTest")
150185
}
151186
}
152187
if (config.localResultsDir.hasValue) {
@@ -172,45 +207,93 @@ class FladlePluginDelegate {
172207
private fun automaticallyConfigureTestOrchestrator(
173208
project: Project,
174209
config: FladleConfig,
175-
androidExtension: AppExtension,
210+
androidExtension: ApplicationExtension,
176211
) {
177212
project.afterEvaluate {
213+
val execution = androidExtension.testOptions.execution.uppercase()
178214
val useOrchestrator =
179-
androidExtension.testOptions.getExecutionEnum() == TestOptions.Execution.ANDROIDX_TEST_ORCHESTRATOR ||
180-
androidExtension.testOptions.getExecutionEnum() == TestOptions.Execution.ANDROID_TEST_ORCHESTRATOR
215+
execution == "ANDROIDX_TEST_ORCHESTRATOR" ||
216+
execution == "ANDROID_TEST_ORCHESTRATOR"
181217
if (useOrchestrator) {
182218
log("Automatically detected the use of Android Test Orchestrator")
219+
config.useOrchestrator.set(true)
183220
}
184-
config.useOrchestrator.set(useOrchestrator)
185221
}
186222
}
187223

188224
private fun findDebugAndInstrumentationApk(
189225
project: Project,
190226
config: FladleConfig,
191227
) {
192-
val baseExtension =
193-
requireNotNull(project.extensions.findByType(AppExtension::class.java)) { "Could not find AppExtension in ${project.name}" }
194-
automaticallyConfigureTestOrchestrator(project, config, baseExtension)
195-
baseExtension.testVariants.configureEach {
196-
val appVariant = testedVariant
197-
outputs.configureEach test@{
198-
appVariant.outputs
199-
.matching { it.isExpectedAbiOutput(config) }
200-
.configureEach app@{
201-
if (appVariant.isExpectedVariant(config)) {
202-
if (!config.debugApk.isPresent) {
203-
// Don't set debug apk if not already set. #172
204-
project.log("Configuring fladle.debugApk from variant ${this@app.name}")
205-
config.debugApk.set(this@app.outputFile.absolutePath)
206-
}
207-
if (!config.roboScript.isPresent && !config.instrumentationApk.isPresent && !config.sanityRobo.get()) {
208-
// Don't set instrumentation apk if not already set. #172
209-
project.log("Configuring fladle.instrumentationApk from variant ${this@test.name}")
210-
config.instrumentationApk.set(this@test.outputFile.absolutePath)
211-
}
212-
}
228+
val androidExtension =
229+
requireNotNull(
230+
project.extensions.findByType(ApplicationExtension::class.java),
231+
) { "Could not find ApplicationExtension in ${project.name}" }
232+
automaticallyConfigureTestOrchestrator(project, config, androidExtension)
233+
234+
val androidComponents =
235+
requireNotNull(project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)) {
236+
"Could not find ApplicationAndroidComponentsExtension in ${project.name}"
237+
}
238+
239+
androidComponents.onVariants { variant ->
240+
if (!variant.isExpectedVariant(config)) return@onVariants
241+
val androidTest = (variant as? HasAndroidTest)?.androidTest ?: return@onVariants
242+
243+
val buildType = variant.buildType ?: return@onVariants
244+
val flavorName = variant.productFlavors.joinToString("") { it.second }
245+
val flavorPath = variant.productFlavors.joinToString("/") { it.second }
246+
val archivesName =
247+
project.extensions
248+
.getByType(BasePluginExtension::class.java)
249+
.archivesName
250+
.get()
251+
val buildDir = project.layout.buildDirectory
252+
253+
// Test APK path
254+
val testApkDirPath = if (flavorPath.isNotEmpty()) "androidTest/$flavorPath/$buildType" else "androidTest/$buildType"
255+
val testApkFileName =
256+
if (flavorName.isNotEmpty()) {
257+
"$archivesName-$flavorName-$buildType-androidTest.apk"
258+
} else {
259+
"$archivesName-$buildType-androidTest.apk"
260+
}
261+
val testApkPath =
262+
buildDir
263+
.file("outputs/apk/$testApkDirPath/$testApkFileName")
264+
.get()
265+
.asFile.absolutePath
266+
267+
variant.outputs.forEach { output ->
268+
if (!output.isExpectedAbiOutput(config)) return@forEach
269+
270+
val abiFilter = output.filters.firstOrNull { it.filterType == FilterConfiguration.FilterType.ABI }
271+
val abiName = abiFilter?.identifier
272+
273+
val appApkDirPath = if (flavorPath.isNotEmpty()) "$flavorPath/$buildType" else buildType
274+
val appApkFileName =
275+
buildString {
276+
append(archivesName)
277+
if (flavorName.isNotEmpty()) append("-$flavorName")
278+
if (abiName != null) append("-$abiName")
279+
append("-$buildType.apk")
213280
}
281+
val appApkPath =
282+
buildDir
283+
.file("outputs/apk/$appApkDirPath/$appApkFileName")
284+
.get()
285+
.asFile.absolutePath
286+
287+
if (!config.debugApk.isPresent) {
288+
// Don't set debug apk if not already set. #172
289+
project.log("Configuring fladle.debugApk from variant ${variant.name}")
290+
config.debugApk.set(appApkPath)
291+
}
292+
if (!config.roboScript.isPresent && !config.instrumentationApk.isPresent && !config.sanityRobo.get()) {
293+
// Don't set instrumentation apk if not already set. #172
294+
project.log("Configuring fladle.instrumentationApk from variant ${variant.name}")
295+
config.instrumentationApk.set(testApkPath)
296+
}
214297
}
215298
}
216299
}
@@ -219,7 +302,7 @@ class FladlePluginDelegate {
219302
get() = configurations.getByName(FLADLE_CONFIG)
220303

221304
companion object {
222-
val GRADLE_MIN_VERSION: GradleVersion = GradleVersion.version("7.3")
305+
val GRADLE_MIN_VERSION: GradleVersion = GradleVersion.version("9.1")
223306
const val TASK_GROUP = "fladle"
224307
const val FLADLE_CONFIG = "fladle"
225308

0 commit comments

Comments
 (0)