diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1146ab..bc861c2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,4 +91,7 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") + + // Timber + implementation(libs.timber) } \ No newline at end of file diff --git a/build-logic/README.md b/build-logic/README.md new file mode 100644 index 0000000..a0deaf8 --- /dev/null +++ b/build-logic/README.md @@ -0,0 +1,38 @@ +# Convention Plugins + +The `build-logic` folder defines project-specific convention plugins, used to keep a single +source of truth for common module configurations. + +This approach is heavily based on +[https://developer.squareup.com/blog/herding-elephants/](https://developer.squareup.com/blog/herding-elephants/) +and +[https://github.com/jjohannes/idiomatic-gradle](https://github.com/jjohannes/idiomatic-gradle). + +By setting up convention plugins in `build-logic`, we can avoid duplicated build script setup, +messy `subproject` configurations, without the pitfalls of the `buildSrc` directory. + +`build-logic` is an included build, as configured in the root +[`settings.gradle.kts`](../settings.gradle.kts). + +Inside `build-logic` is a `convention` module, which defines a set of plugins that all normal +modules can use to configure themselves. + +`build-logic` also includes a set of `Kotlin` files used to share logic between plugins themselves, +which is most useful for configuring Android components (libraries vs applications) with shared +code. + +These plugins are *additive* and *composable*, and try to only accomplish a single responsibility. +Modules can then pick and choose the configurations they need. +If there is one-off logic for a module without shared code, it's preferable to define that directly +in the module's `build.gradle`, as opposed to creating a convention plugin with module-specific +setup. + +Current list of convention plugins: + +- [`pq.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt), + [`pq.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt), + [`pq.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt): + Configures common Android and Kotlin options. +- [`pq.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt), + [`pq.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt): + Configures Jetpack Compose options \ No newline at end of file diff --git a/build-logic/convention/.gitignore b/build-logic/convention/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/build-logic/convention/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 0000000..d41674d --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,66 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `kotlin-dsl` +} + +group = "com.wei.picquest.buildlogic" + +// Configure the build-logic plugins to target JDK 17 +// This matches the JDK used to build the project, and is not related to what is running on device. +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) + compileOnly(libs.ksp.gradlePlugin) +} + +gradlePlugin { + plugins { + register("androidApplicationCompose") { + id = "pq.android.application.compose" + implementationClass = "AndroidApplicationComposeConventionPlugin" + } + register("androidApplication") { + id = "pq.android.application" + implementationClass = "AndroidApplicationConventionPlugin" + } + register("androidLibraryCompose") { + id = "pq.android.library.compose" + implementationClass = "AndroidLibraryComposeConventionPlugin" + } + register("androidLibrary") { + id = "pq.android.library" + implementationClass = "AndroidLibraryConventionPlugin" + } + register("androidFeature") { + id = "pq.android.feature" + implementationClass = "AndroidFeatureConventionPlugin" + } + register("androidTest") { + id = "pq.android.test" + implementationClass = "AndroidTestConventionPlugin" + } + register("androidHilt") { + id = "pq.android.hilt" + implementationClass = "AndroidHiltConventionPlugin" + } + register("androidFlavors") { + id = "pq.android.application.flavors" + implementationClass = "AndroidApplicationFlavorsConventionPlugin" + } + register("jvmLibrary") { + id = "pq.jvm.library" + implementationClass = "JvmLibraryConventionPlugin" + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt new file mode 100644 index 0000000..273f21c --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -0,0 +1,19 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.wei.picquest.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +class AndroidApplicationComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.android.application") + // Screenshot Tests + pluginManager.apply("io.github.takahirom.roborazzi") + + val extension = extensions.getByType() + configureAndroidCompose(extension) + } + } + +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt new file mode 100644 index 0000000..61a3896 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -0,0 +1,29 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.wei.picquest.configureGradleManagedDevices +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.wei.picquest.configureKotlinAndroid +import com.wei.picquest.configurePrintApksTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidApplicationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 34 + configureGradleManagedDevices(this) + } + extensions.configure { + configurePrintApksTask(this) + } + } + } + +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt new file mode 100644 index 0000000..e4cddc9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt @@ -0,0 +1,15 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.wei.picquest.configureFlavors +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidApplicationFlavorsConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + extensions.configure { + configureFlavors(this) + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt new file mode 100644 index 0000000..c652ee5 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -0,0 +1,52 @@ +import com.android.build.gradle.LibraryExtension +import com.wei.picquest.configureGradleManagedDevices +import com.wei.picquest.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin + +class AndroidFeatureConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply { + apply("pq.android.library") + apply("pq.android.hilt") + apply("androidx.navigation.safeargs.kotlin") + } + extensions.configure { + defaultConfig { + testInstrumentationRunner = + "com.wei.picquest.core.testing.AtTestRunner" + } + configureGradleManagedDevices(this) + } + + dependencies { + add("implementation", project(":core:network")) + add("implementation", project(":core:common")) + add("implementation", project(":core:designsystem")) + add("implementation", project(":core:data")) + add("implementation", project(":core:model")) + add("implementation", project(":core:domain")) + + add("testImplementation", kotlin("test")) + add("testImplementation", project(":core:testing")) + add("androidTestImplementation", kotlin("test")) + add("androidTestImplementation", project(":core:testing")) + add("androidTestImplementation", project(":core:designsystem")) + + add("implementation", libs.findLibrary("coil.kt").get()) + add("implementation", libs.findLibrary("coil.kt.compose").get()) + add("implementation", libs.findLibrary("coil.kt.svg").get()) + + add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) + add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) + add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + + add("implementation", libs.findLibrary("kotlinx.coroutines.android").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt new file mode 100644 index 0000000..9def536 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt @@ -0,0 +1,26 @@ +import com.wei.picquest.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +class AndroidHiltConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("dagger.hilt.android.plugin") + // KAPT must go last to avoid build warnings. + // See: https://stackoverflow.com/questions/70550883/warning-the-following-options-were-not-recognized-by-any-processor-dagger-f + apply("org.jetbrains.kotlin.kapt") + } + + dependencies { + "implementation"(libs.findLibrary("hilt.android").get()) + "kapt"(libs.findLibrary("hilt.compiler").get()) + "kaptAndroidTest"(libs.findLibrary("hilt.compiler").get()) + "kaptTest"(libs.findLibrary("hilt.compiler").get()) + } + + } + } + +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt new file mode 100644 index 0000000..cb2da40 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -0,0 +1,19 @@ +import com.android.build.gradle.LibraryExtension +import com.wei.picquest.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +class AndroidLibraryComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.android.library") + // Screenshot Tests + pluginManager.apply("io.github.takahirom.roborazzi") + + val extension = extensions.getByType() + configureAndroidCompose(extension) + } + } + +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt new file mode 100644 index 0000000..9cf2027 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -0,0 +1,44 @@ +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.gradle.LibraryExtension +import com.wei.picquest.configureFlavors +import com.wei.picquest.configureGradleManagedDevices +import com.wei.picquest.configureKotlinAndroid +import com.wei.picquest.configurePrintApksTask +import com.wei.picquest.disableUnnecessaryAndroidTests +import com.wei.picquest.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin + +class AndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 34 + configureFlavors(this) + configureGradleManagedDevices(this) + } + extensions.configure { + configurePrintApksTask(this) + disableUnnecessaryAndroidTests(target) + } + dependencies { + add("testImplementation", kotlin("test")) + add("testImplementation", project(":core:testing")) + add("androidTestImplementation", kotlin("test")) + add("androidTestImplementation", project(":core:testing")) + + // Timber + add("implementation", libs.findLibrary("timber").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt new file mode 100644 index 0000000..9baf003 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -0,0 +1,24 @@ +import com.android.build.gradle.TestExtension +import com.wei.picquest.configureGradleManagedDevices +import com.wei.picquest.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidTestConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.test") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + configureKotlinAndroid(this) + defaultConfig.targetSdk = 34 + configureGradleManagedDevices(this) + } + } + } + +} diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt new file mode 100644 index 0000000..43fdffd --- /dev/null +++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt @@ -0,0 +1,14 @@ +import com.wei.picquest.configureKotlinJvm +import org.gradle.api.Plugin +import org.gradle.api.Project + +class JvmLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("org.jetbrains.kotlin.jvm") + } + configureKotlinJvm() + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidCompose.kt new file mode 100644 index 0000000..4939afa --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidCompose.kt @@ -0,0 +1,74 @@ +package com.wei.picquest + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** + * Configure Compose-specific options + */ +internal fun Project.configureAndroidCompose( + commonExtension: CommonExtension<*, *, *, *, *>, +) { + commonExtension.apply { + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString() + } + + dependencies { + val bom = libs.findLibrary("androidx-compose-bom").get() + add("implementation", platform(bom)) + add("androidTestImplementation", platform(bom)) + // Add ComponentActivity to debug manifest + add("debugImplementation", libs.findLibrary("androidx.compose.ui.testManifest").get()) + // Screenshot Tests on JVM + add("testImplementation", libs.findLibrary("robolectric").get()) + add("testImplementation", libs.findLibrary("roborazzi").get()) + } + + testOptions { + unitTests { + // For Robolectric + isIncludeAndroidResources = true + } + } + } + + tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + buildComposeMetricsParameters() + } + } +} + +private fun Project.buildComposeMetricsParameters(): List { + val metricParameters = mutableListOf() + val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics") + val relativePath = projectDir.relativeTo(rootDir) + + val enableMetrics = (enableMetricsProvider.orNull == "true") + if (enableMetrics) { + val metricsFolder = rootProject.buildDir.resolve("compose-metrics").resolve(relativePath) + metricParameters.add("-P") + metricParameters.add( + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath + ) + } + + val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports") + val enableReports = (enableReportsProvider.orNull == "true") + if (enableReports) { + val reportsFolder = rootProject.buildDir.resolve("compose-reports").resolve(relativePath) + metricParameters.add("-P") + metricParameters.add( + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath + ) + } + return metricParameters.toList() +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidInstrumentedTests.kt new file mode 100644 index 0000000..fbe1a4b --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/AndroidInstrumentedTests.kt @@ -0,0 +1,19 @@ +package com.wei.picquest + +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import org.gradle.api.Project + +/** + * Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder. + * Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message: + * + * > Starting 0 tests on AVD + * + * Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors. + */ +internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( + project: Project, +) = beforeVariants { + it.enableAndroidTest = it.enableAndroidTest + && project.projectDir.resolve("src/androidTest").exists() +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/GradleManagedDevices.kt new file mode 100644 index 0000000..dd326f4 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/GradleManagedDevices.kt @@ -0,0 +1,54 @@ +package com.wei.picquest + +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ManagedVirtualDevice +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.invoke + +/** + * Configure project for Gradle managed devices + */ +internal fun configureGradleManagedDevices( + commonExtension: CommonExtension<*, *, *, *, *>, +) { + val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd") + val pixel6 = DeviceConfig("Pixel 6", 31, "aosp") + val pixelC = DeviceConfig("Pixel C", 30, "aosp-atd") + + val allDevices = listOf(pixel4, pixel6, pixelC) + val ciDevices = listOf(pixel4, pixelC) + + commonExtension.testOptions { + managedDevices { + devices { + allDevices.forEach { deviceConfig -> + maybeCreate(deviceConfig.taskName, ManagedVirtualDevice::class.java).apply { + device = deviceConfig.device + apiLevel = deviceConfig.apiLevel + systemImageSource = deviceConfig.systemImageSource + } + } + } + groups { + maybeCreate("ci").apply { + ciDevices.forEach { deviceConfig -> + targetDevices.add(devices[deviceConfig.taskName]) + } + } + } + } + } +} + +private data class DeviceConfig( + val device: String, + val apiLevel: Int, + val systemImageSource: String, +) { + val taskName = buildString { + append(device.lowercase().replace(" ", "")) + append("api") + append(apiLevel.toString()) + append(systemImageSource.replace("-", "")) + } +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/KotlinAndroid.kt new file mode 100644 index 0000000..ef0a2f1 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/KotlinAndroid.kt @@ -0,0 +1,73 @@ +package com.wei.picquest + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** + * Configure base Kotlin with Android options + */ +internal fun Project.configureKotlinAndroid( + commonExtension: CommonExtension<*, *, *, *, *>, +) { + commonExtension.apply { + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true + } + } + + configureKotlin() + + dependencies { + add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get()) + } +} + +/** + * Configure base Kotlin options for JVM (non-Android) + */ +internal fun Project.configureKotlinJvm() { + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + configureKotlin() +} + +/** + * Configure base Kotlin options + */ +private fun Project.configureKotlin() { + // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947 + tasks.withType().configureEach { + kotlinOptions { + // Set JVM target to 17 + jvmTarget = JavaVersion.VERSION_17.toString() + // Treat all Kotlin warnings as errors (disabled by default) + // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties + val warningsAsErrors: String? by project + allWarningsAsErrors = warningsAsErrors.toBoolean() + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.RequiresOptIn", + // Enable experimental coroutines APIs, including Flow + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + ) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/PqBuildType.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/PqBuildType.kt new file mode 100644 index 0000000..aa9604f --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/PqBuildType.kt @@ -0,0 +1,9 @@ +package com.wei.picquest + +/** + * This is shared between :app module to provide configurations type safety. + */ +enum class PqBuildType(val applicationIdSuffix: String? = null) { + DEBUG(".debug"), + RELEASE +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/PqFlavor.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/PqFlavor.kt new file mode 100644 index 0000000..0d0d7b0 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/PqFlavor.kt @@ -0,0 +1,42 @@ +package com.wei.picquest + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.ApplicationProductFlavor +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ProductFlavor + +@Suppress("EnumEntryName") +enum class FlavorDimension { + contentType +} + +// The content for the app can either come from local static data which is useful for demo +// purposes, or from a production backend server which supplies up-to-date, real content. +// These two product flavors reflect this behaviour. +@Suppress("EnumEntryName") +enum class PqFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) { + demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"), + prod(FlavorDimension.contentType) +} + +fun configureFlavors( + commonExtension: CommonExtension<*, *, *, *, *>, + flavorConfigurationBlock: ProductFlavor.(flavor: PqFlavor) -> Unit = {} +) { + commonExtension.apply { + flavorDimensions += FlavorDimension.contentType.name + productFlavors { + PqFlavor.values().forEach { + create(it.name) { + dimension = it.dimension.name + flavorConfigurationBlock(this, it) + if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { + if (it.applicationIdSuffix != null) { + applicationIdSuffix = it.applicationIdSuffix + } + } + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/PrintTestApks.kt new file mode 100644 index 0000000..76ec407 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/PrintTestApks.kt @@ -0,0 +1,82 @@ +package com.wei.picquest + +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.BuiltArtifactsLoader +import com.android.build.api.variant.HasAndroidTest +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import java.io.File + +internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) { + extension.onVariants { variant -> + if (variant is HasAndroidTest) { + val loader = variant.artifacts.getBuiltArtifactsLoader() + val artifact = variant.androidTest?.artifacts?.get(SingleArtifact.APK) + val javaSources = variant.androidTest?.sources?.java?.all + val kotlinSources = variant.androidTest?.sources?.kotlin?.all + + val testSources = if (javaSources != null && kotlinSources != null) { + javaSources.zip(kotlinSources) { javaDirs, kotlinDirs -> + javaDirs + kotlinDirs + } + } else javaSources ?: kotlinSources + + if (artifact != null && testSources != null) { + tasks.register( + "${variant.name}PrintTestApk", + PrintApkLocationTask::class.java + ) { + apkFolder.set(artifact) + builtArtifactsLoader.set(loader) + variantName.set(variant.name) + sources.set(testSources) + } + } + } + } +} + +internal abstract class PrintApkLocationTask : DefaultTask() { + @get:InputDirectory + abstract val apkFolder: DirectoryProperty + + @get:InputFiles + abstract val sources: ListProperty + + @get:Internal + abstract val builtArtifactsLoader: Property + + @get:Input + abstract val variantName: Property + + @TaskAction + fun taskAction() { + val hasFiles = sources.orNull?.any { directory -> + directory.asFileTree.files.any { + it.isFile && it.parentFile.path.contains("build${File.separator}generated").not() + } + } ?: throw RuntimeException("Cannot check androidTest sources") + + // Don't print APK location if there are no androidTest source files + if (!hasFiles) { + return + } + + val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) + ?: throw RuntimeException("Cannot load APKs") + if (builtArtifacts.elements.size != 1) + throw RuntimeException("Expected one APK !") + val apk = File(builtArtifacts.elements.single().outputFile).toPath() + println(apk) + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/wei/picquest/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/com/wei/picquest/ProjectExtensions.kt new file mode 100644 index 0000000..cfeb772 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/wei/picquest/ProjectExtensions.kt @@ -0,0 +1,9 @@ +package com.wei.picquest + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +val Project.libs + get(): VersionCatalog = extensions.getByType().named("libs") diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties new file mode 100644 index 0000000..1c9073e --- /dev/null +++ b/build-logic/gradle.properties @@ -0,0 +1,4 @@ +# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..2907fbf --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,14 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/build.gradle.kts b/build.gradle.kts index b9dd0ae..2a085f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,11 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } +} + plugins { id("com.android.application") version "8.1.0-rc01" apply false id("org.jetbrains.kotlin.android") version "1.9.10" apply false diff --git a/settings.gradle.kts b/settings.gradle.kts index 6949e8c..ccd61e3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,10 +1,12 @@ pluginManagement { + includeBuild("build-logic") repositories { google() mavenCentral() gradlePluginPortal() } } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {