Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion android-library-no-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
id("com.android.library")
kotlin("android")
}

android {
Expand Down
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ buildscript {

plugins {
alias(libs.plugins.agp) apply false
alias(libs.plugins.kgp) apply false
alias(libs.plugins.ben.manes.versions)
id "com.osacky.fulladle"
alias(libs.plugins.kotlinter)
Expand All @@ -22,7 +21,7 @@ fladle {
}

tasks.wrapper.configure {
gradleVersion = '8.14.4'
gradleVersion = '9.1.0'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the settings for the overall project, this is the minimum required version for AGP 9.0.1

}

def isNonStable = { String version ->
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## Unreleased
* Minimum required Gradle version is now 9.1
* Fixed support for Android Gradle Plugin version 9.0.1

## 0.19.0
* Minimum required JVM version is now 17.
Expand Down
18 changes: 9 additions & 9 deletions fladle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ plugins {
alias(libs.plugins.vanniktech.publish)
}

// See https://github.com/slackhq/keeper/pull/11#issuecomment-579544375 for context
val isReleaseMode : Boolean = hasProperty("fladle.releaseMode")

dependencies {
compileOnly(gradleApi())
if (isReleaseMode) {
compileOnly(libs.agp)
} else {
implementation(libs.agp)
compileOnly(libs.agp) {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-embeddable")
exclude(group = "org.jetbrains.kotlin", module = "kotlin-compiler-runner")
}
compileOnly(libs.gradle.enterprise)

// AGP must be on the runtime classpath so GradleTestKit's withPluginClasspath()
// can resolve the com.android.application and com.android.library plugins.
runtimeOnly(libs.agp)

testImplementation(gradleTestKit())
testImplementation(libs.junit)
testImplementation(libs.truth)
Expand Down Expand Up @@ -106,8 +106,8 @@ tasks.withType(ValidatePlugins::class.java).configureEach {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_7)
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
}
}

Expand Down
4 changes: 4 additions & 0 deletions fladle-plugin/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
rootProject.name = "fladle"

plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}

dependencyResolutionManagement {
versionCatalogs {
create("libs") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.osacky.flank.gradle

import com.android.build.gradle.AppExtension
import com.android.build.gradle.TestedExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.builder.model.TestOptions
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.FilterConfiguration
import com.android.build.api.variant.HasAndroidTest
import com.osacky.flank.gradle.validation.checkForExclusionUsage
import com.osacky.flank.gradle.validation.validateOptionsUsed
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.plugins.BasePluginExtension
import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.create
import org.gradle.util.GradleVersion
Expand Down Expand Up @@ -46,24 +47,22 @@ class FladlePluginDelegate {
project: Project,
base: FlankGradleExtension,
) {
if (GradleVersion.current() > GradleVersion.version("6.1")) {
base.flankVersion.finalizeValueOnRead()
base.flankCoordinates.finalizeValueOnRead()
base.serviceAccountCredentials.finalizeValueOnRead()
base.flankVersion.finalizeValueOnRead()
base.flankCoordinates.finalizeValueOnRead()
base.serviceAccountCredentials.finalizeValueOnRead()

// Register onVariants callbacks before afterEvaluate for APK path detection
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious is there a reason for the order change here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the 9.0.1 upgrade - we used testVariants.configureEach before which was a lazy API that worked fine inside afterEvaluate. The new version must be registered during the configuration phase, before variants are finalized and so if we kept it using afterEvaluate it could be executed too late in the lifecycle which could make the callback flaky. The change in the execution order prevents that.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. 👍

project.pluginManager.withPlugin("com.android.application") {
if (!base.debugApk.isPresent || !base.instrumentationApk.isPresent) {
findDebugAndInstrumentationApk(project, base)
}
}

project.afterEvaluate {
// Add Flank dependency to Fladle Configuration
// Must be done afterEvaluate otherwise extension values will not be set.
project.dependencies.add(FLADLE_CONFIG, "${base.flankCoordinates.get()}:${base.flankVersion.get()}")

// Only use automatic apk path detection for 'com.android.application' projects.
project.pluginManager.withPlugin("com.android.application") {
// 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
if (!base.debugApk.isPresent || !base.instrumentationApk.isPresent) {
findDebugAndInstrumentationApk(project, base)
}
}

tasks.apply {
createTasksForConfig(base, base, project, "")

Expand Down Expand Up @@ -97,7 +96,7 @@ class FladlePluginDelegate {

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

writeConfigProps.dependsOn(validateFladle)
writeConfigProps.configure { dependsOn(validateFladle) }

register("printYml$name") {
description = "Print the flank.yml file to the console."
Expand Down Expand Up @@ -174,17 +173,15 @@ class FladlePluginDelegate {
}
dependsOn(writeConfigProps)
if (config.dependOnAssemble.isPresent && config.dependOnAssemble.get()) {
val testedExtension =
requireNotNull(project.extensions.findByType(TestedExtension::class.java)) { "Could not find TestedExtension in ${project.name}" }
testedExtension.testVariants.configureEach {
if (testedVariant.isExpectedVariant(config)) {
if (testedVariant.assembleProvider.isPresent) {
dependsOn(testedVariant.assembleProvider)
}
if (assembleProvider.isPresent) {
dependsOn(assembleProvider)
}
}
// Find assemble tasks by convention name pattern
val variantName = config.variant.orNull
if (variantName != null) {
val capitalizedVariant = variantName.capitalize()
dependsOn("assemble$capitalizedVariant")
dependsOn("assemble${capitalizedVariant}AndroidTest")
} else {
dependsOn("assembleDebug")
dependsOn("assembleDebugAndroidTest")
}
}
if (config.localResultsDir.hasValue) {
Expand All @@ -210,45 +207,95 @@ class FladlePluginDelegate {
private fun automaticallyConfigureTestOrchestrator(
project: Project,
config: FladleConfig,
androidExtension: AppExtension,
androidExtension: ApplicationExtension,
) {
project.afterEvaluate {
val execution = androidExtension.testOptions.execution.uppercase()
val useOrchestrator =
androidExtension.testOptions.getExecutionEnum() == TestOptions.Execution.ANDROIDX_TEST_ORCHESTRATOR ||
androidExtension.testOptions.getExecutionEnum() == TestOptions.Execution.ANDROID_TEST_ORCHESTRATOR
execution == "ANDROIDX_TEST_ORCHESTRATOR" ||
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use the constants here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestOptions actually goes away with the upgrade, so we can't use it anymore. I replaced them with string comparison since the enum was removed from the public API.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah makes sense!

execution == "ANDROID_TEST_ORCHESTRATOR"
if (useOrchestrator) {
log("Automatically detected the use of Android Test Orchestrator")
config.useOrchestrator.set(true)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:O was this a bug earlier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was sort of a bug! Consider this:

  • testOptions.execution = AGP's setting (in the android {} block)
  • config.useOrchestrator = Fladle's setting (in the fladle {} block)

The auto-detection reads from AGP's testOptions and writes to Fladle's config.useOrchestrator. Both happen in afterEvaluate, but the auto-detection runs last, so it wins.

An example: a user sets useOrchestrator = true in their Fladle config, but does not configure testOptions.execution for orchestrator in AGP. The old code reads AGP's testOptions → sees no orchestrator → calls config.useOrchestrator.set(false) → stomps on the user's explicit Fladle setting.

Setting it to true explicitly like this preserves their config when it's not also configured in fladle.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing this!

}
config.useOrchestrator.set(useOrchestrator)
}
}

private fun findDebugAndInstrumentationApk(
project: Project,
config: FladleConfig,
) {
val baseExtension =
requireNotNull(project.extensions.findByType(AppExtension::class.java)) { "Could not find AppExtension in ${project.name}" }
automaticallyConfigureTestOrchestrator(project, config, baseExtension)
baseExtension.testVariants.configureEach {
val appVariant = testedVariant
outputs.configureEach test@{
appVariant.outputs
.matching { it.isExpectedAbiOutput(config) }
.configureEach app@{
if (appVariant.isExpectedVariant(config)) {
if (!config.debugApk.isPresent) {
// Don't set debug apk if not already set. #172
project.log("Configuring fladle.debugApk from variant ${this@app.name}")
config.debugApk.set(this@app.outputFile.absolutePath)
}
if (!config.roboScript.isPresent && !config.instrumentationApk.isPresent && !config.sanityRobo.get()) {
// Don't set instrumentation apk if not already set. #172
project.log("Configuring fladle.instrumentationApk from variant ${this@test.name}")
config.instrumentationApk.set(this@test.outputFile.absolutePath)
}
}
val androidExtension =
requireNotNull(
project.extensions.findByType(ApplicationExtension::class.java),
) { "Could not find ApplicationExtension in ${project.name}" }
automaticallyConfigureTestOrchestrator(project, config, androidExtension)

val androidComponents =
requireNotNull(project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)) {
"Could not find ApplicationAndroidComponentsExtension in ${project.name}"
}

androidComponents.onVariants { variant ->
if (!variant.isExpectedVariant(config)) return@onVariants
val androidTest = (variant as? HasAndroidTest)?.androidTest ?: return@onVariants

val buildType = variant.buildType ?: return@onVariants
val flavorName = variant.productFlavors.joinToString("") { it.second }
val flavorPath = variant.productFlavors.joinToString("/") { it.second }
val archivesName =
project.extensions
.getByType(BasePluginExtension::class.java)
.archivesName
.get()
val buildDir = project.layout.buildDirectory

// Test APK path
val testApkDirPath = if (flavorPath.isNotEmpty()) "androidTest/$flavorPath/$buildType" else "androidTest/$buildType"
val testApkFileName =
if (flavorName.isNotEmpty()) {
"$archivesName-$flavorName-$buildType-androidTest.apk"
} else {
"$archivesName-$buildType-androidTest.apk"
}
val testApkPath =
buildDir
.file("outputs/apk/$testApkDirPath/$testApkFileName")
.get()
.asFile
.absolutePath

variant.outputs.forEach { output ->
if (!output.isExpectedAbiOutput(config)) return@forEach

val abiFilter = output.filters.firstOrNull { it.filterType == FilterConfiguration.FilterType.ABI }
val abiName = abiFilter?.identifier

val appApkDirPath = if (flavorPath.isNotEmpty()) "$flavorPath/$buildType" else buildType
val appApkFileName =
buildString {
append(archivesName)
if (flavorName.isNotEmpty()) append("-$flavorName")
if (abiName != null) append("-$abiName")
append("-$buildType.apk")
}
val appApkPath =
buildDir
.file("outputs/apk/$appApkDirPath/$appApkFileName")
.get()
.asFile
.absolutePath

if (!config.debugApk.isPresent) {
// Don't set debug apk if not already set. #172
project.log("Configuring fladle.debugApk from variant ${variant.name}")
config.debugApk.set(appApkPath)
}
if (!config.roboScript.isPresent && !config.instrumentationApk.isPresent && !config.sanityRobo.get()) {
// Don't set instrumentation apk if not already set. #172
project.log("Configuring fladle.instrumentationApk from variant ${variant.name}")
config.instrumentationApk.set(testApkPath)
}
}
}
}
Expand All @@ -257,7 +304,7 @@ class FladlePluginDelegate {
get() = configurations.getByName(FLADLE_CONFIG)

companion object {
val GRADLE_MIN_VERSION: GradleVersion = GradleVersion.version("7.3")
val GRADLE_MIN_VERSION: GradleVersion = GradleVersion.version("9.1")
const val TASK_GROUP = "fladle"
const val FLADLE_CONFIG = "fladle"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import javax.inject.Inject
@DisableCachingByDefault(
because = "Flank executions are dependent on resources such as network connection and server and therefore cannot be cached.",
)
open class FlankExecutionTask
abstract class FlankExecutionTask
@Inject
constructor(
projectLayout: ProjectLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import javax.inject.Inject
@DisableCachingByDefault(
because = "Flank executions are dependent on resources such as network connection and server and therefore cannot be cached.",
)
open class FlankJavaExec
abstract class FlankJavaExec
@Inject
constructor(
projectLayout: ProjectLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,10 @@ open class FulladleModuleExtension
* can be a match.
*/
val variant: Property<String> = objects.property<String>().convention(null as String?)

/**
* Variant APK info collected during configuration via onVariants callbacks.
* Used by FulladlePlugin at execution time to build YAML entries.
*/
internal val variantApks: MutableList<VariantApkInfo> = mutableListOf()
}
Loading